use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum BlockstateDefinition {
Variants(HashMap<String, Vec<ModelVariant>>),
Multipart(Vec<MultipartCase>),
}
impl<'de> Deserialize<'de> for BlockstateDefinition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawBlockstate {
variants: Option<HashMap<String, VariantValue>>,
multipart: Option<Vec<MultipartCase>>,
}
let raw = RawBlockstate::deserialize(deserializer)?;
if let Some(variants) = raw.variants {
let parsed: HashMap<String, Vec<ModelVariant>> = variants
.into_iter()
.map(|(k, v)| (k, v.into_vec()))
.collect();
Ok(BlockstateDefinition::Variants(parsed))
} else if let Some(multipart) = raw.multipart {
Ok(BlockstateDefinition::Multipart(multipart))
} else {
Ok(BlockstateDefinition::Variants(HashMap::new()))
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum VariantValue {
Single(ModelVariant),
Multiple(Vec<ModelVariant>),
}
impl VariantValue {
fn into_vec(self) -> Vec<ModelVariant> {
match self {
VariantValue::Single(v) => vec![v],
VariantValue::Multiple(v) => v,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelVariant {
pub model: String,
#[serde(default)]
pub x: i32,
#[serde(default)]
pub y: i32,
#[serde(default)]
pub uvlock: bool,
#[serde(default = "default_weight")]
pub weight: u32,
}
fn default_weight() -> u32 {
1
}
impl ModelVariant {
pub fn model_location(&self) -> String {
if self.model.contains(':') {
self.model.clone()
} else {
format!("minecraft:{}", self.model)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultipartCase {
#[serde(default)]
pub when: Option<MultipartCondition>,
pub apply: ApplyValue,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ApplyValue {
Single(ModelVariant),
Multiple(Vec<ModelVariant>),
}
impl ApplyValue {
pub fn variants(&self) -> Vec<&ModelVariant> {
match self {
ApplyValue::Single(v) => vec![v],
ApplyValue::Multiple(v) => v.iter().collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MultipartCondition {
Or { OR: Vec<HashMap<String, String>> },
And { AND: Vec<HashMap<String, String>> },
Simple(HashMap<String, String>),
}
impl MultipartCondition {
pub fn matches(&self, properties: &HashMap<String, String>) -> bool {
match self {
MultipartCondition::Or { OR } => {
OR.iter().any(|cond| Self::matches_simple(cond, properties))
}
MultipartCondition::And { AND } => {
AND.iter().all(|cond| Self::matches_simple(cond, properties))
}
MultipartCondition::Simple(cond) => Self::matches_simple(cond, properties),
}
}
fn matches_simple(
condition: &HashMap<String, String>,
properties: &HashMap<String, String>,
) -> bool {
condition.iter().all(|(key, expected_value)| {
if expected_value.contains('|') {
let allowed: Vec<&str> = expected_value.split('|').collect();
properties
.get(key)
.map(|v| allowed.contains(&v.as_str()))
.unwrap_or_else(|| {
allowed.iter().any(|v| Self::is_default_value(v))
})
} else {
properties
.get(key)
.map(|v| v == expected_value)
.unwrap_or_else(|| {
Self::is_default_value(expected_value)
})
}
})
}
fn is_default_value(value: &str) -> bool {
matches!(
value,
"false" | "none" | "0" | "normal" | "bottom" | "floor"
)
}
}
pub fn build_property_string(properties: &HashMap<String, String>) -> String {
if properties.is_empty() {
return String::new();
}
let mut pairs: Vec<_> = properties.iter().collect();
pairs.sort_by_key(|(k, _)| *k);
pairs
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_variants() {
let json = r#"{
"variants": {
"": { "model": "block/stone" }
}
}"#;
let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
match def {
BlockstateDefinition::Variants(variants) => {
assert!(variants.contains_key(""));
assert_eq!(variants[""].len(), 1);
assert_eq!(variants[""][0].model, "block/stone");
}
_ => panic!("Expected Variants"),
}
}
#[test]
fn test_parse_variants_with_rotation() {
let json = r#"{
"variants": {
"facing=north": { "model": "block/furnace", "y": 0 },
"facing=east": { "model": "block/furnace", "y": 90 },
"facing=south": { "model": "block/furnace", "y": 180 },
"facing=west": { "model": "block/furnace", "y": 270 }
}
}"#;
let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
match def {
BlockstateDefinition::Variants(variants) => {
assert_eq!(variants.len(), 4);
assert_eq!(variants["facing=east"][0].y, 90);
}
_ => panic!("Expected Variants"),
}
}
#[test]
fn test_parse_weighted_variants() {
let json = r#"{
"variants": {
"": [
{ "model": "block/stone", "weight": 10 },
{ "model": "block/stone_mirrored", "weight": 5 }
]
}
}"#;
let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
match def {
BlockstateDefinition::Variants(variants) => {
assert_eq!(variants[""].len(), 2);
assert_eq!(variants[""][0].weight, 10);
assert_eq!(variants[""][1].weight, 5);
}
_ => panic!("Expected Variants"),
}
}
#[test]
fn test_parse_multipart() {
let json = r#"{
"multipart": [
{ "apply": { "model": "block/fence_post" } },
{ "when": { "north": "true" }, "apply": { "model": "block/fence_side" } }
]
}"#;
let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
match def {
BlockstateDefinition::Multipart(cases) => {
assert_eq!(cases.len(), 2);
assert!(cases[0].when.is_none());
assert!(cases[1].when.is_some());
}
_ => panic!("Expected Multipart"),
}
}
#[test]
fn test_multipart_condition_simple() {
let cond = MultipartCondition::Simple(
[("facing".to_string(), "north".to_string())]
.into_iter()
.collect(),
);
let props: HashMap<String, String> =
[("facing".to_string(), "north".to_string())].into_iter().collect();
assert!(cond.matches(&props));
let wrong_props: HashMap<String, String> =
[("facing".to_string(), "south".to_string())].into_iter().collect();
assert!(!cond.matches(&wrong_props));
}
#[test]
fn test_multipart_condition_or() {
let json = r#"{ "OR": [{ "facing": "north" }, { "facing": "south" }] }"#;
let cond: MultipartCondition = serde_json::from_str(json).unwrap();
let north: HashMap<String, String> =
[("facing".to_string(), "north".to_string())].into_iter().collect();
let south: HashMap<String, String> =
[("facing".to_string(), "south".to_string())].into_iter().collect();
let east: HashMap<String, String> =
[("facing".to_string(), "east".to_string())].into_iter().collect();
assert!(cond.matches(&north));
assert!(cond.matches(&south));
assert!(!cond.matches(&east));
}
#[test]
fn test_multipart_condition_pipe_values() {
let cond = MultipartCondition::Simple(
[("facing".to_string(), "north|south".to_string())]
.into_iter()
.collect(),
);
let north: HashMap<String, String> =
[("facing".to_string(), "north".to_string())].into_iter().collect();
let south: HashMap<String, String> =
[("facing".to_string(), "south".to_string())].into_iter().collect();
let east: HashMap<String, String> =
[("facing".to_string(), "east".to_string())].into_iter().collect();
assert!(cond.matches(&north));
assert!(cond.matches(&south));
assert!(!cond.matches(&east));
}
#[test]
fn test_build_property_string() {
let props: HashMap<String, String> = [
("facing".to_string(), "north".to_string()),
("half".to_string(), "bottom".to_string()),
]
.into_iter()
.collect();
assert_eq!(build_property_string(&props), "facing=north,half=bottom");
}
#[test]
fn test_build_property_string_empty() {
let props: HashMap<String, String> = HashMap::new();
assert_eq!(build_property_string(&props), "");
}
}