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 res_kind {
121 STRING = "string",
122 IMAGE = typed::IMAGE,
123 FILE = "file",
124 SOUND = "sound",
125 COLOR = "color",
126 NUMBER = "number",
127 BOOL = "bool",
128 }
129
130 image_scheme {
132 ITEM = kind::ITEM,
133 BLOCK = kind::BLOCK,
134 SPRITE = "sprite",
135 MOD = "mod",
136 }
137
138 file {
140 MANIFEST = "yog.toml",
142 LOCK = "yog.lock",
144 }
145
146 pack {
148 DATA = "data",
150 ASSETS = "assets",
152 TAGS = "tags",
154 OVERLAYS = "when",
156 ANY_LOADER = "any",
158 ANY_VERSION = "any",
160 }
161
162 project {
164 RESOURCES = "resources",
166 CONTENT = "content",
168 SRC = "src",
170 BUILD = ".yog-build",
172 }
173
174 game_dir {
176 MODS = "yog-mods",
178 }
179
180 program {
182 CLI = "yog",
184 SERVE = "serve",
186 }
187
188 res {
190 TEXTURES = concatcp!("textures", sep::PATH),
192 MODELS = concatcp!("models", sep::PATH),
194 BLOCKSTATES = concatcp!("blockstates", sep::PATH),
196 ITEM_DIR = concatcp!(kind::ITEM, sep::PATH),
198 ITEM_DEFINITIONS = concatcp!("items", sep::PATH),
200 EQUIPMENT = concatcp!("equipment", sep::PATH),
202 EQUIPMENT_TEXTURES = concatcp!("entity", sep::PATH, "equipment", sep::PATH),
204 ARMOR_TEXTURES = concatcp!("models", sep::PATH, "armor", sep::PATH),
206 ARMOR_LAYER = "_layer_",
208 SPAWN_EGG_SUFFIX = "_spawn_egg",
210 BLOCK_DIR = concatcp!(kind::BLOCK, sep::PATH),
212 GUI_SPRITES = concatcp!("gui", sep::PATH, "sprites", sep::PATH),
214 ATLASES = concatcp!("atlases", sep::PATH),
216 LANG = concatcp!("lang", sep::PATH),
218 BUILTIN = concatcp!("builtin", sep::PATH),
220 PNG = ".png",
221 SVG = ".svg",
222 JSON = ".json",
223 MCMETA = ".mcmeta",
224 }
225
226 yog {
228 STATE = id!(ns::YOG, "state"),
230 HANGING = id!(ns::YOG, "hanging"),
232 ENTITY_DATA = id!(ns::YOG, "entity_data"),
234 DIMENSIONS = id!(ns::YOG, "dimensions"),
237 SLOT_PREFIX = id!(ns::YOG, "slot:"),
239 PLAYER_INVENTORY = id!(ns::YOG, "player_inventory"),
241 MACHINE_PREFIX = id!(ns::YOG, "machine:"),
243 POTION_ALIAS = id!(ns::YOG, "potion/"),
245 SPAWN_EGG_MODEL = id!(ns::YOG, "item/spawn_egg"),
247 BOOK_SHEET = id!(ns::YOG, "gui/book/book"),
249 CRAFTING_SHEET = id!(ns::YOG, "gui/book/crafting"),
250 DESIGNER_SCREEN = id!(ns::YOG, "designer"),
252 BOOK_TEXTURE = id!(ns::YOG, "gui/book"),
254 NO_IMAGE = id!(ns::YOG, "none"),
256 DYNAMIC_SVG_PREFIX = id!(ns::YOG, "dynamic/svg_"),
258 }
259
260 game {
262 DEFAULT_FONT = id!(ns::MINECRAFT, "default"),
264 VANILLA_THEME = id!(ns::MINECRAFT, "vanilla"),
266 OVERWORLD = id!(ns::MINECRAFT, "overworld"),
267 THE_NETHER = id!(ns::MINECRAFT, "the_nether"),
268 THE_END = id!(ns::MINECRAFT, "the_end"),
269 MISSING_TEXTURE = id!(ns::MINECRAFT, "missingno"),
271 WRITTEN_BOOK = id!(ns::MINECRAFT, "written_book"),
273 BOOK = id!(ns::MINECRAFT, "book"),
274 LOCALE = "en_us",
276 }
277
278 external {
280 ITEMS = "items",
281 BLOCKS = "blocks",
282 ENTITIES = "entities",
283 RECIPES = "recipes",
284 }
285
286 yui {
288 SCREEN = "Screen",
289 COMPONENT = "Component",
290 PROPERTY = "Property",
292 EVENT = "Event",
293 STYLE = "Style",
294 CANVAS = "Canvas",
295 COLUMN = "Column",
296 ROW = "Row",
297 GRID = "Grid",
298 SCROLL_VIEW = "ScrollView",
299 LABEL = "Label",
300 BUTTON = "Button",
301 TOGGLE_BUTTON = "ToggleButton",
302 TEXT_BOX = "TextBox",
303 IMAGE = "Image",
304 SURFACE = "Surface",
305 SPACER = "Spacer",
306 PILL = "Pill",
307 STAT_BAR = "StatBar",
308 LIST_VIEW = "ListView",
309 ITEMS_CONTROL = "ItemsControl",
310 CONTENT_PRESENTER = "ContentPresenter",
311 ITEM_SLOT = "ItemSlot",
313 INV_SLOT = "InvSlot",
314 SLOT = "Slot",
315 PLAYER_INVENTORY = "PlayerInventory",
316 MACHINE = "Machine",
318 RECIPE_AREA = "RecipeArea",
320 RECIPE_SLOT = "RecipeSlot",
322 RECIPE_RESULT = "RecipeResult",
323 RECIPE_COUNT = "RecipeCount",
324 }
325
326 item_def {
328 MODEL = "model",
329 TYPE = "type",
330 TINTS = "tints",
331 VALUE = "value",
332 PLAIN = "minecraft:model",
334 CONSTANT = "minecraft:constant",
336 }
337
338 equipment {
340 LAYERS = "layers",
341 TEXTURE = "texture",
342 HUMANOID = "humanoid",
344 HUMANOID_LEGGINGS = "humanoid_leggings",
346 }
347
348 model {
350 PARENT = "parent",
351 TEXTURES = "textures",
352 ELEMENTS = "elements",
353 DISPLAY = "display",
354 FACES = "faces",
355 FROM = "from",
356 TO = "to",
357 UV = "uv",
358 ROTATION = "rotation",
359 TEXTURE = "texture",
360 LAYER0 = "layer0",
362 PARTICLE = "particle",
364 MODEL = "model",
366 ORIGIN = "origin",
368 AXIS = "axis",
369 ANGLE = "angle",
370 RESCALE = "rescale",
371 VARIANTS = "variants",
373 MULTIPART = "multipart",
374 WHEN = "when",
375 APPLY = "apply",
376 OR = "OR",
377 AND = "AND",
378 }
379
380 content_field {
382 ID = "id",
383 KIND = "kind",
384 MODEL = "model",
385 ITEM_MODEL = "item_model",
386 MATERIAL = "material",
387 ITEM = "item",
388 POTION = "potion",
389 EFFECTS = "effects",
390 EFFECT = "effect",
391 OF = "of",
392 COLOR = "color",
393 }
394
395 client_op {
398 STATE = "state",
400 DRIVE = "drive",
402 RELEASE = "release",
404 LOOK = "look",
406 ATTACK = "attack",
408 USE = "use",
410 MINE = "mine",
412 PLACE = "place",
414 SLOT = "slot",
416 SCAN = "scan",
418 AREA = "area",
421 ENTITIES = "entities",
423 INVENTORY = "inventory",
425 CLICK = "click",
427 CHAT = "chat",
429 }
430
431 yui_attr {
433 ID = "id",
434 X = "x",
435 Y = "y",
436 W = "w",
437 H = "h",
438 MIN_W = "min_w",
439 MIN_H = "min_h",
440 FLEX = "flex",
441 DOCK = "dock",
442 GAP = "gap",
443 PADDING = "padding",
444 BG = "bg",
445 COLOR = "color",
446 ALIGN = "align",
447 HALIGN = "halign",
448 VALIGN = "valign",
449 MARGIN = "margin",
450 FONT_SCALE = "font_scale",
451 FONT = "font",
452 SHADOW = "shadow",
453 NO_WRAP = "no_wrap",
454 ELLIPSIS = "ellipsis",
455 FOCUS_STYLE = "focus_style",
456 FOCUS_COLOR = "focus_color",
457 ENABLED = "enabled",
458 ON_CLICK = "on_click",
459 TOOLTIP = "tooltip",
460 WIDTH = "width",
461 HEIGHT = "height",
462 ANCHOR = "anchor",
463 MODAL = "modal",
464 PAUSE = "pause",
465 LIVE = "live",
466 THEME = "theme",
467 TEXT = "text",
468 PLACEHOLDER = "placeholder",
469 MAX_LENGTH = "max_length",
470 ON_CHANGE = "on_change",
471 ON_SUBMIT = "on_submit",
472 ITEM = "item",
473 COUNT = "count",
474 INDEX = "index",
475 ACCEPTS = "accepts",
476 SOURCE = "source",
477 STRETCH = "stretch",
478 UV = "uv",
479 TINT = "tint",
480 KIND = "kind",
481 RESULT = "result",
482 PLACES = "places",
483 RECIPE_LAYOUT = "recipe_layout",
484 SLOT = "slot",
485 BLOCK = "block",
486 STYLE = "style",
487 BASED_ON = "based_on",
488 ORIENTATION = "orientation",
489 ROWS = "rows",
490 COLUMNS = "columns",
491 ROW_SPACING = "row_spacing",
492 COLUMN_SPACING = "column_spacing",
493 VALUE = "value",
494 SELECTED = "selected",
495 CHECKED = "checked",
496 ITEM_HEIGHT = "item_height",
497 SCROLL_KEY = "scroll_key",
498 VISIBLE = "visible",
499 GRID_ROW = "Grid.Row",
500 GRID_COLUMN = "Grid.Column",
501 GRID_ROWSPAN = "Grid.RowSpan",
502 GRID_COLUMNSPAN = "Grid.ColumnSpan",
503 }
504}
505
506macro_rules! formats {
507 ($( $(#[$doc:meta])* $variant:ident = $ext:expr, $noun:expr; )*) => {
508 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
511 pub enum Format { $( $(#[$doc])* $variant ),* }
512
513 impl Format {
514 pub const ALL: &'static [Format] = &[$(Format::$variant),*];
516
517 #[must_use]
519 pub const fn ext(self) -> &'static str {
520 match self { $(Format::$variant => $ext),* }
521 }
522
523 #[must_use]
525 pub const fn noun(self) -> &'static str {
526 match self { $(Format::$variant => $noun),* }
527 }
528 }
529 };
530}
531
532formats! {
533 Block = "yblock", kind::BLOCK;
534 Item = "yitem", kind::ITEM;
535 Entity = "yentity", kind::ENTITY;
536 Recipe = "yrecipe", kind::RECIPE;
537 Book = "ybook", kind::BOOK;
538 Def = "ydef", "definition";
540 Res = "yres", "resources";
542 Ui = "yui", "screen";
544 Model = "ymodel", "model";
545 Anim = "yanim", "animation";
546 Font = "yfont", "font";
547 Texture = "ytexture", kind::TEXTURE;
549 Material = "ymaterial", kind::MATERIAL;
551 Data = "ydata", "compiled data";
553 Mod = "yog", "mod";
555}
556
557impl Format {
558 #[must_use]
560 pub fn of(ext: &str) -> Option<Format> {
561 let ext = ext.trim_start_matches('.');
562 Format::ALL.iter().copied().find(|f| f.ext() == ext)
563 }
564
565 #[must_use]
567 pub fn of_path(path: &std::path::Path) -> Option<Format> {
568 Format::of(path.extension()?.to_str()?)
569 }
570
571 #[must_use]
573 pub fn is(self, path: &std::path::Path) -> bool {
574 Format::of_path(path) == Some(self)
575 }
576
577 #[must_use]
579 pub fn file(self, name: &str) -> String {
580 format!("{name}.{}", self.ext())
581 }
582}
583
584#[must_use]
586pub fn texture_file(path: &str) -> String {
587 format!("{}{path}{}", res::TEXTURES, res::PNG)
588}
589
590#[must_use]
592pub fn model_file(path: &str) -> String {
593 format!("{}{path}{}", res::MODELS, res::JSON)
594}
595
596#[must_use]
598pub fn blockstate_file(path: &str) -> String {
599 format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
600}
601
602#[must_use]
605pub fn registry_of(kind: &str) -> &str {
606 match kind {
607 kind::ENTITY => "entity_type",
608 kind::BIOME => "worldgen/biome",
609 other => other,
610 }
611}
612
613const PLURAL_BEFORE_1_21: &[&str] = &[kind::ITEM, kind::BLOCK, kind::ENTITY, kind::FLUID, kind::LOOT_TABLE, kind::ADVANCEMENT, kind::RECIPE];
615
616#[must_use]
619pub fn registry_dirs(kind: &str) -> Vec<String> {
620 let dir = registry_of(kind);
621 let mut out = vec![dir.to_string()];
622 if PLURAL_BEFORE_1_21.contains(&kind) {
623 out.push(format!("{dir}s"));
624 }
625 out
626}
627
628#[must_use]
630pub fn lang_file(locale: &str) -> String {
631 format!("{}{locale}{}", res::LANG, res::JSON)
632}
633
634#[must_use]
636pub fn lang_prefix(kind: &str, ns: &str) -> String {
637 format!("{kind}.{ns}.")
638}
639
640#[must_use]
642pub fn lang_key(kind: &str, id: &str) -> String {
643 let (ns, path) = split(id);
644 format!("{}{}", lang_prefix(kind, ns), path.replace(sep::PATH, "."))
645}
646
647pub fn join(ns: &str, path: &str) -> String {
649 format!("{ns}{}{path}", sep::NAMESPACE)
650}
651
652pub fn split(id: &str) -> (&str, &str) {
654 id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
655}
656
657pub fn qualify(id: &str) -> String {
659 qualify_in(ns::MINECRAFT, id)
660}
661
662pub fn qualify_in(ns: &str, id: &str) -> String {
664 if id.contains(sep::NAMESPACE) { id.to_string() } else { join(ns, id) }
665}
666
667pub mod export {
669 use super::ALL;
670
671 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`.";
672
673 pub fn typescript() -> String {
675 let mut out = format!("// {HEADER}\n");
676 for (group, names) in ALL {
677 out.push_str(&format!("\nexport const {} = {{\n", camel(group)));
678 for (name, value) in *names {
679 out.push_str(&format!(" {name}: {value:?},\n"));
680 }
681 out.push_str("} as const;\n");
682 }
683 out.push_str("\n/** Yog's file formats: extension and noun (`Format`). */\nexport const formats = {\n");
684 for f in crate::Format::ALL {
685 out.push_str(&format!(" {}: {{ ext: {:?}, noun: {:?} }},\n", camel(&format!("{f:?}").to_lowercase()), f.ext(), f.noun()));
686 }
687 out.push_str("} as const;\n");
688 out
689 }
690
691 pub fn java(package: &str, class: &str) -> String {
693 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");
694 for (group, names) in ALL {
695 out.push_str(&format!("\n public static final class {} {{\n private {}() {{}}\n\n", pascal(group), pascal(group)));
696 for (name, value) in *names {
697 out.push_str(&format!(" public static final String {name} = {value:?};\n"));
698 }
699 out.push_str(" }\n");
700 }
701 out.push_str("\n /** Yog's file formats' extensions (`Format`). */\n public static final class Ext {\n private Ext() {}\n\n");
702 for f in crate::Format::ALL {
703 out.push_str(&format!(" public static final String {} = {:?};\n", format!("{f:?}").to_uppercase(), f.ext()));
704 }
705 out.push_str(" }\n}\n");
706 out
707 }
708
709 fn pascal(s: &str) -> String {
710 s.split('_').map(|w| w[..1].to_uppercase() + &w[1..]).collect()
711 }
712
713 fn camel(s: &str) -> String {
714 let p = pascal(s);
715 p[..1].to_lowercase() + &p[1..]
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn ids_are_composed_from_their_namespace() {
725 assert_eq!(yog::STATE, "yog:state");
726 assert_eq!(game::OVERWORLD, "minecraft:overworld");
727 assert_eq!(image_scheme::ITEM, kind::ITEM);
728 assert_eq!(split("stone"), ("minecraft", "stone"));
729 assert_eq!(qualify("mymod:x"), "mymod:x");
730 assert_eq!(Format::of(".yblock"), Some(Format::Block));
731 assert_eq!(registry_of(kind::BIOME), "worldgen/biome");
732 assert_eq!(Format::Anim.file("walk"), "walk.yanim");
733 }
734
735 #[test]
737 fn the_java_names_are_current() {
738 let file = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../java-common/src/main/java/dev/yog/YogNames.java");
739 let Ok(java) = std::fs::read_to_string(&file) else { return };
740 assert!(
741 java == export::java("dev.yog", "YogNames"),
742 "{} is stale: cargo run -p yog-ids --bin yog-ids-export -- --java ../java-common/src/main/java/dev/yog dev.yog YogNames",
743 file.display()
744 );
745 }
746
747 #[test]
748 fn the_exports_name_every_group() {
749 let ts = export::typescript();
750 assert!(ts.contains("export const imageScheme = {") && ts.contains(" YOG: \"yog\","), "{ts}");
751 assert!(ts.contains(" block: { ext: \"yblock\", noun: \"block\" },"), "{ts}");
752 let java = export::java("dev.yog.generated", "YogNames");
753 assert!(java.contains("public static final class ImageScheme {") && java.contains("public static final String STATE = \"yog:state\";"), "{java}");
754 }
755}