/*
Hurry Curry! - a game about cooking
Copyright (C) 2026 Hurry Curry! Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License only.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
use crate::registry::ItemTileRegistry;
use anyhow::{Result, anyhow};
use hurrycurry_protocol::{Demand, ItemIndex, Recipe};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[rustfmt::skip]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RecipeDecl {
tile: Option,
#[serde(default)] inputs: Vec,
#[serde(default)] outputs: Vec,
#[serde(default)] action: RecipeDeclAction,
#[serde(default)] warn: bool,
revert_duration: Option,
duration: Option,
points: Option,
group: Option,
#[serde(default)] group_hidden: bool,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum RecipeDeclAction {
#[default]
Never,
Passive,
Active,
Instant,
Demand,
}
#[allow(clippy::type_complexity)]
pub(crate) fn load_recipes(
recipes_in: Vec,
reg: &ItemTileRegistry,
) -> Result<(
Vec,
Vec,
BTreeMap>,
)> {
let mut recipes = Vec::new();
let mut demands = Vec::new();
let mut recipe_groups = BTreeMap::>::new();
for mut r in recipes_in {
#[cfg(feature = "fast_recipes")]
match r.action {
RecipeDeclAction::Passive | RecipeDeclAction::Active => {
if !r.warn {
r.duration = Some(0.5)
}
}
_ => (),
}
let r2 = r.clone();
let mut inputs = r.inputs.into_iter().map(|i| reg.register_item(i));
let mut outputs = r.outputs.into_iter().map(|o| reg.register_item(o));
let tile = r.tile.map(|t| reg.register_tile(t));
if let Some(g) = r.group
&& !r.group_hidden
{
recipe_groups.entry(g).or_default().extend(inputs.clone());
}
match r.action {
RecipeDeclAction::Never => {}
RecipeDeclAction::Passive => recipes.push(Recipe::Passive {
speed: 1. / r.duration.ok_or(anyhow!("duration for passive missing"))?,
warn: r.warn,
tile,
revert_speed: r.revert_duration.map(|d| 1. / d),
input: inputs
.next()
.ok_or(anyhow!("passive recipe without input"))?,
output: outputs.next(),
}),
RecipeDeclAction::Active => recipes.push(Recipe::Active {
speed: 1. / r.duration.ok_or(anyhow!("duration for active missing"))?,
tile,
input: inputs
.next()
.ok_or(anyhow!("active recipe without input"))?,
outputs: [outputs.next(), outputs.next()],
}),
RecipeDeclAction::Instant => {
recipes.push(Recipe::Instant {
points: r.points.take().unwrap_or(0),
tile,
inputs: [inputs.next(), inputs.next()],
outputs: [outputs.next(), outputs.next()],
});
}
RecipeDeclAction::Demand => demands.push(Demand {
input: inputs.next().ok_or(anyhow!("demand needs inputs"))?,
output: outputs.next(),
duration: r.duration.unwrap_or(10.),
points: 0, // assigned later when filtering
}),
}
assert_eq!(inputs.next(), None, "{r2:?} inputs left over");
assert_eq!(outputs.next(), None, "{r2:?} outputs left over");
assert_eq!(r.points, None, "points specified where not possible")
}
Ok((recipes, demands, recipe_groups))
}