Skip to main content

yog_data/schema/
typed_values.rs

1//! The schemas of typed values: references, tags, resources, texts, images, colors.
2use super::{PhantomData, Reader, Ref, Res, Schema, Shape, Value, ValueRef, fmt, typed};
3
4// ── Typed values ────────────────────────────────────────────────────────────
5
6/// A kind of content: `item`, `block`, ... — a mod declares its own with
7/// [`kind!`](crate::kind).
8pub trait Kind {
9    const NAME: &'static str;
10}
11
12/// One kind or several (`(Item, Block)`): what a reference field takes.
13pub trait Kinds {
14    const NAMES: &'static [&'static str];
15}
16
17impl<K: Kind> Kinds for K {
18    const NAMES: &'static [&'static str] = &[K::NAME];
19}
20
21impl<A: Kind, B: Kind> Kinds for (A, B) {
22    const NAMES: &'static [&'static str] = &[A::NAME, B::NAME];
23}
24
25impl<A: Kind, B: Kind, C: Kind> Kinds for (A, B, C) {
26    const NAMES: &'static [&'static str] = &[A::NAME, B::NAME, C::NAME];
27}
28
29/// Declares kinds of content: `yog_data::kind!(Pattern = "pattern");`.
30#[macro_export]
31macro_rules! kind {
32    ($($name:ident = $kind:expr),* $(,)?) => {$(
33        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
34        pub struct $name;
35        impl $crate::Kind for $name {
36            const NAME: &'static str = $kind;
37        }
38    )*};
39}
40
41/// The kinds every Yog format knows.
42pub mod kinds {
43    crate::kind! {
44        Item = yog_ids::kind::ITEM,
45        Block = yog_ids::kind::BLOCK,
46        Entity = yog_ids::kind::ENTITY,
47        Recipe = yog_ids::kind::RECIPE,
48        Entry = yog_ids::kind::ENTRY,
49        Category = yog_ids::kind::CATEGORY,
50        Fluid = yog_ids::kind::FLUID,
51        Biome = yog_ids::kind::BIOME,
52        Dimension = yog_ids::kind::DIMENSION,
53        Texture = yog_ids::kind::TEXTURE,
54        Material = yog_ids::kind::MATERIAL,
55        Advancement = yog_ids::kind::ADVANCEMENT,
56    }
57}
58
59/// The id of content of kind `K`: written `@item:minecraft:stick` or, in a
60/// field that can only be an item, just `minecraft:stick` (`stick`).
61pub struct Id<K> {
62    pub id: String,
63    kind: PhantomData<K>,
64}
65
66pub type ItemId = Id<kinds::Item>;
67pub type BlockId = Id<kinds::Block>;
68pub type EntityId = Id<kinds::Entity>;
69pub type RecipeId = Id<kinds::Recipe>;
70
71impl<K> Id<K> {
72    pub fn new(id: impl Into<String>) -> Self {
73        Self {
74            id: id.into(),
75            kind: PhantomData,
76        }
77    }
78}
79
80impl<K> Clone for Id<K> {
81    fn clone(&self) -> Self {
82        Self::new(self.id.clone())
83    }
84}
85impl<K> PartialEq for Id<K> {
86    fn eq(&self, o: &Self) -> bool {
87        self.id == o.id
88    }
89}
90impl<K> Eq for Id<K> {}
91impl<K> std::hash::Hash for Id<K> {
92    fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
93        self.id.hash(h);
94    }
95}
96impl<K: Kind> fmt::Debug for Id<K> {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "@{}:{}", K::NAME, self.id)
99    }
100}
101impl<K> fmt::Display for Id<K> {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.write_str(&self.id)
104    }
105}
106
107/// `v` as a reference to content of one of `kinds` (any kind if empty),
108/// written `@kind:id` — a bare id is an error that says how to write it.
109pub fn read_ref(v: &ValueRef, cx: &mut Reader, kinds: &[&str]) -> Option<Ref> {
110    let wanted = || {
111        kinds
112            .iter()
113            .map(|k| format!("`@{k}:`"))
114            .collect::<Vec<_>>()
115            .join(" or ")
116    };
117    let r = match v {
118        ValueRef::Ref { kind, id } => Ref::new(*kind, *id),
119        ValueRef::Str(s) if s.starts_with('@') => match typed::classify(s) {
120            Ok(Value::Ref(r)) => r,
121            Ok(other) => {
122                cx.problem(format!(
123                    "expected a reference ({}), found {}",
124                    if kinds.is_empty() {
125                        "`@kind:id`".into()
126                    } else {
127                        wanted()
128                    },
129                    other.kind_name()
130                ));
131                return None;
132            }
133            Err(e) => {
134                cx.problem_of(yog_ids::problem_code::BAD_TYPED, e);
135                return None;
136            }
137        },
138        ValueRef::Str(s) => {
139            let kind = kinds.first().copied().unwrap_or(yog_ids::kind::ITEM);
140            cx.problem_of(
141                yog_ids::problem_code::NOT_A_REFERENCE,
142                format!("`{s}`: name it by a reference — `@{kind}:{s}`"),
143            );
144            return None;
145        }
146        other => {
147            cx.expected("a reference `@kind:id`", other);
148            return None;
149        }
150    };
151    if !kinds.is_empty() && !kinds.contains(&r.kind.as_str()) {
152        cx.problem_of(
153            yog_ids::problem_code::WRONG_KIND,
154            format!(
155                "`@{}:{}` is a reference to {} — this takes {}",
156                r.kind,
157                r.id,
158                r.kind,
159                wanted()
160            ),
161        );
162        return None;
163    }
164    cx.note_ref(r.clone());
165    Some(r)
166}
167
168fn read_tag(v: &ValueRef, cx: &mut Reader, kind: &str) -> Option<Ref> {
169    let r = match v {
170        ValueRef::Tag { kind, id } => Ref::new(*kind, *id),
171        ValueRef::Str(s) => match typed::classify(s) {
172            Ok(Value::Tag(r)) => r,
173            Ok(_) => {
174                cx.problem(format!("expected a tag `@tag:{kind}:id`, found {s:?}"));
175                return None;
176            }
177            Err(e) => {
178                cx.problem_of(yog_ids::problem_code::BAD_TYPED, e);
179                return None;
180            }
181        },
182        other => {
183            cx.expected("a tag", other);
184            return None;
185        }
186    };
187    if !kind.is_empty() && r.kind != kind {
188        cx.problem(format!(
189            "expected a tag of {kind}, found a tag of {}",
190            r.kind
191        ));
192        return None;
193    }
194    cx.note_tag(r.clone());
195    Some(r)
196}
197
198impl<K: Kind> Schema for Id<K> {
199    fn shape() -> Shape {
200        Shape::Ref(<K as Kinds>::NAMES)
201    }
202    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
203        read_ref(v, cx, <K as Kinds>::NAMES).map(|r| Id::new(r.id))
204    }
205    fn write(&self) -> Value {
206        Value::Ref(Ref::new(K::NAME, self.id.clone()))
207    }
208}
209
210impl Schema for Ref {
211    fn shape() -> Shape {
212        Shape::Ref(&[])
213    }
214    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
215        if let ValueRef::Str(s) = v
216            && !s.starts_with('@')
217        {
218            cx.problem(format!("expected a reference `@kind:id`, found {s:?}"));
219            return None;
220        }
221        read_ref(v, cx, &[])
222    }
223    fn write(&self) -> Value {
224        Value::Ref(self.clone())
225    }
226}
227
228/// Content of any kind or a tag of it: `@entity:minecraft:pig`,
229/// `@tag:entity:c:bosses` — a tag's entries, whose kind the tag says.
230#[derive(Debug, Clone, PartialEq)]
231pub enum AnyRefOrTag {
232    One(Ref),
233    Tag(Ref),
234}
235
236impl AnyRefOrTag {
237    /// The kind it names (`entity`).
238    pub fn kind(&self) -> &str {
239        match self {
240            AnyRefOrTag::One(r) | AnyRefOrTag::Tag(r) => &r.kind,
241        }
242    }
243}
244
245impl fmt::Display for AnyRefOrTag {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            AnyRefOrTag::One(r) => write!(f, "@{}:{}", r.kind, r.id),
249            AnyRefOrTag::Tag(r) => write!(f, "@tag:{}:{}", r.kind, r.id),
250        }
251    }
252}
253
254impl Schema for AnyRefOrTag {
255    fn shape() -> Shape {
256        Shape::RefOrTag(&[])
257    }
258    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
259        let is_tag = match v {
260            ValueRef::Tag { .. } => true,
261            ValueRef::Str(s) => s.starts_with("@tag:"),
262            _ => false,
263        };
264        if is_tag {
265            let r = match v {
266                ValueRef::Tag { kind, id } => Ref::new(*kind, *id),
267                ValueRef::Str(s) => match typed::classify(s) {
268                    Ok(Value::Tag(r)) => r,
269                    Ok(_) => {
270                        cx.problem(format!("expected a tag `@tag:kind:id`, found {s:?}"));
271                        return None;
272                    }
273                    Err(e) => {
274                        cx.problem_of(yog_ids::problem_code::BAD_TYPED, e);
275                        return None;
276                    }
277                },
278                _ => return None,
279            };
280            return Some(AnyRefOrTag::Tag(r));
281        }
282        if let ValueRef::Str(s) = v
283            && !s.starts_with('@')
284        {
285            cx.problem(format!(
286                "`{s}`: write `@<kind>:{s}` or a tag `@tag:<kind>:{s}`"
287            ));
288            return None;
289        }
290        read_ref(v, cx, &[]).map(AnyRefOrTag::One)
291    }
292    fn write(&self) -> Value {
293        match self {
294            AnyRefOrTag::One(r) => Value::Ref(r.clone()),
295            AnyRefOrTag::Tag(r) => Value::Tag(r.clone()),
296        }
297    }
298}
299
300/// A tag of content of kind `K`: `@tag:item:minecraft:planks`.
301pub struct TagId<K> {
302    pub id: String,
303    kind: PhantomData<K>,
304}
305
306impl<K> TagId<K> {
307    pub fn new(id: impl Into<String>) -> Self {
308        Self {
309            id: id.into(),
310            kind: PhantomData,
311        }
312    }
313}
314impl<K> Clone for TagId<K> {
315    fn clone(&self) -> Self {
316        Self::new(self.id.clone())
317    }
318}
319impl<K> PartialEq for TagId<K> {
320    fn eq(&self, o: &Self) -> bool {
321        self.id == o.id
322    }
323}
324impl<K: Kind> fmt::Debug for TagId<K> {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        write!(f, "@tag:{}:{}", K::NAME, self.id)
327    }
328}
329
330impl<K: Kind> Schema for TagId<K> {
331    fn shape() -> Shape {
332        Shape::Tag(<K as Kinds>::NAMES)
333    }
334    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
335        read_tag(v, cx, K::NAME).map(|r| TagId::new(r.id))
336    }
337    fn write(&self) -> Value {
338        Value::Tag(Ref::new(K::NAME, self.id.clone()))
339    }
340}
341
342/// Content of kind `K` or a tag of it — a recipe ingredient.
343#[derive(Debug, Clone, PartialEq)]
344pub enum OneOrTag<K: Kind> {
345    One(Id<K>),
346    Tag(TagId<K>),
347}
348
349/// An item or an item tag: `"minecraft:stick"`, `"@tag:item:minecraft:planks"`.
350pub type ItemIdOrTag = OneOrTag<kinds::Item>;
351
352impl<K: Kind> Schema for OneOrTag<K> {
353    fn shape() -> Shape {
354        Shape::RefOrTag(<K as Kinds>::NAMES)
355    }
356    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
357        if let ValueRef::Str(s) = v
358            && let Some(tag) = s.strip_prefix('#')
359        {
360            cx.problem(format!("`{s}`: write a tag as `@tag:{}:{tag}`", K::NAME));
361            return None;
362        }
363        let is_tag = match v {
364            ValueRef::Tag { .. } => true,
365            ValueRef::Str(s) => s.starts_with("@tag:"),
366            _ => false,
367        };
368        if is_tag {
369            TagId::read(v, cx).map(OneOrTag::Tag)
370        } else {
371            Id::read(v, cx).map(OneOrTag::One)
372        }
373    }
374    fn write(&self) -> Value {
375        match self {
376            OneOrTag::One(i) => i.write(),
377            OneOrTag::Tag(t) => t.write(),
378        }
379    }
380}
381
382impl Schema for Res {
383    fn shape() -> Shape {
384        Shape::Res
385    }
386    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
387        let r = match v {
388            ValueRef::Res { set, key } => Res {
389                set: set.to_string(),
390                key: key.to_string(),
391            },
392            ValueRef::Str(s) => match typed::classify(s) {
393                Ok(Value::Res(r)) => r,
394                Ok(_) => {
395                    cx.problem(format!("expected a resource `@Set.key`, found {s:?}"));
396                    return None;
397                }
398                Err(e) => {
399                    cx.problem_of(yog_ids::problem_code::BAD_TYPED, e);
400                    return None;
401                }
402            },
403            other => {
404                cx.expected("a resource `@Set.key`", other);
405                return None;
406            }
407        };
408        cx.note_res(r.clone());
409        Some(r)
410    }
411    fn write(&self) -> Value {
412        Value::Res(self.clone())
413    }
414}
415
416/// A text a player reads: Yog text as written, a `.yres` text, the name
417/// of some content (`@item:minecraft:stick` shows "Stick"), or any
418/// translation key (`@lang:gui.done`).
419#[derive(Debug, Clone, PartialEq)]
420pub enum Text {
421    Literal(String),
422    Res(Res),
423    NameOf(Ref),
424    Lang(String),
425}
426
427impl Default for Text {
428    fn default() -> Self {
429        Text::Literal(String::new())
430    }
431}
432
433impl Schema for Text {
434    fn shape() -> Shape {
435        Shape::Text
436    }
437    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
438        let typed = match v {
439            // Words as they are, the usual case: copied once.
440            ValueRef::Str(s) if !s.starts_with('@') => return Some(Text::Literal(s.to_string())),
441            ValueRef::Text(t) => return Some(Text::Literal(t.to_string())),
442            ValueRef::Lang(k) => return Some(Text::Lang(k.to_string())),
443            ValueRef::Str(s) => typed::classify(s).map_err(|e| cx.problem(e)).ok()?,
444            ValueRef::Res { .. } | ValueRef::Ref { .. } => v.to_value(),
445            other => {
446                cx.expected("a text", other);
447                return None;
448            }
449        };
450        match typed {
451            Value::Str(s) => Some(Text::Literal(s)),
452            Value::Res(r) => {
453                cx.note_res(r.clone());
454                Some(Text::Res(r))
455            }
456            Value::Ref(r) => {
457                cx.note_ref(r.clone());
458                Some(Text::NameOf(r))
459            }
460            Value::Lang(k) => Some(Text::Lang(k)),
461            other => {
462                cx.problem(format!("expected a text, found {}", other.kind_name()));
463                None
464            }
465        }
466    }
467    fn write(&self) -> Value {
468        match self {
469            Text::Literal(s) => Value::Text(s.clone()),
470            Text::Res(r) => Value::Res(r.clone()),
471            Text::NameOf(r) => Value::Ref(r.clone()),
472            Text::Lang(k) => Value::Lang(k.clone()),
473        }
474    }
475}
476
477/// An image: a source (`sprite:button`, `mymod:gui/logo`, `item:minecraft:stick`)
478/// or a `.yres` image.
479#[derive(Debug, Clone, PartialEq)]
480pub enum Image {
481    Source(String),
482    Res(Res),
483}
484
485impl Schema for Image {
486    fn shape() -> Shape {
487        Shape::Image
488    }
489    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
490        match v {
491            ValueRef::Image(s) => Some(Image::Source(s.to_string())),
492            ValueRef::Res { .. } => Res::read(v, cx).map(Image::Res),
493            ValueRef::Str(s) if !s.starts_with('@') => Some(Image::Source(s.to_string())),
494            ValueRef::Str(s) => match typed::classify(s) {
495                Ok(Value::Image(src)) => Some(Image::Source(src)),
496                Ok(Value::Res(r)) => {
497                    cx.note_res(r.clone());
498                    Some(Image::Res(r))
499                }
500                Ok(Value::Str(s)) => Some(Image::Source(s)),
501                Ok(other) => {
502                    cx.problem(format!("expected an image, found {}", other.kind_name()));
503                    None
504                }
505                Err(e) => {
506                    cx.problem_of(yog_ids::problem_code::BAD_TYPED, e);
507                    None
508                }
509            },
510            other => {
511                cx.expected("an image", other);
512                None
513            }
514        }
515    }
516    fn write(&self) -> Value {
517        match self {
518            Image::Source(s) => Value::Image(s.clone()),
519            Image::Res(r) => Value::Res(r.clone()),
520        }
521    }
522}
523
524/// A color: `#RRGGBB`, `#AARRGGBB`, or a `.yres` color.
525#[derive(Debug, Clone, PartialEq)]
526pub enum Color {
527    Argb(u32),
528    Res(Res),
529}
530
531impl Schema for Color {
532    fn shape() -> Shape {
533        Shape::Color
534    }
535    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
536        match v {
537            ValueRef::Color(c) => Some(Color::Argb(*c)),
538            ValueRef::Res { .. } => Res::read(v, cx).map(Color::Res),
539            ValueRef::Str(s) if s.starts_with('#') => {
540                typed::parse_color(s).map(Color::Argb).or_else(|| {
541                    cx.problem(format!("`{s}` isn't a color (#RRGGBB or #AARRGGBB)"));
542                    None
543                })
544            }
545            ValueRef::Str(s) if s.starts_with('@') => Res::read(v, cx).map(Color::Res),
546            other => {
547                cx.expected("a color (#RRGGBB or #AARRGGBB)", other);
548                None
549            }
550        }
551    }
552    fn write(&self) -> Value {
553        match self {
554            Color::Argb(c) => Value::Color(*c),
555            Color::Res(r) => Value::Res(r.clone()),
556        }
557    }
558}