Skip to main content

yog_formats/content/
model.rs

1//! Models: textures, elements, display transforms, and a block's states.
2
3use yog_data::{Ordered, Reader, Schema, Shape, Value, ValueRef};
4
5/// A texture: a slot of the model (`#side`), or a texture by id —
6/// `@texture:mymod:block/ruby` (the mod's `.ytexture`) or the game's
7/// (`@texture:minecraft:block/stone`).
8#[derive(Debug, Clone, PartialEq)]
9pub enum Texture {
10    Slot(String),
11    /// A texture's id, its namespace given (the game's if none).
12    Ref(String),
13}
14
15/// What a texture slot starts with: `#side`.
16pub const SLOT_MARK: char = '#';
17
18impl Schema for Texture {
19    fn shape() -> Shape {
20        Shape::Ref(&[yog_ids::kind::TEXTURE])
21    }
22    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
23        match v {
24            ValueRef::Str(s) if s.starts_with(SLOT_MARK) => {
25                if s.len() == SLOT_MARK.len_utf8() {
26                    cx.problem("`#` needs a slot name");
27                    return None;
28                }
29                Some(Texture::Slot(s[SLOT_MARK.len_utf8()..].to_string()))
30            }
31            ValueRef::Ref { kind, id } if *kind == yog_ids::kind::TEXTURE => {
32                Some(Texture::Ref(yog_ids::qualify(id)))
33            }
34            ValueRef::Str(s) if s.starts_with(yog_ids::sep::TYPED) => {
35                match yog_data::typed::classify(s) {
36                    Ok(Value::Ref(r)) if r.kind == yog_ids::kind::TEXTURE => {
37                        Some(Texture::Ref(yog_ids::qualify(&r.id)))
38                    }
39                    Ok(_) => {
40                        cx.problem(format!("`{s}`: a texture is `#slot` or `@texture:<ns>:<path>` (a .ytexture, or the game's)"));
41                        None
42                    }
43                    Err(e) => {
44                        cx.problem(e);
45                        None
46                    }
47                }
48            }
49            other => {
50                cx.expected(
51                    "a texture: `#slot` or `@texture:<ns>:<path>` (a .ytexture, or the game's)",
52                    other,
53                );
54                None
55            }
56        }
57    }
58    fn write(&self) -> Value {
59        match self {
60            Texture::Slot(s) => Value::Str(format!("{SLOT_MARK}{s}")),
61            Texture::Ref(id) => Value::Ref(yog_data::Ref::new(yog_ids::kind::TEXTURE, id.clone())),
62        }
63    }
64}
65
66#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Axis {
68    X,
69    Y,
70    Z,
71}
72
73#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
74pub enum GuiLight {
75    Front,
76    Side,
77}
78
79/// An element's rotation about one axis.
80#[derive(Schema, Debug, Clone)]
81pub struct ElementRotation {
82    /// The point it turns about; the block's middle by default.
83    #[data(range = "-16..=32")]
84    pub origin: Option<[f64; 3]>,
85    pub axis: Axis,
86    /// -45, -22.5, 0, 22.5 or 45 degrees.
87    #[data(range = "-45..=45")]
88    pub angle: f64,
89    #[data(default)]
90    pub rescale: bool,
91}
92
93/// One face of an element.
94#[derive(Schema, Debug, Clone)]
95pub struct Face {
96    /// `#slot` — a slot of the model's textures.
97    pub texture: String,
98    #[data(range = "0..=16")]
99    pub uv: Option<[f64; 4]>,
100    /// 0, 90, 180 or 270.
101    pub rotation: Option<u32>,
102    /// Not drawn when this side touches a full block.
103    pub cullface: Option<yog_core::Face>,
104    /// The tint index (-1: none).
105    #[data(range = "-1..=255")]
106    pub tint: Option<i64>,
107}
108
109#[derive(Schema, Debug, Clone, Default)]
110pub struct Faces {
111    pub down: Option<Face>,
112    pub up: Option<Face>,
113    pub north: Option<Face>,
114    pub south: Option<Face>,
115    pub west: Option<Face>,
116    pub east: Option<Face>,
117}
118
119impl Faces {
120    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Face)> {
121        [
122            ("down", &self.down),
123            ("up", &self.up),
124            ("north", &self.north),
125            ("south", &self.south),
126            ("west", &self.west),
127            ("east", &self.east),
128        ]
129        .into_iter()
130        .filter_map(|(d, f)| f.as_ref().map(|f| (d, f)))
131    }
132}
133
134/// A box of a model.
135#[derive(Schema, Debug, Clone)]
136pub struct Element {
137    pub name: Option<String>,
138    #[data(range = "-16..=32")]
139    pub from: [f64; 3],
140    #[data(range = "-16..=32")]
141    pub to: [f64; 3],
142    pub rotation: Option<ElementRotation>,
143    pub shade: Option<bool>,
144    pub faces: Option<Faces>,
145}
146
147/// How an item model sits in one place.
148#[derive(Schema, Debug, Clone)]
149pub struct Transform {
150    #[data(range = "-360..=360")]
151    pub rotation: Option<[f64; 3]>,
152    #[data(range = "-80..=80")]
153    pub translation: Option<[f64; 3]>,
154    #[data(range = "-4..=4")]
155    pub scale: Option<[f64; 3]>,
156}
157
158#[derive(Schema, Debug, Clone, Default)]
159pub struct Display {
160    pub thirdperson_righthand: Option<Transform>,
161    pub thirdperson_lefthand: Option<Transform>,
162    pub firstperson_righthand: Option<Transform>,
163    pub firstperson_lefthand: Option<Transform>,
164    pub gui: Option<Transform>,
165    pub head: Option<Transform>,
166    pub ground: Option<Transform>,
167    pub fixed: Option<Transform>,
168}
169
170impl Display {
171    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Transform)> {
172        [
173            ("thirdperson_righthand", &self.thirdperson_righthand),
174            ("thirdperson_lefthand", &self.thirdperson_lefthand),
175            ("firstperson_righthand", &self.firstperson_righthand),
176            ("firstperson_lefthand", &self.firstperson_lefthand),
177            ("gui", &self.gui),
178            ("head", &self.head),
179            ("ground", &self.ground),
180            ("fixed", &self.fixed),
181        ]
182        .into_iter()
183        .filter_map(|(d, t)| t.as_ref().map(|t| (d, t)))
184    }
185}
186
187/// A model: a template (`cube_all`, `generated`, …) or a model id as
188/// `parent`, its textures, or its own elements.
189#[derive(Schema, Debug, Clone)]
190pub struct Model {
191    pub parent: Option<String>,
192    /// A `.ymaterial` (`@material:mymod:block/ruby`): its textures, under the
193    /// model's own, and — a block's — its render layer when the block gives none.
194    #[data(with = "yog_data::with::Opt<yog_data::with::RefTo<yog_data::kinds::Material>>")]
195    pub material: Option<String>,
196    /// Slot → texture.
197    #[data(default)]
198    pub textures: Ordered<Texture>,
199    pub elements: Option<Vec<Element>>,
200    pub display: Option<Display>,
201    pub ambient_occlusion: Option<bool>,
202    pub gui_light: Option<GuiLight>,
203}
204
205/// One way the block stands.
206#[derive(Schema, Debug, Clone)]
207pub struct Rotation {
208    /// 0, 90, 180 or 270.
209    pub x: Option<u32>,
210    /// 0, 90, 180 or 270.
211    pub y: Option<u32>,
212    #[data(range = "1..=1000000")]
213    pub weight: Option<f64>,
214    #[data(default)]
215    pub uvlock: bool,
216}
217
218/// Arms towards neighbours that connect (see `block.connect_groups`).
219#[derive(Schema, Debug, Clone)]
220pub struct Connect {
221    /// The model of the arm towards north.
222    pub arm: Model,
223    #[data(default)]
224    pub uvlock: bool,
225}
226
227#[derive(Schema, Debug, Clone)]
228pub struct States {
229    pub rotations: Option<Vec<Rotation>>,
230    pub connect: Option<Connect>,
231    /// The block turns when placed (a `facing` property); its model's north
232    /// side is its front.
233    pub facing: Option<Facing>,
234    /// Which way it turns when placed: its front toward the player (as a
235    /// furnace, the default) or the way they look (as an observer).
236    pub placement: Option<Placement>,
237    /// It hangs on a floor, wall or ceiling, as a button does: a `face`
238    /// property (`floor`, `wall`, `ceiling`) and a horizontal `facing`. Its
239    /// model lies on the floor.
240    #[data(default)]
241    pub attach: bool,
242    /// It can stand in water (a `waterlogged` property).
243    #[data(default)]
244    pub waterlogged: bool,
245    /// Its textures stay as they are in the world when it turns (a
246    /// blockstate's `uvlock`).
247    #[data(default)]
248    pub uvlock: bool,
249    /// Properties of its own: a name and its values, the first the
250    /// default; `["false", "true"]` is a yes/no.
251    #[data(default)]
252    pub properties: Ordered<Vec<String>>,
253    /// More models, by name, for `variants`.
254    #[data(default)]
255    pub models: Ordered<Model>,
256    /// The model a state shows: some of its properties
257    /// (`"energized=true,facing=up"`) → a name in `models`, or a list of
258    /// names to pick from at random per position. The first that matches
259    /// wins, else the block's own model; turning for `facing` and `face` is
260    /// added.
261    #[data(default)]
262    pub variants: Ordered<ModelNames>,
263}
264
265/// One model name, or several to pick from at random (a blockstate's list of variants).
266pub type ModelNames = yog_data::OneOrList<String>;
267
268pub use yog_registry::{Facing, Placement};