Skip to main content

yog_formats/content/
kind.rs

1//! The game's kinds of block with a class of their own (`kind: "stairs"`).
2
3use yog_data::{Ordered, Reader, Schema, Shape, Value, ValueRef};
4
5/// A kind of block the game has a class for — `kind: "stairs"` on a
6/// `.yblock`: the texture slots its models take, the game's template models
7/// it's drawn with, which state shows which, its item's model and its drops.
8/// The game's kinds are data (`data/block_kinds.ydata`); the hosts make each
9/// of the game's class by name.
10#[derive(Schema, Debug, Clone)]
11pub struct BlockKind {
12    pub name: String,
13    /// The texture slots of its models, given by the `.yblock`'s `model.textures`.
14    pub slots: Vec<String>,
15    /// Its models: a name → the game's template model. `""` is the block's
16    /// own model (`block/<id>`), another name `block/<id>_<name>`.
17    pub models: Ordered<String>,
18    /// The model its item shows (a name of `models`); none: the `.yblock`
19    /// gives `item_model`.
20    pub item: Option<String>,
21    /// The `block` fields it takes of `base`, `set`, `press_ticks`, `triggered_by`.
22    #[data(default)]
23    pub takes: Vec<String>,
24    /// Its default for `block.occludes`.
25    pub occludes: Option<bool>,
26    /// Its default for `block.collision`.
27    pub collision: Option<bool>,
28    /// It drops only in states with these properties (`half=lower`: a door
29    /// drops once, from its bottom half).
30    pub drops_when: Option<String>,
31    /// It drops two in states with these properties (`type=double`).
32    pub drops_two_when: Option<String>,
33    /// A state's properties (`facing=east,half=bottom`) → the model it shows.
34    #[data(default)]
35    pub variants: Ordered<KindModel>,
36    /// Or models drawn together, each when its properties match (a fence's
37    /// post and arms).
38    #[data(default)]
39    pub multipart: Vec<KindPart>,
40}
41
42/// A model a kind's state shows: a name of its `models` (`base`: the
43/// model of `block.base`), turned.
44#[derive(Schema, Debug, Clone)]
45pub struct KindModel {
46    pub model: String,
47    pub x: Option<u32>,
48    pub y: Option<u32>,
49    #[data(default)]
50    pub uvlock: bool,
51}
52
53/// A part of a kind's multipart model: drawn always, or `when` its state
54/// has these properties (`north=true`).
55#[derive(Schema, Debug, Clone)]
56pub struct KindPart {
57    pub apply: KindModel,
58    pub when: Option<String>,
59}
60
61/// The `block` fields only some kinds take (`BlockKind::takes`).
62pub const KIND_FIELDS: [&str; 4] = ["base", "set", "press_ticks", "triggered_by"];
63
64static BUILT_IN_KINDS: yog_sync::LazyLock<Vec<BlockKind>> = yog_sync::LazyLock::new(|| {
65    yog_data::built_in(
66        "yog-formats' built-in block kinds",
67        include_str!("../../data/block_kinds.ydata"),
68    )
69});
70
71impl BlockKind {
72    /// The game's kinds: stairs, slabs, doors, trapdoors, buttons, pressure
73    /// plates, fences, fence gates.
74    pub fn built_ins() -> &'static [BlockKind] {
75        &BUILT_IN_KINDS
76    }
77
78    pub fn built_in(name: &str) -> Option<&'static BlockKind> {
79        Self::built_ins().iter().find(|k| k.name == name)
80    }
81
82    pub fn takes(&self, field: &str) -> bool {
83        self.takes.iter().any(|t| t == field)
84    }
85}
86
87/// A kind of block by name, one of [`BlockKind::built_ins`].
88pub struct KindRef;
89
90impl yog_data::with::With<String> for KindRef {
91    fn shape() -> Shape {
92        Shape::Any
93    }
94    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<String> {
95        let s = String::read(v, cx)?;
96        if BlockKind::built_in(&s).is_none() {
97            let names: Vec<&str> = BlockKind::built_ins()
98                .iter()
99                .map(|k| k.name.as_str())
100                .collect();
101            cx.problem(format!(
102                "`{s}`: one of the game's kinds of block — {}",
103                names.join(", ")
104            ));
105            return None;
106        }
107        Some(s)
108    }
109    fn write(f: &String) -> Value {
110        Value::from(f.as_str())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn the_game_kinds_of_block_read_and_name_their_own_models() {
120        let kinds = BlockKind::built_ins();
121        assert_eq!(kinds.len(), 8);
122        for k in kinds {
123            let names = k
124                .variants
125                .0
126                .iter()
127                .map(|(_, m)| m)
128                .chain(k.multipart.iter().map(|p| &p.apply))
129                .map(|m| m.model.as_str());
130            for name in names.chain(k.item.as_deref()) {
131                assert!(
132                    name == "base" && k.takes("base") || k.models.0.iter().any(|(n, _)| n == name),
133                    "{}: no model `{name}`",
134                    k.name
135                );
136            }
137            for (n, _) in &k.models.0 {
138                assert_ne!(n, "base", "{}: `base` is block.base's model", k.name);
139            }
140            assert!(
141                k.takes.iter().all(|t| KIND_FIELDS.contains(&t.as_str())),
142                "{}",
143                k.name
144            );
145            assert!(
146                k.variants.0.is_empty() != k.multipart.is_empty(),
147                "{}: variants or multipart",
148                k.name
149            );
150        }
151    }
152}