schematic_mesher/resolver/
state_resolver.rs

1//! Block state to model variant resolution.
2
3use crate::error::{MesherError, Result};
4use crate::resource_pack::{
5    blockstate::build_property_string, BlockstateDefinition, ModelVariant, ResourcePack,
6};
7use crate::types::InputBlock;
8
9/// Resolves block states to model variants.
10pub struct StateResolver<'a> {
11    pack: &'a ResourcePack,
12}
13
14impl<'a> StateResolver<'a> {
15    pub fn new(pack: &'a ResourcePack) -> Self {
16        Self { pack }
17    }
18
19    /// Resolve a block to its model variants.
20    pub fn resolve(&self, block: &InputBlock) -> Result<Vec<ModelVariant>> {
21        // Get the blockstate definition
22        let blockstate = self.pack.get_blockstate(&block.name).ok_or_else(|| {
23            MesherError::BlockstateResolution(format!(
24                "No blockstate found for {}",
25                block.name
26            ))
27        })?;
28
29        match blockstate {
30            BlockstateDefinition::Variants(variants) => {
31                self.resolve_variants(variants, block)
32            }
33            BlockstateDefinition::Multipart(cases) => {
34                self.resolve_multipart(cases, block)
35            }
36        }
37    }
38
39    /// Resolve using the variants format.
40    fn resolve_variants(
41        &self,
42        variants: &std::collections::HashMap<String, Vec<ModelVariant>>,
43        block: &InputBlock,
44    ) -> Result<Vec<ModelVariant>> {
45        // Build the property string to look up
46        let prop_string = build_property_string(&block.properties);
47
48        // Try exact match first
49        if let Some(variant_list) = variants.get(&prop_string) {
50            return Ok(vec![variant_list[0].clone()]);
51        }
52
53        // Try empty string (default variant)
54        if let Some(variant_list) = variants.get("") {
55            return Ok(vec![variant_list[0].clone()]);
56        }
57
58        // Find all variants that match the user's specified properties
59        // (user properties are a subset of variant properties)
60        let matching_variants: Vec<_> = variants
61            .iter()
62            .filter(|(key, _)| self.user_properties_match_variant(key, &block.properties))
63            .collect();
64
65        if !matching_variants.is_empty() {
66            // Among matching variants, pick the one with the best default score
67            // for the properties NOT specified by the user
68            let best = matching_variants
69                .into_iter()
70                .max_by_key(|(key, _)| self.calculate_default_score_for_unspecified(key, &block.properties))
71                .unwrap();
72            return Ok(vec![best.1[0].clone()]);
73        }
74
75        // Last resort: find the most "default-like" variant overall
76        if let Some((_, variant_list)) = self.find_default_variant(variants) {
77            return Ok(vec![variant_list[0].clone()]);
78        }
79
80        Err(MesherError::BlockstateResolution(format!(
81            "No matching variant for {} with properties {:?}",
82            block.name, block.properties
83        )))
84    }
85
86    /// Find the most "default-like" variant when no properties are specified.
87    /// Prefers variants with values like 0, false, none, north, bottom, etc.
88    fn find_default_variant<'b>(
89        &self,
90        variants: &'b std::collections::HashMap<String, Vec<ModelVariant>>,
91    ) -> Option<(&'b String, &'b Vec<ModelVariant>)> {
92        let mut best_key: Option<&String> = None;
93        let mut best_score = i32::MIN;
94
95        for key in variants.keys() {
96            let score = self.calculate_default_score(key);
97            if score > best_score {
98                best_score = score;
99                best_key = Some(key);
100            }
101        }
102
103        best_key.and_then(|k| variants.get_key_value(k))
104    }
105
106    /// Calculate a "default-ness" score for a variant key.
107    /// Higher scores indicate more default-like values.
108    fn calculate_default_score(&self, key: &str) -> i32 {
109        if key.is_empty() {
110            return i32::MAX; // Empty key is the most default
111        }
112
113        let mut score = 0;
114
115        for pair in key.split(',') {
116            if let Some((prop, value)) = pair.split_once('=') {
117                score += self.value_default_score(prop, value);
118            }
119        }
120
121        score
122    }
123
124    /// Score how "default-like" a property value is.
125    fn value_default_score(&self, property: &str, value: &str) -> i32 {
126        // Numeric properties: lower is more default (power=0 > power=15)
127        if let Ok(num) = value.parse::<i32>() {
128            return -num * 10; // Prefer 0 over higher numbers
129        }
130
131        // Property-specific defaults
132        match property {
133            "axis" => match value {
134                "y" => return 50,  // Y is default for logs, pillars
135                _ => return 0,
136            },
137            "waterlogged" | "powered" | "open" | "lit" | "enabled" |
138            "triggered" | "inverted" | "extended" | "locked" | "attached" |
139            "disarmed" | "occupied" | "has_record" | "has_book" | "signal_fire" |
140            "hanging" | "persistent" | "unstable" | "bottom" | "drag" |
141            "eye" | "in_wall" | "snowy" | "up" | "conditional" => {
142                match value {
143                    "false" => return 100,
144                    "true" => return -100,
145                    _ => return 0,
146                }
147            }
148            "half" => match value {
149                "bottom" | "lower" => return 50,
150                "top" | "upper" => return -50,
151                _ => return 0,
152            },
153            "type" => match value {
154                "single" | "normal" | "bottom" => return 50,
155                "double" | "top" => return -50,
156                _ => return 0,
157            },
158            "facing" => match value {
159                "north" => return 50,
160                "south" => return 40,
161                "east" => return 30,
162                "west" => return 20,
163                "up" => return 10,
164                "down" => return 0,
165                _ => return 0,
166            },
167            "shape" => match value {
168                "straight" => return 50,
169                "ascending_north" | "ascending_south" | "ascending_east" | "ascending_west" => return 0,
170                _ => return -20,
171            },
172            // Connection properties (fences, walls, redstone)
173            "north" | "south" | "east" | "west" => match value {
174                "none" | "false" => return 50,
175                "low" | "side" => return 0,
176                "tall" | "up" => return -20,
177                "true" => return -50,
178                _ => return 0,
179            },
180            _ => {}
181        }
182
183        // Generic value defaults
184        match value {
185            "false" | "off" | "none" | "0" => 100,
186            "true" | "on" => -100,
187            _ => 0,
188        }
189    }
190
191    /// Resolve using the multipart format.
192    fn resolve_multipart(
193        &self,
194        cases: &[crate::resource_pack::MultipartCase],
195        block: &InputBlock,
196    ) -> Result<Vec<ModelVariant>> {
197        let mut result = Vec::new();
198
199        for case in cases {
200            // Check if this case applies
201            let applies = match &case.when {
202                Some(condition) => condition.matches(&block.properties),
203                None => true, // No condition = always applies
204            };
205
206            if applies {
207                // Add all variants from this case
208                for variant in case.apply.variants() {
209                    result.push(variant.clone());
210                }
211            }
212        }
213
214        if result.is_empty() {
215            Err(MesherError::BlockstateResolution(format!(
216                "No multipart cases matched for {} with properties {:?}",
217                block.name, block.properties
218            )))
219        } else {
220            Ok(result)
221        }
222    }
223
224    /// Check if all user-specified properties match those in the variant key.
225    /// The variant key may have additional properties not specified by the user.
226    fn user_properties_match_variant(
227        &self,
228        variant_key: &str,
229        user_properties: &std::collections::HashMap<String, String>,
230    ) -> bool {
231        // If user specified no properties, any variant matches
232        if user_properties.is_empty() {
233            return true;
234        }
235
236        // Parse variant key into a map
237        let mut variant_props = std::collections::HashMap::new();
238        for pair in variant_key.split(',') {
239            if let Some((k, v)) = pair.split_once('=') {
240                variant_props.insert(k, v);
241            }
242        }
243
244        // Check that all user-specified properties match
245        for (user_key, user_value) in user_properties {
246            match variant_props.get(user_key.as_str()) {
247                Some(variant_value) => {
248                    if *variant_value != user_value {
249                        return false; // Value mismatch
250                    }
251                }
252                None => {
253                    return false; // Variant doesn't have this property at all
254                }
255            }
256        }
257
258        true
259    }
260
261    /// Calculate default score only for properties NOT specified by the user.
262    fn calculate_default_score_for_unspecified(
263        &self,
264        variant_key: &str,
265        user_properties: &std::collections::HashMap<String, String>,
266    ) -> i32 {
267        if variant_key.is_empty() {
268            return i32::MAX;
269        }
270
271        let mut score = 0;
272
273        for pair in variant_key.split(',') {
274            if let Some((prop, value)) = pair.split_once('=') {
275                // Only score properties not specified by the user
276                if !user_properties.contains_key(prop) {
277                    score += self.value_default_score(prop, value);
278                }
279            }
280        }
281
282        score
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::resource_pack::blockstate::BlockstateDefinition;
290
291    fn create_test_pack() -> ResourcePack {
292        let mut pack = ResourcePack::new();
293
294        // Add a simple stone blockstate
295        let stone_json = r#"{
296            "variants": {
297                "": { "model": "block/stone" }
298            }
299        }"#;
300        let stone_def: BlockstateDefinition = serde_json::from_str(stone_json).unwrap();
301        pack.add_blockstate("minecraft", "stone", stone_def);
302
303        // Add a directional block
304        let furnace_json = r#"{
305            "variants": {
306                "facing=north": { "model": "block/furnace", "y": 0 },
307                "facing=east": { "model": "block/furnace", "y": 90 },
308                "facing=south": { "model": "block/furnace", "y": 180 },
309                "facing=west": { "model": "block/furnace", "y": 270 }
310            }
311        }"#;
312        let furnace_def: BlockstateDefinition = serde_json::from_str(furnace_json).unwrap();
313        pack.add_blockstate("minecraft", "furnace", furnace_def);
314
315        pack
316    }
317
318    #[test]
319    fn test_resolve_simple_block() {
320        let pack = create_test_pack();
321        let resolver = StateResolver::new(&pack);
322
323        let block = InputBlock::new("minecraft:stone");
324        let variants = resolver.resolve(&block).unwrap();
325
326        assert_eq!(variants.len(), 1);
327        assert_eq!(variants[0].model, "block/stone");
328    }
329
330    #[test]
331    fn test_resolve_directional_block() {
332        let pack = create_test_pack();
333        let resolver = StateResolver::new(&pack);
334
335        let block = InputBlock::new("minecraft:furnace")
336            .with_property("facing", "east");
337        let variants = resolver.resolve(&block).unwrap();
338
339        assert_eq!(variants.len(), 1);
340        assert_eq!(variants[0].model, "block/furnace");
341        assert_eq!(variants[0].y, 90);
342    }
343
344    #[test]
345    fn test_missing_blockstate() {
346        let pack = create_test_pack();
347        let resolver = StateResolver::new(&pack);
348
349        let block = InputBlock::new("minecraft:nonexistent");
350        let result = resolver.resolve(&block);
351
352        assert!(result.is_err());
353    }
354
355    #[test]
356    fn test_partial_properties() {
357        let mut pack = ResourcePack::new();
358
359        // Add a piston blockstate with multiple properties
360        let piston_json = r#"{
361            "variants": {
362                "extended=false,facing=down": { "model": "block/piston", "x": 180 },
363                "extended=false,facing=east": { "model": "block/piston", "y": 90 },
364                "extended=false,facing=north": { "model": "block/piston" },
365                "extended=false,facing=south": { "model": "block/piston", "y": 180 },
366                "extended=false,facing=up": { "model": "block/piston", "x": 270 },
367                "extended=false,facing=west": { "model": "block/piston", "y": 270 },
368                "extended=true,facing=down": { "model": "block/piston_extended", "x": 180 },
369                "extended=true,facing=east": { "model": "block/piston_extended", "y": 90 },
370                "extended=true,facing=north": { "model": "block/piston_extended" },
371                "extended=true,facing=south": { "model": "block/piston_extended", "y": 180 },
372                "extended=true,facing=up": { "model": "block/piston_extended", "x": 270 },
373                "extended=true,facing=west": { "model": "block/piston_extended", "y": 270 }
374            }
375        }"#;
376        let piston_def: BlockstateDefinition = serde_json::from_str(piston_json).unwrap();
377        pack.add_blockstate("minecraft", "piston", piston_def);
378
379        let resolver = StateResolver::new(&pack);
380
381        // Test with only facing specified - should pick extended=false (more default)
382        let block = InputBlock::new("minecraft:piston")
383            .with_property("facing", "north");
384        let variants = resolver.resolve(&block).unwrap();
385
386        assert_eq!(variants.len(), 1);
387        assert_eq!(variants[0].model, "block/piston"); // extended=false version
388
389        // Test with only extended specified - should pick facing=north (more default)
390        let block = InputBlock::new("minecraft:piston")
391            .with_property("extended", "true");
392        let variants = resolver.resolve(&block).unwrap();
393
394        assert_eq!(variants.len(), 1);
395        assert_eq!(variants[0].model, "block/piston_extended");
396
397        // Test with no properties - should get extended=false, facing=north
398        let block = InputBlock::new("minecraft:piston");
399        let variants = resolver.resolve(&block).unwrap();
400
401        assert_eq!(variants.len(), 1);
402        assert_eq!(variants[0].model, "block/piston");
403    }
404}