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 material: the textures a surface is drawn with, and how.
49    Material = "ymaterial", kind::MATERIAL;
50    /// Compiled Yog Data.
51    Data = "ydata", "compiled data";
52    /// A built mod.
53    Mod = "yog", "mod";
54}
55
56impl Format {
57    /// The format with extension `ext` (with or without the dot).
58    #[must_use]
59    pub fn of(ext: &str) -> Option<Format> {
60        let ext = ext.trim_start_matches('.');
61        Format::ALL.iter().copied().find(|f| f.ext() == ext)
62    }
63
64    /// The format of the file at `path`, by its extension.
65    #[must_use]
66    pub fn of_path(path: &std::path::Path) -> Option<Format> {
67        Format::of(path.extension()?.to_str()?)
68    }
69
70    /// Whether `path` is a file of this format.
71    #[must_use]
72    pub fn is(self, path: &std::path::Path) -> bool {
73        Format::of_path(path) == Some(self)
74    }
75
76    /// Whether its files are Yog Data — kept binary on disk, shown as text
77    /// (every format but `.yui` markup and built `.yog` mods).
78    #[must_use]
79    pub const fn is_data(self) -> bool {
80        !matches!(self, Format::Ui | Format::Mod)
81    }
82
83    /// `name` with this format's extension: `stone` → `stone.yblock`.
84    #[must_use]
85    pub fn file(self, name: &str) -> String {
86        format!("{name}.{}", self.ext())
87    }
88}
89
90/// Texture `path`'s file under its namespace: `block/stone` → `textures/block/stone.png`.
91#[must_use]
92pub fn texture_file(path: &str) -> String {
93    format!("{}{path}{}", res::TEXTURES, res::PNG)
94}
95
96/// Model `path`'s file: `item/apple` → `models/item/apple.json`.
97#[must_use]
98pub fn model_file(path: &str) -> String {
99    format!("{}{path}{}", res::MODELS, res::JSON)
100}
101
102/// Item `path`'s definition (1.21.4+): `apple` → `items/apple.json`.
103#[must_use]
104pub fn item_definition_file(path: &str) -> String {
105    format!("{}{path}{}", res::ITEM_DEFINITIONS, res::JSON)
106}
107
108/// Block `path`'s states' file: `stone` → `blockstates/stone.json`.
109#[must_use]
110pub fn blockstate_file(path: &str) -> String {
111    format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
112}
113
114/// The registries of entities and biomes, which aren't named as their kind.
115const ENTITY_REGISTRY: &str = crate::renamed_tag_dir::ENTITY_TYPE;
116const BIOME_REGISTRY: &str = crate::concatcp!(crate::pack::WORLDGEN, sep::PATH, kind::BIOME);
117
118/// Asset `rel` of namespace `ns`'s, as the pack holds it: `assets/<ns>/<rel>`.
119#[must_use]
120pub fn asset_file(ns: &str, rel: &str) -> String {
121    format!("{}{}{ns}{}{rel}", crate::pack::ASSETS, sep::PATH, sep::PATH)
122}
123
124/// Crate `name` as Rust code names it: `yog-api` → `yog_api`.
125#[must_use]
126pub fn rust_name(name: &str) -> String {
127    name.replace(sep::HYPHEN, sep::WORD)
128}
129
130/// The game's registry of content `kind` — what its tags' and data's
131/// folders are named: `entity` → `entity_type`, `biome` → `worldgen/biome`.
132#[must_use]
133pub fn registry_of(kind: &str) -> &str {
134    match kind {
135        kind::ENTITY => ENTITY_REGISTRY,
136        kind::BIOME => BIOME_REGISTRY,
137        other => other,
138    }
139}
140
141/// Whether folder `dir` (a data or tag folder) was plural before 1.21.
142fn renamed_in_1_21(dir: &str) -> bool {
143    crate::renamed_data_dir::ALL_VALUES.contains(&dir)
144        || crate::renamed_tag_dir::ALL_VALUES.contains(&dir)
145}
146
147/// The folders of content `kind`'s data or tags, the current one first, then
148/// 1.20's plural one when it was named differently: `item` → `item`, `items`.
149#[must_use]
150pub fn registry_dirs(kind: &str) -> Vec<String> {
151    let dir = registry_of(kind);
152    let mut out = vec![dir.to_string()];
153    if renamed_in_1_21(dir) {
154        out.push(format!("{dir}{}", crate::data_layout::PLURAL));
155    }
156    out
157}
158
159/// Language `locale`'s file under a namespace: `lang/en_us.json`.
160#[must_use]
161pub fn lang_file(locale: &str) -> String {
162    format!("{}{locale}{}", res::LANG, res::JSON)
163}
164
165/// What the game's translation keys of content `kind` in namespace `ns` start with: `item.minecraft.`.
166#[must_use]
167pub fn lang_prefix(kind: &str, ns: &str) -> String {
168    format!("{kind}.{ns}.")
169}
170
171/// The game's translation key of content `kind` `id`: `item.minecraft.iron_ingot`, a path's `/` as `.`.
172#[must_use]
173pub fn lang_key(kind: &str, id: &str) -> String {
174    let (ns, path) = split(id);
175    format!("{}{}", lang_prefix(kind, ns), path.replace(sep::PATH, "."))
176}
177
178/// `ns:path`.
179pub fn join(ns: &str, path: &str) -> String {
180    format!("{ns}{}{path}", sep::NAMESPACE)
181}
182
183/// A namespace as the game takes it: `[a-z0-9_.-]+`.
184pub fn valid_namespace(s: &str) -> bool {
185    !s.is_empty()
186        && s.bytes().all(|b| {
187            b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b'-')
188        })
189}
190
191/// A resource path as the game takes it: `[a-z0-9_./-]+`, not starting or
192/// ending with a `/`.
193pub fn valid_path(s: &str) -> bool {
194    let sep = sep::PATH.as_bytes()[0];
195    !s.is_empty()
196        && !s.starts_with(sep::PATH)
197        && !s.ends_with(sep::PATH)
198        && s.bytes().all(|b| {
199            b.is_ascii_lowercase()
200                || b.is_ascii_digit()
201                || matches!(b, b'_' | b'.' | b'-')
202                || b == sep
203        })
204}
205
206/// `ns:path` (or `path`, the game's) as the game takes it.
207pub fn valid_id(s: &str) -> bool {
208    match s.split_once(sep::NAMESPACE) {
209        Some((ns, path)) => valid_namespace(ns) && valid_path(path),
210        None => valid_path(s),
211    }
212}
213
214/// A name of Yog's in snake case: `[a-z0-9_]+` (a state's property, a kind of recipe).
215pub fn valid_name(s: &str) -> bool {
216    !s.is_empty()
217        && s.bytes()
218            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == sep::WORD.as_bytes()[0])
219}
220
221/// `id`'s namespace and path; a bare path is the game's.
222pub fn split(id: &str) -> (&str, &str) {
223    id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
224}
225
226/// The path of the game's `id` (`minecraft:stone` → `stone`); none for
227/// another namespace's, or a bare path.
228pub fn game_path(id: &str) -> Option<&str> {
229    id.split_once(sep::NAMESPACE)
230        .and_then(|(n, path)| (n == ns::MINECRAFT).then_some(path))
231}
232
233/// `id` with a namespace: the game's if it has none.
234pub fn qualify(id: &str) -> String {
235    qualify_in(ns::MINECRAFT, id)
236}
237
238/// `id` with a namespace: `ns` if it has none (what a mod writes bare is its own).
239pub fn qualify_in(ns: &str, id: &str) -> String {
240    if id.contains(sep::NAMESPACE) {
241        id.to_string()
242    } else {
243        join(ns, id)
244    }
245}
246
247/// The game font `font` stands for when it's Yog's alias of one
248/// (`yog:font/uniform` → `minecraft:uniform`).
249#[must_use]
250pub fn game_font_of(font: &str) -> Option<String> {
251    font.strip_prefix(yog::FONT_ALIAS)
252        .filter(|name| !name.is_empty())
253        .map(|name| join(ns::MINECRAFT, name))
254}
255
256/// Yog's alias of the game's font `name`: `yog:font/<name>`.
257#[must_use]
258pub fn font_alias(name: &str) -> String {
259    format!("{}{name}", yog::FONT_ALIAS)
260}