schematic_mesher/resource_pack/
blockstate.rs

1//! Blockstate definition parsing.
2//!
3//! Blockstates define how block properties map to model variants.
4//! There are two formats: "variants" and "multipart".
5
6use serde::{Deserialize, Deserializer, Serialize};
7use std::collections::HashMap;
8
9/// A blockstate definition from blockstates/*.json.
10#[derive(Debug, Clone)]
11pub enum BlockstateDefinition {
12    /// Simple variants: property combinations map to models.
13    Variants(HashMap<String, Vec<ModelVariant>>),
14    /// Multipart: conditional model application.
15    Multipart(Vec<MultipartCase>),
16}
17
18impl<'de> Deserialize<'de> for BlockstateDefinition {
19    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
20    where
21        D: Deserializer<'de>,
22    {
23        #[derive(Deserialize)]
24        struct RawBlockstate {
25            variants: Option<HashMap<String, VariantValue>>,
26            multipart: Option<Vec<MultipartCase>>,
27        }
28
29        let raw = RawBlockstate::deserialize(deserializer)?;
30
31        if let Some(variants) = raw.variants {
32            let parsed: HashMap<String, Vec<ModelVariant>> = variants
33                .into_iter()
34                .map(|(k, v)| (k, v.into_vec()))
35                .collect();
36            Ok(BlockstateDefinition::Variants(parsed))
37        } else if let Some(multipart) = raw.multipart {
38            Ok(BlockstateDefinition::Multipart(multipart))
39        } else {
40            // Empty blockstate (shouldn't happen but handle gracefully)
41            Ok(BlockstateDefinition::Variants(HashMap::new()))
42        }
43    }
44}
45
46/// A variant value can be a single model or an array of weighted models.
47#[derive(Debug, Clone, Deserialize)]
48#[serde(untagged)]
49enum VariantValue {
50    Single(ModelVariant),
51    Multiple(Vec<ModelVariant>),
52}
53
54impl VariantValue {
55    fn into_vec(self) -> Vec<ModelVariant> {
56        match self {
57            VariantValue::Single(v) => vec![v],
58            VariantValue::Multiple(v) => v,
59        }
60    }
61}
62
63/// A model variant reference with optional rotation.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ModelVariant {
66    /// Model resource location (e.g., "block/stone" or "minecraft:block/stone").
67    pub model: String,
68    /// X rotation in degrees (0, 90, 180, 270).
69    #[serde(default)]
70    pub x: i32,
71    /// Y rotation in degrees (0, 90, 180, 270).
72    #[serde(default)]
73    pub y: i32,
74    /// If true, UV coordinates don't rotate with the block.
75    #[serde(default)]
76    pub uvlock: bool,
77    /// Weight for random selection (default 1).
78    #[serde(default = "default_weight")]
79    pub weight: u32,
80}
81
82fn default_weight() -> u32 {
83    1
84}
85
86impl ModelVariant {
87    /// Get the full resource location for the model.
88    pub fn model_location(&self) -> String {
89        if self.model.contains(':') {
90            self.model.clone()
91        } else {
92            format!("minecraft:{}", self.model)
93        }
94    }
95}
96
97/// A multipart case with optional condition.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct MultipartCase {
100    /// Condition for when this case applies.
101    #[serde(default)]
102    pub when: Option<MultipartCondition>,
103    /// Model(s) to apply when condition is met.
104    pub apply: ApplyValue,
105}
106
107/// The apply value can be a single model or array.
108#[derive(Debug, Clone, Deserialize, Serialize)]
109#[serde(untagged)]
110pub enum ApplyValue {
111    Single(ModelVariant),
112    Multiple(Vec<ModelVariant>),
113}
114
115impl ApplyValue {
116    pub fn variants(&self) -> Vec<&ModelVariant> {
117        match self {
118            ApplyValue::Single(v) => vec![v],
119            ApplyValue::Multiple(v) => v.iter().collect(),
120        }
121    }
122}
123
124/// Multipart condition for when a case applies.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(untagged)]
127pub enum MultipartCondition {
128    /// OR condition: any of the sub-conditions must match.
129    Or { OR: Vec<HashMap<String, String>> },
130    /// AND condition: all of the sub-conditions must match.
131    And { AND: Vec<HashMap<String, String>> },
132    /// Simple condition: all properties must match.
133    Simple(HashMap<String, String>),
134}
135
136impl MultipartCondition {
137    /// Check if the condition matches the given block properties.
138    pub fn matches(&self, properties: &HashMap<String, String>) -> bool {
139        match self {
140            MultipartCondition::Or { OR } => {
141                OR.iter().any(|cond| Self::matches_simple(cond, properties))
142            }
143            MultipartCondition::And { AND } => {
144                AND.iter().all(|cond| Self::matches_simple(cond, properties))
145            }
146            MultipartCondition::Simple(cond) => Self::matches_simple(cond, properties),
147        }
148    }
149
150    /// Check if a simple condition (property map) matches.
151    fn matches_simple(
152        condition: &HashMap<String, String>,
153        properties: &HashMap<String, String>,
154    ) -> bool {
155        condition.iter().all(|(key, expected_value)| {
156            // Handle pipe-separated values (e.g., "north|south")
157            if expected_value.contains('|') {
158                let allowed: Vec<&str> = expected_value.split('|').collect();
159                properties
160                    .get(key)
161                    .map(|v| allowed.contains(&v.as_str()))
162                    .unwrap_or_else(|| {
163                        // If property is missing, check if any allowed value is a default
164                        allowed.iter().any(|v| Self::is_default_value(v))
165                    })
166            } else {
167                properties
168                    .get(key)
169                    .map(|v| v == expected_value)
170                    .unwrap_or_else(|| {
171                        // If property is missing, check if expected value is a default
172                        Self::is_default_value(expected_value)
173                    })
174            }
175        })
176    }
177
178    /// Check if a value is a common default for missing properties.
179    /// Missing properties in Minecraft typically default to false/none/0.
180    fn is_default_value(value: &str) -> bool {
181        matches!(
182            value,
183            "false" | "none" | "0" | "normal" | "bottom" | "floor"
184        )
185    }
186}
187
188/// Build a property string from a properties map for variant lookup.
189/// Properties are sorted alphabetically and joined with commas.
190/// e.g., {"facing": "north", "half": "bottom"} -> "facing=north,half=bottom"
191pub fn build_property_string(properties: &HashMap<String, String>) -> String {
192    if properties.is_empty() {
193        return String::new();
194    }
195
196    let mut pairs: Vec<_> = properties.iter().collect();
197    pairs.sort_by_key(|(k, _)| *k);
198
199    pairs
200        .into_iter()
201        .map(|(k, v)| format!("{}={}", k, v))
202        .collect::<Vec<_>>()
203        .join(",")
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_parse_simple_variants() {
212        let json = r#"{
213            "variants": {
214                "": { "model": "block/stone" }
215            }
216        }"#;
217
218        let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
219        match def {
220            BlockstateDefinition::Variants(variants) => {
221                assert!(variants.contains_key(""));
222                assert_eq!(variants[""].len(), 1);
223                assert_eq!(variants[""][0].model, "block/stone");
224            }
225            _ => panic!("Expected Variants"),
226        }
227    }
228
229    #[test]
230    fn test_parse_variants_with_rotation() {
231        let json = r#"{
232            "variants": {
233                "facing=north": { "model": "block/furnace", "y": 0 },
234                "facing=east": { "model": "block/furnace", "y": 90 },
235                "facing=south": { "model": "block/furnace", "y": 180 },
236                "facing=west": { "model": "block/furnace", "y": 270 }
237            }
238        }"#;
239
240        let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
241        match def {
242            BlockstateDefinition::Variants(variants) => {
243                assert_eq!(variants.len(), 4);
244                assert_eq!(variants["facing=east"][0].y, 90);
245            }
246            _ => panic!("Expected Variants"),
247        }
248    }
249
250    #[test]
251    fn test_parse_weighted_variants() {
252        let json = r#"{
253            "variants": {
254                "": [
255                    { "model": "block/stone", "weight": 10 },
256                    { "model": "block/stone_mirrored", "weight": 5 }
257                ]
258            }
259        }"#;
260
261        let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
262        match def {
263            BlockstateDefinition::Variants(variants) => {
264                assert_eq!(variants[""].len(), 2);
265                assert_eq!(variants[""][0].weight, 10);
266                assert_eq!(variants[""][1].weight, 5);
267            }
268            _ => panic!("Expected Variants"),
269        }
270    }
271
272    #[test]
273    fn test_parse_multipart() {
274        let json = r#"{
275            "multipart": [
276                { "apply": { "model": "block/fence_post" } },
277                { "when": { "north": "true" }, "apply": { "model": "block/fence_side" } }
278            ]
279        }"#;
280
281        let def: BlockstateDefinition = serde_json::from_str(json).unwrap();
282        match def {
283            BlockstateDefinition::Multipart(cases) => {
284                assert_eq!(cases.len(), 2);
285                assert!(cases[0].when.is_none());
286                assert!(cases[1].when.is_some());
287            }
288            _ => panic!("Expected Multipart"),
289        }
290    }
291
292    #[test]
293    fn test_multipart_condition_simple() {
294        let cond = MultipartCondition::Simple(
295            [("facing".to_string(), "north".to_string())]
296                .into_iter()
297                .collect(),
298        );
299
300        let props: HashMap<String, String> =
301            [("facing".to_string(), "north".to_string())].into_iter().collect();
302        assert!(cond.matches(&props));
303
304        let wrong_props: HashMap<String, String> =
305            [("facing".to_string(), "south".to_string())].into_iter().collect();
306        assert!(!cond.matches(&wrong_props));
307    }
308
309    #[test]
310    fn test_multipart_condition_or() {
311        let json = r#"{ "OR": [{ "facing": "north" }, { "facing": "south" }] }"#;
312        let cond: MultipartCondition = serde_json::from_str(json).unwrap();
313
314        let north: HashMap<String, String> =
315            [("facing".to_string(), "north".to_string())].into_iter().collect();
316        let south: HashMap<String, String> =
317            [("facing".to_string(), "south".to_string())].into_iter().collect();
318        let east: HashMap<String, String> =
319            [("facing".to_string(), "east".to_string())].into_iter().collect();
320
321        assert!(cond.matches(&north));
322        assert!(cond.matches(&south));
323        assert!(!cond.matches(&east));
324    }
325
326    #[test]
327    fn test_multipart_condition_pipe_values() {
328        let cond = MultipartCondition::Simple(
329            [("facing".to_string(), "north|south".to_string())]
330                .into_iter()
331                .collect(),
332        );
333
334        let north: HashMap<String, String> =
335            [("facing".to_string(), "north".to_string())].into_iter().collect();
336        let south: HashMap<String, String> =
337            [("facing".to_string(), "south".to_string())].into_iter().collect();
338        let east: HashMap<String, String> =
339            [("facing".to_string(), "east".to_string())].into_iter().collect();
340
341        assert!(cond.matches(&north));
342        assert!(cond.matches(&south));
343        assert!(!cond.matches(&east));
344    }
345
346    #[test]
347    fn test_build_property_string() {
348        let props: HashMap<String, String> = [
349            ("facing".to_string(), "north".to_string()),
350            ("half".to_string(), "bottom".to_string()),
351        ]
352        .into_iter()
353        .collect();
354
355        assert_eq!(build_property_string(&props), "facing=north,half=bottom");
356    }
357
358    #[test]
359    fn test_build_property_string_empty() {
360        let props: HashMap<String, String> = HashMap::new();
361        assert_eq!(build_property_string(&props), "");
362    }
363}