/* 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)) }