use std::collections::BTreeMap;
use std::fmt;
use std::sync::OnceLock;
const REGISTRY_JSON: &str = include_str!("../data/blocks-1.21.11.json");
const SHAPE_JSON: &str = include_str!("../data/blockstate-shape-props-1.21.11.json");
const DEFAULTS_JSON: &str = include_str!("../data/block-defaults-1.21.11.json");
const RENAMES_JSON: &str = include_str!("../data/block-renames-1.21.11.json");
pub const MC_VERSION: &str = "1.21.11";
pub const PIN_DATA_VERSION: i32 = 4671;
crate::dw_code! {
pub const DW_STATE_PRE_PIN: &str = "DW0734";
}
crate::dw_code! {
pub const DW_SHAPE_OMITTED: &str = "DW0735";
}
crate::dw_code! {
pub const DW_ORIENTED_FILL_UNGUARDED: &str = "DW0736";
}
crate::dw_code! {
pub const DW_STATE_UNDER_SPECIFIED: &str = "DW0737";
}
crate::dw_code! {
pub const DW_LOCAL_FRAME_UNRESOLVABLE: &str = "DW0738";
}
crate::dw_code! {
pub const DW_ORIENTED_FILL_UNDECIDED: &str = "DW0742";
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateJudgement {
Valid,
InvalidAtPin(BlockError),
PrePin(BlockError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockError {
UnknownBlock {
name: String,
suggestions: Vec<String>,
},
UnknownProperty {
name: String,
property: String,
known: Vec<String>,
},
BadValue {
name: String,
property: String,
value: String,
legal: Vec<String>,
},
}
impl fmt::Display for BlockError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BlockError::UnknownBlock { name, suggestions } => {
write!(f, "{name} is not a block in Minecraft {MC_VERSION}")?;
if !suggestions.is_empty() {
write!(f, " — did you mean {}?", suggestions.join(", "))?;
}
write!(
f,
" (a structure template loads an unknown block as AIR, so this would ship a \
hole rather than fail)"
)
}
BlockError::UnknownProperty {
name,
property,
known,
} => write!(
f,
"{name} has no property {property:?} in Minecraft {MC_VERSION}; it has {}",
if known.is_empty() {
"none".to_string()
} else {
known.join(", ")
}
),
BlockError::BadValue {
name,
property,
value,
legal,
} => write!(
f,
"{name}[{property}={value}] is not legal in Minecraft {MC_VERSION}; {property} is \
one of {}",
legal.join(", ")
),
}
}
}
impl std::error::Error for BlockError {}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct BlockRename {
pub to: String,
pub valid_through: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadedId<'a> {
AsWritten,
Renamed {
to: &'a str,
valid_through: i32,
},
Unresolved,
}
pub struct BlockRegistry {
blocks: BTreeMap<String, BTreeMap<String, Vec<String>>>,
shape: BTreeMap<String, Vec<String>>,
defaults: BTreeMap<String, BTreeMap<String, String>>,
renames: BTreeMap<String, BlockRename>,
}
impl BlockRegistry {
pub fn v1_21_11() -> &'static BlockRegistry {
static REGISTRY: OnceLock<BlockRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| BlockRegistry {
blocks: serde_json::from_str(REGISTRY_JSON)
.expect("the vendored block registry is valid JSON"),
shape: serde_json::from_str(SHAPE_JSON)
.expect("the vendored shape-property table is valid JSON"),
defaults: serde_json::from_str(DEFAULTS_JSON)
.expect("the vendored block default-state table is valid JSON"),
renames: serde_json::from_str(RENAMES_JSON)
.expect("the vendored block rename table is valid JSON"),
})
}
pub fn renames(&self) -> &BTreeMap<String, BlockRename> {
&self.renames
}
pub fn loaded_id_at(&self, name: &str, data_version: i32) -> LoadedId<'_> {
let namespaced = namespace(name).into_owned();
if !namespaced.starts_with("minecraft:") || self.blocks.contains_key(&namespaced) {
return LoadedId::AsWritten;
}
match self.renames.get(&namespaced) {
Some(r) if data_version <= r.valid_through => LoadedId::Renamed {
to: &r.to,
valid_through: r.valid_through,
},
_ => LoadedId::Unresolved,
}
}
pub fn len(&self) -> usize {
self.blocks.len()
}
pub fn ids(&self) -> impl Iterator<Item = &str> {
self.blocks.keys().map(String::as_str)
}
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
pub fn has(&self, name: &str) -> bool {
self.blocks.contains_key(name)
}
pub fn validate(
&self,
name: &str,
properties: &BTreeMap<String, String>,
) -> Result<(), BlockError> {
let namespaced = namespace(name).into_owned();
if !namespaced.starts_with("minecraft:") {
return Ok(());
}
let Some(known) = self.blocks.get(&namespaced) else {
return Err(BlockError::UnknownBlock {
suggestions: self.suggest(&namespaced),
name: namespaced,
});
};
for (property, value) in properties {
let Some(legal) = known.get(property) else {
return Err(BlockError::UnknownProperty {
name: namespaced,
property: property.clone(),
known: known.keys().cloned().collect(),
});
};
if !legal.contains(value) {
return Err(BlockError::BadValue {
name: namespaced,
property: property.clone(),
value: value.clone(),
legal: legal.clone(),
});
}
}
Ok(())
}
pub fn validate_state_string(&self, state: &str) -> Result<(), BlockError> {
let (name, properties) = parse_state(state);
self.validate(name, &properties)
}
pub fn judge_at(
&self,
name: &str,
properties: &BTreeMap<String, String>,
data_version: i32,
) -> StateJudgement {
match self.validate(name, properties) {
Ok(()) => StateJudgement::Valid,
Err(e) if data_version >= PIN_DATA_VERSION => StateJudgement::InvalidAtPin(e),
Err(e) => StateJudgement::PrePin(e),
}
}
pub fn properties(&self, name: &str) -> Option<&BTreeMap<String, Vec<String>>> {
self.blocks.get(namespace(name).as_ref())
}
pub fn default_state(&self, name: &str) -> Option<&BTreeMap<String, String>> {
self.defaults.get(namespace(name).as_ref())
}
pub fn unwritten(
&self,
name: &str,
written: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
let Some(default) = self.defaults.get(namespace(name).as_ref()) else {
return BTreeMap::new();
};
default
.iter()
.filter(|(k, _)| !written.contains_key(*k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn shape_carrying(&self, name: &str) -> &[String] {
let namespaced = namespace(name);
self.shape
.get(namespaced.as_ref())
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn values(&self, name: &str, property: &str) -> &[String] {
let namespaced = namespace(name);
self.blocks
.get(namespaced.as_ref())
.and_then(|props| props.get(property))
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn is_stairs(&self, name: &str) -> bool {
let values = self.values(name, "shape");
values.len() == 5
&& [
"straight",
"inner_left",
"inner_right",
"outer_left",
"outer_right",
]
.iter()
.all(|v| values.iter().any(|has| has == v))
}
pub fn omitted_properties(
&self,
name: &str,
properties: &BTreeMap<String, String>,
) -> Vec<String> {
let namespaced = namespace(name);
let Some(known) = self.blocks.get(namespaced.as_ref()) else {
return Vec::new();
};
known
.keys()
.filter(|p| !properties.contains_key(*p))
.cloned()
.collect()
}
pub fn omitted_shape_carrying(
&self,
name: &str,
properties: &BTreeMap<String, String>,
) -> Vec<String> {
self.shape_carrying(name)
.iter()
.filter(|p| !properties.contains_key(*p))
.cloned()
.collect()
}
pub fn oriented_mismatch(
&self,
name: &str,
properties: &BTreeMap<String, String>,
local_to_world: [usize; 3],
reflected: [bool; 3],
) -> Option<String> {
if local_to_world == [0, 1, 2] && reflected == [false; 3] {
return None;
}
let namespaced = namespace(name);
let known = self.blocks.get(namespaced.as_ref())?;
for (k, v) in properties {
if known.get(k).is_none() {
continue; }
match property_image(known, k, v, local_to_world, reflected) {
PropertyImage::Fixed => {}
PropertyImage::Moved { key, value } => {
if key != *k {
if properties.get(&key) != Some(v) {
return Some(format!("{k}={v}"));
}
} else if value != *v {
return Some(format!("{k}={v}"));
}
}
PropertyImage::Undetermined => return Some(format!("{k}={v}")),
}
}
None
}
pub fn frame_sensitive(
&self,
name: &str,
properties: &BTreeMap<String, String>,
frames: impl IntoIterator<Item = ([usize; 3], [bool; 3])>,
) -> Vec<String> {
let mut found: std::collections::BTreeSet<String> = Default::default();
for (perm, reflected) in frames {
if let Some(hit) = self.oriented_mismatch(name, properties, perm, reflected) {
found.insert(hit);
}
}
found.into_iter().collect()
}
pub fn permuted_properties(
&self,
name: &str,
properties: &BTreeMap<String, String>,
local_to_world: [usize; 3],
reflected: [bool; 3],
) -> Result<BTreeMap<String, String>, String> {
if local_to_world == [0, 1, 2] && reflected == [false; 3] {
return Ok(properties.clone());
}
let namespaced = namespace(name);
let Some(known) = self.blocks.get(namespaced.as_ref()) else {
return Ok(properties.clone());
};
let mut out = BTreeMap::new();
for (k, v) in properties {
if known.get(k).is_none() {
out.insert(k.clone(), v.clone()); continue;
}
match property_image(known, k, v, local_to_world, reflected) {
PropertyImage::Fixed => {
out.insert(k.clone(), v.clone());
}
PropertyImage::Moved { key, value } => {
out.insert(key, value);
}
PropertyImage::Undetermined => return Err(format!("{k}={v}")),
}
}
Ok(out)
}
fn suggest(&self, name: &str) -> Vec<String> {
let path = name.split_once(':').map(|(_, p)| p).unwrap_or(name);
let words: Vec<&str> = path.split('_').collect();
let mut ranked: Vec<(u8, usize, &String)> = Vec::new();
for id in self.blocks.keys() {
let candidate = id.split_once(':').map(|(_, p)| p).unwrap_or(id);
if candidate == path {
continue;
}
let candidate_words: Vec<&str> = candidate.split('_').collect();
if candidate_words.len() <= words.len() {
continue;
}
let extra = candidate_words.len() - words.len();
let rank = if candidate_words.ends_with(words.as_slice()) {
0
} else if candidate_words.starts_with(words.as_slice()) {
1
} else if candidate_words
.windows(words.len())
.any(|w| w == words.as_slice())
{
2
} else {
continue;
};
ranked.push((rank, extra, id));
}
ranked.sort();
ranked
.into_iter()
.take(3)
.map(|(_, _, id)| id.clone())
.collect()
}
}
enum PropertyImage {
Fixed,
Moved {
key: String,
value: String,
},
Undetermined,
}
fn property_image(
known: &BTreeMap<String, Vec<String>>,
key: &str,
value: &str,
local_to_world: [usize; 3],
reflected: [bool; 3],
) -> PropertyImage {
let legal = match known.get(key) {
Some(l) => l,
None => return PropertyImage::Fixed,
};
let image = |axis: usize, sign: i8| {
let sign = if reflected[axis] { -sign } else { sign };
axis_sign_direction(local_to_world[axis], sign)
};
if let Some((axis, sign)) = direction_axis_sign(key) {
let moved = image(axis, sign);
if moved == key {
return PropertyImage::Fixed;
}
return match known.get(moved) {
Some(l) if l.iter().any(|v| v == value) => PropertyImage::Moved {
key: moved.to_string(),
value: value.to_string(),
},
_ => PropertyImage::Undetermined,
};
}
if !legal.is_empty() && legal.iter().all(|l| direction_axis_sign(l).is_some()) {
let Some((axis, sign)) = direction_axis_sign(value) else {
return PropertyImage::Undetermined;
};
return image_of_value(key, value, image(axis, sign), legal);
}
if !legal.is_empty() && legal.iter().all(|l| axis_index(l).is_some()) {
let Some(axis) = axis_index(value) else {
return PropertyImage::Undetermined;
};
let moved = ["x", "y", "z"][local_to_world[axis]];
return image_of_value(key, value, moved, legal);
}
if !legal.is_empty() && legal.iter().all(|l| is_direction_pair(l)) {
let Some((a, b)) = value.split_once('_') else {
return PropertyImage::Undetermined;
};
let (Some((aa, asig)), Some((ba, bsig))) = (direction_axis_sign(a), direction_axis_sign(b))
else {
return PropertyImage::Undetermined;
};
let moved = format!("{}_{}", image(aa, asig), image(ba, bsig));
return image_of_value(key, value, &moved, legal);
}
let turn_about_the_vertical = local_to_world[1] == 1 && reflected == [false; 3];
let vertical_kept = local_to_world[1] == 1 && !reflected[1];
if key == "rotation" && legal.iter().all(|l| l.parse::<u8>().is_ok()) {
let (true, Ok(r)) = (turn_about_the_vertical, value.parse::<u32>()) else {
return PropertyImage::Undetermined;
};
let moved = ((28 - r % 16) % 16).to_string();
return image_of_value(key, value, &moved, legal);
}
if legal
.iter()
.any(|l| l.split('_').any(|w| w == "left" || w == "right"))
{
let moved: String = value
.split('_')
.map(|w| match w {
"left" => "right",
"right" => "left",
other => other,
})
.collect::<Vec<_>>()
.join("_");
if moved == value {
return PropertyImage::Fixed;
}
if !turn_about_the_vertical {
return PropertyImage::Undetermined;
}
return image_of_value(key, value, &moved, legal);
}
if !legal.is_empty()
&& legal
.iter()
.all(|l| matches!(l.as_str(), "top" | "bottom" | "double" | "upper" | "lower"))
{
return if value == "double" || vertical_kept {
PropertyImage::Fixed
} else {
PropertyImage::Undetermined
};
}
if legal.iter().any(|l| {
l.split('_')
.any(|w| direction_axis_sign(w).is_some() || axis_index(w).is_some())
}) {
return PropertyImage::Undetermined;
}
PropertyImage::Fixed
}
fn image_of_value(key: &str, value: &str, moved: &str, legal: &[String]) -> PropertyImage {
if moved == value {
PropertyImage::Fixed
} else if legal.iter().any(|l| l == moved) {
PropertyImage::Moved {
key: key.to_string(),
value: moved.to_string(),
}
} else {
PropertyImage::Undetermined
}
}
fn namespace(name: &str) -> std::borrow::Cow<'_, str> {
if name.contains(':') {
std::borrow::Cow::Borrowed(name)
} else {
std::borrow::Cow::Owned(format!("minecraft:{name}"))
}
}
fn direction_axis_sign(word: &str) -> Option<(usize, i8)> {
match word {
"west" => Some((0, -1)),
"east" => Some((0, 1)),
"down" => Some((1, -1)),
"up" => Some((1, 1)),
"north" => Some((2, -1)),
"south" => Some((2, 1)),
_ => None,
}
}
fn axis_sign_direction(axis: usize, sign: i8) -> &'static str {
match (axis, sign) {
(0, -1) => "west",
(0, 1) => "east",
(1, -1) => "down",
(1, 1) => "up",
(2, -1) => "north",
(2, 1) => "south",
_ => unreachable!("axis index is always 0..3 and sign ±1"),
}
}
fn axis_index(word: &str) -> Option<usize> {
match word {
"x" => Some(0),
"y" => Some(1),
"z" => Some(2),
_ => None,
}
}
fn is_direction_pair(value: &str) -> bool {
value
.split_once('_')
.is_some_and(|(a, b)| direction_axis_sign(a).is_some() && direction_axis_sign(b).is_some())
}
pub(crate) fn parse_state(state: &str) -> (&str, BTreeMap<String, String>) {
let Some(open) = state.find('[') else {
return (state.trim(), BTreeMap::new());
};
let name = state[..open].trim();
let inner = state[open + 1..].trim_end().trim_end_matches(']');
let mut properties = BTreeMap::new();
for pair in inner.split(',') {
if let Some((k, v)) = pair.split_once('=') {
properties.insert(k.trim().to_string(), v.trim().to_string());
}
}
(name, properties)
}
#[cfg(test)]
mod tests {
use super::*;
fn props(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn every_block_has_a_legal_default_state() {
let reg = BlockRegistry::v1_21_11();
let mut with_properties = 0usize;
for (name, properties) in ®.blocks {
let default = reg
.default_state(name)
.unwrap_or_else(|| panic!("{name} has no default state"));
assert_eq!(
default.keys().collect::<Vec<_>>(),
properties.keys().collect::<Vec<_>>(),
"{name}: the default state and the property list name different properties"
);
if !properties.is_empty() {
with_properties += 1;
}
for (property, value) in default {
assert!(
properties[property].contains(value),
"{name}[{property}={value}] is not one of that property's legal values"
);
}
}
assert_eq!(reg.defaults.len(), reg.blocks.len());
assert_eq!(
with_properties, 777,
"1.21.11 has 777 blocks with at least one property"
);
}
#[test]
fn every_shape_carrying_property_has_a_default_to_complete_it() {
let reg = BlockRegistry::v1_21_11();
let mut bound = 0usize;
for name in reg.shape.keys() {
let omitted = reg.omitted_shape_carrying(name, &BTreeMap::new());
let unwritten = reg.unwritten(name, &BTreeMap::new());
assert!(
!omitted.is_empty(),
"{name} is in the shape table with no properties"
);
for property in &omitted {
assert!(
unwritten.contains_key(property),
"{name}[{property}] carries shape but the default-state table cannot complete it"
);
bound += 1;
}
}
assert_eq!(
reg.shape.len(),
95,
"1.21.11 has 95 blocks whose model is assembled from parts"
);
assert!(
bound >= 95,
"only {bound} property pairs examined — the scan has come unbound"
);
}
#[test]
fn a_bare_wall_completes_to_a_post_not_to_the_first_legal_value() {
let reg = BlockRegistry::v1_21_11();
let unwritten = reg.unwritten("minecraft:cobblestone_wall", &BTreeMap::new());
assert_eq!(unwritten["up"], "true");
assert_eq!(unwritten["east"], "none");
let properties = reg.properties("minecraft:cobblestone_wall").unwrap();
assert_eq!(properties["up"].first().unwrap(), "false");
assert_eq!(properties["east"].first().unwrap(), "low");
let written = props(&[("up", "false")]);
assert!(
!reg.unwritten("minecraft:cobblestone_wall", &written)
.contains_key("up")
);
assert!(
reg.unwritten("minecraft:stone", &BTreeMap::new())
.is_empty()
);
}
#[test]
fn the_registry_binds_to_the_whole_pinned_version() {
let reg = BlockRegistry::v1_21_11();
assert_eq!(reg.len(), 1166, "1.21.11 has 1166 blocks");
for id in [
"minecraft:air",
"minecraft:stone",
"minecraft:water",
"minecraft:oak_stairs",
] {
assert!(reg.has(id), "{id} missing from the pinned registry");
}
}
#[test]
fn the_renamed_chain_is_caught_and_the_rename_is_suggested() {
let reg = BlockRegistry::v1_21_11();
assert!(!reg.has("minecraft:chain"));
assert!(reg.has("minecraft:iron_chain"));
let err = reg
.validate(
"minecraft:chain",
&props(&[("axis", "y"), ("waterlogged", "false")]),
)
.unwrap_err();
let BlockError::UnknownBlock { suggestions, .. } = &err else {
panic!("expected an unknown-block refusal, got {err}");
};
assert!(
suggestions.contains(&"minecraft:iron_chain".to_string()),
"the rename was not suggested: {suggestions:?}"
);
assert!(err.to_string().contains("loads an unknown block as AIR"));
}
#[test]
fn properties_and_values_are_checked_too() {
let reg = BlockRegistry::v1_21_11();
assert!(
reg.validate(
"minecraft:oak_stairs",
&props(&[("facing", "east"), ("half", "top")])
)
.is_ok()
);
assert_eq!(
reg.validate("minecraft:oak_stairs", &props(&[("orientation", "east")]))
.unwrap_err(),
BlockError::UnknownProperty {
name: "minecraft:oak_stairs".into(),
property: "orientation".into(),
known: vec![
"facing".into(),
"half".into(),
"shape".into(),
"waterlogged".into()
],
}
);
let err = reg
.validate("minecraft:oak_stairs", &props(&[("facing", "up")]))
.unwrap_err();
assert!(matches!(err, BlockError::BadValue { .. }), "{err}");
assert!(err.to_string().contains("north"), "{err}");
}
#[test]
fn a_state_string_validates_the_same_way_as_its_parts() {
let reg = BlockRegistry::v1_21_11();
assert!(reg.validate_state_string("minecraft:stone").is_ok());
assert!(reg.validate_state_string("stone").is_ok());
assert!(
reg.validate_state_string("minecraft:iron_chain[axis=y,waterlogged=false]")
.is_ok()
);
assert!(
reg.validate_state_string("minecraft:chain[axis=y,waterlogged=false]")
.is_err()
);
}
#[test]
fn a_foreign_namespace_is_left_alone() {
let reg = BlockRegistry::v1_21_11();
assert!(
reg.validate("delvewright:nonesuch", &BTreeMap::new())
.is_ok()
);
}
#[test]
fn judge_at_separates_the_bell_tower_defect_from_the_ruin_arch_false_positive() {
let reg = BlockRegistry::v1_21_11();
let chain = props(&[("axis", "y")]);
assert!(matches!(
reg.judge_at("minecraft:chain", &chain, PIN_DATA_VERSION),
StateJudgement::InvalidAtPin(_)
));
assert!(matches!(
reg.judge_at("minecraft:chain", &chain, 2975),
StateJudgement::PrePin(_)
));
assert!(matches!(
reg.judge_at("minecraft:chain", &chain, PIN_DATA_VERSION + 1),
StateJudgement::InvalidAtPin(_)
));
assert_eq!(
reg.judge_at("minecraft:iron_chain", &chain, PIN_DATA_VERSION),
StateJudgement::Valid
);
}
#[test]
fn loaded_id_at_resolves_a_pre_pin_rename_and_refuses_to_guess_past_its_bound() {
let reg = BlockRegistry::v1_21_11();
assert_eq!(
reg.loaded_id_at("minecraft:chain", 2975),
LoadedId::Renamed {
to: "minecraft:iron_chain",
valid_through: 4440,
}
);
assert_eq!(
reg.loaded_id_at("chain", 4440),
LoadedId::Renamed {
to: "minecraft:iron_chain",
valid_through: 4440,
}
);
assert_eq!(
reg.loaded_id_at("minecraft:chain", 4441),
LoadedId::Unresolved
);
assert_eq!(
reg.loaded_id_at("minecraft:chain", PIN_DATA_VERSION),
LoadedId::Unresolved
);
assert_eq!(
reg.loaded_id_at("minecraft:nonesuch", 2975),
LoadedId::Unresolved
);
assert_eq!(
reg.loaded_id_at("minecraft:iron_chain", 2975),
LoadedId::AsWritten
);
assert_eq!(
reg.loaded_id_at("minecraft:stone", PIN_DATA_VERSION),
LoadedId::AsWritten
);
assert_eq!(
reg.loaded_id_at("delvewright:nonesuch", 2975),
LoadedId::AsWritten
);
}
#[test]
fn every_vendored_rename_leaves_the_registry_and_lands_inside_it() {
let reg = BlockRegistry::v1_21_11();
assert!(
!reg.renames().is_empty(),
"binding count is zero: the rename table has no rows, so nothing below examined anything"
);
for (from, rename) in reg.renames() {
assert!(!reg.has(from), "{from} is still a block at the pin");
assert!(
reg.has(&rename.to),
"{from} -> {} is not a block at the pin",
rename.to
);
assert!(
rename.valid_through < PIN_DATA_VERSION,
"{from}: valid_through {} is not below the pin",
rename.valid_through
);
}
}
#[test]
fn shape_carrying_is_the_multipart_class_not_a_hand_list() {
let reg = BlockRegistry::v1_21_11();
assert_eq!(
reg.shape_carrying("minecraft:cobblestone_wall"),
["east", "north", "south", "up", "west"]
);
assert_eq!(
reg.shape_carrying("iron_bars"),
["east", "north", "south", "west"]
);
assert!(!reg.shape_carrying("minecraft:vine").is_empty());
assert!(!reg.shape_carrying("minecraft:glow_lichen").is_empty());
assert!(reg.shape_carrying("minecraft:lantern").is_empty());
assert!(reg.shape_carrying("minecraft:grass_block").is_empty());
assert!(reg.shape_carrying("minecraft:spruce_button").is_empty());
assert!(reg.shape_carrying("minecraft:oak_stairs").is_empty());
assert!(reg.shape_carrying("minecraft:deepslate").is_empty());
assert!(reg.shape_carrying("minecraft:iron_chain").is_empty());
assert!(reg.shape_carrying("delvewright:nonesuch").is_empty());
assert!(reg.shape_carrying("minecraft:chain").is_empty());
assert_eq!(reg.shape.len(), 95, "95 blocks assemble their model");
}
#[test]
fn omitted_shape_carrying_reports_exactly_the_missing_ones() {
let reg = BlockRegistry::v1_21_11();
assert_eq!(
reg.omitted_shape_carrying("minecraft:iron_bars", &BTreeMap::new()),
["east", "north", "south", "west"]
);
assert_eq!(
reg.omitted_shape_carrying(
"minecraft:vine",
&props(&[("north", "true"), ("waterlogged", "false")])
),
["east", "south", "up", "west"]
);
assert!(
reg.omitted_shape_carrying(
"minecraft:iron_bars",
&props(&[
("east", "false"),
("north", "true"),
("south", "true"),
("west", "false")
])
)
.is_empty()
);
assert!(
reg.omitted_shape_carrying("minecraft:lantern", &BTreeMap::new())
.is_empty()
);
}
const STRAIGHT: [bool; 3] = [false, false, false];
#[test]
fn oriented_mismatch_transforms_through_the_registry_vocabulary() {
let reg = BlockRegistry::v1_21_11();
let keep = [0, 1, 2];
let swap_xz = [2, 1, 0]; let move_y = [0, 2, 1];
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_stairs",
&props(&[("facing", "north")]),
keep,
STRAIGHT
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_stairs",
&props(&[("facing", "north")]),
swap_xz,
STRAIGHT
),
Some("facing=north".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:barrel",
&props(&[("facing", "up")]),
swap_xz,
STRAIGHT
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:barrel",
&props(&[("facing", "up")]),
move_y,
STRAIGHT
),
Some("facing=up".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:spruce_log",
&props(&[("axis", "y")]),
swap_xz,
STRAIGHT
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:spruce_log",
&props(&[("axis", "x")]),
swap_xz,
STRAIGHT
),
Some("axis=x".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:iron_bars",
&props(&[
("east", "false"),
("north", "true"),
("south", "true"),
("west", "false")
]),
swap_xz,
STRAIGHT
),
Some("east=false".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:iron_bars",
&props(&[
("east", "true"),
("north", "true"),
("south", "true"),
("west", "true")
]),
swap_xz,
STRAIGHT
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:skeleton_skull",
&props(&[("rotation", "8")]),
swap_xz,
STRAIGHT
),
Some("rotation=8".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:skeleton_skull",
&props(&[("rotation", "8")]),
move_y,
STRAIGHT
),
Some("rotation=8".to_string()),
"a moved Z scrambles a yaw too — the residue is conservative on purpose"
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_slab",
&props(&[("type", "top"), ("waterlogged", "false")]),
swap_xz,
STRAIGHT
),
None
);
}
#[test]
fn oriented_mismatch_reads_the_reflection_half_of_the_frame() {
let reg = BlockRegistry::v1_21_11();
let keep = [0, 1, 2];
let flip_z = [false, false, true];
let flip_x = [true, false, false];
let flip_y = [false, true, false];
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_stairs",
&props(&[("facing", "north")]),
keep,
flip_z
),
Some("facing=north".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_stairs",
&props(&[("facing", "north")]),
keep,
flip_x
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_stairs",
&props(&[("facing", "east")]),
keep,
flip_x
),
Some("facing=east".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:barrel",
&props(&[("facing", "up")]),
keep,
flip_y
),
Some("facing=up".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:barrel",
&props(&[("facing", "up")]),
keep,
flip_x
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:spruce_log",
&props(&[("axis", "x")]),
keep,
flip_x
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:iron_bars",
&props(&[
("east", "true"),
("north", "false"),
("south", "true"),
("west", "true")
]),
keep,
flip_z
),
Some("north=false".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:iron_bars",
&props(&[
("east", "true"),
("north", "false"),
("south", "false"),
("west", "true")
]),
keep,
flip_z
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_door",
&props(&[("hinge", "left")]),
keep,
flip_x
),
Some("hinge=left".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:skeleton_skull",
&props(&[("rotation", "8")]),
keep,
flip_z
),
Some("rotation=8".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:jigsaw",
&props(&[("orientation", "north_up")]),
keep,
flip_z
),
Some("orientation=north_up".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_slab",
&props(&[("type", "top"), ("waterlogged", "false")]),
keep,
flip_x
),
None
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:oak_stairs",
&props(&[("facing", "north")]),
[2, 1, 0],
[false, false, true]
),
Some("facing=north".to_string())
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:barrel",
&props(&[("facing", "up")]),
[2, 1, 0],
[false, false, true]
),
None
);
}
#[test]
fn permuted_properties_is_the_state_the_mismatch_predicate_wanted() {
let reg = BlockRegistry::v1_21_11();
let swap_xz = [2, 1, 0];
let bars = props(&[
("east", "false"),
("north", "true"),
("south", "true"),
("waterlogged", "false"),
("west", "false"),
]);
assert_eq!(
reg.oriented_mismatch("minecraft:iron_bars", &bars, swap_xz, STRAIGHT),
Some("east=false".to_string()),
"the literal is wrong under the swap…"
);
assert_eq!(
reg.permuted_properties("minecraft:iron_bars", &bars, swap_xz, STRAIGHT),
Ok(props(&[
("east", "true"),
("north", "false"),
("south", "false"),
("waterlogged", "false"),
("west", "true"),
])),
"…and this is what it had to say instead"
);
assert_eq!(
reg.permuted_properties(
"minecraft:oak_stairs",
&props(&[
("facing", "north"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
]),
swap_xz,
STRAIGHT
),
Ok(props(&[
("facing", "west"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
]))
);
assert_eq!(
reg.permuted_properties(
"minecraft:barrel",
&props(&[("facing", "up")]),
swap_xz,
STRAIGHT
),
Ok(props(&[("facing", "up")]))
);
assert_eq!(
reg.permuted_properties(
"minecraft:skeleton_skull",
&props(&[("powered", "false"), ("rotation", "8")]),
swap_xz,
STRAIGHT
),
Ok(props(&[("powered", "false"), ("rotation", "4")]))
);
assert_eq!(
reg.permuted_properties(
"minecraft:skeleton_skull",
&props(&[("rotation", "6")]),
swap_xz,
STRAIGHT
),
Ok(props(&[("rotation", "6")]))
);
assert_eq!(
reg.oriented_mismatch(
"minecraft:skeleton_skull",
&props(&[("rotation", "6")]),
swap_xz,
STRAIGHT
),
None
);
assert_eq!(
reg.permuted_properties(
"minecraft:oak_door",
&props(&[("facing", "north"), ("hinge", "left")]),
swap_xz,
STRAIGHT
),
Ok(props(&[("facing", "west"), ("hinge", "right")]))
);
}
#[test]
fn a_property_with_no_image_is_refused_rather_than_guessed() {
let reg = BlockRegistry::v1_21_11();
let move_y = [0, 2, 1];
assert_eq!(DW_LOCAL_FRAME_UNRESOLVABLE, "DW0738");
assert_eq!(
reg.permuted_properties(
"minecraft:skeleton_skull",
&props(&[("rotation", "8")]),
move_y,
STRAIGHT
),
Err("rotation=8".to_string())
);
assert_eq!(
reg.permuted_properties(
"minecraft:oak_slab",
&props(&[("type", "top")]),
move_y,
STRAIGHT
),
Err("type=top".to_string())
);
assert_eq!(
reg.permuted_properties(
"minecraft:iron_bars",
&props(&[("north", "true")]),
move_y,
STRAIGHT
),
Err("north=true".to_string())
);
assert_eq!(
reg.permuted_properties(
"minecraft:rail",
&props(&[("shape", "ascending_north")]),
[2, 1, 0],
STRAIGHT
),
Err("shape=ascending_north".to_string())
);
assert_eq!(
reg.permuted_properties(
"minecraft:skeleton_skull",
&props(&[("rotation", "8")]),
[0, 1, 2],
STRAIGHT
),
Ok(props(&[("rotation", "8")]))
);
}
#[test]
fn a_local_frame_resolves_through_the_reflection_half_too() {
let reg = BlockRegistry::v1_21_11();
let keep = [0, 1, 2];
let flip_x = [true, false, false];
let flip_z = [false, false, true];
assert_eq!(
reg.permuted_properties(
"minecraft:oak_stairs",
&props(&[
("facing", "east"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
]),
keep,
flip_x
),
Ok(props(&[
("facing", "west"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
])),
"a reflected local X sends the scope's east to the world's west"
);
assert_eq!(
reg.permuted_properties(
"minecraft:iron_bars",
&props(&[
("east", "true"),
("north", "true"),
("south", "false"),
("waterlogged", "false"),
("west", "true"),
]),
keep,
flip_z
),
Ok(props(&[
("east", "true"),
("north", "false"),
("south", "true"),
("waterlogged", "false"),
("west", "true"),
]))
);
assert_eq!(
reg.permuted_properties(
"minecraft:spruce_log",
&props(&[("axis", "x")]),
keep,
flip_x
),
Ok(props(&[("axis", "x")]))
);
assert_eq!(
reg.permuted_properties(
"minecraft:barrel",
&props(&[("facing", "up")]),
keep,
flip_x
),
Ok(props(&[("facing", "up")]))
);
assert_eq!(
reg.permuted_properties(
"minecraft:oak_stairs",
&props(&[
("facing", "north"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
]),
[2, 1, 0],
flip_z
),
Ok(props(&[
("facing", "east"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
]))
);
}
#[test]
fn the_frame_relative_residue_refuses_under_a_reflection_and_the_judge_agrees() {
let reg = BlockRegistry::v1_21_11();
let keep = [0, 1, 2];
let flip_x = [true, false, false];
let swap_xz = [2, 1, 0];
for (perm, refl, state, prop) in [
(keep, flip_x, "minecraft:skeleton_skull", "rotation=8"),
(swap_xz, flip_x, "minecraft:skeleton_skull", "rotation=8"),
(keep, flip_x, "minecraft:oak_door", "hinge=left"),
(swap_xz, flip_x, "minecraft:oak_door", "hinge=left"),
] {
let (k, v) = prop.split_once('=').unwrap();
let p = props(&[(k, v)]);
assert_eq!(
reg.permuted_properties(state, &p, perm, refl),
Err(prop.to_string()),
"{state} {prop} under {perm:?}/{refl:?} must be refused, not guessed"
);
assert_eq!(
reg.oriented_mismatch(state, &p, perm, refl),
Some(prop.to_string()),
"…and the judge must call the same state wrong"
);
}
assert_eq!(
reg.permuted_properties(
"minecraft:oak_slab",
&props(&[("type", "top")]),
keep,
[false, true, false]
),
Err("type=top".to_string())
);
assert_eq!(
reg.permuted_properties(
"minecraft:oak_slab",
&props(&[("type", "top")]),
keep,
flip_x
),
Ok(props(&[("type", "top")]))
);
}
#[test]
fn the_judge_and_the_resolver_agree_over_every_frame_the_grammar_can_make() {
let reg = BlockRegistry::v1_21_11();
let states: [(&str, &[(&str, &str)]); 8] = [
(
"minecraft:oak_stairs",
&[
("facing", "east"),
("half", "bottom"),
("shape", "straight"),
("waterlogged", "false"),
],
),
(
"minecraft:iron_bars",
&[
("east", "true"),
("north", "true"),
("south", "false"),
("waterlogged", "false"),
("west", "false"),
],
),
("minecraft:spruce_log", &[("axis", "x")]),
("minecraft:barrel", &[("facing", "up"), ("open", "false")]),
(
"minecraft:skeleton_skull",
&[("powered", "false"), ("rotation", "3")],
),
(
"minecraft:oak_door",
&[
("facing", "north"),
("half", "lower"),
("hinge", "left"),
("open", "false"),
("powered", "false"),
],
),
(
"minecraft:oak_slab",
&[("type", "top"), ("waterlogged", "false")],
),
("minecraft:jigsaw", &[("orientation", "north_up")]),
];
let perms = [
[0usize, 1, 2],
[2, 1, 0],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
];
let mut checked = 0usize;
let mut resolved = 0usize;
for (name, pairs) in states {
let properties = props(pairs);
for perm in perms {
for bits in 0..8u8 {
let refl = [bits & 1 != 0, bits & 2 != 0, bits & 4 != 0];
checked += 1;
let judged = reg.oriented_mismatch(name, &properties, perm, refl);
match reg.permuted_properties(name, &properties, perm, refl) {
Ok(out) => {
resolved += 1;
assert!(
reg.validate(name, &out).is_ok(),
"{name} under {perm:?}/{refl:?} resolved to {out:?}, which \
the pin does not accept"
);
assert_eq!(
judged.is_none(),
out == properties,
"{name} under {perm:?}/{refl:?}: judge said {judged:?} while \
the resolver wrote {out:?} for {properties:?}"
);
}
Err(refused) => assert!(
judged.is_some(),
"{name} under {perm:?}/{refl:?} was refused as {refused} while \
the judge called the state safe — the two ends disagree"
),
}
}
}
}
assert_eq!(
checked,
8 * 6 * 8,
"binding count: states x perms x mirrors"
);
assert!(
resolved > 0 && resolved < checked,
"binding count {resolved} of {checked}: the sweep must contain both \
resolutions and refusals, or it discriminates nothing"
);
}
}