use yog_data::Diagnostic;
use yog_ids::{yui, yui_attr};
use super::{ICON_PX, LayoutPart, RecipeLayout, game_kinds};
use crate::yui::Element;
const KIND_SEPARATORS: [char; 2] = [' ', ','];
const VALUE: &str = "{";
const YES: &str = "true";
pub fn is_recipe_layout(root: &Element) -> bool {
root.name == yui::COMPONENT && root.attr(yui_attr::RECIPE_LAYOUT).is_some()
}
fn err<T>(el: &Element, msg: impl std::fmt::Display) -> Result<T, Diagnostic> {
Err(Diagnostic::from(format!("<{}>: {msg}", el.name)).at_line(el.line))
}
fn num(el: &Element, key: &str, default: Option<f32>) -> Result<f32, Diagnostic> {
match el.attr(key) {
Some(v) => v
.trim()
.parse::<f32>()
.or_else(|_| err(el, format_args!("{key}=\"{v}\": a number of GUI pixels"))),
None => default
.map(Ok)
.unwrap_or_else(|| err(el, format_args!("needs {key}=\"…\""))),
}
}
fn at(el: &Element) -> Result<[f32; 2], Diagnostic> {
Ok([
num(el, yui_attr::X, Some(0.0))?,
num(el, yui_attr::Y, Some(0.0))?,
])
}
fn flag(el: &Element, key: &str) -> bool {
el.attr(key).is_some_and(|v| v.trim() == YES)
}
fn needs<'a>(el: &'a Element, key: &str, what: &str) -> Result<&'a str, Diagnostic> {
el.attr(key)
.map(Ok)
.unwrap_or_else(|| err(el, format_args!("needs {key}=\"…\"{what}")))
}
pub fn compile(root: &Element, ns: &str) -> Result<RecipeLayout, Diagnostic> {
let kinds = kinds(root, ns);
if kinds.is_empty() {
return err(
root,
"recipe_layout=\"…\": the kinds it shows — the mod's own or the game's",
);
}
let size = [
num(root, yui_attr::WIDTH, None)?,
num(root, yui_attr::HEIGHT, None)?,
];
let mut parts = Parts::default();
for el in elements(root) {
parts.add(el)?;
}
Ok(RecipeLayout {
kinds,
size,
parts: parts.finish()?,
})
}
fn kinds(root: &Element, ns: &str) -> Vec<String> {
root.attr(yui_attr::RECIPE_LAYOUT)
.unwrap_or_default()
.split(KIND_SEPARATORS)
.filter(|k| !k.is_empty())
.map(|k| {
if game_kinds().any(|g| g == k) {
k.to_string()
} else {
yog_ids::qualify_in(ns, k)
}
})
.collect()
}
fn elements(root: &Element) -> Vec<&Element> {
let mut out = Vec::new();
for c in &root.children {
if c.name == yui::CANVAS {
out.extend(c.children.iter());
} else {
out.push(c);
}
}
out
}
type Places = Vec<(usize, [f32; 2])>;
#[derive(Default)]
struct Parts {
parts: Vec<LayoutPart>,
places: Vec<(String, Places, bool)>,
}
impl Parts {
fn add(&mut self, el: &Element) -> Result<(), Diagnostic> {
let part = match el.name.as_str() {
yui::IMAGE => image(el)?,
yui::RECIPE_SLOT => return self.slot(el),
yui::RECIPE_RESULT => LayoutPart::Result {
at: at(el)?,
frame: flag(el, yui_attr::FRAME),
},
yui::RECIPE_COUNT => LayoutPart::Count { at: at(el)? },
yui::ITEM_SLOT => {
let item = needs(el, yui_attr::ITEM, " (the station: minecraft:furnace)")?;
LayoutPart::Item {
item: yog_ids::qualify(
item.trim().trim_start_matches(yog_ids::reference::ITEM),
),
at: at(el)?,
}
}
yui::BLOCK_SLOT => block(el)?,
yui::LABEL => LayoutPart::Text {
text: needs(el, yui_attr::TEXT, "")?.replace(yog_ids::yui_markup::BIND_OPEN, VALUE),
at: at(el)?,
},
other => {
return err(
el,
format_args!(
"not a recipe layout's element (`{other}`) — {}",
[
yui::IMAGE,
yui::RECIPE_SLOT,
yui::RECIPE_RESULT,
yui::RECIPE_COUNT,
yui::ITEM_SLOT,
yui::BLOCK_SLOT,
yui::LABEL
]
.join(", ")
),
);
}
};
self.parts.push(part);
Ok(())
}
fn slot(&mut self, el: &Element) -> Result<(), Diagnostic> {
let slot = needs(el, yui_attr::SLOT, " (the kind's slot)")?.to_string();
let index = match el.attr(yui_attr::INDEX) {
Some(i) => i
.trim()
.parse::<usize>()
.or_else(|_| err(el, format_args!("index=\"{i}\": 0, 1, …")))?,
None => self
.places
.iter()
.find(|(s, ..)| *s == slot)
.map_or(0, |(_, p, _)| {
p.iter().map(|(i, _)| i + 1).max().unwrap_or(0)
}),
};
let place = (index, at(el)?);
let framed = flag(el, yui_attr::FRAME);
match self.places.iter_mut().find(|(s, ..)| *s == slot) {
Some((_, p, frame)) => {
if p.iter().any(|(i, _)| *i == index) {
return err(el, format_args!("{slot}'s place {index} is laid out twice"));
}
p.push(place);
*frame |= framed;
}
None => {
self.places.push((slot.clone(), vec![place], framed));
self.parts.push(LayoutPart::Slots {
slot,
at: Vec::new(),
frame: false,
});
}
}
Ok(())
}
fn finish(mut self) -> Result<Vec<LayoutPart>, Diagnostic> {
for part in &mut self.parts {
if let LayoutPart::Slots { slot, at, frame } = part
&& let Some((_, p, f)) = self.places.iter().find(|(s, ..)| s == slot)
{
let mut p = p.clone();
p.sort_by_key(|(i, _)| *i);
if let Some(gap) = p.iter().enumerate().find(|(n, (i, _))| n != i) {
return Err(format!(
"{slot}: place {} isn't laid out (a slot's places are 0, 1, … in a row)",
gap.0
)
.into());
}
*at = p.into_iter().map(|(_, a)| a).collect();
*frame = *f;
}
}
Ok(self.parts)
}
}
fn image(el: &Element) -> Result<LayoutPart, Diagnostic> {
let source = needs(el, yui_attr::SOURCE, "")?.to_string();
let uv = match el.attr(yui_attr::UV) {
Some(v) => {
let n: Vec<f32> = v
.split_whitespace()
.filter_map(|x| x.parse().ok())
.collect();
let [u, v, w, h] = n[..] else {
return err(el, format_args!("uv=\"{v}\": u v width height"));
};
[u, v, w, h]
}
None => [
0.0,
0.0,
num(el, yui_attr::W, None)?,
num(el, yui_attr::H, None)?,
],
};
Ok(LayoutPart::Image {
source,
uv,
at: at(el)?,
})
}
fn block(el: &Element) -> Result<LayoutPart, Diagnostic> {
let block = needs(
el,
yui_attr::BLOCK,
" (a block state: minecraft:brewing_stand[has_bottle_0=true])",
)?;
Ok(LayoutPart::Block {
block: yog_ids::qualify(block.trim().trim_start_matches(yog_ids::reference::BLOCK)),
at: at(el)?,
size: num(el, yui_attr::SIZE, Some(ICON_PX))?,
})
}