1use crate::blockshape::{
27 Face, PinnedRows, face_is_sturdy, namespaced_id, pinned_row, pinned_rows, state_has,
28};
29
30const SUPPORT_TSV: &str = include_str!("../data/support-1.21.11.tsv");
32const BASES_TSV: &str = include_str!("../data/support-bases-1.21.11.tsv");
34
35const FACES: [Face; 6] = [
37 Face::Down,
38 Face::Up,
39 Face::North,
40 Face::South,
41 Face::West,
42 Face::East,
43];
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Base {
48 None,
50 Full,
54 Center,
56 Rigid,
58 NonAir,
60 Solid,
62 Water,
64}
65
66impl Base {
67 fn parse(s: &str) -> Base {
68 match s {
69 "none" => Base::None,
70 "full" => Base::Full,
71 "center" => Base::Center,
72 "rigid" => Base::Rigid,
73 "nonair" => Base::NonAir,
74 "solid" => Base::Solid,
75 "water" => Base::Water,
76 other => panic!("support table: unknown base {other:?}"),
77 }
78 }
79
80 pub fn describe(self, face: Face) -> String {
83 match self {
84 Base::None => "nothing in general".to_string(),
85 Base::Full => format!("a full (sturdy) {} face", face.name()),
86 Base::Center => format!("a {} face sturdy at its centre", face.name()),
87 Base::Rigid => format!("a rigid {} face", face.name()),
88 Base::NonAir => "any block but air".to_string(),
89 Base::Solid => "a solid block".to_string(),
90 Base::Water => "still water".to_string(),
91 }
92 }
93
94 fn holds(self, neighbour: &str, face: Face) -> bool {
96 let b = bases();
97 match self {
98 Base::None => false,
99 Base::Full => face_is_sturdy(neighbour, face),
100 Base::Center => b.face(&b.center, neighbour, face, "center"),
101 Base::Rigid => b.face(&b.rigid, neighbour, face, "rigid"),
102 Base::NonAir => !b.flag(neighbour, 'a'),
103 Base::Solid => b.flag(neighbour, 's'),
104 Base::Water => b.flag(neighbour, 'w'),
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Rule {
113 pub cell: Face,
115 pub base: Base,
117 pub added: Vec<String>,
120 pub removed: Vec<String>,
122}
123
124impl Rule {
125 pub fn holds(&self, neighbour: &str) -> bool {
127 let face = self.cell.opposite();
128 if self.removed.iter().any(|p| pattern_has(p, neighbour)) {
129 return false;
130 }
131 self.base.holds(neighbour, face) || self.added.iter().any(|p| pattern_has(p, neighbour))
132 }
133
134 pub fn describe(&self) -> String {
138 let face = self.cell.opposite();
139 let added = match self.added.len() {
140 0 => String::new(),
141 n if n <= 12 => format!("be one of {}", self.added.join(", ")),
142 n => format!(
143 "be one of {n} listed states ({}, …)",
144 self.added[..3].join(", ")
145 ),
146 };
147 match (self.base, added.is_empty()) {
148 (Base::None, false) => added,
149 (base, true) => format!("give {}", base.describe(face)),
150 (base, false) => format!("give {}, or {added}", base.describe(face)),
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum Needs {
158 Free,
160 Support(Vec<Rule>),
162 Unjudged(String),
165}
166
167impl Needs {
168 pub fn kept<'a>(&self, neighbour: impl Fn(Face) -> &'a str) -> Option<bool> {
172 match self {
173 Needs::Free => Some(true),
174 Needs::Support(rules) => Some(rules.iter().any(|r| r.holds(neighbour(r.cell)))),
175 Needs::Unjudged(_) => None,
176 }
177 }
178}
179
180pub fn needs(name: &str) -> Option<&'static Needs> {
184 pinned_row(support_rows(), name)
185}
186
187fn pattern_has(pattern: &str, name: &str) -> bool {
190 let (id, props) = split_pattern(pattern);
191 namespaced_id(name) == id && state_has(name, &props)
192}
193
194fn split_pattern(pattern: &str) -> (&str, Vec<(String, String)>) {
195 match pattern.find('[') {
196 Some(open) => (
197 &pattern[..open],
198 pattern[open + 1..pattern.len() - 1]
199 .split(',')
200 .filter_map(|kv| kv.split_once('='))
201 .map(|(k, v)| (k.to_string(), v.to_string()))
202 .collect(),
203 ),
204 None => (pattern, Vec::new()),
205 }
206}
207
208fn parse_rule(text: &str) -> Rule {
212 let (cell, rest) = text
213 .split_once(':')
214 .unwrap_or_else(|| panic!("support rule has no cell: {text:?}"));
215 let cell = FACES
216 .into_iter()
217 .find(|f| f.name().starts_with(cell) && cell.len() == 1)
218 .unwrap_or_else(|| panic!("support rule names no face: {text:?}"));
219 let base_end = rest.find(['+', '-']).unwrap_or(rest.len());
220 let base = Base::parse(&rest[..base_end]);
221 let tail = &rest[base_end..];
222 let (plus, minus) = match tail.find("-minecraft:") {
223 Some(i) => (&tail[..i], &tail[i + 1..]),
224 None => (tail, ""),
225 };
226 let list = |s: &str| -> Vec<String> {
227 s.trim_start_matches('+')
228 .split('|')
229 .filter(|r| !r.is_empty())
230 .map(str::to_string)
231 .collect()
232 };
233 Rule {
234 cell,
235 base,
236 added: list(plus),
237 removed: list(minus),
238 }
239}
240
241fn support_rows() -> &'static PinnedRows<Needs> {
242 static ROWS: std::sync::OnceLock<PinnedRows<Needs>> = std::sync::OnceLock::new();
243 ROWS.get_or_init(|| {
244 pinned_rows(SUPPORT_TSV, "support", |kind, rule| match kind {
245 "free" => Needs::Free,
246 "support" => Needs::Support(rule.split(' ').map(parse_rule).collect()),
247 "unjudged" => Needs::Unjudged(rule.to_string()),
248 other => panic!("support table: unknown kind {other:?}"),
249 })
250 })
251}
252
253struct Bases {
256 center: PinnedRows<u8>,
257 rigid: PinnedRows<u8>,
258 flags: PinnedRows<String>,
259 center_faces: u8,
260 rigid_faces: u8,
261}
262
263impl Bases {
264 fn face(&self, rows: &PinnedRows<u8>, name: &str, face: Face, what: &str) -> bool {
265 let carried = if what == "center" {
266 self.center_faces
267 } else {
268 self.rigid_faces
269 };
270 assert!(
271 carried & bit(face) != 0,
272 "support-bases table carries no {what} {} face — a rule asked one the \
273 extractor did not see asked",
274 face.name()
275 );
276 pinned_row(rows, name).is_some_and(|m| m & bit(face) != 0)
277 }
278
279 fn flag(&self, name: &str, flag: char) -> bool {
280 pinned_row(&self.flags, name).is_some_and(|f| f.contains(flag))
281 }
282}
283
284fn bit(face: Face) -> u8 {
285 1 << FACES.iter().position(|f| *f == face).expect("a face")
286}
287
288fn mask(letters: &str) -> u8 {
289 FACES
290 .iter()
291 .filter(|f| letters.contains(f.name().chars().next().expect("named")))
292 .fold(0, |m, f| m | bit(*f))
293}
294
295fn bases() -> &'static Bases {
296 static B: std::sync::OnceLock<Bases> = std::sync::OnceLock::new();
297 B.get_or_init(|| {
298 let all = pinned_rows(BASES_TSV, "support-bases", |col, v| {
299 (col.to_string(), v.to_string())
300 });
301 let column = |which: &str| -> PinnedRows<String> {
302 all.iter()
303 .map(|(id, rows)| {
304 (
305 id.clone(),
306 rows.iter()
307 .filter(|(_, (c, _))| c == which)
308 .map(|(p, (_, v))| (p.clone(), v.clone()))
309 .collect(),
310 )
311 })
312 .collect()
313 };
314 let faces = |which: &str| -> PinnedRows<u8> {
315 column(which)
316 .into_iter()
317 .map(|(id, rows)| {
318 let rows = rows
319 .into_iter()
320 .map(|(p, v)| (p, if v == "-" { 0 } else { mask(&v) }))
321 .collect();
322 (id, rows)
323 })
324 .collect()
325 };
326 let carried = |what: &str| -> u8 {
327 let key = format!("# {what} faces carried: ");
328 BASES_TSV
329 .lines()
330 .find_map(|l| l.strip_prefix(key.as_str()))
331 .map(mask)
332 .unwrap_or_else(|| panic!("support-bases table states no carried {what} faces"))
333 };
334 Bases {
335 center: faces("center"),
336 rigid: faces("rigid"),
337 flags: column("flags"),
338 center_faces: carried("center"),
339 rigid_faces: carried("rigid"),
340 }
341 })
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 fn alone<'a>(cell: Face, beside: &'a str) -> impl Fn(Face) -> &'a str {
351 move |f| if f == cell { beside } else { "minecraft:air" }
352 }
353
354 fn kept(block: &str, cell: Face, beside: &str) -> Option<bool> {
355 needs(block)
356 .unwrap_or_else(|| panic!("{block} is in the table"))
357 .kept(alone(cell, beside))
358 }
359
360 #[test]
364 fn a_wall_torch_needs_a_sturdy_face_behind_it() {
365 let t = "minecraft:wall_torch[facing=north]";
366 assert_eq!(kept(t, Face::South, "minecraft:glass_pane"), Some(false));
367 assert_eq!(kept(t, Face::South, "minecraft:stone"), Some(true));
368 assert_eq!(kept(t, Face::North, "minecraft:stone"), Some(false));
370 assert_eq!(kept(t, Face::Down, "minecraft:stone"), Some(false));
371 }
372
373 #[test]
375 fn a_hanging_lantern_needs_the_block_above() {
376 let l = "minecraft:lantern[hanging=true]";
377 assert_eq!(kept(l, Face::Up, "minecraft:air"), Some(false));
378 assert_eq!(kept(l, Face::Up, "minecraft:oak_planks"), Some(true));
379 assert_eq!(kept(l, Face::Up, "minecraft:iron_chain"), Some(true));
381 let s = "minecraft:lantern[hanging=false]";
383 assert_eq!(kept(s, Face::Up, "minecraft:oak_planks"), Some(false));
384 assert_eq!(kept(s, Face::Down, "minecraft:oak_planks"), Some(true));
385 }
386
387 #[test]
391 fn a_torch_on_a_slab_is_kept_as_the_jar_keeps_it() {
392 let t = "minecraft:torch";
393 assert_eq!(
394 kept(t, Face::Down, "minecraft:stone_slab[type=top]"),
395 Some(true)
396 );
397 assert_eq!(
398 kept(t, Face::Down, "minecraft:stone_slab[type=double]"),
399 Some(true)
400 );
401 assert_eq!(
402 kept(t, Face::Down, "minecraft:stone_slab[type=bottom]"),
403 Some(false)
404 );
405 assert_eq!(kept(t, Face::Down, "minecraft:stone_slab"), Some(false));
407 assert_eq!(kept(t, Face::Down, "minecraft:glass_pane"), Some(true));
408 }
409
410 #[test]
413 fn soil_and_carpet() {
414 assert_eq!(
415 kept("minecraft:poppy", Face::Down, "minecraft:grass_block"),
416 Some(true)
417 );
418 assert_eq!(
419 kept("minecraft:poppy", Face::Down, "minecraft:stone"),
420 Some(false)
421 );
422 assert_eq!(
423 kept("minecraft:white_carpet", Face::Down, "minecraft:glass_pane"),
424 Some(true)
425 );
426 assert_eq!(
427 kept("minecraft:white_carpet", Face::Down, "minecraft:air"),
428 Some(false)
429 );
430 }
431
432 #[test]
434 fn a_flower_pot_and_a_campfire_stand_on_nothing() {
435 assert_eq!(needs("minecraft:flower_pot"), Some(&Needs::Free));
436 assert_eq!(needs("minecraft:campfire"), Some(&Needs::Free));
437 assert_eq!(needs("minecraft:stone"), Some(&Needs::Free));
438 assert!(needs("example:not_a_block").is_none());
439 }
440
441 #[test]
446 fn sculk_stands_free_and_a_vein_stack_falls() {
447 for free in [
448 "minecraft:sculk_sensor",
449 "minecraft:calibrated_sculk_sensor",
450 "minecraft:sculk_shrieker",
451 "minecraft:sculk_catalyst",
452 "minecraft:sculk",
453 ] {
454 assert_eq!(needs(free), Some(&Needs::Free), "{free}");
455 }
456 let vein = "minecraft:sculk_vein[down=true,east=false,north=false,south=false,up=false,west=false]";
457 assert_eq!(kept(vein, Face::Down, "minecraft:stone"), Some(true));
458 assert_eq!(kept(vein, Face::Down, "minecraft:sculk"), Some(true));
459 assert_eq!(kept(vein, Face::Down, vein), Some(false));
460 assert_eq!(kept(vein, Face::Down, "minecraft:air"), Some(false));
461 }
462
463 #[test]
465 fn a_door_half_stands_on_its_other_half() {
466 let up = "minecraft:oak_door[half=upper]";
467 assert_eq!(
468 kept(up, Face::Down, "minecraft:oak_door[half=lower]"),
469 Some(true)
470 );
471 assert_eq!(kept(up, Face::Down, "minecraft:stone"), Some(false));
472 }
473
474 #[test]
477 fn every_rule_parses_and_asks_a_carried_face() {
478 let mut rules = 0usize;
479 for rows in support_rows().values() {
480 for (_, n) in rows {
481 if let Needs::Support(rs) = n {
482 for r in rs {
483 rules += 1;
484 let face = r.cell.opposite();
485 r.base.holds("minecraft:air", face);
487 for p in r.added.iter().chain(&r.removed) {
488 assert!(
489 crate::blocks::BlockRegistry::v1_21_11()
490 .validate_state_string(p)
491 .is_ok(),
492 "{p} is not a state the registry holds"
493 );
494 }
495 }
496 }
497 }
498 }
499 assert!(rules > 600, "only {rules} support rules parsed");
500 }
501}