aboutsummaryrefslogtreecommitdiff
path: root/server/data/src/book
diff options
context:
space:
mode:
Diffstat (limited to 'server/data/src/book')
-rw-r--r--server/data/src/book/diagram_layout.rs78
-rw-r--r--server/data/src/book/mod.rs128
-rw-r--r--server/data/src/book/recipe_diagram.rs239
3 files changed, 445 insertions, 0 deletions
diff --git a/server/data/src/book/diagram_layout.rs b/server/data/src/book/diagram_layout.rs
new file mode 100644
index 00000000..0ea26a69
--- /dev/null
+++ b/server/data/src/book/diagram_layout.rs
@@ -0,0 +1,78 @@
+/*
+ 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 anyhow::{Result, anyhow};
+use hurrycurry_protocol::{book::Diagram, glam::Vec2};
+use serde::Deserialize;
+use std::{
+ collections::BTreeMap,
+ fmt::Write as W2,
+ io::Write,
+ process::{Command, Stdio},
+};
+
+pub struct Layout {
+ pub nodes: BTreeMap<String, Vec2>,
+ pub edges: Vec<(usize, usize)>,
+}
+
+pub fn diagram_layout(diagram: &mut Diagram) -> Result<()> {
+ let mut child = Command::new("dot")
+ .arg("-Tjson")
+ .arg("-Kdot")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let mut out = String::new();
+ writeln!(out, "digraph {{")?;
+ for (i, _) in diagram.nodes.iter().enumerate() {
+ writeln!(out, "k{i} [width=1, height=1]")?;
+ }
+ for edge in &diagram.edges {
+ writeln!(out, "k{} -> k{}", edge.src, edge.dst)?;
+ }
+ writeln!(out, "}}")?;
+
+ child.stdin.as_mut().unwrap().write_all(out.as_bytes())?;
+ let output = child.wait_with_output()?;
+
+ #[derive(Deserialize)]
+ struct Out {
+ objects: Vec<Object>,
+ }
+ #[derive(Deserialize)]
+ struct Object {
+ name: String,
+ pos: String,
+ }
+ let graph: Out = serde_json::from_slice(&output.stdout)?;
+ for o in graph.objects {
+ let pos = o.pos.split_once(",").ok_or(anyhow!("malformed position"))?;
+ let pos = Vec2::new(pos.0.parse()?, pos.1.parse()?);
+
+ let index = o
+ .name
+ .strip_prefix("k")
+ .ok_or(anyhow!("invalid node name"))?
+ .parse::<usize>()?;
+ diagram.nodes[index].position = pos
+ }
+
+ Ok(())
+}
diff --git a/server/data/src/book/mod.rs b/server/data/src/book/mod.rs
new file mode 100644
index 00000000..b52779f3
--- /dev/null
+++ b/server/data/src/book/mod.rs
@@ -0,0 +1,128 @@
+/*
+ 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/>.
+
+*/
+
+pub mod diagram_layout;
+pub mod recipe_diagram;
+
+use crate::{
+ Serverdata,
+ book::{diagram_layout::diagram_layout, recipe_diagram::recipe_diagram},
+};
+use anyhow::Result;
+use hurrycurry_locale::trm;
+use hurrycurry_protocol::{
+ Gamedata, Message,
+ book::{Book, BookPage},
+};
+
+struct RecipePageParams<'a> {
+ name: &'a str,
+ repr_items: &'a [&'a str],
+}
+static RECIPE_PAGES: &[RecipePageParams] = &[
+ RecipePageParams {
+ name: "cheese-leek-soup",
+ repr_items: &["plate:cheese-leek-soup"],
+ },
+ RecipePageParams {
+ name: "tomato-soup",
+ repr_items: &["plate:tomato-soup"],
+ },
+ RecipePageParams {
+ name: "mushroom-soup",
+ repr_items: &["plate:mushroom-soup"],
+ },
+ RecipePageParams {
+ name: "burger",
+ repr_items: &[
+ "plate:seared-patty,sliced-bun,sliced-lettuce,sliced-tomato",
+ "plate:seared-patty,sliced-bun,sliced-cheese,sliced-tomato",
+ ],
+ },
+ RecipePageParams {
+ name: "noodles",
+ repr_items: &["plate:cooked-noodles,sliced-cheese,tomato-juice"],
+ },
+ RecipePageParams {
+ name: "pizza",
+ repr_items: &["plate:baked-rolled-dough:sliced-cheese,sliced-mushroom,tomato-juice"],
+ },
+ RecipePageParams {
+ name: "curry",
+ repr_items: &["plate:cooked-rice,curry"],
+ },
+ RecipePageParams {
+ name: "drinks",
+ repr_items: &[
+ "glass:water",
+ "glass:tomato-juice",
+ "glass:strawberry-shake",
+ ],
+ },
+ RecipePageParams {
+ name: "mochi",
+ repr_items: &["plate:strawberry-mochi"],
+ },
+ RecipePageParams {
+ name: "doughnut",
+ repr_items: &["plate:doughnut"],
+ },
+ RecipePageParams {
+ name: "doughnut",
+ repr_items: &["plate:doughnut"],
+ },
+];
+
+pub fn book(data: &Gamedata, serverdata: &Serverdata) -> Result<Book> {
+ let mut pages = Vec::new();
+
+ pages.push(BookPage::Contents {
+ title: trm!("b.toc.title"),
+ table: vec![],
+ });
+ let mut toc = Vec::new();
+
+ for &RecipePageParams { name, repr_items } in RECIPE_PAGES {
+ let mut diagram = recipe_diagram(data, serverdata, repr_items)?;
+ diagram_layout(&mut diagram)?;
+ let title = Message::Translation {
+ id: format!("b.{name}.title"),
+ params: vec![],
+ };
+ toc.push((title.clone(), pages.len()));
+ pages.push(BookPage::Recipe {
+ title,
+ description: Message::Translation {
+ id: format!("b.{name}.desc"),
+ params: vec![],
+ },
+ diagram,
+ });
+ }
+
+ if let BookPage::Contents { table, .. } = &mut pages[0] {
+ *table = toc;
+ }
+ Ok(Book { pages })
+}
+
+pub fn print_book(data: &Gamedata, serverdata: &Serverdata) -> Result<()> {
+ let book = book(data, serverdata)?;
+ println!("{}", serde_json::to_string_pretty(&book).unwrap());
+ Ok(())
+}
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);
+ }
+}