schematic_mesher/resolver/
state_resolver.rs1use crate::error::{MesherError, Result};
4use crate::resource_pack::{
5 blockstate::build_property_string, BlockstateDefinition, ModelVariant, ResourcePack,
6};
7use crate::types::InputBlock;
8
9pub 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 pub fn resolve(&self, block: &InputBlock) -> Result<Vec<ModelVariant>> {
21 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 fn resolve_variants(
41 &self,
42 variants: &std::collections::HashMap<String, Vec<ModelVariant>>,
43 block: &InputBlock,
44 ) -> Result<Vec<ModelVariant>> {
45 let prop_string = build_property_string(&block.properties);
47
48 if let Some(variant_list) = variants.get(&prop_string) {
50 return Ok(vec![variant_list[0].clone()]);
51 }
52
53 if let Some(variant_list) = variants.get("") {
55 return Ok(vec![variant_list[0].clone()]);
56 }
57
58 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 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 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 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 fn calculate_default_score(&self, key: &str) -> i32 {
109 if key.is_empty() {
110 return i32::MAX; }
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 fn value_default_score(&self, property: &str, value: &str) -> i32 {
126 if let Ok(num) = value.parse::<i32>() {
128 return -num * 10; }
130
131 match property {
133 "axis" => match value {
134 "y" => return 50, _ => 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 "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 match value {
185 "false" | "off" | "none" | "0" => 100,
186 "true" | "on" => -100,
187 _ => 0,
188 }
189 }
190
191 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 let applies = match &case.when {
202 Some(condition) => condition.matches(&block.properties),
203 None => true, };
205
206 if applies {
207 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 fn user_properties_match_variant(
227 &self,
228 variant_key: &str,
229 user_properties: &std::collections::HashMap<String, String>,
230 ) -> bool {
231 if user_properties.is_empty() {
233 return true;
234 }
235
236 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 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; }
251 }
252 None => {
253 return false; }
255 }
256 }
257
258 true
259 }
260
261 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 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 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 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 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 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"); 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 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}