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 Sound = "ysound", kind::SOUND;
50 Material = "ymaterial", kind::MATERIAL;
52 Data = "ydata", "compiled data";
54 Mod = "yog", "mod";
56 Script = "ys", "script";
58 Nanites = "yns", "nanite script";
60}
61
62impl Format {
63 #[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 #[must_use]
72 pub fn of_path(path: &std::path::Path) -> Option<Format> {
73 Format::of(path.extension()?.to_str()?)
74 }
75
76 #[must_use]
78 pub fn is(self, path: &std::path::Path) -> bool {
79 Format::of_path(path) == Some(self)
80 }
81
82 #[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 #[must_use]
94 pub fn file(self, name: &str) -> String {
95 format!("{name}.{}", self.ext())
96 }
97}
98
99#[must_use]
101pub fn texture_file(path: &str) -> String {
102 format!("{}{path}{}", res::TEXTURES, res::PNG)
103}
104
105#[must_use]
108pub fn texture_of_file(file: &str) -> Option<&str> {
109 file.strip_prefix(res::TEXTURES)?.strip_suffix(res::PNG)
110}
111
112#[must_use]
114pub fn model_file(path: &str) -> String {
115 format!("{}{path}{}", res::MODELS, res::JSON)
116}
117
118#[must_use]
120pub fn item_definition_file(path: &str) -> String {
121 format!("{}{path}{}", res::ITEM_DEFINITIONS, res::JSON)
122}
123
124#[must_use]
126pub fn blockstate_file(path: &str) -> String {
127 format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
128}
129
130const ENTITY_REGISTRY: &str = crate::renamed_tag_dir::ENTITY_TYPE;
132const BIOME_REGISTRY: &str = crate::concatcp!(crate::pack::WORLDGEN, sep::PATH, kind::BIOME);
133
134#[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#[must_use]
142pub fn rust_name(name: &str) -> String {
143 name.replace(sep::HYPHEN, sep::WORD)
144}
145
146#[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
157fn 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#[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#[must_use]
177pub fn lang_file(locale: &str) -> String {
178 format!("{}{locale}{}", res::LANG, res::JSON)
179}
180
181#[must_use]
183pub fn lang_prefix(kind: &str, ns: &str) -> String {
184 format!("{kind}.{ns}.")
185}
186
187#[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
194pub fn join(ns: &str, path: &str) -> String {
196 format!("{ns}{}{path}", sep::NAMESPACE)
197}
198
199pub 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
207pub 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
222pub 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
230pub 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
237pub fn split(id: &str) -> (&str, &str) {
239 id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
240}
241
242pub 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
249pub fn qualify(id: &str) -> String {
251 qualify_in(ns::MINECRAFT, id)
252}
253
254pub 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#[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#[must_use]
274pub fn font_alias(name: &str) -> String {
275 format!("{}{name}", yog::FONT_ALIAS)
276}