diff options
author | metamuffin <metamuffin@disroot.org> | 2025-10-06 23:03:32 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-10-06 23:03:40 +0200 |
commit | 176e6bc6c4c29bea3be2aceca99743b997c76c97 (patch) | |
tree | 1161e7a966843324756340da4b6452492902fa07 /server/data/src/book/recipe_diagram.rs | |
parent | ea86b11b682500160f37b35ea8f06b081cd05036 (diff) | |
download | hurrycurry-176e6bc6c4c29bea3be2aceca99743b997c76c97.tar hurrycurry-176e6bc6c4c29bea3be2aceca99743b997c76c97.tar.bz2 hurrycurry-176e6bc6c4c29bea3be2aceca99743b997c76c97.tar.zst |
Move data code to own crate + general data refactor
Diffstat (limited to 'server/data/src/book/recipe_diagram.rs')
-rw-r--r-- | server/data/src/book/recipe_diagram.rs | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/server/data/src/book/recipe_diagram.rs b/server/data/src/book/recipe_diagram.rs new file mode 100644 index 00000000..2ec92b68 --- /dev/null +++ b/server/data/src/book/recipe_diagram.rs @@ -0,0 +1,239 @@ +/* + 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 <https://www.gnu.org/licenses/>. + +*/ + +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<Diagram> { + let ambient_items = serverdata + .initial_map + .iter() + .flat_map(|(_, (_, item))| item) + .copied() + .collect::<HashSet<_>>(); + + let mut need = BTreeSet::from_iter( + target_items + .iter() + .map(|name| data.get_item_by_name(name).unwrap()), + ); + let mut have = BTreeSet::<ItemIndex>::new(); + let mut recipes = BTreeSet::new(); + + #[derive(PartialEq, PartialOrd, Eq, Ord)] + struct GraphRecipe { + index: RecipeIndex, + inputs: Vec<ItemIndex>, + outputs: Vec<ItemIndex>, + } + + 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::<Vec<_>>(); + + for ni in instant_nodes { + let inputs = diag + .edges + .iter() + .enumerate() + .filter(|(_, e)| e.dst == ni) + .map(|(i, e)| (i, e.src)) + .collect::<Vec<_>>(); + let outputs = diag + .edges + .iter() + .enumerate() + .filter(|(_, e)| e.src == ni) + .map(|(i, e)| (i, e.dst)) + .collect::<Vec<_>>(); + + 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::<Vec<_>>(); + 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::<Vec<_>>(); + 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); + } +} |