use yog_data::kinds::Item;
use yog_data::with::{Opt, RefTo};
use yog_data::{Ordered, Reader, Res, Schema, Shape, Text, Value, ValueRef};
use crate::def::{ArmorMaterialKind, LevelKind, SoundGroupKind, TOOL_LEVELS, ToolMaterialKind};
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Register {
#[default]
Auto,
Manual,
}
#[derive(Schema, Debug, Clone)]
pub struct Food {
#[data(range = "0..=1000")]
pub nutrition: u32,
#[data(default, range = "0..=100")]
pub saturation: f64,
#[data(default)]
pub always_edible: bool,
}
#[derive(Schema, Debug, Clone, Default)]
pub struct ItemProps {
#[data(range = "1..=99")]
pub stack_size: Option<u8>,
#[data(range = "0..=1000000000")]
pub durability: Option<u32>,
#[data(default)]
pub fire_resistant: bool,
#[data(range = "0..=1000000000")]
pub fuel_ticks: Option<u32>,
pub food: Option<Food>,
pub tool: Option<ToolProps>,
pub armor: Option<ArmorProps>,
pub wearable: Option<ArmorSlot>,
}
#[derive(Schema, Debug, Clone)]
pub struct ToolProps {
#[data(with = "ToolKindRef")]
pub kind: String,
#[data(with = "MaterialRef<ToolMaterialKind>")]
pub material: String,
pub attack_damage: Option<f32>,
pub attack_speed: Option<f32>,
}
impl ToolProps {
pub fn attack(&self) -> (f32, f32) {
let (d, s) = match self.kind.as_str() {
"sword" => (3.0, -2.4),
"axe" => (6.0, -3.1),
"shovel" => (1.5, -3.0),
"hoe" => (0.0, -1.0),
_ => (1.0, -2.8),
};
(self.attack_damage.unwrap_or(d), self.attack_speed.unwrap_or(s))
}
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArmorSlot {
Head,
Chest,
Legs,
Feet,
}
#[derive(Schema, Debug, Clone)]
pub struct ArmorProps {
pub slot: ArmorSlot,
#[data(with = "MaterialRef<ArmorMaterialKind>")]
pub material: String,
}
pub const TOOL_KINDS: [&str; 5] = ["pickaxe", "axe", "shovel", "hoe", "sword"];
pub struct ToolKindRef;
impl yog_data::with::With<String> for ToolKindRef {
fn shape() -> Shape {
Shape::Tag(&["block"])
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<String> {
match v {
ValueRef::Str(s) if TOOL_KINDS.contains(s) => Some(s.to_string()),
ValueRef::Str(s) if !s.starts_with('@') => {
cx.problem(format!("`{s}`: one of {} — or a mod's kind of tool by its mineable tag, `@tag:block:mymod:mineable/hammer`", TOOL_KINDS.join(", ")));
None
}
_ => yog_data::TagId::<yog_data::kinds::Block>::read(v, cx).map(|t| t.id),
}
}
fn write(f: &String) -> Value {
if TOOL_KINDS.contains(&f.as_str()) {
Value::from(f.as_str())
} else {
Value::Tag(yog_data::Ref::new("block", f.clone()))
}
}
}
pub struct MaterialRef<K>(std::marker::PhantomData<K>);
pub trait MaterialKind: yog_data::Kind {
fn built_in(name: &str) -> Option<String>;
fn names() -> Vec<String>;
}
impl MaterialKind for ToolMaterialKind {
fn built_in(name: &str) -> Option<String> {
yog_registry::ToolMaterial::built_in(name).map(|m| m.id.clone())
}
fn names() -> Vec<String> {
yog_registry::ToolMaterial::built_ins().iter().map(|m| m.id.trim_start_matches("minecraft:").to_string()).collect()
}
}
impl MaterialKind for ArmorMaterialKind {
fn built_in(name: &str) -> Option<String> {
yog_registry::ArmorMaterial::built_in(name).map(|m| m.id.clone())
}
fn names() -> Vec<String> {
yog_registry::ArmorMaterial::built_ins().iter().map(|m| m.id.trim_start_matches("minecraft:").to_string()).collect()
}
}
impl<K: MaterialKind> yog_data::with::With<String> for MaterialRef<K> {
fn shape() -> Shape {
Shape::Ref(<K as yog_data::Kinds>::NAMES)
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<String> {
match v {
ValueRef::Str(s) if !s.starts_with('@') => K::built_in(s).or_else(|| {
cx.problem(format!("`{s}`: one of the game's ({}) — or a mod's, `@{}:mymod:…` (a .ydef)", K::names().join(", "), K::NAME));
None
}),
_ => yog_data::with::RefTo::<K>::read(v, cx),
}
}
fn write(f: &String) -> Value {
match f.strip_prefix("minecraft:") {
Some(s) if K::built_in(s).is_some() => Value::from(s),
_ => Value::Ref(yog_data::Ref::new(K::NAME, f.clone())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockTag(pub String);
pub struct ToolTag;
pub struct SoundRef;
pub struct LevelRef;
pub const TOOLS: [&str; 4] = ["pickaxe", "axe", "shovel", "hoe"];
fn block_tag(
v: &ValueRef,
cx: &mut Reader,
short: &[&str],
full: fn(&str) -> String,
what: &str,
) -> Option<BlockTag> {
match v {
ValueRef::Str(s) if short.contains(s) => Some(BlockTag(full(s))),
ValueRef::Str(s) if !s.starts_with('@') => {
cx.problem(format!(
"`{s}`: one of {} — or a tag of the mod's own {what}s, `@tag:block:mymod:…`",
short.join(", ")
));
None
}
_ => yog_data::TagId::<yog_data::kinds::Block>::read(v, cx).map(|t| BlockTag(t.id)),
}
}
impl yog_data::with::With<BlockTag> for ToolTag {
fn shape() -> Shape {
Shape::Tag(&["block"])
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<BlockTag> {
block_tag(v, cx, &TOOLS, |s| format!("minecraft:mineable/{s}"), "tool")
}
fn write(f: &BlockTag) -> Value {
match f.0.strip_prefix("minecraft:mineable/") {
Some(s) if TOOLS.contains(&s) => Value::from(s),
_ => Value::Tag(yog_data::Ref::new("block", f.0.clone())),
}
}
}
impl yog_data::with::With<String> for LevelRef {
fn shape() -> Shape {
Shape::Ref(&["level"])
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<String> {
match v {
ValueRef::Str(s) if TOOL_LEVELS.contains(s) => Some(format!("minecraft:{s}")),
ValueRef::Str(s) if !s.starts_with('@') => {
cx.problem(format!(
"`{s}`: one of {} — or a level of the tool ladder, `@level:mymod:…` (a .ydef)",
TOOL_LEVELS.join(", ")
));
None
}
_ => yog_data::with::RefTo::<LevelKind>::read(v, cx),
}
}
fn write(f: &String) -> Value {
match f.strip_prefix("minecraft:") {
Some(s) if TOOL_LEVELS.contains(&s) => Value::from(s),
_ => Value::Ref(yog_data::Ref::new("level", f.clone())),
}
}
}
impl yog_data::with::With<String> for SoundRef {
fn shape() -> Shape {
Shape::Ref(&["sound_group"])
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<String> {
match v {
ValueRef::Str(s) if !s.starts_with('@') => match yog_registry::SoundGroup::built_in(s) {
Some(g) => Some(g.id.clone()),
None => {
cx.problem(format!("`{s}`: not one of the game's sound groups (stone, wood, metal, amethyst…) — a mod's is `@sound_group:mymod:…` (a .ydef)"));
None
}
},
_ => yog_data::with::RefTo::<SoundGroupKind>::read(v, cx),
}
}
fn write(f: &String) -> Value {
match f.strip_prefix("minecraft:") {
Some(s) if yog_registry::SoundGroup::built_in(s).is_some() => Value::from(s),
_ => Value::Ref(yog_data::Ref::new("sound_group", f.clone())),
}
}
}
pub struct OneOrMany<W>(std::marker::PhantomData<W>);
impl<F, W: yog_data::with::With<F>> yog_data::with::With<Vec<F>> for OneOrMany<W> {
fn shape() -> Shape {
Shape::List(W::shape)
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Vec<F>> {
match v {
ValueRef::List(_) => yog_data::with::List::<W>::read(v, cx),
_ => W::read(v, cx).map(|x| vec![x]),
}
}
fn write(f: &Vec<F>) -> Value {
match f.as_slice() {
[one] => W::write(one),
many => Value::List(many.iter().map(W::write).collect()),
}
}
fn absent() -> Option<Vec<F>> {
Some(Vec::new())
}
fn skip(f: &Vec<F>) -> bool {
f.is_empty()
}
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderLayer {
Solid,
Cutout,
CutoutMipped,
Translucent,
}
pub fn word<T: Schema>(v: &T) -> String {
v.write().as_str().unwrap_or_default().to_string()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BlockShape {
Auto,
Full,
Box([f64; 6]),
}
impl Schema for BlockShape {
fn shape() -> Shape {
Shape::Any
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
match v {
ValueRef::Str(s) if *s == "auto" => Some(BlockShape::Auto),
ValueRef::Str(s) if *s == "full" => Some(BlockShape::Full),
ValueRef::List(_) => {
let b = <[f64; 6]>::read(v, cx)?;
if b.iter().any(|n| !(0.0..=16.0).contains(n)) {
cx.problem("a box is [x1, y1, z1, x2, y2, z2] in pixels, 0–16");
return None;
}
Some(BlockShape::Box(b))
}
other => {
cx.expected(
"\"auto\" (the model's bounds), \"full\" or [x1, y1, z1, x2, y2, z2] in pixels",
other,
);
None
}
}
}
fn write(&self) -> Value {
match self {
BlockShape::Auto => Value::from("auto"),
BlockShape::Full => Value::from("full"),
BlockShape::Box(b) => b.write(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DropCount {
Exactly(f64),
Between(f64, f64),
}
impl Schema for DropCount {
fn shape() -> Shape {
Shape::Any
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
let n = match v {
ValueRef::List(_) => {
let [lo, hi] = <[f64; 2]>::read(v, cx)?;
(lo, hi, DropCount::Between(lo, hi))
}
_ => {
let n = f64::read(v, cx)?;
(n, n, DropCount::Exactly(n))
}
};
if !(0.0..=64.0).contains(&n.0) || !(0.0..=64.0).contains(&n.1) || n.0 > n.1 {
cx.problem("a count is 1–64, or [min, max] within 0–64");
return None;
}
Some(n.2)
}
fn write(&self) -> Value {
match *self {
DropCount::Exactly(n) => Value::Float(n),
DropCount::Between(a, b) => Value::List(vec![Value::Float(a), Value::Float(b)]),
}
}
}
#[derive(Schema, Debug, Clone)]
pub struct DropItem {
#[data(with = "Opt<RefTo<Item>>")]
pub item: Option<String>,
pub count: Option<DropCount>,
}
#[derive(Debug, Clone)]
pub enum Drops {
Itself,
Nothing,
Item(DropItem),
}
impl Schema for Drops {
fn shape() -> Shape {
Shape::Any
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
match v {
ValueRef::Str(s) if *s == "self" => Some(Drops::Itself),
ValueRef::Str(s) if *s == "none" => Some(Drops::Nothing),
ValueRef::Map(_) => DropItem::read(v, cx).map(Drops::Item),
other => {
cx.expected(
"\"self\", \"none\" or { item: \"@item:ns:item\", count: [1, 3] }",
other,
);
None
}
}
}
fn write(&self) -> Value {
match self {
Drops::Itself => Value::from("self"),
Drops::Nothing => Value::from("none"),
Drops::Item(d) => d.write(),
}
}
}
#[derive(Schema, Debug, Clone, Default)]
pub struct BlockProps {
#[data(range = "-1..=1000000")]
pub hardness: Option<f64>,
#[data(range = "0..=10000000")]
pub resistance: Option<f64>,
#[data(with = "Opt<SoundRef>")]
pub sound: Option<String>,
#[data(range = "0..=15")]
pub light: Option<u8>,
#[data(with = "OneOrMany<ToolTag>")]
pub tool: Vec<BlockTag>,
#[data(with = "Opt<LevelRef>")]
pub tool_level: Option<String>,
#[data(default)]
pub requires_tool: bool,
pub collision: Option<bool>,
#[data(range = "0..=1")]
pub slipperiness: Option<f64>,
pub shape: Option<BlockShape>,
pub render_layer: Option<RenderLayer>,
pub drops: Option<Drops>,
#[data(default)]
pub connect_groups: Vec<String>,
pub inventory: Option<String>,
#[data(default)]
pub redstone: bool,
pub powered: Option<String>,
pub comparator: Option<String>,
#[data(default)]
pub accepts_items: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Texture {
Slot(String),
Res(Res),
Plain(String),
}
impl Schema for Texture {
fn shape() -> Shape {
Shape::Image
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
match v {
ValueRef::Res { .. } => Res::read(v, cx).map(Texture::Res),
ValueRef::Str(s) if s.starts_with('#') => {
if s.len() == 1 {
cx.problem("`#` needs a slot name");
return None;
}
Some(Texture::Slot(s[1..].to_string()))
}
ValueRef::Str(s) if s.starts_with('@') => Res::read(v, cx).map(Texture::Res),
ValueRef::Str(s) => Some(Texture::Plain(s.to_string())),
other => {
cx.expected("a texture", other);
None
}
}
}
fn write(&self) -> Value {
match self {
Texture::Slot(s) => Value::Str(format!("#{s}")),
Texture::Res(r) => Value::Res(r.clone()),
Texture::Plain(s) => Value::Str(s.clone()),
}
}
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
X,
Y,
Z,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Down,
Up,
North,
South,
West,
East,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum GuiLight {
Front,
Side,
}
#[derive(Schema, Debug, Clone)]
pub struct ElementRotation {
#[data(range = "-16..=32")]
pub origin: Option<[f64; 3]>,
pub axis: Axis,
#[data(range = "-45..=45")]
pub angle: f64,
#[data(default)]
pub rescale: bool,
}
#[derive(Schema, Debug, Clone)]
pub struct Face {
pub texture: String,
#[data(range = "0..=16")]
pub uv: Option<[f64; 4]>,
pub rotation: Option<u32>,
pub cullface: Option<Direction>,
#[data(range = "-1..=255")]
pub tint: Option<i64>,
}
#[derive(Schema, Debug, Clone, Default)]
pub struct Faces {
pub down: Option<Face>,
pub up: Option<Face>,
pub north: Option<Face>,
pub south: Option<Face>,
pub west: Option<Face>,
pub east: Option<Face>,
}
impl Faces {
pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Face)> {
[
("down", &self.down),
("up", &self.up),
("north", &self.north),
("south", &self.south),
("west", &self.west),
("east", &self.east),
]
.into_iter()
.filter_map(|(d, f)| f.as_ref().map(|f| (d, f)))
}
}
#[derive(Schema, Debug, Clone)]
pub struct Element {
pub name: Option<String>,
#[data(range = "-16..=32")]
pub from: [f64; 3],
#[data(range = "-16..=32")]
pub to: [f64; 3],
pub rotation: Option<ElementRotation>,
pub shade: Option<bool>,
pub faces: Option<Faces>,
}
#[derive(Schema, Debug, Clone)]
pub struct Transform {
#[data(range = "-360..=360")]
pub rotation: Option<[f64; 3]>,
#[data(range = "-80..=80")]
pub translation: Option<[f64; 3]>,
#[data(range = "-4..=4")]
pub scale: Option<[f64; 3]>,
}
#[derive(Schema, Debug, Clone, Default)]
pub struct Display {
pub thirdperson_righthand: Option<Transform>,
pub thirdperson_lefthand: Option<Transform>,
pub firstperson_righthand: Option<Transform>,
pub firstperson_lefthand: Option<Transform>,
pub gui: Option<Transform>,
pub head: Option<Transform>,
pub ground: Option<Transform>,
pub fixed: Option<Transform>,
}
impl Display {
pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Transform)> {
[
("thirdperson_righthand", &self.thirdperson_righthand),
("thirdperson_lefthand", &self.thirdperson_lefthand),
("firstperson_righthand", &self.firstperson_righthand),
("firstperson_lefthand", &self.firstperson_lefthand),
("gui", &self.gui),
("head", &self.head),
("ground", &self.ground),
("fixed", &self.fixed),
]
.into_iter()
.filter_map(|(d, t)| t.as_ref().map(|t| (d, t)))
}
}
#[derive(Schema, Debug, Clone)]
pub struct Model {
pub parent: Option<String>,
#[data(default)]
pub textures: Ordered<Texture>,
pub elements: Option<Vec<Element>>,
pub display: Option<Display>,
pub ambient_occlusion: Option<bool>,
pub gui_light: Option<GuiLight>,
}
#[derive(Schema, Debug, Clone)]
pub struct Rotation {
pub x: Option<u32>,
pub y: Option<u32>,
#[data(range = "1..=1000000")]
pub weight: Option<f64>,
#[data(default)]
pub uvlock: bool,
}
#[derive(Schema, Debug, Clone)]
pub struct Connect {
pub arm: Model,
#[data(default)]
pub uvlock: bool,
}
#[derive(Schema, Debug, Clone)]
pub struct States {
pub rotations: Option<Vec<Rotation>>,
pub connect: Option<Connect>,
pub facing: Option<Facing>,
pub placement: Option<Placement>,
#[data(default)]
pub attach: bool,
#[data(default)]
pub waterlogged: bool,
#[data(default)]
pub uvlock: bool,
#[data(default)]
pub properties: Ordered<Vec<String>>,
#[data(default)]
pub models: Ordered<Model>,
#[data(default)]
pub variants: Ordered<ModelNames>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModelNames(pub Vec<String>);
impl Schema for ModelNames {
fn shape() -> Shape {
Shape::List(String::shape)
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
match v {
ValueRef::List(_) => Vec::<String>::read(v, cx).map(ModelNames),
_ => String::read(v, cx).map(|s| ModelNames(vec![s])),
}
}
fn write(&self) -> Value {
match self.0.as_slice() {
[one] => one.write(),
many => Value::List(many.iter().map(Schema::write).collect()),
}
}
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Facing {
Horizontal,
All,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placement {
Toward,
Away,
}
#[derive(Schema, Debug, Clone)]
pub struct ItemFile {
pub format: Option<u32>,
pub id: Option<String>,
pub name: Option<Text>,
pub tooltip: Option<Text>,
pub register: Option<Register>,
#[data(default)]
pub requires: Vec<String>,
pub model: Model,
pub item: Option<ItemProps>,
}
#[derive(Schema, Debug, Clone)]
pub struct BlockFile {
pub format: Option<u32>,
pub id: Option<String>,
pub name: Option<Text>,
pub tooltip: Option<Text>,
pub register: Option<Register>,
#[data(default)]
pub requires: Vec<String>,
pub model: Model,
pub item_model: Option<Model>,
pub block: Option<BlockProps>,
pub states: Option<States>,
}