Skip to main content

delvewright_dsl/
support.rs

1//! **What a block needs beside it to stay** — the pinned game's
2//! `BlockState.canSurvive`, as a table.
3//!
4//! A wall torch on a glass pane, a lantern hung under air, a flower on stone, a
5//! rail on a fence: each is a block a structure or a `setblock` writes and the
6//! server drops the first time a shape update reaches it, because its
7//! `canSurvive` rule asks a neighbour for something the neighbour does not give.
8//! This module answers that rule for every blockstate of the pin, read from two
9//! tables measured inside the pinned server jar by
10//! `tools/maintenance/dump-support.py` (provenance in
11//! `crates/delvec/data/PROVENANCE.md`):
12//!
13//! * `crates/dsl/data/support-1.21.11.tsv` — per state, `free` (it survives in
14//!   air), `support` (the neighbours any one of which keeps it, each a base set
15//!   the game names plus its exact differences), or `unjudged` (the
16//!   single-neighbour reading cannot be its whole rule, with the reason);
17//! * `crates/dsl/data/support-bases-1.21.11.tsv` — per state, the base sets'
18//!   own questions: CENTER- and RIGID-sturdy faces, air, solid, still water.
19//!   The FULL-sturdy faces are [`crate::blockshape::face_is_sturdy`]'s table.
20//!
21//! Nothing here is classified by hand: a block is "attached" because the jar
22//! says it does not survive in air, and what holds it is the set of neighbours
23//! the jar said yes to. The compiler's `DW1002` judges every block of the world
24//! a build writes with [`Needs::kept`].
25
26use crate::blockshape::{
27    Face, PinnedRows, face_is_sturdy, namespaced_id, pinned_row, pinned_rows, state_has,
28};
29
30/// The support table: `crates/dsl/data/support-1.21.11.tsv`.
31const SUPPORT_TSV: &str = include_str!("../data/support-1.21.11.tsv");
32/// The base table: `crates/dsl/data/support-bases-1.21.11.tsv`.
33const BASES_TSV: &str = include_str!("../data/support-bases-1.21.11.tsv");
34
35/// The faces in the order the tables spell them (`d u n s w e`).
36const FACES: [Face; 6] = [
37    Face::Down,
38    Face::Up,
39    Face::North,
40    Face::South,
41    Face::West,
42    Face::East,
43];
44
45/// A base set of neighbours, as the game names it.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Base {
48    /// No neighbour: only the rule's added states hold.
49    None,
50    /// The neighbour's face toward the block is FULL-sturdy
51    /// (`isFaceSturdy(…, SupportType.FULL)`): its support shape covers the
52    /// whole square.
53    Full,
54    /// CENTER-sturdy: the support shape covers the face's centre.
55    Center,
56    /// RIGID-sturdy: the support shape covers the face but for a rim.
57    Rigid,
58    /// Any block that is not air (`!isAir()`).
59    NonAir,
60    /// A solid block (`isSolid()`).
61    Solid,
62    /// A block whose fluid is still water.
63    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    /// What the base asks of a neighbour, for a message: `face` is the
81    /// neighbour's face toward the block.
82    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    /// Whether `neighbour`, across its `face` toward the block, is in this base.
95    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/// One neighbour that keeps a block: the neighbour across `cell` is in `base`,
110/// or is one of `added`, and is not one of `removed`.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Rule {
113    /// The direction from the block to the neighbour this rule reads.
114    pub cell: Face,
115    /// The base set.
116    pub base: Base,
117    /// States the jar keeps the block beside beyond the base, as table rows
118    /// (`minecraft:block` or `minecraft:block[k=v,…]`).
119    pub added: Vec<String>,
120    /// States of the base the jar does not keep it beside.
121    pub removed: Vec<String>,
122}
123
124impl Rule {
125    /// Whether `neighbour`, the block across [`Rule::cell`], keeps the block.
126    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    /// What the rule asks of its neighbour, as a verb phrase for a message
135    /// ("give a full (sturdy) south face", "be one of …"): the base, then the
136    /// states it adds — listed whole up to a dozen, counted beyond.
137    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/// What one block state needs beside it.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum Needs {
158    /// It survives in air: nothing.
159    Free,
160    /// Any one of these neighbours keeps it.
161    Support(Vec<Rule>),
162    /// The single-neighbour reading cannot be its whole rule; the reason, as
163    /// the table states it. Nothing is judged.
164    Unjudged(String),
165}
166
167impl Needs {
168    /// Whether the block survives with `neighbour(cell)` beside it, by the
169    /// measured rule: `Some(true)` for a free block or one a rule holds,
170    /// `Some(false)` when no rule holds, `None` when the rule is unjudged.
171    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
180/// **What a block state needs beside it to survive**, by the pinned jar, or
181/// `None` for a block the table does not hold (a non-`minecraft:` id, or one
182/// the pin lacks).
183pub fn needs(name: &str) -> Option<&'static Needs> {
184    pinned_row(support_rows(), name)
185}
186
187/// Whether a block state is one a table row names: the row's block, carrying
188/// every property the row lists ([`crate::blockshape`]'s one comparison).
189fn 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
208/// `<cell>:<base>[+<rows>][-<rows>]`, rows `|`-separated, each beginning
209/// `minecraft:` (which is how the added and removed lists are told apart:
210/// no vanilla id or property value holds `-minecraft:`).
211fn 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
253/// The base table, one [`PinnedRows`] per column, and the faces each face
254/// column carries.
255struct 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    /// The neighbourhood of a block: `beside` at the one cell named, air at
349    /// every other.
350    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    /// The finding: a wall torch hung on a glass pane is dropped (a pane's side
361    /// is not a sturdy face), and kept on stone. `wall_torch[facing=north]`
362    /// hangs on the block SOUTH of it.
363    #[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        // Only the block behind it holds it.
369        assert_eq!(kept(t, Face::North, "minecraft:stone"), Some(false));
370        assert_eq!(kept(t, Face::Down, "minecraft:stone"), Some(false));
371    }
372
373    /// A hanging lantern hangs on the block above it, and air holds nothing.
374    #[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        // A chain's bottom is sturdy at its centre: a lantern hangs from one.
380        assert_eq!(kept(l, Face::Up, "minecraft:iron_chain"), Some(true));
381        // Standing, it needs the block under it instead.
382        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    /// A torch stands on a face sturdy at its centre (`canSupportCenter`): the
388    /// top of a top slab is one, the top of a bottom slab is not (it is half a
389    /// block below the cell's floor), and a glass pane's post is.
390    #[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        // A bare slab is its default: the bottom half.
406        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    /// A flower roots in the blocks the jar's `#dirt`-and-friends rule names,
411    /// and nowhere else; a carpet sits on anything but air.
412    #[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    /// The jar's rule, not a guess: a flower pot and a campfire need nothing.
433    #[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    /// The sculk blocks spec-0100 places stand on nothing (sensor, calibrated
442    /// sensor, shrieker, catalyst, sculk), and a vein on a floor is held only by
443    /// a full face under it: on stone or on sculk it stays, and a vein stacked
444    /// on another vein is dropped, because a vein's top is not a full face.
445    #[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    /// A door's upper half stands on its own lower half.
464    #[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    /// Every rule parses, every face a centre or rigid rule asks is carried, and
475    /// every listed state is one the registry holds.
476    #[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                        // Asking the base of air panics if the face is not carried.
486                        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}