1use crate::{kind, ns, res, sep, yog};
4
5macro_rules! formats {
6 ($( $(#[$doc:meta])* $variant:ident = $ext:expr, $noun:expr; )*) => {
7 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10 pub enum Format { $( $(#[$doc])* $variant ),* }
11
12 impl Format {
13 pub const ALL: &'static [Format] = &[$(Format::$variant),*];
15
16 #[must_use]
18 pub const fn ext(self) -> &'static str {
19 match self { $(Format::$variant => $ext),* }
20 }
21
22 #[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 Def = "ydef", "definition";
39 Res = "yres", "resources";
41 Ui = "yui", "screen";
43 Model = "ymodel", "model";
44 Anim = "yanim", "animation";
45 Font = "yfont", "font";
46 Texture = "ytexture", kind::TEXTURE;
48 Material = "ymaterial", kind::MATERIAL;
50 Data = "ydata", "compiled data";
52 Mod = "yog", "mod";
54}
55
56impl Format {
57 #[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 #[must_use]
66 pub fn of_path(path: &std::path::Path) -> Option<Format> {
67 Format::of(path.extension()?.to_str()?)
68 }
69
70 #[must_use]
72 pub fn is(self, path: &std::path::Path) -> bool {
73 Format::of_path(path) == Some(self)
74 }
75
76 #[must_use]
79 pub const fn is_data(self) -> bool {
80 !matches!(self, Format::Ui | Format::Mod)
81 }
82
83 #[must_use]
85 pub fn file(self, name: &str) -> String {
86 format!("{name}.{}", self.ext())
87 }
88}
89
90#[must_use]
92pub fn texture_file(path: &str) -> String {
93 format!("{}{path}{}", res::TEXTURES, res::PNG)
94}
95
96#[must_use]
98pub fn model_file(path: &str) -> String {
99 format!("{}{path}{}", res::MODELS, res::JSON)
100}
101
102#[must_use]
104pub fn item_definition_file(path: &str) -> String {
105 format!("{}{path}{}", res::ITEM_DEFINITIONS, res::JSON)
106}
107
108#[must_use]
110pub fn blockstate_file(path: &str) -> String {
111 format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
112}
113
114const ENTITY_REGISTRY: &str = crate::renamed_tag_dir::ENTITY_TYPE;
116const BIOME_REGISTRY: &str = crate::concatcp!(crate::pack::WORLDGEN, sep::PATH, kind::BIOME);
117
118#[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#[must_use]
126pub fn rust_name(name: &str) -> String {
127 name.replace(sep::HYPHEN, sep::WORD)
128}
129
130#[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
141fn 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#[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#[must_use]
161pub fn lang_file(locale: &str) -> String {
162 format!("{}{locale}{}", res::LANG, res::JSON)
163}
164
165#[must_use]
167pub fn lang_prefix(kind: &str, ns: &str) -> String {
168 format!("{kind}.{ns}.")
169}
170
171#[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
178pub fn join(ns: &str, path: &str) -> String {
180 format!("{ns}{}{path}", sep::NAMESPACE)
181}
182
183pub 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
191pub 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
206pub 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
214pub 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
221pub fn split(id: &str) -> (&str, &str) {
223 id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
224}
225
226pub 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
233pub fn qualify(id: &str) -> String {
235 qualify_in(ns::MINECRAFT, id)
236}
237
238pub 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#[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#[must_use]
258pub fn font_alias(name: &str) -> String {
259 format!("{}{name}", yog::FONT_ALIAS)
260}