pub use const_format::concatcp;
#[macro_export]
macro_rules! id {
($ns:expr, $path:expr) => {
$crate::concatcp!($ns, $crate::sep::NAMESPACE, $path)
};
}
macro_rules! names {
($( $(#[$gdoc:meta])* $group:ident { $( $(#[$doc:meta])* $name:ident = $value:expr ),* $(,)? } )*) => {
$(
$(#[$gdoc])*
pub mod $group {
#[allow(unused_imports)] use super::*;
$( $(#[$doc])* pub const $name: &str = $value; )*
}
)*
pub const ALL: &[(&str, &[(&str, &str)])] = &[ $( (stringify!($group), &[ $( (stringify!($name), $group::$name) ),* ]) ),* ];
};
}
names! {
ns {
MINECRAFT = "minecraft",
YOG = "yog",
}
sep {
NAMESPACE = ":",
PATH = "/",
TYPED = "@",
TAG_SHOWN = "#",
ALTERNATIVES = "|",
FIELD = "\t",
}
kind {
ITEM = "item",
BLOCK = "block",
ENTITY = "entity",
RECIPE = "recipe",
ENTRY = "entry",
CATEGORY = "category",
BOOK = "book",
FLUID = "fluid",
BIOME = "biome",
BIOME_FEATURE = "biome_feature",
DIMENSION = "dimension",
ATTRIBUTE = "attribute",
EFFECT = "effect",
POTION = "potion",
INVENTORY = "inventory",
STARTUP_GRANT = "startup_grant",
ADVANCEMENT = "advancement",
LADDER = "ladder",
LEVEL = "level",
SOUND_GROUP = "sound_group",
RECIPE_KIND = "recipe_kind",
TOOL_MATERIAL = "tool_material",
ARMOR_MATERIAL = "armor_material",
ORE = "ore",
TABLE = "table",
TAG = "tag",
DAMAGE_TYPE = "damage_type",
TREE = "tree",
LOOT_MODIFIER = "loot_modifier",
LOOT_TABLE = "loot_table",
TEXTURE = "texture",
MATERIAL = "material",
}
reference {
ITEM = concatcp!(sep::TYPED, kind::ITEM, sep::NAMESPACE),
BLOCK = concatcp!(sep::TYPED, kind::BLOCK, sep::NAMESPACE),
ENTITY = concatcp!(sep::TYPED, kind::ENTITY, sep::NAMESPACE),
RECIPE = concatcp!(sep::TYPED, kind::RECIPE, sep::NAMESPACE),
ENTRY = concatcp!(sep::TYPED, kind::ENTRY, sep::NAMESPACE),
CATEGORY = concatcp!(sep::TYPED, kind::CATEGORY, sep::NAMESPACE),
FLUID = concatcp!(sep::TYPED, kind::FLUID, sep::NAMESPACE),
BIOME = concatcp!(sep::TYPED, kind::BIOME, sep::NAMESPACE),
DIMENSION = concatcp!(sep::TYPED, kind::DIMENSION, sep::NAMESPACE),
TEXTURE = concatcp!(sep::TYPED, kind::TEXTURE, sep::NAMESPACE),
MATERIAL = concatcp!(sep::TYPED, kind::MATERIAL, sep::NAMESPACE),
}
typed {
TAG = kind::TAG,
LANG = "lang",
IMAGE = "image",
BYTES = "bytes",
}
image_scheme {
ITEM = kind::ITEM,
BLOCK = kind::BLOCK,
SPRITE = "sprite",
MOD = "mod",
}
file {
MANIFEST = "yog.toml",
LOCK = "yog.lock",
}
pack {
DATA = "data",
ASSETS = "assets",
TAGS = "tags",
OVERLAYS = "when",
ANY_LOADER = "any",
ANY_VERSION = "any",
}
project {
RESOURCES = "resources",
CONTENT = "content",
SRC = "src",
BUILD = ".yog-build",
}
program {
CLI = "yog",
SERVE = "serve",
}
res {
TEXTURES = concatcp!("textures", sep::PATH),
MODELS = concatcp!("models", sep::PATH),
BLOCKSTATES = concatcp!("blockstates", sep::PATH),
ITEM_DIR = concatcp!(kind::ITEM, sep::PATH),
ITEM_DEFINITIONS = concatcp!("items", sep::PATH),
EQUIPMENT = concatcp!("equipment", sep::PATH),
EQUIPMENT_TEXTURES = concatcp!("entity", sep::PATH, "equipment", sep::PATH),
ARMOR_TEXTURES = concatcp!("models", sep::PATH, "armor", sep::PATH),
ARMOR_LAYER = "_layer_",
SPAWN_EGG_SUFFIX = "_spawn_egg",
BLOCK_DIR = concatcp!(kind::BLOCK, sep::PATH),
GUI_SPRITES = concatcp!("gui", sep::PATH, "sprites", sep::PATH),
ATLASES = concatcp!("atlases", sep::PATH),
LANG = concatcp!("lang", sep::PATH),
BUILTIN = concatcp!("builtin", sep::PATH),
PNG = ".png",
SVG = ".svg",
JSON = ".json",
MCMETA = ".mcmeta",
}
yog {
STATE = id!(ns::YOG, "state"),
HANGING = id!(ns::YOG, "hanging"),
ENTITY_DATA = id!(ns::YOG, "entity_data"),
DIMENSIONS = id!(ns::YOG, "dimensions"),
SLOT_PREFIX = id!(ns::YOG, "slot:"),
PLAYER_INVENTORY = id!(ns::YOG, "player_inventory"),
MACHINE_PREFIX = id!(ns::YOG, "machine:"),
POTION_ALIAS = id!(ns::YOG, "potion/"),
SPAWN_EGG_MODEL = id!(ns::YOG, "item/spawn_egg"),
BOOK_SHEET = id!(ns::YOG, "gui/book/book"),
CRAFTING_SHEET = id!(ns::YOG, "gui/book/crafting"),
DESIGNER_SCREEN = id!(ns::YOG, "designer"),
BOOK_TEXTURE = id!(ns::YOG, "gui/book"),
NO_IMAGE = id!(ns::YOG, "none"),
DYNAMIC_SVG_PREFIX = id!(ns::YOG, "dynamic/svg_"),
}
game {
DEFAULT_FONT = id!(ns::MINECRAFT, "default"),
VANILLA_THEME = id!(ns::MINECRAFT, "vanilla"),
OVERWORLD = id!(ns::MINECRAFT, "overworld"),
THE_NETHER = id!(ns::MINECRAFT, "the_nether"),
THE_END = id!(ns::MINECRAFT, "the_end"),
MISSING_TEXTURE = id!(ns::MINECRAFT, "missingno"),
WRITTEN_BOOK = id!(ns::MINECRAFT, "written_book"),
BOOK = id!(ns::MINECRAFT, "book"),
LOCALE = "en_us",
}
external {
ITEMS = "items",
BLOCKS = "blocks",
ENTITIES = "entities",
RECIPES = "recipes",
}
yui {
SCREEN = "Screen",
COMPONENT = "Component",
PROPERTY = "Property",
EVENT = "Event",
STYLE = "Style",
CANVAS = "Canvas",
COLUMN = "Column",
ROW = "Row",
GRID = "Grid",
SCROLL_VIEW = "ScrollView",
LABEL = "Label",
BUTTON = "Button",
TOGGLE_BUTTON = "ToggleButton",
TEXT_BOX = "TextBox",
IMAGE = "Image",
SURFACE = "Surface",
SPACER = "Spacer",
PILL = "Pill",
STAT_BAR = "StatBar",
LIST_VIEW = "ListView",
ITEMS_CONTROL = "ItemsControl",
CONTENT_PRESENTER = "ContentPresenter",
ITEM_SLOT = "ItemSlot",
INV_SLOT = "InvSlot",
SLOT = "Slot",
PLAYER_INVENTORY = "PlayerInventory",
MACHINE = "Machine",
RECIPE_AREA = "RecipeArea",
RECIPE_SLOT = "RecipeSlot",
RECIPE_RESULT = "RecipeResult",
RECIPE_COUNT = "RecipeCount",
}
item_def {
MODEL = "model",
TYPE = "type",
TINTS = "tints",
VALUE = "value",
PLAIN = "minecraft:model",
CONSTANT = "minecraft:constant",
}
equipment {
LAYERS = "layers",
TEXTURE = "texture",
HUMANOID = "humanoid",
HUMANOID_LEGGINGS = "humanoid_leggings",
}
model {
PARENT = "parent",
TEXTURES = "textures",
ELEMENTS = "elements",
DISPLAY = "display",
FACES = "faces",
FROM = "from",
TO = "to",
UV = "uv",
ROTATION = "rotation",
TEXTURE = "texture",
LAYER0 = "layer0",
PARTICLE = "particle",
MODEL = "model",
ORIGIN = "origin",
AXIS = "axis",
ANGLE = "angle",
RESCALE = "rescale",
VARIANTS = "variants",
MULTIPART = "multipart",
WHEN = "when",
APPLY = "apply",
OR = "OR",
AND = "AND",
}
content_field {
ID = "id",
KIND = "kind",
MODEL = "model",
ITEM_MODEL = "item_model",
MATERIAL = "material",
ITEM = "item",
POTION = "potion",
EFFECTS = "effects",
EFFECT = "effect",
OF = "of",
COLOR = "color",
}
client_op {
STATE = "state",
DRIVE = "drive",
RELEASE = "release",
LOOK = "look",
ATTACK = "attack",
USE = "use",
MINE = "mine",
PLACE = "place",
SLOT = "slot",
SCAN = "scan",
AREA = "area",
ENTITIES = "entities",
INVENTORY = "inventory",
CLICK = "click",
CHAT = "chat",
}
yui_attr {
ID = "id",
X = "x",
Y = "y",
W = "w",
H = "h",
MIN_W = "min_w",
MIN_H = "min_h",
FLEX = "flex",
DOCK = "dock",
GAP = "gap",
PADDING = "padding",
BG = "bg",
COLOR = "color",
ALIGN = "align",
HALIGN = "halign",
VALIGN = "valign",
MARGIN = "margin",
FONT_SCALE = "font_scale",
FONT = "font",
SHADOW = "shadow",
NO_WRAP = "no_wrap",
ELLIPSIS = "ellipsis",
FOCUS_STYLE = "focus_style",
FOCUS_COLOR = "focus_color",
ENABLED = "enabled",
ON_CLICK = "on_click",
TOOLTIP = "tooltip",
WIDTH = "width",
HEIGHT = "height",
ANCHOR = "anchor",
MODAL = "modal",
PAUSE = "pause",
LIVE = "live",
THEME = "theme",
TEXT = "text",
PLACEHOLDER = "placeholder",
MAX_LENGTH = "max_length",
ON_CHANGE = "on_change",
ON_SUBMIT = "on_submit",
ITEM = "item",
COUNT = "count",
INDEX = "index",
ACCEPTS = "accepts",
SOURCE = "source",
STRETCH = "stretch",
UV = "uv",
TINT = "tint",
KIND = "kind",
RESULT = "result",
PLACES = "places",
RECIPE_LAYOUT = "recipe_layout",
SLOT = "slot",
BLOCK = "block",
STYLE = "style",
BASED_ON = "based_on",
ORIENTATION = "orientation",
ROWS = "rows",
COLUMNS = "columns",
ROW_SPACING = "row_spacing",
COLUMN_SPACING = "column_spacing",
VALUE = "value",
SELECTED = "selected",
CHECKED = "checked",
ITEM_HEIGHT = "item_height",
SCROLL_KEY = "scroll_key",
VISIBLE = "visible",
GRID_ROW = "Grid.Row",
GRID_COLUMN = "Grid.Column",
GRID_ROWSPAN = "Grid.RowSpan",
GRID_COLUMNSPAN = "Grid.ColumnSpan",
}
}
macro_rules! formats {
($( $(#[$doc:meta])* $variant:ident = $ext:expr, $noun:expr; )*) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format { $( $(#[$doc])* $variant ),* }
impl Format {
pub const ALL: &'static [Format] = &[$(Format::$variant),*];
#[must_use]
pub const fn ext(self) -> &'static str {
match self { $(Format::$variant => $ext),* }
}
#[must_use]
pub const fn noun(self) -> &'static str {
match self { $(Format::$variant => $noun),* }
}
}
};
}
formats! {
Block = "yblock", kind::BLOCK;
Item = "yitem", kind::ITEM;
Entity = "yentity", kind::ENTITY;
Recipe = "yrecipe", kind::RECIPE;
Book = "ybook", kind::BOOK;
Def = "ydef", "definition";
Res = "yres", "resources";
Ui = "yui", "screen";
Model = "ymodel", "model";
Anim = "yanim", "animation";
Font = "yfont", "font";
Texture = "ytexture", kind::TEXTURE;
Material = "ymaterial", kind::MATERIAL;
Data = "ydata", "compiled data";
Mod = "yog", "mod";
}
impl Format {
#[must_use]
pub fn of(ext: &str) -> Option<Format> {
let ext = ext.trim_start_matches('.');
Format::ALL.iter().copied().find(|f| f.ext() == ext)
}
#[must_use]
pub fn of_path(path: &std::path::Path) -> Option<Format> {
Format::of(path.extension()?.to_str()?)
}
#[must_use]
pub fn is(self, path: &std::path::Path) -> bool {
Format::of_path(path) == Some(self)
}
#[must_use]
pub fn file(self, name: &str) -> String {
format!("{name}.{}", self.ext())
}
}
#[must_use]
pub fn texture_file(path: &str) -> String {
format!("{}{path}{}", res::TEXTURES, res::PNG)
}
#[must_use]
pub fn model_file(path: &str) -> String {
format!("{}{path}{}", res::MODELS, res::JSON)
}
#[must_use]
pub fn blockstate_file(path: &str) -> String {
format!("{}{path}{}", res::BLOCKSTATES, res::JSON)
}
#[must_use]
pub fn registry_of(kind: &str) -> &str {
match kind {
kind::ENTITY => "entity_type",
kind::BIOME => "worldgen/biome",
other => other,
}
}
const PLURAL_BEFORE_1_21: &[&str] = &[kind::ITEM, kind::BLOCK, kind::ENTITY, kind::FLUID, kind::LOOT_TABLE, kind::ADVANCEMENT, kind::RECIPE];
#[must_use]
pub fn registry_dirs(kind: &str) -> Vec<String> {
let dir = registry_of(kind);
let mut out = vec![dir.to_string()];
if PLURAL_BEFORE_1_21.contains(&kind) {
out.push(format!("{dir}s"));
}
out
}
#[must_use]
pub fn lang_file(locale: &str) -> String {
format!("{}{locale}{}", res::LANG, res::JSON)
}
#[must_use]
pub fn lang_prefix(kind: &str, ns: &str) -> String {
format!("{kind}.{ns}.")
}
#[must_use]
pub fn lang_key(kind: &str, id: &str) -> String {
let (ns, path) = split(id);
format!("{}{}", lang_prefix(kind, ns), path.replace(sep::PATH, "."))
}
pub fn join(ns: &str, path: &str) -> String {
format!("{ns}{}{path}", sep::NAMESPACE)
}
pub fn split(id: &str) -> (&str, &str) {
id.split_once(sep::NAMESPACE).unwrap_or((ns::MINECRAFT, id))
}
pub fn qualify(id: &str) -> String {
qualify_in(ns::MINECRAFT, id)
}
pub fn qualify_in(ns: &str, id: &str) -> String {
if id.contains(sep::NAMESPACE) { id.to_string() } else { join(ns, id) }
}
pub mod export {
use super::ALL;
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`.";
pub fn typescript() -> String {
let mut out = format!("// {HEADER}\n");
for (group, names) in ALL {
out.push_str(&format!("\nexport const {} = {{\n", camel(group)));
for (name, value) in *names {
out.push_str(&format!(" {name}: {value:?},\n"));
}
out.push_str("} as const;\n");
}
out.push_str("\n/** Yog's file formats: extension and noun (`Format`). */\nexport const formats = {\n");
for f in crate::Format::ALL {
out.push_str(&format!(" {}: {{ ext: {:?}, noun: {:?} }},\n", camel(&format!("{f:?}").to_lowercase()), f.ext(), f.noun()));
}
out.push_str("} as const;\n");
out
}
pub fn java(package: &str, class: &str) -> String {
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");
for (group, names) in ALL {
out.push_str(&format!("\n public static final class {} {{\n private {}() {{}}\n\n", pascal(group), pascal(group)));
for (name, value) in *names {
out.push_str(&format!(" public static final String {name} = {value:?};\n"));
}
out.push_str(" }\n");
}
out.push_str("\n /** Yog's file formats' extensions (`Format`). */\n public static final class Ext {\n private Ext() {}\n\n");
for f in crate::Format::ALL {
out.push_str(&format!(" public static final String {} = {:?};\n", format!("{f:?}").to_uppercase(), f.ext()));
}
out.push_str(" }\n}\n");
out
}
fn pascal(s: &str) -> String {
s.split('_').map(|w| w[..1].to_uppercase() + &w[1..]).collect()
}
fn camel(s: &str) -> String {
let p = pascal(s);
p[..1].to_lowercase() + &p[1..]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_are_composed_from_their_namespace() {
assert_eq!(yog::STATE, "yog:state");
assert_eq!(game::OVERWORLD, "minecraft:overworld");
assert_eq!(image_scheme::ITEM, kind::ITEM);
assert_eq!(split("stone"), ("minecraft", "stone"));
assert_eq!(qualify("mymod:x"), "mymod:x");
assert_eq!(Format::of(".yblock"), Some(Format::Block));
assert_eq!(registry_of(kind::BIOME), "worldgen/biome");
assert_eq!(Format::Anim.file("walk"), "walk.yanim");
}
#[test]
fn the_java_names_are_current() {
let file = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../java-common/src/main/java/dev/yog/YogNames.java");
let Ok(java) = std::fs::read_to_string(&file) else { return };
assert!(
java == export::java("dev.yog", "YogNames"),
"{} is stale: cargo run -p yog-ids --bin yog-ids-export -- --java ../java-common/src/main/java/dev/yog dev.yog YogNames",
file.display()
);
}
#[test]
fn the_exports_name_every_group() {
let ts = export::typescript();
assert!(ts.contains("export const imageScheme = {") && ts.contains(" YOG: \"yog\","), "{ts}");
assert!(ts.contains(" block: { ext: \"yblock\", noun: \"block\" },"), "{ts}");
let java = export::java("dev.yog.generated", "YogNames");
assert!(java.contains("public static final class ImageScheme {") && java.contains("public static final String STATE = \"yog:state\";"), "{java}");
}
}