Skip to main content

yog_ids/
formats.rs

1//! Yog's file formats and where files live.
2
3use crate::{kind, ns, res, sep, yog};
4
5macro_rules! formats {
6    ($( $(#[$doc:meta])* $variant:ident = $ext:expr, $noun:expr; )*) => {
7        /// Yog's file formats: each one's extension and what a file of it is
8        /// called in messages. What each looks like is `yog_formats::shape`.
9        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10        pub enum Format { $( $(#[$doc])* $variant ),* }
11
12        impl Format {
13            /// Every format.
14            pub const ALL: &'static [Format] = &[$(Format::$variant),*];
15
16            /// Its extension, without the dot: `yblock`.
17            #[must_use]
18            pub const fn ext(self) -> &'static str {
19                match self { $(Format::$variant => $ext),* }
20            }
21
22            /// What one file of it is: `block`, `animation`.
23            #[must_use]
24            pub const fn noun(self) -> &'static str {
25                match self { $(Format::$variant => $noun),* }
26            }
27        }
28    };
29}
30
31formats! {
32    Block = "yblock", kind::BLOCK;
33    Item = "yitem", kind::ITEM;
34    Entity = "yentity", kind::ENTITY;
35    Recipe = "yrecipe", kind::RECIPE;
36    Book = "ybook", kind::BOOK;
37    /// A definition of some kind (`kind: "ladder"`).
38    Def = "ydef", "definition";
39    /// Named resources: strings, colours, sounds, numbers.
40    Res = "yres", "resources";
41    /// A screen or a component.
42    Ui = "yui", "screen";
43    Model = "ymodel", "model";
44    Anim = "yanim", "animation";
45    Font = "yfont", "font";
46    /// A texture: its picture as given and how it animates.
47    Texture = "ytexture", kind::TEXTURE;
48    /// A sound event: the sounds it plays, and how.
49    Sound = "ysound", kind::SOUND;
50    /// A material: the textures a surface is drawn with, and how.
51    Material = "ymaterial", kind::MATERIAL;
52    /// Compiled Yog Data.
53    Data = "ydata", "compiled data";
54    /// A built mod.
55    Mod = "yog", "mod";
56    /// Yog Script: a mod's code.
57    Script = "ys", "script";
58    /// Yog Nanite Script: Yog Script that patches others' classes.
59    Nanites = "yns", "nanite script";
60}
61
62impl Format {
63    /// The format with extension `ext` (with or without the dot).
64    #[must_use]
65    pub fn of(ext: &str) -> Option<Format> {
66        let ext = ext.trim_start_matches('.');
67        Format::ALL.iter().copied().find(|f| f.ext() == ext)
68    }
69
70    /// The format of the file at `path`, by its extension.
71    #[must_use]
72    pub fn of_path(path: &std::path::Path) -> Option<Format> {
73        Format::of(path.extension()?.to_str()?)
74    }
75
76    /// Whether `path` is a file of this format.
77    #[must_use]
78    pub fn is(self, path: &std::path::Path) -> bool {
79        Format::of_path(path) == Some(self)
80    }
81
82    /// Whether its files are Yog Data — kept binary on disk, shown as text
83    /// (every format but `.yui` markup and built `.yog` mods).
84    #[must_use]
85    pub const fn is_data(self) -> bool {
86        !matches!(
87            self,
88            Format::Ui | Format::Mod | Format::Script | Format::Nanites
89        )
90    }
91
92    /// `name` with this format's extension: `stone` → `stone.yblock`.
93    #[must_use]
94    pub fn file(self, name: &str) -> String {
95        format!("{name}.{}", self.ext())
96    }
97}
98
99/// Texture `path`'s file under its namespace: `block/stone` → `textures/block/stone.png`.
100#[must_use]
101pub fn texture_file(path: &str) -> String {
102    format!("{}{path}{}", res::TEXTURES, res::PNG)
103}
104
105/// The texture a file under its namespace is: `textures/block/stone.png` →
106/// `block/stone`; none for another file.
107#[must_use]
108pub fn texture_of_file(file: &str) -> Option<&str> {
109    file.strip_prefix(res::TEXTURES)?.strip_suffix(res::PNG)
110}
111
112/// Model `path`'s file: `item/apple` → `models/item/apple.json`.
113#[must_use]
114pub fn model_file(path: &str) -> String {
115    format!("{}{path}{}", res::MODELS, res::JSON)
116}
117
118/// Item `path`'s definition (1.21.4+): `apple` → `items/apple.json`.
119#[must_use]
120pub fn item_definition_file(path: &str) -> String {
121    format!("{}{path}{}", res::ITEM_DEFINITIONS, res::JSON)
122}
123
124/// Block `path`'s states' file: `stone` → `blockstates/stone.json`.
125#[must_use]
126pub fn blockstate_file(path: &str) -> String {
127    format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
128}
129
130/// The registries of entities and biomes, which aren't named as their kind.
131const ENTITY_REGISTRY: &str = crate::renamed_tag_dir::ENTITY_TYPE;
132const BIOME_REGISTRY: &str = crate::concatcp!(crate::pack::WORLDGEN, sep::PATH, kind::BIOME);
133
134/// Asset `rel` of namespace `ns`'s, as the pack holds it: `assets/<ns>/<rel>`.
135#[must_use]
136pub fn asset_file(ns: &str, rel: &str) -> String {
137    format!("{}{}{ns}{}{rel}", crate::pack::ASSETS, sep::PATH, sep::PATH)
138}
139
140/// Crate `name` as Rust code names it: `yog-api` → `yog_api`.
141#[must_use]
142pub fn rust_name(name: &str) -> String {
143    name.replace(sep::HYPHEN, sep::WORD)
144}
145
146/// The game's registry of content `kind` — what its tags' and data's
147/// folders are named: `entity` → `entity_type`, `biome` → `worldgen/biome`.
148#[must_use]
149pub fn registry_of(kind: &str) -> &str {
150    match kind {
151        kind::ENTITY => ENTITY_REGISTRY,
152        kind::BIOME => BIOME_REGISTRY,
153        other => other,
154    }
155}
156
157/// Whether folder `dir` (a data or tag folder) was plural before 1.21.
158fn renamed_in_1_21(dir: &str) -> bool {
159    crate::renamed_data_dir::ALL_VALUES.contains(&dir)
160        || crate::renamed_tag_dir::ALL_VALUES.contains(&dir)
161}
162
163/// The folders of content `kind`'s data or tags, the current one first, then
164/// 1.20's plural one when it was named differently: `item` → `item`, `items`.
165#[must_use]
166pub fn registry_dirs(kind: &str) -> Vec<String> {
167    let dir = registry_of(kind);
168    let mut out = vec![dir.to_string()];
169    if renamed_in_1_21(dir) {
170        out.push(format!("{dir}{}", crate::data_layout::PLURAL));
171    }
172    out
173}
174
175/// Language `locale`'s file under a namespace: `lang/en_us.json`.
176#[must_use]
177pub fn lang_file(locale: &str) -> String {
178    format!("{}{locale}{}", res::LANG, res::JSON)
179}
180
181/// What the game's translation keys of content `kind` in namespace `ns` start with: `item.minecraft.`.
182#[must_use]
183pub fn lang_prefix(kind: &str, ns: &str) -> String {
184    format!("{kind}.{ns}.")
185}
186
187/// The game's translation key of content `kind` `id`: `item.minecraft.iron_ingot`, a path's `/` as `.`.
188#[must_use]
189pub fn lang_key(kind: &str, id: &str) -> String {
190    let (ns, path) = split(id);
191    format!("{}{}", lang_prefix(kind, ns), path.replace(sep::PATH, "."))
192}
193
194/// `ns:path`.
195pub fn join(ns: &str, path: &str) -> String {
196    format!("{ns}{}{path}", sep::NAMESPACE)
197}
198
199/// A namespace as the game takes it: `[a-z0-9_.-]+`.
200pub fn valid_namespace(s: &str) -> bool {
201    !s.is_empty()
202        && s.bytes().all(|b| {
203            b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b'-')
204        })
205}
206
207/// A resource path as the game takes it: `[a-z0-9_./-]+`, not starting or
208/// ending with a `/`.
209pub fn valid_path(s: &str) -> bool {
210    let sep = sep::PATH.as_bytes()[0];
211    !s.is_empty()
212        && !s.starts_with(sep::PATH)
213        && !s.ends_with(sep::PATH)
214        && s.bytes().all(|b| {
215            b.is_ascii_lowercase()
216                || b.is_ascii_digit()
217                || matches!(b, b'_' | b'.' | b'-')
218                || b == sep
219        })
220}
221
222/// `ns:path` (or `path`, the game's) as the game takes it.
223pub fn valid_id(s: &str) -> bool {
224    match s.split_once(sep::NAMESPACE) {
225        Some((ns, path)) => valid_namespace(ns) && valid_path(path),
226        None => valid_path(s),
227    }
228}
229
230/// A name of Yog's in snake case: `[a-z0-9_]+` (a state's property, a kind of recipe).
231pub fn valid_name(s: &str) -> bool {
232    !s.is_empty()
233        && s.bytes()
234            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == sep::WORD.as_bytes()[0])
235}
236
237/// `id`'s namespace and path; a bare path is the game's.
238pub fn split(id: &str) -> (&str, &str) {
239    id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
240}
241
242/// The path of the game's `id` (`minecraft:stone` → `stone`); none for
243/// another namespace's, or a bare path.
244pub fn game_path(id: &str) -> Option<&str> {
245    id.split_once(sep::NAMESPACE)
246        .and_then(|(n, path)| (n == ns::MINECRAFT).then_some(path))
247}
248
249/// `id` with a namespace: the game's if it has none.
250pub fn qualify(id: &str) -> String {
251    qualify_in(ns::MINECRAFT, id)
252}
253
254/// `id` with a namespace: `ns` if it has none (what a mod writes bare is its own).
255pub fn qualify_in(ns: &str, id: &str) -> String {
256    if id.contains(sep::NAMESPACE) {
257        id.to_string()
258    } else {
259        join(ns, id)
260    }
261}
262
263/// The game font `font` stands for when it's Yog's alias of one
264/// (`yog:font/uniform` → `minecraft:uniform`).
265#[must_use]
266pub fn game_font_of(font: &str) -> Option<String> {
267    font.strip_prefix(yog::FONT_ALIAS)
268        .filter(|name| !name.is_empty())
269        .map(|name| join(ns::MINECRAFT, name))
270}
271
272/// Yog's alias of the game's font `name`: `yog:font/<name>`.
273#[must_use]
274pub fn font_alias(name: &str) -> String {
275    format!("{}{name}", yog::FONT_ALIAS)
276}