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    /// A game's folders Yog uses.
164    game_dir {
165        /// The `.yog` mods the game loads: `<game>/yog-mods`.
166        MODS = "yog-mods",
167    }
168
169    /// Yog's programs.
170    program {
171        /// The command-line tool (`yog build`, `yog schema`, …).
172        CLI = "yog",
173        /// The CLI's service command: tools read and write every format through it.
174        SERVE = "serve",
175    }
176
177    /// Where the game keeps resources, and what their files end with.
178    res {
179        /// The textures' folder: `<ns>:textures/<path>.png`.
180        TEXTURES = concatcp!("textures", sep::PATH),
181        /// The models' folder: `<ns>:models/<path>.json`.
182        MODELS = concatcp!("models", sep::PATH),
183        /// The block states' folder: `<ns>:blockstates/<path>.json`.
184        BLOCKSTATES = concatcp!("blockstates", sep::PATH),
185        /// Items' textures and models: `item/apple`.
186        ITEM_DIR = concatcp!(kind::ITEM, sep::PATH),
187        /// Item definitions (1.21.4+): `<ns>:items/<path>.json`.
188        ITEM_DEFINITIONS = concatcp!("items", sep::PATH),
189        /// Equipment's worn looks (1.21.4+): `<ns>:equipment/<path>.json`.
190        EQUIPMENT = concatcp!("equipment", sep::PATH),
191        /// Worn equipment's textures (1.21.4+): `entity/equipment/<layer>/<path>`.
192        EQUIPMENT_TEXTURES = concatcp!("entity", sep::PATH, "equipment", sep::PATH),
193        /// Armor's worn textures before 1.21.4: `models/armor/<path>_layer_<n>`.
194        ARMOR_TEXTURES = concatcp!("models", sep::PATH, "armor", sep::PATH),
195        /// Between an armor texture's name and its layer's number.
196        ARMOR_LAYER = "_layer_",
197        /// What a spawn egg's item path ends with: `<mob>_spawn_egg`.
198        SPAWN_EGG_SUFFIX = "_spawn_egg",
199        /// Blocks' textures and models: `block/stone`.
200        BLOCK_DIR = concatcp!(kind::BLOCK, sep::PATH),
201        /// The GUI atlas's sprites under `textures/` (1.20.2+).
202        GUI_SPRITES = concatcp!("gui", sep::PATH, "sprites", sep::PATH),
203        /// The atlases' definitions: `<ns>:atlases/<atlas>.json` (every pack's sources combine).
204        ATLASES = concatcp!("atlases", sep::PATH),
205        /// The language files' folder: `<ns>:lang/<locale>.json`.
206        LANG = concatcp!("lang", sep::PATH),
207        /// Models the game draws in code (`builtin/entity`), not from elements.
208        BUILTIN = concatcp!("builtin", sep::PATH),
209        PNG = ".png",
210        SVG = ".svg",
211        JSON = ".json",
212        MCMETA = ".mcmeta",
213    }
214
215    /// Yog's own ids.
216    yog {
217        /// An entity's data the hosts keep for the mods.
218        STATE = id!(ns::YOG, "state"),
219        /// A hanging entity's attachment (its face, size, item).
220        HANGING = id!(ns::YOG, "hanging"),
221        /// The channel entity data is synced on.
222        ENTITY_DATA = id!(ns::YOG, "entity_data"),
223        /// The entity data key an entity's own hitbox travels under: width,
224        /// height, eye height as three little-endian f32.
225        DIMENSIONS = id!(ns::YOG, "dimensions"),
226        /// An inventory slot widget's id: `yog:slot:<index>`.
227        SLOT_PREFIX = id!(ns::YOG, "slot:"),
228        /// The player's inventory widget.
229        PLAYER_INVENTORY = id!(ns::YOG, "player_inventory"),
230        /// A machine screen's id: `yog:machine:<id>`.
231        MACHINE_PREFIX = id!(ns::YOG, "machine:"),
232        /// Yog's alias of a game potion: `yog:potion/<name>`.
233        POTION_ALIAS = id!(ns::YOG, "potion/"),
234        /// The model every mod's spawn egg inherits: Yog's egg in two layers, base and spots.
235        SPAWN_EGG_MODEL = id!(ns::YOG, "item/spawn_egg"),
236        /// The book's pictures.
237        BOOK_SHEET = id!(ns::YOG, "gui/book/book"),
238        CRAFTING_SHEET = id!(ns::YOG, "gui/book/crafting"),
239        /// A screen being designed in the IDE.
240        DESIGNER_SCREEN = id!(ns::YOG, "designer"),
241        /// A book's default texture (its sheet is [`BOOK_SHEET`]).
242        BOOK_TEXTURE = id!(ns::YOG, "gui/book"),
243        /// An image source that draws nothing (a bare surface).
244        NO_IMAGE = id!(ns::YOG, "none"),
245        /// The textures the runtime renders SVG images into: `<prefix><key>_<w>x<h>`.
246        DYNAMIC_SVG_PREFIX = id!(ns::YOG, "dynamic/svg_"),
247    }
248
249    /// The game's ids Yog's code names.
250    game {
251        /// The font text is drawn in by default.
252        DEFAULT_FONT = id!(ns::MINECRAFT, "default"),
253        /// The UI theme drawn like the game's own screens.
254        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        /// The texture the game draws where one is missing.
259        MISSING_TEXTURE = id!(ns::MINECRAFT, "missingno"),
260        /// What a book is given to players as.
261        WRITTEN_BOOK = id!(ns::MINECRAFT, "written_book"),
262        BOOK = id!(ns::MINECRAFT, "book"),
263        /// The language every lang file falls back to, and a mod's texts are written in.
264        LOCALE = "en_us",
265    }
266
267    /// What `[external.<namespace>]` in `yog.toml` lists another mod's content under.
268    external {
269        ITEMS = "items",
270        BLOCKS = "blocks",
271        ENTITIES = "entities",
272        RECIPES = "recipes",
273    }
274
275    /// The elements of a `.yui` screen or component.
276    yui {
277        SCREEN = "Screen",
278        COMPONENT = "Component",
279        /// A component's declarations.
280        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        /// Slots: an item shown, an inventory's, the container's.
301        ITEM_SLOT = "ItemSlot",
302        INV_SLOT = "InvSlot",
303        SLOT = "Slot",
304        PLAYER_INVENTORY = "PlayerInventory",
305        /// Which container slot is which, for a block that makes recipes.
306        MACHINE = "Machine",
307        /// A kind of recipe's layout on an inventory screen.
308        RECIPE_AREA = "RecipeArea",
309        /// A recipe layout's places.
310        RECIPE_SLOT = "RecipeSlot",
311        RECIPE_RESULT = "RecipeResult",
312        RECIPE_COUNT = "RecipeCount",
313    }
314
315    /// The fields and types of the game's item definitions (`items/<path>.json`, 1.21.4+).
316    item_def {
317        MODEL = "model",
318        TYPE = "type",
319        TINTS = "tints",
320        VALUE = "value",
321        /// A plain model.
322        PLAIN = "minecraft:model",
323        /// A tint of one colour.
324        CONSTANT = "minecraft:constant",
325    }
326
327    /// The fields and layers of the game's equipment assets (`equipment/<path>.json`, 1.21.4+).
328    equipment {
329        LAYERS = "layers",
330        TEXTURE = "texture",
331        /// Worn on the body: helmet, chestplate, boots.
332        HUMANOID = "humanoid",
333        /// Worn on the legs: leggings.
334        HUMANOID_LEGGINGS = "humanoid_leggings",
335    }
336
337    /// The fields of the game's model JSON (block and item models, block states' variants).
338    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        /// A sprite item's (`item/generated`) first layer.
350        LAYER0 = "layer0",
351        /// The texture a block's particles take, and what a face falls back to.
352        PARTICLE = "particle",
353        /// A block state variant's model.
354        MODEL = "model",
355        /// An element's rotation.
356        ORIGIN = "origin",
357        AXIS = "axis",
358        ANGLE = "angle",
359        RESCALE = "rescale",
360        /// Block states: variants by property values, or parts applied when they match.
361        VARIANTS = "variants",
362        MULTIPART = "multipart",
363        WHEN = "when",
364        APPLY = "apply",
365        OR = "OR",
366        AND = "AND",
367    }
368
369    /// The fields of Yog's content files (`.yblock`, `.yitem`, `.ydef`) the tools read directly.
370    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    /// What a mod asks of this client (`client_op`, ABI 3.9) — what the
385    /// local player is and sees, and what it does: arguments and answers are JSON.
386    client_op {
387        /// Where it is and how it's doing: position, look, motion, health, food, dimension, slot.
388        STATE = "state",
389        /// Moves as if keys were held — `forward`, `strafe` in -1..1, `jump`, `sneak`, `sprint` — till `release`.
390        DRIVE = "drive",
391        /// Hands the movement keys back to the player.
392        RELEASE = "release",
393        /// Turns to `yaw`, `pitch` (degrees).
394        LOOK = "look",
395        /// Attacks entity `entity` (its UUID), or what the crosshair is on.
396        ATTACK = "attack",
397        /// Uses the held item (`off_hand` for the other hand).
398        USE = "use",
399        /// Keeps breaking the block at `x y z` from `face`; `stop` to give up.
400        MINE = "mine",
401        /// Places the held block against the block at `x y z`, on `face`.
402        PLACE = "place",
403        /// Selects hotbar slot `slot` (0–8).
404        SLOT = "slot",
405        /// The blocks among `ids` between `min` and `max`: `[x, y, z, id]` each.
406        SCAN = "scan",
407        /// The blocks between `min` and `max` for finding a way: a palette
408        /// (`id`, `solid`, `liquid`, `climbable`) and an index per block, x fastest.
409        AREA = "area",
410        /// The entities within `radius`: `uuid`, `type`, position, health, a player's `name`.
411        ENTITIES = "entities",
412        /// The player's inventory: `slot`, `item`, `count` each.
413        INVENTORY = "inventory",
414        /// Clicks slot `slot` of the open container (`button`, `mode` as the game's).
415        CLICK = "click",
416        /// Sends `text` to the chat (a command when it starts with `/`).
417        CHAT = "chat",
418    }
419
420    /// The attributes of `.yui` elements.
421    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        /// Yog's file formats: each one's extension and what a file of it is
498        /// called in messages. What each looks like is `yog_formats::shape`.
499        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
500        pub enum Format { $( $(#[$doc])* $variant ),* }
501
502        impl Format {
503            /// Every format.
504            pub const ALL: &'static [Format] = &[$(Format::$variant),*];
505
506            /// Its extension, without the dot: `yblock`.
507            #[must_use]
508            pub const fn ext(self) -> &'static str {
509                match self { $(Format::$variant => $ext),* }
510            }
511
512            /// What one file of it is: `block`, `animation`.
513            #[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    /// A definition of some kind (`kind: "ladder"`).
528    Def = "ydef", "definition";
529    /// Named resources: strings, colours, sounds, numbers.
530    Res = "yres", "resources";
531    /// A screen or a component.
532    Ui = "yui", "screen";
533    Model = "ymodel", "model";
534    Anim = "yanim", "animation";
535    Font = "yfont", "font";
536    /// A texture: its picture as given and how it animates.
537    Texture = "ytexture", kind::TEXTURE;
538    /// A material: the textures a surface is drawn with, and how.
539    Material = "ymaterial", kind::MATERIAL;
540    /// Compiled Yog Data.
541    Data = "ydata", "compiled data";
542    /// A built mod.
543    Mod = "yog", "mod";
544}
545
546impl Format {
547    /// The format with extension `ext` (with or without the dot).
548    #[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    /// The format of the file at `path`, by its extension.
555    #[must_use]
556    pub fn of_path(path: &std::path::Path) -> Option<Format> {
557        Format::of(path.extension()?.to_str()?)
558    }
559
560    /// Whether `path` is a file of this format.
561    #[must_use]
562    pub fn is(self, path: &std::path::Path) -> bool {
563        Format::of_path(path) == Some(self)
564    }
565
566    /// `name` with this format's extension: `stone` → `stone.yblock`.
567    #[must_use]
568    pub fn file(self, name: &str) -> String {
569        format!("{name}.{}", self.ext())
570    }
571}
572
573/// Texture `path`'s file under its namespace: `block/stone` → `textures/block/stone.png`.
574#[must_use]
575pub fn texture_file(path: &str) -> String {
576    format!("{}{path}{}", res::TEXTURES, res::PNG)
577}
578
579/// Model `path`'s file: `item/apple` → `models/item/apple.json`.
580#[must_use]
581pub fn model_file(path: &str) -> String {
582    format!("{}{path}{}", res::MODELS, res::JSON)
583}
584
585/// Block `path`'s states' file: `stone` → `blockstates/stone.json`.
586#[must_use]
587pub fn blockstate_file(path: &str) -> String {
588    format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
589}
590
591/// The game's registry of content `kind` — what its tags' and data's
592/// folders are named: `entity` → `entity_type`, `biome` → `worldgen/biome`.
593#[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
602/// The kinds whose data and tag folders were plural before 1.21 (`items`, `loot_tables`).
603const PLURAL_BEFORE_1_21: &[&str] = &[kind::ITEM, kind::BLOCK, kind::ENTITY, kind::FLUID, kind::LOOT_TABLE, kind::ADVANCEMENT, kind::RECIPE];
604
605/// The folders of content `kind`'s data or tags, the current one first, then
606/// 1.20's plural one when it was named differently: `item` → `item`, `items`.
607#[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/// Language `locale`'s file under a namespace: `lang/en_us.json`.
618#[must_use]
619pub fn lang_file(locale: &str) -> String {
620    format!("{}{locale}{}", res::LANG, res::JSON)
621}
622
623/// What the game's translation keys of content `kind` in namespace `ns` start with: `item.minecraft.`.
624#[must_use]
625pub fn lang_prefix(kind: &str, ns: &str) -> String {
626    format!("{kind}.{ns}.")
627}
628
629/// The game's translation key of content `kind` `id`: `item.minecraft.iron_ingot`, a path's `/` as `.`.
630#[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
636/// `ns:path`.
637pub fn join(ns: &str, path: &str) -> String {
638    format!("{ns}{}{path}", sep::NAMESPACE)
639}
640
641/// `id`'s namespace and path; a bare path is the game's.
642pub fn split(id: &str) -> (&str, &str) {
643    id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
644}
645
646/// `id` with a namespace: the game's if it has none.
647pub fn qualify(id: &str) -> String {
648    qualify_in(ns::MINECRAFT, id)
649}
650
651/// `id` with a namespace: `ns` if it has none (what a mod writes bare is its own).
652pub fn qualify_in(ns: &str, id: &str) -> String {
653    if id.contains(sep::NAMESPACE) { id.to_string() } else { join(ns, id) }
654}
655
656/// The names for the other languages: generated, never edited by hand.
657pub 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    /// A TypeScript module: one `as const` object per group.
663    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    /// A Java class (`package`): a nested class of constants per group.
681    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    /// The hosts' copy (`java-common`), when this crate is in its repository.
725    #[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}