1pub use const_format::concatcp;
8
9#[macro_export]
11macro_rules! id {
12 ($ns:expr, $path:expr) => {
13 $crate::concatcp!($ns, $crate::sep::NAMESPACE, $path)
14 };
15}
16
17macro_rules! names {
18 ($( $(#[$gdoc:meta])* $group:ident { $( $(#[$doc:meta])* $name:ident = $value:expr ),* $(,)? } )*) => {
19 $(
20 $(#[$gdoc])*
21 pub mod $group {
22 #[allow(unused_imports)] use super::*;
24 $( $(#[$doc])* pub const $name: &str = $value; )*
25 }
26 )*
27 pub const ALL: &[(&str, &[(&str, &str)])] = &[ $( (stringify!($group), &[ $( (stringify!($name), $group::$name) ),* ]) ),* ];
29 };
30}
31
32names! {
33 ns {
35 MINECRAFT = "minecraft",
37 YOG = "yog",
39 }
40
41 sep {
43 NAMESPACE = ":",
45 PATH = "/",
47 TYPED = "@",
49 TAG_SHOWN = "#",
51 ALTERNATIVES = "|",
53 FIELD = "\t",
55 }
56
57 kind {
61 ITEM = "item",
62 BLOCK = "block",
63 ENTITY = "entity",
64 RECIPE = "recipe",
65 ENTRY = "entry",
66 CATEGORY = "category",
67 BOOK = "book",
68 FLUID = "fluid",
69 BIOME = "biome",
70 BIOME_FEATURE = "biome_feature",
71 DIMENSION = "dimension",
72 ATTRIBUTE = "attribute",
73 EFFECT = "effect",
74 POTION = "potion",
75 INVENTORY = "inventory",
76 STARTUP_GRANT = "startup_grant",
77 ADVANCEMENT = "advancement",
78 LADDER = "ladder",
79 LEVEL = "level",
80 SOUND_GROUP = "sound_group",
81 RECIPE_KIND = "recipe_kind",
82 TOOL_MATERIAL = "tool_material",
83 ARMOR_MATERIAL = "armor_material",
84 ORE = "ore",
85 TABLE = "table",
86 TAG = "tag",
87 DAMAGE_TYPE = "damage_type",
88 TREE = "tree",
89 LOOT_MODIFIER = "loot_modifier",
90 LOOT_TABLE = "loot_table",
91 TEXTURE = "texture",
92 MATERIAL = "material",
93 }
94
95 reference {
97 ITEM = concatcp!(sep::TYPED, kind::ITEM, sep::NAMESPACE),
98 BLOCK = concatcp!(sep::TYPED, kind::BLOCK, sep::NAMESPACE),
99 ENTITY = concatcp!(sep::TYPED, kind::ENTITY, sep::NAMESPACE),
100 RECIPE = concatcp!(sep::TYPED, kind::RECIPE, sep::NAMESPACE),
101 ENTRY = concatcp!(sep::TYPED, kind::ENTRY, sep::NAMESPACE),
102 CATEGORY = concatcp!(sep::TYPED, kind::CATEGORY, sep::NAMESPACE),
103 FLUID = concatcp!(sep::TYPED, kind::FLUID, sep::NAMESPACE),
104 BIOME = concatcp!(sep::TYPED, kind::BIOME, sep::NAMESPACE),
105 DIMENSION = concatcp!(sep::TYPED, kind::DIMENSION, sep::NAMESPACE),
106 TEXTURE = concatcp!(sep::TYPED, kind::TEXTURE, sep::NAMESPACE),
107 MATERIAL = concatcp!(sep::TYPED, kind::MATERIAL, sep::NAMESPACE),
108 }
109
110 typed {
112 TAG = kind::TAG,
113 LANG = "lang",
114 IMAGE = "image",
115 BYTES = "bytes",
117 }
118
119 image_scheme {
121 ITEM = kind::ITEM,
122 BLOCK = kind::BLOCK,
123 SPRITE = "sprite",
124 MOD = "mod",
125 }
126
127 file {
129 MANIFEST = "yog.toml",
131 LOCK = "yog.lock",
133 }
134
135 pack {
137 DATA = "data",
139 ASSETS = "assets",
141 TAGS = "tags",
143 OVERLAYS = "when",
145 ANY_LOADER = "any",
147 ANY_VERSION = "any",
149 }
150
151 project {
153 RESOURCES = "resources",
155 CONTENT = "content",
157 SRC = "src",
159 BUILD = ".yog-build",
161 }
162
163 program {
165 CLI = "yog",
167 SERVE = "serve",
169 }
170
171 res {
173 TEXTURES = concatcp!("textures", sep::PATH),
175 MODELS = concatcp!("models", sep::PATH),
177 BLOCKSTATES = concatcp!("blockstates", sep::PATH),
179 ITEM_DIR = concatcp!(kind::ITEM, sep::PATH),
181 BLOCK_DIR = concatcp!(kind::BLOCK, sep::PATH),
183 GUI_SPRITES = concatcp!("gui", sep::PATH, "sprites", sep::PATH),
185 ATLASES = concatcp!("atlases", sep::PATH),
187 LANG = concatcp!("lang", sep::PATH),
189 BUILTIN = concatcp!("builtin", sep::PATH),
191 PNG = ".png",
192 SVG = ".svg",
193 JSON = ".json",
194 MCMETA = ".mcmeta",
195 }
196
197 yog {
199 STATE = id!(ns::YOG, "state"),
201 HANGING = id!(ns::YOG, "hanging"),
203 ENTITY_DATA = id!(ns::YOG, "entity_data"),
205 DIMENSIONS = id!(ns::YOG, "dimensions"),
208 SLOT_PREFIX = id!(ns::YOG, "slot:"),
210 PLAYER_INVENTORY = id!(ns::YOG, "player_inventory"),
212 MACHINE_PREFIX = id!(ns::YOG, "machine:"),
214 POTION_ALIAS = id!(ns::YOG, "potion/"),
216 BOOK_SHEET = id!(ns::YOG, "gui/book/book"),
218 CRAFTING_SHEET = id!(ns::YOG, "gui/book/crafting"),
219 DESIGNER_SCREEN = id!(ns::YOG, "designer"),
221 BOOK_TEXTURE = id!(ns::YOG, "gui/book"),
223 NO_IMAGE = id!(ns::YOG, "none"),
225 DYNAMIC_SVG_PREFIX = id!(ns::YOG, "dynamic/svg_"),
227 }
228
229 game {
231 DEFAULT_FONT = id!(ns::MINECRAFT, "default"),
233 VANILLA_THEME = id!(ns::MINECRAFT, "vanilla"),
235 OVERWORLD = id!(ns::MINECRAFT, "overworld"),
236 THE_NETHER = id!(ns::MINECRAFT, "the_nether"),
237 THE_END = id!(ns::MINECRAFT, "the_end"),
238 MISSING_TEXTURE = id!(ns::MINECRAFT, "missingno"),
240 WRITTEN_BOOK = id!(ns::MINECRAFT, "written_book"),
242 BOOK = id!(ns::MINECRAFT, "book"),
243 LOCALE = "en_us",
245 }
246
247 external {
249 ITEMS = "items",
250 BLOCKS = "blocks",
251 ENTITIES = "entities",
252 RECIPES = "recipes",
253 }
254
255 yui {
257 SCREEN = "Screen",
258 COMPONENT = "Component",
259 PROPERTY = "Property",
261 EVENT = "Event",
262 STYLE = "Style",
263 CANVAS = "Canvas",
264 COLUMN = "Column",
265 ROW = "Row",
266 GRID = "Grid",
267 SCROLL_VIEW = "ScrollView",
268 LABEL = "Label",
269 BUTTON = "Button",
270 TOGGLE_BUTTON = "ToggleButton",
271 TEXT_BOX = "TextBox",
272 IMAGE = "Image",
273 SURFACE = "Surface",
274 SPACER = "Spacer",
275 PILL = "Pill",
276 STAT_BAR = "StatBar",
277 LIST_VIEW = "ListView",
278 ITEMS_CONTROL = "ItemsControl",
279 CONTENT_PRESENTER = "ContentPresenter",
280 ITEM_SLOT = "ItemSlot",
282 INV_SLOT = "InvSlot",
283 SLOT = "Slot",
284 PLAYER_INVENTORY = "PlayerInventory",
285 MACHINE = "Machine",
287 RECIPE_AREA = "RecipeArea",
289 RECIPE_SLOT = "RecipeSlot",
291 RECIPE_RESULT = "RecipeResult",
292 RECIPE_COUNT = "RecipeCount",
293 }
294
295 model {
297 PARENT = "parent",
298 TEXTURES = "textures",
299 ELEMENTS = "elements",
300 DISPLAY = "display",
301 FACES = "faces",
302 FROM = "from",
303 TO = "to",
304 UV = "uv",
305 ROTATION = "rotation",
306 TEXTURE = "texture",
307 LAYER0 = "layer0",
309 PARTICLE = "particle",
311 MODEL = "model",
313 ORIGIN = "origin",
315 AXIS = "axis",
316 ANGLE = "angle",
317 RESCALE = "rescale",
318 VARIANTS = "variants",
320 MULTIPART = "multipart",
321 WHEN = "when",
322 APPLY = "apply",
323 OR = "OR",
324 AND = "AND",
325 }
326
327 content_field {
329 ID = "id",
330 KIND = "kind",
331 MODEL = "model",
332 ITEM_MODEL = "item_model",
333 ITEM = "item",
334 POTION = "potion",
335 EFFECTS = "effects",
336 EFFECT = "effect",
337 OF = "of",
338 COLOR = "color",
339 }
340
341 yui_attr {
343 ID = "id",
344 X = "x",
345 Y = "y",
346 W = "w",
347 H = "h",
348 MIN_W = "min_w",
349 MIN_H = "min_h",
350 FLEX = "flex",
351 DOCK = "dock",
352 GAP = "gap",
353 PADDING = "padding",
354 BG = "bg",
355 COLOR = "color",
356 ALIGN = "align",
357 HALIGN = "halign",
358 VALIGN = "valign",
359 MARGIN = "margin",
360 FONT_SCALE = "font_scale",
361 FONT = "font",
362 SHADOW = "shadow",
363 NO_WRAP = "no_wrap",
364 ELLIPSIS = "ellipsis",
365 FOCUS_STYLE = "focus_style",
366 FOCUS_COLOR = "focus_color",
367 ENABLED = "enabled",
368 ON_CLICK = "on_click",
369 TOOLTIP = "tooltip",
370 WIDTH = "width",
371 HEIGHT = "height",
372 ANCHOR = "anchor",
373 MODAL = "modal",
374 PAUSE = "pause",
375 LIVE = "live",
376 THEME = "theme",
377 TEXT = "text",
378 PLACEHOLDER = "placeholder",
379 MAX_LENGTH = "max_length",
380 ON_CHANGE = "on_change",
381 ON_SUBMIT = "on_submit",
382 ITEM = "item",
383 COUNT = "count",
384 INDEX = "index",
385 ACCEPTS = "accepts",
386 SOURCE = "source",
387 STRETCH = "stretch",
388 UV = "uv",
389 TINT = "tint",
390 KIND = "kind",
391 RESULT = "result",
392 PLACES = "places",
393 RECIPE_LAYOUT = "recipe_layout",
394 SLOT = "slot",
395 BLOCK = "block",
396 STYLE = "style",
397 BASED_ON = "based_on",
398 ORIENTATION = "orientation",
399 ROWS = "rows",
400 COLUMNS = "columns",
401 ROW_SPACING = "row_spacing",
402 COLUMN_SPACING = "column_spacing",
403 VALUE = "value",
404 SELECTED = "selected",
405 CHECKED = "checked",
406 ITEM_HEIGHT = "item_height",
407 SCROLL_KEY = "scroll_key",
408 VISIBLE = "visible",
409 GRID_ROW = "Grid.Row",
410 GRID_COLUMN = "Grid.Column",
411 GRID_ROWSPAN = "Grid.RowSpan",
412 GRID_COLUMNSPAN = "Grid.ColumnSpan",
413 }
414}
415
416macro_rules! formats {
417 ($( $(#[$doc:meta])* $variant:ident = $ext:expr, $noun:expr; )*) => {
418 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
421 pub enum Format { $( $(#[$doc])* $variant ),* }
422
423 impl Format {
424 pub const ALL: &'static [Format] = &[$(Format::$variant),*];
426
427 #[must_use]
429 pub const fn ext(self) -> &'static str {
430 match self { $(Format::$variant => $ext),* }
431 }
432
433 #[must_use]
435 pub const fn noun(self) -> &'static str {
436 match self { $(Format::$variant => $noun),* }
437 }
438 }
439 };
440}
441
442formats! {
443 Block = "yblock", kind::BLOCK;
444 Item = "yitem", kind::ITEM;
445 Entity = "yentity", kind::ENTITY;
446 Recipe = "yrecipe", kind::RECIPE;
447 Book = "ybook", kind::BOOK;
448 Def = "ydef", "definition";
450 Res = "yres", "resources";
452 Ui = "yui", "screen";
454 Model = "ymodel", "model";
455 Anim = "yanim", "animation";
456 Font = "yfont", "font";
457 Texture = "ytexture", kind::TEXTURE;
459 Material = "ymaterial", kind::MATERIAL;
461 Data = "ydata", "compiled data";
463 Mod = "yog", "mod";
465}
466
467impl Format {
468 #[must_use]
470 pub fn of(ext: &str) -> Option<Format> {
471 let ext = ext.trim_start_matches('.');
472 Format::ALL.iter().copied().find(|f| f.ext() == ext)
473 }
474
475 #[must_use]
477 pub fn of_path(path: &std::path::Path) -> Option<Format> {
478 Format::of(path.extension()?.to_str()?)
479 }
480
481 #[must_use]
483 pub fn is(self, path: &std::path::Path) -> bool {
484 Format::of_path(path) == Some(self)
485 }
486
487 #[must_use]
489 pub fn file(self, name: &str) -> String {
490 format!("{name}.{}", self.ext())
491 }
492}
493
494#[must_use]
496pub fn texture_file(path: &str) -> String {
497 format!("{}{path}{}", res::TEXTURES, res::PNG)
498}
499
500#[must_use]
502pub fn model_file(path: &str) -> String {
503 format!("{}{path}{}", res::MODELS, res::JSON)
504}
505
506#[must_use]
508pub fn blockstate_file(path: &str) -> String {
509 format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
510}
511
512#[must_use]
515pub fn registry_of(kind: &str) -> &str {
516 match kind {
517 kind::ENTITY => "entity_type",
518 kind::BIOME => "worldgen/biome",
519 other => other,
520 }
521}
522
523const PLURAL_BEFORE_1_21: &[&str] = &[kind::ITEM, kind::BLOCK, kind::ENTITY, kind::FLUID, kind::LOOT_TABLE, kind::ADVANCEMENT, kind::RECIPE];
525
526#[must_use]
529pub fn registry_dirs(kind: &str) -> Vec<String> {
530 let dir = registry_of(kind);
531 let mut out = vec![dir.to_string()];
532 if PLURAL_BEFORE_1_21.contains(&kind) {
533 out.push(format!("{dir}s"));
534 }
535 out
536}
537
538#[must_use]
540pub fn lang_file(locale: &str) -> String {
541 format!("{}{locale}{}", res::LANG, res::JSON)
542}
543
544#[must_use]
546pub fn lang_prefix(kind: &str, ns: &str) -> String {
547 format!("{kind}.{ns}.")
548}
549
550#[must_use]
552pub fn lang_key(kind: &str, id: &str) -> String {
553 let (ns, path) = split(id);
554 format!("{}{}", lang_prefix(kind, ns), path.replace(sep::PATH, "."))
555}
556
557pub fn join(ns: &str, path: &str) -> String {
559 format!("{ns}{}{path}", sep::NAMESPACE)
560}
561
562pub fn split(id: &str) -> (&str, &str) {
564 id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
565}
566
567pub fn qualify(id: &str) -> String {
569 qualify_in(ns::MINECRAFT, id)
570}
571
572pub fn qualify_in(ns: &str, id: &str) -> String {
574 if id.contains(sep::NAMESPACE) { id.to_string() } else { join(ns, id) }
575}
576
577pub mod export {
579 use super::ALL;
580
581 const HEADER: &str = "Generated by yog-ids from Yog-Mod-Loader/rust/crates/yog-ids — don't edit; regenerate with `cargo run -p yog-ids --bin yog-ids-export`.";
582
583 pub fn typescript() -> String {
585 let mut out = format!("// {HEADER}\n");
586 for (group, names) in ALL {
587 out.push_str(&format!("\nexport const {} = {{\n", camel(group)));
588 for (name, value) in *names {
589 out.push_str(&format!(" {name}: {value:?},\n"));
590 }
591 out.push_str("} as const;\n");
592 }
593 out.push_str("\n/** Yog's file formats: extension and noun (`Format`). */\nexport const formats = {\n");
594 for f in crate::Format::ALL {
595 out.push_str(&format!(" {}: {{ ext: {:?}, noun: {:?} }},\n", camel(&format!("{f:?}").to_lowercase()), f.ext(), f.noun()));
596 }
597 out.push_str("} as const;\n");
598 out
599 }
600
601 pub fn java(package: &str, class: &str) -> String {
603 let mut out = format!("// {HEADER}\npackage {package};\n\n/** The names Yog's code shares (yog-ids). */\npublic final class {class} {{\n private {class}() {{}}\n");
604 for (group, names) in ALL {
605 out.push_str(&format!("\n public static final class {} {{\n private {}() {{}}\n\n", pascal(group), pascal(group)));
606 for (name, value) in *names {
607 out.push_str(&format!(" public static final String {name} = {value:?};\n"));
608 }
609 out.push_str(" }\n");
610 }
611 out.push_str("\n /** Yog's file formats' extensions (`Format`). */\n public static final class Ext {\n private Ext() {}\n\n");
612 for f in crate::Format::ALL {
613 out.push_str(&format!(" public static final String {} = {:?};\n", format!("{f:?}").to_uppercase(), f.ext()));
614 }
615 out.push_str(" }\n}\n");
616 out
617 }
618
619 fn pascal(s: &str) -> String {
620 s.split('_').map(|w| w[..1].to_uppercase() + &w[1..]).collect()
621 }
622
623 fn camel(s: &str) -> String {
624 let p = pascal(s);
625 p[..1].to_lowercase() + &p[1..]
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 #[test]
634 fn ids_are_composed_from_their_namespace() {
635 assert_eq!(yog::STATE, "yog:state");
636 assert_eq!(game::OVERWORLD, "minecraft:overworld");
637 assert_eq!(image_scheme::ITEM, kind::ITEM);
638 assert_eq!(split("stone"), ("minecraft", "stone"));
639 assert_eq!(qualify("mymod:x"), "mymod:x");
640 assert_eq!(Format::of(".yblock"), Some(Format::Block));
641 assert_eq!(registry_of(kind::BIOME), "worldgen/biome");
642 assert_eq!(Format::Anim.file("walk"), "walk.yanim");
643 }
644
645 #[test]
647 fn the_java_names_are_current() {
648 let file = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../java-common/src/main/java/dev/yog/YogNames.java");
649 let Ok(java) = std::fs::read_to_string(&file) else { return };
650 assert!(
651 java == export::java("dev.yog", "YogNames"),
652 "{} is stale: cargo run -p yog-ids --bin yog-ids-export -- --java ../java-common/src/main/java/dev/yog dev.yog YogNames",
653 file.display()
654 );
655 }
656
657 #[test]
658 fn the_exports_name_every_group() {
659 let ts = export::typescript();
660 assert!(ts.contains("export const imageScheme = {") && ts.contains(" YOG: \"yog\","), "{ts}");
661 assert!(ts.contains(" block: { ext: \"yblock\", noun: \"block\" },"), "{ts}");
662 let java = export::java("dev.yog.generated", "YogNames");
663 assert!(java.contains("public static final class ImageScheme {") && java.contains("public static final String STATE = \"yog:state\";"), "{java}");
664 }
665}