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