Skip to main content

yog_ids/
lib.rs

1//! yog-ids — the names every part of Yog shares, defined once: namespaces,
2//! kinds of content, typed-value words, image-source schemes, separators and
3//! Yog's own ids. The lowest crate: everything above takes these from here
4//! (composing its own constants with [`concatcp!`] / [`id!`]); the IDE
5//! (TypeScript) and the hosts (Java) get them generated ([`export`]).
6
7pub use const_format::concatcp;
8
9/// `id!(ns::YOG, "state")` → `"yog:state"`, at compile time.
10#[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)] // a group composes names from the ones above
23                use super::*;
24                $( $(#[$doc])* pub const $name: &str = $value; )*
25            }
26        )*
27        /// Every name, by group — what [`export`] writes.
28        pub const ALL: &[(&str, &[(&str, &str)])] = &[ $( (stringify!($group), &[ $( (stringify!($name), $group::$name) ),* ]) ),* ];
29    };
30}
31
32names! {
33    /// Namespaces.
34    ns {
35        /// The game's: ids without a namespace are its.
36        MINECRAFT = "minecraft",
37        /// Yog's own: its tags, its channels, its aliases of the game's content.
38        YOG = "yog",
39    }
40
41    /// What separates the parts of names.
42    sep {
43        /// A namespace from its path: `minecraft:stone`.
44        NAMESPACE = ":",
45        /// A path's parts: `textures/block/stone`.
46        PATH = "/",
47        /// What starts a typed value: `@item:minecraft:stick`.
48        TYPED = "@",
49        /// What marks a tag where an ingredient is shown: `#c:dusts`.
50        TAG_SHOWN = "#",
51        /// What joins an ingredient's alternatives where it's shown: `a | b`.
52        ALTERNATIVES = "|",
53        /// What separates the fields of a host's reply to the runtime.
54        FIELD = "\t",
55    }
56
57    /// The kinds of content: what a reference names (`@item:…`), what a
58    /// `.ydef` defines (`kind: "ladder"`), what a mod registers with the
59    /// host (`content("potion", …)`).
60    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    /// What a reference to content of a kind starts with: `@item:`.
96    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 values that aren't references to content.
111    typed {
112        TAG = kind::TAG,
113        LANG = "lang",
114        IMAGE = "image",
115        /// Bytes as they are, base64 in text: `@bytes:iVBORw0…`.
116        BYTES = "bytes",
117    }
118
119    /// What an image source starts with (`item:apple`).
120    image_scheme {
121        ITEM = kind::ITEM,
122        BLOCK = kind::BLOCK,
123        SPRITE = "sprite",
124        MOD = "mod",
125    }
126
127    /// Yog's files with a fixed name.
128    file {
129        /// A mod's manifest.
130        MANIFEST = "yog.toml",
131        /// The versions a mod's build resolved.
132        LOCK = "yog.lock",
133    }
134
135    /// The pack the build writes for the hosts.
136    pack {
137        /// The game's data: `data/<ns>/…`.
138        DATA = "data",
139        /// The game's assets: `assets/<ns>/…`.
140        ASSETS = "assets",
141        /// Tags, under a namespace's data: `data/<ns>/tags/<registry>/<path>.json`.
142        TAGS = "tags",
143        /// Overlays for one loader and version: `when/<loader>/<version>/<file>`.
144        OVERLAYS = "when",
145        /// An overlay's loader when it's for every loader.
146        ANY_LOADER = "any",
147        /// An overlay's version when it's for every version.
148        ANY_VERSION = "any",
149    }
150
151    /// A mod project's folders.
152    project {
153        /// Its resources: `.yres` sets, `.ytexture`s, ….
154        RESOURCES = "resources",
155        /// Its content: blocks, items, recipes, books, definitions.
156        CONTENT = "content",
157        /// Its code and screens.
158        SRC = "src",
159        /// Where `yog build` writes: the generated crate, generated code, listings.
160        BUILD = ".yog-build",
161    }
162
163    /// Yog's programs.
164    program {
165        /// The command-line tool (`yog build`, `yog schema`, …).
166        CLI = "yog",
167        /// The CLI's service command: tools read and write every format through it.
168        SERVE = "serve",
169    }
170
171    /// Where the game keeps resources, and what their files end with.
172    res {
173        /// The textures' folder: `<ns>:textures/<path>.png`.
174        TEXTURES = concatcp!("textures", sep::PATH),
175        /// The models' folder: `<ns>:models/<path>.json`.
176        MODELS = concatcp!("models", sep::PATH),
177        /// The block states' folder: `<ns>:blockstates/<path>.json`.
178        BLOCKSTATES = concatcp!("blockstates", sep::PATH),
179        /// Items' textures and models: `item/apple`.
180        ITEM_DIR = concatcp!(kind::ITEM, sep::PATH),
181        /// Blocks' textures and models: `block/stone`.
182        BLOCK_DIR = concatcp!(kind::BLOCK, sep::PATH),
183        /// The GUI atlas's sprites under `textures/` (1.20.2+).
184        GUI_SPRITES = concatcp!("gui", sep::PATH, "sprites", sep::PATH),
185        /// The atlases' definitions: `<ns>:atlases/<atlas>.json` (every pack's sources combine).
186        ATLASES = concatcp!("atlases", sep::PATH),
187        /// The language files' folder: `<ns>:lang/<locale>.json`.
188        LANG = concatcp!("lang", sep::PATH),
189        /// Models the game draws in code (`builtin/entity`), not from elements.
190        BUILTIN = concatcp!("builtin", sep::PATH),
191        PNG = ".png",
192        SVG = ".svg",
193        JSON = ".json",
194        MCMETA = ".mcmeta",
195    }
196
197    /// Yog's own ids.
198    yog {
199        /// An entity's data the hosts keep for the mods.
200        STATE = id!(ns::YOG, "state"),
201        /// A hanging entity's attachment (its face, size, item).
202        HANGING = id!(ns::YOG, "hanging"),
203        /// The channel entity data is synced on.
204        ENTITY_DATA = id!(ns::YOG, "entity_data"),
205        /// The entity data key an entity's own hitbox travels under: width,
206        /// height, eye height as three little-endian f32.
207        DIMENSIONS = id!(ns::YOG, "dimensions"),
208        /// An inventory slot widget's id: `yog:slot:<index>`.
209        SLOT_PREFIX = id!(ns::YOG, "slot:"),
210        /// The player's inventory widget.
211        PLAYER_INVENTORY = id!(ns::YOG, "player_inventory"),
212        /// A machine screen's id: `yog:machine:<id>`.
213        MACHINE_PREFIX = id!(ns::YOG, "machine:"),
214        /// Yog's alias of a game potion: `yog:potion/<name>`.
215        POTION_ALIAS = id!(ns::YOG, "potion/"),
216        /// The book's pictures.
217        BOOK_SHEET = id!(ns::YOG, "gui/book/book"),
218        CRAFTING_SHEET = id!(ns::YOG, "gui/book/crafting"),
219        /// A screen being designed in the IDE.
220        DESIGNER_SCREEN = id!(ns::YOG, "designer"),
221        /// A book's default texture (its sheet is [`BOOK_SHEET`]).
222        BOOK_TEXTURE = id!(ns::YOG, "gui/book"),
223        /// An image source that draws nothing (a bare surface).
224        NO_IMAGE = id!(ns::YOG, "none"),
225        /// The textures the runtime renders SVG images into: `<prefix><key>_<w>x<h>`.
226        DYNAMIC_SVG_PREFIX = id!(ns::YOG, "dynamic/svg_"),
227    }
228
229    /// The game's ids Yog's code names.
230    game {
231        /// The font text is drawn in by default.
232        DEFAULT_FONT = id!(ns::MINECRAFT, "default"),
233        /// The UI theme drawn like the game's own screens.
234        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        /// The texture the game draws where one is missing.
239        MISSING_TEXTURE = id!(ns::MINECRAFT, "missingno"),
240        /// What a book is given to players as.
241        WRITTEN_BOOK = id!(ns::MINECRAFT, "written_book"),
242        BOOK = id!(ns::MINECRAFT, "book"),
243        /// The language every lang file falls back to, and a mod's texts are written in.
244        LOCALE = "en_us",
245    }
246
247    /// What `[external.<namespace>]` in `yog.toml` lists another mod's content under.
248    external {
249        ITEMS = "items",
250        BLOCKS = "blocks",
251        ENTITIES = "entities",
252        RECIPES = "recipes",
253    }
254
255    /// The elements of a `.yui` screen or component.
256    yui {
257        SCREEN = "Screen",
258        COMPONENT = "Component",
259        /// A component's declarations.
260        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        /// Slots: an item shown, an inventory's, the container's.
281        ITEM_SLOT = "ItemSlot",
282        INV_SLOT = "InvSlot",
283        SLOT = "Slot",
284        PLAYER_INVENTORY = "PlayerInventory",
285        /// Which container slot is which, for a block that makes recipes.
286        MACHINE = "Machine",
287        /// A kind of recipe's layout on an inventory screen.
288        RECIPE_AREA = "RecipeArea",
289        /// A recipe layout's places.
290        RECIPE_SLOT = "RecipeSlot",
291        RECIPE_RESULT = "RecipeResult",
292        RECIPE_COUNT = "RecipeCount",
293    }
294
295    /// The fields of the game's model JSON (block and item models, block states' variants).
296    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        /// A sprite item's (`item/generated`) first layer.
308        LAYER0 = "layer0",
309        /// The texture a block's particles take, and what a face falls back to.
310        PARTICLE = "particle",
311        /// A block state variant's model.
312        MODEL = "model",
313        /// An element's rotation.
314        ORIGIN = "origin",
315        AXIS = "axis",
316        ANGLE = "angle",
317        RESCALE = "rescale",
318        /// Block states: variants by property values, or parts applied when they match.
319        VARIANTS = "variants",
320        MULTIPART = "multipart",
321        WHEN = "when",
322        APPLY = "apply",
323        OR = "OR",
324        AND = "AND",
325    }
326
327    /// The fields of Yog's content files (`.yblock`, `.yitem`, `.ydef`) the tools read directly.
328    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    /// The attributes of `.yui` elements.
342    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        /// Yog's file formats: each one's extension and what a file of it is
419        /// called in messages. What each looks like is `yog_formats::shape`.
420        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
421        pub enum Format { $( $(#[$doc])* $variant ),* }
422
423        impl Format {
424            /// Every format.
425            pub const ALL: &'static [Format] = &[$(Format::$variant),*];
426
427            /// Its extension, without the dot: `yblock`.
428            #[must_use]
429            pub const fn ext(self) -> &'static str {
430                match self { $(Format::$variant => $ext),* }
431            }
432
433            /// What one file of it is: `block`, `animation`.
434            #[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    /// A definition of some kind (`kind: "ladder"`).
449    Def = "ydef", "definition";
450    /// Named resources: strings, colours, sounds, numbers.
451    Res = "yres", "resources";
452    /// A screen or a component.
453    Ui = "yui", "screen";
454    Model = "ymodel", "model";
455    Anim = "yanim", "animation";
456    Font = "yfont", "font";
457    /// A texture: its picture as given and how it animates.
458    Texture = "ytexture", kind::TEXTURE;
459    /// A material: the textures a surface is drawn with, and how.
460    Material = "ymaterial", kind::MATERIAL;
461    /// Compiled Yog Data.
462    Data = "ydata", "compiled data";
463    /// A built mod.
464    Mod = "yog", "mod";
465}
466
467impl Format {
468    /// The format with extension `ext` (with or without the dot).
469    #[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    /// The format of the file at `path`, by its extension.
476    #[must_use]
477    pub fn of_path(path: &std::path::Path) -> Option<Format> {
478        Format::of(path.extension()?.to_str()?)
479    }
480
481    /// Whether `path` is a file of this format.
482    #[must_use]
483    pub fn is(self, path: &std::path::Path) -> bool {
484        Format::of_path(path) == Some(self)
485    }
486
487    /// `name` with this format's extension: `stone` → `stone.yblock`.
488    #[must_use]
489    pub fn file(self, name: &str) -> String {
490        format!("{name}.{}", self.ext())
491    }
492}
493
494/// Texture `path`'s file under its namespace: `block/stone` → `textures/block/stone.png`.
495#[must_use]
496pub fn texture_file(path: &str) -> String {
497    format!("{}{path}{}", res::TEXTURES, res::PNG)
498}
499
500/// Model `path`'s file: `item/apple` → `models/item/apple.json`.
501#[must_use]
502pub fn model_file(path: &str) -> String {
503    format!("{}{path}{}", res::MODELS, res::JSON)
504}
505
506/// Block `path`'s states' file: `stone` → `blockstates/stone.json`.
507#[must_use]
508pub fn blockstate_file(path: &str) -> String {
509    format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
510}
511
512/// The game's registry of content `kind` — what its tags' and data's
513/// folders are named: `entity` → `entity_type`, `biome` → `worldgen/biome`.
514#[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
523/// The kinds whose data and tag folders were plural before 1.21 (`items`, `loot_tables`).
524const PLURAL_BEFORE_1_21: &[&str] = &[kind::ITEM, kind::BLOCK, kind::ENTITY, kind::FLUID, kind::LOOT_TABLE, kind::ADVANCEMENT, kind::RECIPE];
525
526/// The folders of content `kind`'s data or tags, the current one first, then
527/// 1.20's plural one when it was named differently: `item` → `item`, `items`.
528#[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/// Language `locale`'s file under a namespace: `lang/en_us.json`.
539#[must_use]
540pub fn lang_file(locale: &str) -> String {
541    format!("{}{locale}{}", res::LANG, res::JSON)
542}
543
544/// What the game's translation keys of content `kind` in namespace `ns` start with: `item.minecraft.`.
545#[must_use]
546pub fn lang_prefix(kind: &str, ns: &str) -> String {
547    format!("{kind}.{ns}.")
548}
549
550/// The game's translation key of content `kind` `id`: `item.minecraft.iron_ingot`, a path's `/` as `.`.
551#[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
557/// `ns:path`.
558pub fn join(ns: &str, path: &str) -> String {
559    format!("{ns}{}{path}", sep::NAMESPACE)
560}
561
562/// `id`'s namespace and path; a bare path is the game's.
563pub fn split(id: &str) -> (&str, &str) {
564    id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
565}
566
567/// `id` with a namespace: the game's if it has none.
568pub fn qualify(id: &str) -> String {
569    qualify_in(ns::MINECRAFT, id)
570}
571
572/// `id` with a namespace: `ns` if it has none (what a mod writes bare is its own).
573pub fn qualify_in(ns: &str, id: &str) -> String {
574    if id.contains(sep::NAMESPACE) { id.to_string() } else { join(ns, id) }
575}
576
577/// The names for the other languages: generated, never edited by hand.
578pub 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    /// A TypeScript module: one `as const` object per group.
584    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    /// A Java class (`package`): a nested class of constants per group.
602    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    /// The hosts' copy (`java-common`), when this crate is in its repository.
646    #[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}