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 ITEM_DEFINITIONS = concatcp!("items", sep::PATH),
183 EQUIPMENT = concatcp!("equipment", sep::PATH),
185 EQUIPMENT_TEXTURES = concatcp!("entity", sep::PATH, "equipment", sep::PATH),
187 ARMOR_TEXTURES = concatcp!("models", sep::PATH, "armor", sep::PATH),
189 ARMOR_LAYER = "_layer_",
191 SPAWN_EGG_SUFFIX = "_spawn_egg",
193 BLOCK_DIR = concatcp!(kind::BLOCK, sep::PATH),
195 GUI_SPRITES = concatcp!("gui", sep::PATH, "sprites", sep::PATH),
197 ATLASES = concatcp!("atlases", sep::PATH),
199 LANG = concatcp!("lang", sep::PATH),
201 BUILTIN = concatcp!("builtin", sep::PATH),
203 PNG = ".png",
204 SVG = ".svg",
205 JSON = ".json",
206 MCMETA = ".mcmeta",
207 }
208
209 yog {
211 STATE = id!(ns::YOG, "state"),
213 HANGING = id!(ns::YOG, "hanging"),
215 ENTITY_DATA = id!(ns::YOG, "entity_data"),
217 DIMENSIONS = id!(ns::YOG, "dimensions"),
220 SLOT_PREFIX = id!(ns::YOG, "slot:"),
222 PLAYER_INVENTORY = id!(ns::YOG, "player_inventory"),
224 MACHINE_PREFIX = id!(ns::YOG, "machine:"),
226 POTION_ALIAS = id!(ns::YOG, "potion/"),
228 SPAWN_EGG_MODEL = id!(ns::YOG, "item/spawn_egg"),
230 BOOK_SHEET = id!(ns::YOG, "gui/book/book"),
232 CRAFTING_SHEET = id!(ns::YOG, "gui/book/crafting"),
233 DESIGNER_SCREEN = id!(ns::YOG, "designer"),
235 BOOK_TEXTURE = id!(ns::YOG, "gui/book"),
237 NO_IMAGE = id!(ns::YOG, "none"),
239 DYNAMIC_SVG_PREFIX = id!(ns::YOG, "dynamic/svg_"),
241 }
242
243 game {
245 DEFAULT_FONT = id!(ns::MINECRAFT, "default"),
247 VANILLA_THEME = id!(ns::MINECRAFT, "vanilla"),
249 OVERWORLD = id!(ns::MINECRAFT, "overworld"),
250 THE_NETHER = id!(ns::MINECRAFT, "the_nether"),
251 THE_END = id!(ns::MINECRAFT, "the_end"),
252 MISSING_TEXTURE = id!(ns::MINECRAFT, "missingno"),
254 WRITTEN_BOOK = id!(ns::MINECRAFT, "written_book"),
256 BOOK = id!(ns::MINECRAFT, "book"),
257 LOCALE = "en_us",
259 }
260
261 external {
263 ITEMS = "items",
264 BLOCKS = "blocks",
265 ENTITIES = "entities",
266 RECIPES = "recipes",
267 }
268
269 yui {
271 SCREEN = "Screen",
272 COMPONENT = "Component",
273 PROPERTY = "Property",
275 EVENT = "Event",
276 STYLE = "Style",
277 CANVAS = "Canvas",
278 COLUMN = "Column",
279 ROW = "Row",
280 GRID = "Grid",
281 SCROLL_VIEW = "ScrollView",
282 LABEL = "Label",
283 BUTTON = "Button",
284 TOGGLE_BUTTON = "ToggleButton",
285 TEXT_BOX = "TextBox",
286 IMAGE = "Image",
287 SURFACE = "Surface",
288 SPACER = "Spacer",
289 PILL = "Pill",
290 STAT_BAR = "StatBar",
291 LIST_VIEW = "ListView",
292 ITEMS_CONTROL = "ItemsControl",
293 CONTENT_PRESENTER = "ContentPresenter",
294 ITEM_SLOT = "ItemSlot",
296 INV_SLOT = "InvSlot",
297 SLOT = "Slot",
298 PLAYER_INVENTORY = "PlayerInventory",
299 MACHINE = "Machine",
301 RECIPE_AREA = "RecipeArea",
303 RECIPE_SLOT = "RecipeSlot",
305 RECIPE_RESULT = "RecipeResult",
306 RECIPE_COUNT = "RecipeCount",
307 }
308
309 item_def {
311 MODEL = "model",
312 TYPE = "type",
313 TINTS = "tints",
314 VALUE = "value",
315 PLAIN = "minecraft:model",
317 CONSTANT = "minecraft:constant",
319 }
320
321 equipment {
323 LAYERS = "layers",
324 TEXTURE = "texture",
325 HUMANOID = "humanoid",
327 HUMANOID_LEGGINGS = "humanoid_leggings",
329 }
330
331 model {
333 PARENT = "parent",
334 TEXTURES = "textures",
335 ELEMENTS = "elements",
336 DISPLAY = "display",
337 FACES = "faces",
338 FROM = "from",
339 TO = "to",
340 UV = "uv",
341 ROTATION = "rotation",
342 TEXTURE = "texture",
343 LAYER0 = "layer0",
345 PARTICLE = "particle",
347 MODEL = "model",
349 ORIGIN = "origin",
351 AXIS = "axis",
352 ANGLE = "angle",
353 RESCALE = "rescale",
354 VARIANTS = "variants",
356 MULTIPART = "multipart",
357 WHEN = "when",
358 APPLY = "apply",
359 OR = "OR",
360 AND = "AND",
361 }
362
363 content_field {
365 ID = "id",
366 KIND = "kind",
367 MODEL = "model",
368 ITEM_MODEL = "item_model",
369 MATERIAL = "material",
370 ITEM = "item",
371 POTION = "potion",
372 EFFECTS = "effects",
373 EFFECT = "effect",
374 OF = "of",
375 COLOR = "color",
376 }
377
378 client_op {
381 STATE = "state",
383 DRIVE = "drive",
385 RELEASE = "release",
387 LOOK = "look",
389 ATTACK = "attack",
391 USE = "use",
393 MINE = "mine",
395 PLACE = "place",
397 SLOT = "slot",
399 SCAN = "scan",
401 AREA = "area",
404 ENTITIES = "entities",
406 INVENTORY = "inventory",
408 CLICK = "click",
410 CHAT = "chat",
412 }
413
414 yui_attr {
416 ID = "id",
417 X = "x",
418 Y = "y",
419 W = "w",
420 H = "h",
421 MIN_W = "min_w",
422 MIN_H = "min_h",
423 FLEX = "flex",
424 DOCK = "dock",
425 GAP = "gap",
426 PADDING = "padding",
427 BG = "bg",
428 COLOR = "color",
429 ALIGN = "align",
430 HALIGN = "halign",
431 VALIGN = "valign",
432 MARGIN = "margin",
433 FONT_SCALE = "font_scale",
434 FONT = "font",
435 SHADOW = "shadow",
436 NO_WRAP = "no_wrap",
437 ELLIPSIS = "ellipsis",
438 FOCUS_STYLE = "focus_style",
439 FOCUS_COLOR = "focus_color",
440 ENABLED = "enabled",
441 ON_CLICK = "on_click",
442 TOOLTIP = "tooltip",
443 WIDTH = "width",
444 HEIGHT = "height",
445 ANCHOR = "anchor",
446 MODAL = "modal",
447 PAUSE = "pause",
448 LIVE = "live",
449 THEME = "theme",
450 TEXT = "text",
451 PLACEHOLDER = "placeholder",
452 MAX_LENGTH = "max_length",
453 ON_CHANGE = "on_change",
454 ON_SUBMIT = "on_submit",
455 ITEM = "item",
456 COUNT = "count",
457 INDEX = "index",
458 ACCEPTS = "accepts",
459 SOURCE = "source",
460 STRETCH = "stretch",
461 UV = "uv",
462 TINT = "tint",
463 KIND = "kind",
464 RESULT = "result",
465 PLACES = "places",
466 RECIPE_LAYOUT = "recipe_layout",
467 SLOT = "slot",
468 BLOCK = "block",
469 STYLE = "style",
470 BASED_ON = "based_on",
471 ORIENTATION = "orientation",
472 ROWS = "rows",
473 COLUMNS = "columns",
474 ROW_SPACING = "row_spacing",
475 COLUMN_SPACING = "column_spacing",
476 VALUE = "value",
477 SELECTED = "selected",
478 CHECKED = "checked",
479 ITEM_HEIGHT = "item_height",
480 SCROLL_KEY = "scroll_key",
481 VISIBLE = "visible",
482 GRID_ROW = "Grid.Row",
483 GRID_COLUMN = "Grid.Column",
484 GRID_ROWSPAN = "Grid.RowSpan",
485 GRID_COLUMNSPAN = "Grid.ColumnSpan",
486 }
487}
488
489macro_rules! formats {
490 ($( $(#[$doc:meta])* $variant:ident = $ext:expr, $noun:expr; )*) => {
491 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
494 pub enum Format { $( $(#[$doc])* $variant ),* }
495
496 impl Format {
497 pub const ALL: &'static [Format] = &[$(Format::$variant),*];
499
500 #[must_use]
502 pub const fn ext(self) -> &'static str {
503 match self { $(Format::$variant => $ext),* }
504 }
505
506 #[must_use]
508 pub const fn noun(self) -> &'static str {
509 match self { $(Format::$variant => $noun),* }
510 }
511 }
512 };
513}
514
515formats! {
516 Block = "yblock", kind::BLOCK;
517 Item = "yitem", kind::ITEM;
518 Entity = "yentity", kind::ENTITY;
519 Recipe = "yrecipe", kind::RECIPE;
520 Book = "ybook", kind::BOOK;
521 Def = "ydef", "definition";
523 Res = "yres", "resources";
525 Ui = "yui", "screen";
527 Model = "ymodel", "model";
528 Anim = "yanim", "animation";
529 Font = "yfont", "font";
530 Texture = "ytexture", kind::TEXTURE;
532 Material = "ymaterial", kind::MATERIAL;
534 Data = "ydata", "compiled data";
536 Mod = "yog", "mod";
538}
539
540impl Format {
541 #[must_use]
543 pub fn of(ext: &str) -> Option<Format> {
544 let ext = ext.trim_start_matches('.');
545 Format::ALL.iter().copied().find(|f| f.ext() == ext)
546 }
547
548 #[must_use]
550 pub fn of_path(path: &std::path::Path) -> Option<Format> {
551 Format::of(path.extension()?.to_str()?)
552 }
553
554 #[must_use]
556 pub fn is(self, path: &std::path::Path) -> bool {
557 Format::of_path(path) == Some(self)
558 }
559
560 #[must_use]
562 pub fn file(self, name: &str) -> String {
563 format!("{name}.{}", self.ext())
564 }
565}
566
567#[must_use]
569pub fn texture_file(path: &str) -> String {
570 format!("{}{path}{}", res::TEXTURES, res::PNG)
571}
572
573#[must_use]
575pub fn model_file(path: &str) -> String {
576 format!("{}{path}{}", res::MODELS, res::JSON)
577}
578
579#[must_use]
581pub fn blockstate_file(path: &str) -> String {
582 format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
583}
584
585#[must_use]
588pub fn registry_of(kind: &str) -> &str {
589 match kind {
590 kind::ENTITY => "entity_type",
591 kind::BIOME => "worldgen/biome",
592 other => other,
593 }
594}
595
596const PLURAL_BEFORE_1_21: &[&str] = &[kind::ITEM, kind::BLOCK, kind::ENTITY, kind::FLUID, kind::LOOT_TABLE, kind::ADVANCEMENT, kind::RECIPE];
598
599#[must_use]
602pub fn registry_dirs(kind: &str) -> Vec<String> {
603 let dir = registry_of(kind);
604 let mut out = vec![dir.to_string()];
605 if PLURAL_BEFORE_1_21.contains(&kind) {
606 out.push(format!("{dir}s"));
607 }
608 out
609}
610
611#[must_use]
613pub fn lang_file(locale: &str) -> String {
614 format!("{}{locale}{}", res::LANG, res::JSON)
615}
616
617#[must_use]
619pub fn lang_prefix(kind: &str, ns: &str) -> String {
620 format!("{kind}.{ns}.")
621}
622
623#[must_use]
625pub fn lang_key(kind: &str, id: &str) -> String {
626 let (ns, path) = split(id);
627 format!("{}{}", lang_prefix(kind, ns), path.replace(sep::PATH, "."))
628}
629
630pub fn join(ns: &str, path: &str) -> String {
632 format!("{ns}{}{path}", sep::NAMESPACE)
633}
634
635pub fn split(id: &str) -> (&str, &str) {
637 id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
638}
639
640pub fn qualify(id: &str) -> String {
642 qualify_in(ns::MINECRAFT, id)
643}
644
645pub fn qualify_in(ns: &str, id: &str) -> String {
647 if id.contains(sep::NAMESPACE) { id.to_string() } else { join(ns, id) }
648}
649
650pub mod export {
652 use super::ALL;
653
654 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`.";
655
656 pub fn typescript() -> String {
658 let mut out = format!("// {HEADER}\n");
659 for (group, names) in ALL {
660 out.push_str(&format!("\nexport const {} = {{\n", camel(group)));
661 for (name, value) in *names {
662 out.push_str(&format!(" {name}: {value:?},\n"));
663 }
664 out.push_str("} as const;\n");
665 }
666 out.push_str("\n/** Yog's file formats: extension and noun (`Format`). */\nexport const formats = {\n");
667 for f in crate::Format::ALL {
668 out.push_str(&format!(" {}: {{ ext: {:?}, noun: {:?} }},\n", camel(&format!("{f:?}").to_lowercase()), f.ext(), f.noun()));
669 }
670 out.push_str("} as const;\n");
671 out
672 }
673
674 pub fn java(package: &str, class: &str) -> String {
676 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");
677 for (group, names) in ALL {
678 out.push_str(&format!("\n public static final class {} {{\n private {}() {{}}\n\n", pascal(group), pascal(group)));
679 for (name, value) in *names {
680 out.push_str(&format!(" public static final String {name} = {value:?};\n"));
681 }
682 out.push_str(" }\n");
683 }
684 out.push_str("\n /** Yog's file formats' extensions (`Format`). */\n public static final class Ext {\n private Ext() {}\n\n");
685 for f in crate::Format::ALL {
686 out.push_str(&format!(" public static final String {} = {:?};\n", format!("{f:?}").to_uppercase(), f.ext()));
687 }
688 out.push_str(" }\n}\n");
689 out
690 }
691
692 fn pascal(s: &str) -> String {
693 s.split('_').map(|w| w[..1].to_uppercase() + &w[1..]).collect()
694 }
695
696 fn camel(s: &str) -> String {
697 let p = pascal(s);
698 p[..1].to_lowercase() + &p[1..]
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn ids_are_composed_from_their_namespace() {
708 assert_eq!(yog::STATE, "yog:state");
709 assert_eq!(game::OVERWORLD, "minecraft:overworld");
710 assert_eq!(image_scheme::ITEM, kind::ITEM);
711 assert_eq!(split("stone"), ("minecraft", "stone"));
712 assert_eq!(qualify("mymod:x"), "mymod:x");
713 assert_eq!(Format::of(".yblock"), Some(Format::Block));
714 assert_eq!(registry_of(kind::BIOME), "worldgen/biome");
715 assert_eq!(Format::Anim.file("walk"), "walk.yanim");
716 }
717
718 #[test]
720 fn the_java_names_are_current() {
721 let file = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../java-common/src/main/java/dev/yog/YogNames.java");
722 let Ok(java) = std::fs::read_to_string(&file) else { return };
723 assert!(
724 java == export::java("dev.yog", "YogNames"),
725 "{} is stale: cargo run -p yog-ids --bin yog-ids-export -- --java ../java-common/src/main/java/dev/yog dev.yog YogNames",
726 file.display()
727 );
728 }
729
730 #[test]
731 fn the_exports_name_every_group() {
732 let ts = export::typescript();
733 assert!(ts.contains("export const imageScheme = {") && ts.contains(" YOG: \"yog\","), "{ts}");
734 assert!(ts.contains(" block: { ext: \"yblock\", noun: \"block\" },"), "{ts}");
735 let java = export::java("dev.yog.generated", "YogNames");
736 assert!(java.contains("public static final class ImageScheme {") && java.contains("public static final String STATE = \"yog:state\";"), "{java}");
737 }
738}