/* Hurry Curry! - a game about cooking Copyright (C) 2025 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::Serverdata; use anyhow::Result; use hurrycurry_protocol::{ Gamedata, ItemIndex, Message, Recipe, RecipeIndex, book::{Diagram, DiagramEdge, DiagramNode, NodeStyle}, glam::Vec2, }; use std::{ cmp::Reverse, collections::{BTreeMap, BTreeSet, HashSet}, }; pub fn recipe_diagram( data: &Gamedata, serverdata: &Serverdata, target_items: &[&str], ) -> Result { let ambient_items = serverdata .initial_map .iter() .flat_map(|(_, (_, item))| item) .copied() .collect::>(); let mut need = BTreeSet::from_iter( target_items .iter() .map(|name| data.get_item_by_name(name).unwrap()), ); let mut have = BTreeSet::::new(); let mut recipes = BTreeSet::new(); #[derive(PartialEq, PartialOrd, Eq, Ord)] struct GraphRecipe { index: RecipeIndex, inputs: Vec, outputs: Vec, } while let Some(item) = need.pop_last() { for (ri, r) in data.recipes() { if r.outputs().contains(&item) { let gr = GraphRecipe { inputs: r .inputs() .iter() .filter(|i| !ambient_items.contains(i)) .copied() .collect(), outputs: r .outputs() .iter() .filter(|i| !ambient_items.contains(i)) .copied() .collect(), index: ri, }; need.extend(gr.inputs.iter().filter(|i| !have.contains(&i))); have.extend(&gr.outputs); recipes.insert(gr); } } } let mut diag = Diagram::default(); let mut item_index = BTreeMap::new(); for i in have { item_index.insert(i, diag.nodes.len()); diag.nodes.push(DiagramNode { label: Message::Item(i), position: Vec2::ZERO, style: if target_items.contains(&data.item_name(i)) { NodeStyle::FinalProduct } else { NodeStyle::IntermediateProduct }, }); } for r in recipes { let index = diag.nodes.len(); let recipe = data.recipe(r.index); if matches!(recipe, Recipe::Instant { .. }) && r.inputs.len() >= 1 && r.outputs.len() >= 1 { for i in r.inputs { diag.edges.push(DiagramEdge { src: item_index[&i], dst: item_index[&r.outputs[0]], label: None, }); } continue; } if matches!(recipe, Recipe::Passive { tile: None, .. }) && r.inputs.len() == 1 && r.outputs.len() == 1 { diag.nodes[item_index[&r.inputs[0]]].style = NodeStyle::ProcessPassive; diag.edges.push(DiagramEdge { src: item_index[&r.inputs[0]], dst: item_index[&r.outputs[0]], label: None, }); continue; } let (kind, style) = match recipe { Recipe::Passive { .. } => ("Passive", NodeStyle::ProcessPassive), Recipe::Active { .. } => ("Active", NodeStyle::ProcessActive), Recipe::Instant { .. } => ("Instant", NodeStyle::ProcessInstant), }; diag.nodes.push(DiagramNode { position: Vec2::ZERO, label: if let Some(tile) = recipe.tile() { Message::Tile(tile) } else { Message::Text(kind.to_string()) }, style, }); for i in r.inputs { diag.edges.push(DiagramEdge { src: item_index[&i], dst: index, label: None, }); } for o in r.outputs { diag.edges.push(DiagramEdge { src: index, dst: item_index[&o], label: None, }); } } merge_combine_clusters(&mut diag); remove_orphan_nodes(&mut diag); Ok(diag) } fn merge_combine_clusters(diag: &mut Diagram) { let instant_nodes = diag .nodes .iter() .enumerate() .filter(|(_, n)| matches!(n.style, NodeStyle::IntermediateProduct)) .map(|(i, _)| i) .collect::>(); for ni in instant_nodes { let inputs = diag .edges .iter() .enumerate() .filter(|(_, e)| e.dst == ni) .map(|(i, e)| (i, e.src)) .collect::>(); let outputs = diag .edges .iter() .enumerate() .filter(|(_, e)| e.src == ni) .map(|(i, e)| (i, e.dst)) .collect::>(); if outputs .iter() .all(|&(_, ni)| diag.nodes[ni].style.is_procuct()) && inputs .iter() .all(|&(_, ni)| diag.nodes[ni].style.is_procuct()) { let mut to_remove = inputs .iter() .map(|&(i, _)| i) .chain(outputs.iter().map(|&(i, _)| i)) .collect::>(); to_remove.sort_by_key(|x| Reverse(*x)); for i in to_remove { diag.edges.remove(i); } for &input in &inputs { for &output in &outputs { if !diag .edges .iter() .any(|e| e.src == input.1 && e.dst == output.1) { diag.edges.push(DiagramEdge { src: input.1, dst: output.1, label: None, }); } } } } } } fn remove_orphan_nodes(diag: &mut Diagram) { let mut to_remove = diag .nodes .iter() .enumerate() .filter(|&(i, _)| !diag.edges.iter().any(|e| e.src == i || e.dst == i)) .map(|(i, _)| i) .collect::>(); to_remove.sort_by_key(|x| Reverse(*x)); for e in &mut diag.edges { e.src -= to_remove.iter().filter(|&&i| i < e.src).count(); e.dst -= to_remove.iter().filter(|&&i| i < e.dst).count(); } for i in to_remove { diag.nodes.remove(i); } }