From 176e6bc6c4c29bea3be2aceca99743b997c76c97 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 6 Oct 2025 23:03:32 +0200 Subject: Move data code to own crate + general data refactor --- Cargo.lock | 17 +- Cargo.toml | 1 + data/makefile | 4 +- data/recipes/default.js | 104 +++----- server/Cargo.toml | 4 +- server/data/Cargo.toml | 17 ++ server/data/src/book/diagram_layout.rs | 78 ++++++ server/data/src/book/mod.rs | 128 ++++++++++ server/data/src/book/recipe_diagram.rs | 239 ++++++++++++++++++ server/data/src/demands.rs | 88 +++++++ server/data/src/entities.rs | 99 ++++++++ server/data/src/index.rs | 94 +++++++ server/data/src/lib.rs | 357 ++++++++++++++++++++++++++ server/src/data/demands.rs | 88 ------- server/src/data/mod.rs | 434 -------------------------------- server/src/entity/campaign.rs | 28 +-- server/src/entity/conveyor.rs | 23 +- server/src/entity/customers.rs | 6 +- server/src/entity/environment_effect.rs | 14 +- server/src/entity/mod.rs | 157 ++---------- server/src/lib.rs | 1 - server/src/main.rs | 6 +- server/src/server.rs | 13 +- server/tools/Cargo.toml | 1 + server/tools/src/book.rs | 123 --------- server/tools/src/diagram_layout.rs | 78 ------ server/tools/src/graph.rs | 4 +- server/tools/src/graph_summary.rs | 5 +- server/tools/src/main.rs | 23 +- server/tools/src/map_linter.rs | 4 +- server/tools/src/recipe_diagram.rs | 239 ------------------ 31 files changed, 1229 insertions(+), 1248 deletions(-) create mode 100644 server/data/Cargo.toml create mode 100644 server/data/src/book/diagram_layout.rs create mode 100644 server/data/src/book/mod.rs create mode 100644 server/data/src/book/recipe_diagram.rs create mode 100644 server/data/src/demands.rs create mode 100644 server/data/src/entities.rs create mode 100644 server/data/src/index.rs create mode 100644 server/data/src/lib.rs delete mode 100644 server/src/data/demands.rs delete mode 100644 server/src/data/mod.rs delete mode 100644 server/tools/src/book.rs delete mode 100644 server/tools/src/diagram_layout.rs delete mode 100644 server/tools/src/recipe_diagram.rs diff --git a/Cargo.lock b/Cargo.lock index 2d166b39..50cf378b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -996,6 +996,20 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "hurrycurry-data" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "hurrycurry-locale", + "hurrycurry-protocol", + "serde", + "serde_json", + "serde_yml", + "shlex", +] + [[package]] name = "hurrycurry-discover" version = "2.3.5" @@ -1098,6 +1112,7 @@ dependencies = [ "get_if_addrs", "hurrycurry-bot", "hurrycurry-client-lib", + "hurrycurry-data", "hurrycurry-locale", "hurrycurry-protocol", "igd", @@ -1109,7 +1124,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "serde_yml", "shlex", "tokio", "tokio-tungstenite", @@ -1123,6 +1137,7 @@ dependencies = [ "anyhow", "clap", "env_logger", + "hurrycurry-data", "hurrycurry-locale", "hurrycurry-protocol", "hurrycurry-server", diff --git a/Cargo.toml b/Cargo.toml index 887ac828..7c4252db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "server/replaytool", "server/registry", "server/discover", + "server/data", "server/editor", "server/tools", "locale/tools", diff --git a/data/makefile b/data/makefile index c9361258..104fc876 100644 --- a/data/makefile +++ b/data/makefile @@ -14,7 +14,7 @@ # along with this program. If not, see . # SETS = default none anticurry -ALL = $(patsubst %,recipes/%.yaml,$(SETS)) book.json +ALL = $(patsubst %,recipes/%.yaml,$(SETS)) all: $(ALL) graphs: recipes/default.svg @@ -29,8 +29,6 @@ recipes/default.yaml: recipes/default.js recipes/%.gv.txt: recipes/%.yaml { pushd .. >/dev/null; cargo $(CARGOFLAGS) run --release --bin hurrycurry-tools -- graph; popd >/dev/null; } > $@~ && cp $@~ $@ -book.json: - { pushd .. >/dev/null; cargo $(CARGOFLAGS) run --release --bin hurrycurry-tools -- book; popd >/dev/null; } > $@~ && cp $@~ $@ recipes/%.svg: recipes/%.gv.txt dot -Tsvg -Kdot < $< > $@~ && cp $@~ $@ diff --git a/data/recipes/default.js b/data/recipes/default.js index 22f4f037..6bcfb315 100644 --- a/data/recipes/default.js +++ b/data/recipes/default.js @@ -45,6 +45,8 @@ function finish() { s += ` inputs: [${r.inputs.map(e => JSON.stringify(e.toString())).join(",")}]\n` s += ` outputs: [${r.outputs.map(e => JSON.stringify(e.toString())).join(",")}]\n` if (r.warn) s += ` warn: true\n` + if (r.group) s += ` group: ${r.group}\n` + if (r.inputs[0]?.group_hidden) s += ` group_hidden: true\n` if (r.duration) s += ` duration: ${r.duration}\n` if (r.revert_duration) s += ` revert_duration: ${r.revert_duration}\n` if (r.points) s += ` points: ${r.points}\n` @@ -96,6 +98,11 @@ function auto_burn() { class Item { constructor(name, container) { this.name = name; this.container = container } as(s) { this.name = s; return this } + tr_nested(container) { + const o = new Item(this.toString(), container) + out({ action: "instant", inputs: [container, this], outputs: [o] }) + return o + } tr(container) { const o = new Item(this.name, container) if (this.container == container) return o @@ -151,7 +158,9 @@ function sear(s, duration = 15) { return o } function bake(s, duration = 25) { - const o = new Item(`baked-${s.name}`, s.container) + const o = s.container == "rolled-dough" + ? new Item(s.name, `baked-${s.container}`) + : new Item(`baked-${s.name}`, s.container) out({ action: "passive", duration, revert_duration: duration * 2, tile: "oven", inputs: [s], outputs: [o] }) out({ action: "passive", duration: duration / 2, revert_duration: duration / 4, tile: "oven", inputs: [o], outputs: [new Item("burned")], warn: true }) return o @@ -201,48 +210,9 @@ function combine(c, ...items) { } return result } -// function combine(container, ...items) { -// items.push(container) -// const open = items.map(i => [i.name]) -// const seen = new Set() -// let final_result -// let components_before -// while ((components_before = open.pop())) { -// for (const new_item of items) { -// if (components_before.includes(new_item.name)) continue - -// // generate key `old,old,old#new` to avoid duplicated recipes -// const dedup_key = components_before.join(",") + "#" + new_item -// if (seen.has(dedup_key)) continue -// seen.add(dedup_key) - -// if (new_item.container && !components_before.includes(container.name)) -// continue // new item is likely a liquid and needs target container to be there already - -// const components_after = [...components_before, new_item.name] -// components_after.sort() -// open.push(components_after) - -// const content_in = components_before.filter(e => e != container.name).join(",") -// const item_in = content_in == "" ? container : new Item(content_in, components_before.includes(container.name) ? container : null) -// const content_out = components_after.filter(e => e != container.name).join(",") -// const item_out = content_out == "" ? container : new Item(content_out, components_after.includes(container.name) ? container : null) - -// if (components_after.length == items.length) final_result = item_out -// out({ -// action: "instant", -// inputs: [item_in, new_item], -// outputs: new_item == container // put result in containers positions if container was added -// ? [null, item_out] : [item_out, new_item.container] -// }) -// } -// } -// console.error(final_result); -// return final_result -// } -function edible(...items) { +function edible(group, ...items) { for (const i of items) { - out({ action: "demand", inputs: [i], outputs: [i.container?.dispose ?? i.container], duration: 10 }) + out({ action: "demand", inputs: [i], outputs: [i.container?.dispose ?? i.container], duration: 10, group }) } } function either(a, b) { @@ -255,8 +225,12 @@ function sink_fill(c) { out({ action: "active", inputs: [c], outputs: [o], tile: "sink", duration: 1 }) return o } +function group_hidden(i) { + i.group_hidden = true + return i +} -out({ action: "active", duration: 2, tile: "sink", inputs: [new Item("dirty-plate")], outputs: [PL] }) +out({ group: "cleanup", action: "active", duration: 2, tile: "sink", inputs: [new Item("dirty-plate")], outputs: [PL] }) const tomato = crate("tomato") const steak = crate("steak") @@ -278,9 +252,9 @@ const dough = process(flour.tr(FP)).as("dough").tr() // Pizza const pizza_dough = roll(dough) -edible( - bake(combine(pizza_dough, tomato_juice, cut(cheese))).tr(PL), - bake(combine(pizza_dough, tomato_juice, cut(cheese), cut(mushroom))).tr(PL), +edible("pizza", + bake(combine(pizza_dough, tomato_juice, cut(cheese), cut(mushroom))).tr_nested(PL), + group_hidden(bake(combine(pizza_dough, tomato_juice, cut(cheese))).tr_nested(PL)) // bake(combine(pizza_dough, patty, cut(cheese))).tr(PL), // bake(combine(pizza_dough, patty, leek)).tr(PL), ) @@ -291,9 +265,9 @@ edible( // Steak const french_fries = deep_fry(cut(potato).tr(BA)).as("french-fries"); const bun = either(bake(dough).as("bun"), crate("bun")) -edible( - bun.tr(PL), - french_fries.tr(PL), +edible("steak", + group_hidden(bun.tr(PL)), + group_hidden(french_fries.tr(PL)), combine(PL, sear(steak), bun), combine(PL, sear(steak), french_fries), // combine(PL, sear(steak), cook(potato.tr(POT))), @@ -303,32 +277,32 @@ edible( ) // Salad -edible( +edible("salad", combine(PL, cut(tomato), cut(lettuce)), combine(PL, cut(lettuce)), ) // Burger -edible( - combine(PL, cut(bun), sear(patty), cut(cheese)), - combine(PL, cut(bun), sear(patty), cut(cheese), cut(lettuce)), +edible("burger", combine(PL, cut(bun), sear(patty), cut(tomato), cut(lettuce)), combine(PL, cut(bun), sear(patty), cut(cheese), cut(tomato)), - combine(PL, cut(bun), cut(cheese), cut(lettuce), cut(tomato)), - combine(PL, cut(bun), cut(lettuce), cut(tomato)), - combine(PL, cut(bun), cut(fish)) + group_hidden(combine(PL, cut(bun), sear(patty), cut(cheese))), + group_hidden(combine(PL, cut(bun), sear(patty), cut(cheese), cut(lettuce))), + group_hidden(combine(PL, cut(bun), cut(cheese), cut(lettuce), cut(tomato))), + group_hidden(combine(PL, cut(bun), cut(lettuce), cut(tomato))), + group_hidden(combine(PL, cut(bun), cut(fish))) ) // Noodles const noodle = cook(cut(roll(dough)).as("noodles").tr(POT)) -edible( +edible("noodles", combine(PL, noodle, tomato_juice), combine(PL, noodle, tomato_juice, cut(cheese)), // combine(PL, noodle, tomato_juice, cut(cheese), sear(patty)), ) // Soup -edible( +edible("soups", cook(combine(POT, tomato_juice)).as("tomato-soup").tr(PL), cook(combine(POT, cut(mushroom))).as("mushroom-soup").tr(PL), cook(combine(POT, cheese, cut(leek))).as("cheese-leek-soup").tr(PL), @@ -336,9 +310,9 @@ edible( ) // Rice and nigiri -edible( +edible("nigiri", container_add(cut(fish), cook(rice.tr(POT))).as("nigiri").tr(PL), - container_add(cook(rice.tr(POT).tr(PL)), cut(fish)).as("nigiri") + container_add(cook(rice.tr(POT)).tr(PL), cut(fish)).as("nigiri"), ) // coconut milk and strawberry puree @@ -350,19 +324,19 @@ const strawberry_shake = either( ) // Doughnut (Pfannkuchen) -edible(deep_fry(dough.tr(BA)).as("doughnut").tr(PL)) +edible("doughnut", deep_fry(dough.tr(BA)).as("doughnut").tr(PL)) // Icecream -edible(freeze(strawberry_shake.tr(GL)).as("strawberry-icecream")) +edible("icecream", freeze(strawberry_shake.tr(GL)).as("strawberry-icecream")) // Mochi const rice_flour = process(rice.tr(FP)).as("rice-flour") const mochi_dough = cook(rice_flour.tr(POT), 5).as("mochi-dough") const strawberry_mochi = container_add(strawberry, mochi_dough).as("strawberry-mochi") -edible(strawberry_mochi.tr(PL)) +edible("mochi", strawberry_mochi.tr(PL)) // Drinks -edible( +edible("drinks", strawberry_shake.tr(GL), tomato_juice.tr(GL), sink_fill(GL) @@ -370,7 +344,7 @@ edible( // Curry const curry_with_rice = combine(PL, cook(rice.tr(POT)), cook(combine(POT, milk, tomato, leek)).as("curry")) -edible(curry_with_rice) +edible("curry", curry_with_rice) auto_trash() auto_burn() diff --git a/server/Cargo.toml b/server/Cargo.toml index e14bb84d..f72cb7fa 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -13,7 +13,6 @@ tokio = { version = "1.47.1", features = ["full"] } serde_json = "1.0.145" tokio-tungstenite = "0.27.0" futures-util = "0.3.31" -serde_yml = "0.0.12" rand = "0.9.2" rand_distr = "0.5.1" shlex = "1.3.0" @@ -34,6 +33,7 @@ hurrycurry-locale = { path = "locale" } hurrycurry-protocol = { path = "protocol" } hurrycurry-client-lib = { path = "client-lib" } hurrycurry-bot = { path = "bot" } +hurrycurry-data = { path = "data" } [target.'cfg(windows)'.dependencies] windows-registry = "0.6" @@ -44,4 +44,4 @@ mdns = ["dep:mdns-sd", "dep:get_if_addrs"] register = ["dep:reqwest"] upnp = ["dep:igd", "dep:get_if_addrs"] -fast_recipes = [] +fast_recipes = ["hurrycurry-data/fast_recipes"] diff --git a/server/data/Cargo.toml b/server/data/Cargo.toml new file mode 100644 index 00000000..5ba266a8 --- /dev/null +++ b/server/data/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "hurrycurry-data" +version = "0.1.0" +edition = "2024" + +[dependencies] +hurrycurry-protocol = { path = "../protocol" } +hurrycurry-locale = { path = "../locale" } +anyhow = "1.0.100" +serde_json = "1.0.145" +serde = { version = "1.0.225", features = ["derive"] } +shlex = "1.3.0" +clap = { version = "4.5.47", features = ["derive"] } +serde_yml = "0.0.12" + +[features] +fast_recipes = [] 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 . + +*/ + +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, + 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, + } + #[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::()?; + 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 . + +*/ + +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 { + 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 . + +*/ + +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); + } +} diff --git a/server/data/src/demands.rs b/server/data/src/demands.rs new file mode 100644 index 00000000..77e187af --- /dev/null +++ b/server/data/src/demands.rs @@ -0,0 +1,88 @@ +/* + 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 hurrycurry_protocol::{Demand, ItemIndex, Recipe, TileIndex}; +use std::collections::{HashMap, HashSet}; + +pub fn generate_demands( + tiles: &HashSet, + items: &HashSet, + raw_demands: &[(ItemIndex, Option, f32)], + recipes: &[Recipe], +) -> Vec { + let recipes = recipes + .iter() + .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) + .collect::>(); + + let mut producable = HashMap::new(); + + for i in items { + producable.insert(*i, 0.0); + } + + loop { + let prod_count = producable.len(); + + for r in &recipes { + let output_count = r.outputs().iter().filter(|o| !items.contains(o)).count(); + let Some(ingred_cost) = r + .inputs() + .iter() + .map(|i| producable.get(i).copied()) + .reduce(|a, b| { + if let (Some(a), Some(b)) = (a, b) { + Some(a + b) + } else { + None + } + }) + .unwrap_or(Some(0.)) + else { + continue; + }; + + let base_cost = match r { + Recipe::Passive { speed, .. } => 2. + (1. / speed) * 0.1, + Recipe::Active { speed, .. } => 2. + (1. / speed), + Recipe::Instant { .. } => 1., + }; + + let output_cost = (ingred_cost + base_cost) / output_count as f32; + for o in r.outputs() { + let cost = producable.entry(o).or_insert(f32::INFINITY); + *cost = cost.min(output_cost); + } + } + + if prod_count == producable.len() { + break; + } + } + + raw_demands + .iter() + .filter_map(|(i, o, d)| { + producable.get(i).map(|cost| Demand { + input: *i, + output: *o, + duration: *d, + points: *cost as i64, + }) + }) + .collect() +} diff --git a/server/data/src/entities.rs b/server/data/src/entities.rs new file mode 100644 index 00000000..68dbe479 --- /dev/null +++ b/server/data/src/entities.rs @@ -0,0 +1,99 @@ +/* + 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 hurrycurry_protocol::glam::{IVec2, Vec2}; +use serde::{Deserialize, Serialize}; + +use crate::ItemTileRegistry; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum EntityDecl { + Conveyor { + from: IVec2, + to: IVec2, + speed: Option, + }, + ItemPortal { + from: IVec2, + to: IVec2, + }, + PlayerPortal { + from: Vec2, + to: Vec2, + }, + Customers { + scaling_factor: Option, + }, + Map { + name: String, + pos: Vec2, + }, + EnvironmentEffect(EnvironmentEffect), + Environment(Vec), + Gate { + condition: GateCondition, + pos: IVec2, + }, + Tram { + length: usize, + color: Option, + points: Vec, + spacing: f32, + smoothing: f32, + }, + Book { + pos: IVec2, + }, + Pedestrians { + spawn_delay: f32, + spawn_delay_stdev: Option, + speed: Option, + points: Vec, + }, +} + +impl EntityDecl { + pub(crate) fn run_register(&self, reg: &ItemTileRegistry) { + match self { + Self::Gate { .. } => drop(reg.register_tile("fence".into())), + Self::Customers { .. } => drop(reg.register_item("unknown-order".into())), + _ => (), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum GateCondition { + All(Vec), + Any(Vec), + Stars(String, u8), +} + +#[derive(Clone, Debug, Deserialize, Serialize, Default)] +pub struct EnvironmentEffect { + pub name: String, + #[serde(default = "default_onoff")] + pub on: f32, + #[serde(default = "default_onoff")] + pub off: f32, +} +fn default_onoff() -> f32 { + 40. +} diff --git a/server/data/src/index.rs b/server/data/src/index.rs new file mode 100644 index 00000000..a5ec8d97 --- /dev/null +++ b/server/data/src/index.rs @@ -0,0 +1,94 @@ +/* + 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 anyhow::{Context, Result, anyhow, bail}; +use hurrycurry_protocol::{Gamedata, MapMetadata}; +use serde::Deserialize; +use std::{ + collections::{HashMap, HashSet}, + fs::{File, read_to_string}, + path::PathBuf, + str::FromStr, + sync::Mutex, +}; + +use crate::{MapDecl, Serverdata, book::book, build_data}; + +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { + pub maps: HashMap, + pub recipes: HashSet, +} + +pub static DATA_DIR: Mutex> = Mutex::new(None); +fn data_dir() -> PathBuf { + DATA_DIR + .lock() + .unwrap() + .to_owned() + .unwrap_or_else(|| PathBuf::from_str("data").unwrap()) +} + +impl DataIndex { + pub fn load() -> Result { + let mut s = Self::default(); + s.reload()?; + Ok(s) + } + pub fn reload(&mut self) -> Result<()> { + *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; + Ok(()) + } + + pub fn read_map(&self, name: &str) -> Result { + // Scary! + if name.contains("..") || name.starts_with("/") || name.contains("//") { + bail!("illegal map path"); + } + let path = data_dir().join(format!("maps/{name}.yaml")); + Ok(read_to_string(path)?) + } + pub fn read_recipes(&self, name: &str) -> Result { + if !self.recipes.contains(name) { + bail!("unknown recipes: {name:?}"); + } + let path = data_dir().join(format!("recipes/{name}.yaml")); + Ok(read_to_string(path)?) + } + pub fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata)> { + let map_in: MapDecl = serde_yml::from_str( + &self + .read_map(map) + .context(anyhow!("Failed to read map file ({map})"))?, + ) + .context(anyhow!("Failed to parse map file ({map})"))?; + let recipes_in = serde_yml::from_str( + &self + .read_recipes(map_in.recipes.as_deref().unwrap_or("default")) + .context("Failed read recipe file")?, + ) + .context("Failed to parse recipe file")?; + + build_data(&self.maps, map.to_string(), map_in, recipes_in) + } + pub fn generate_with_book(&self, map: &str) -> Result<(Gamedata, Serverdata)> { + let (gd, mut sd) = self.generate(map)?; + sd.book = book(&gd, &sd).context("within book")?; + Ok((gd, sd)) + } +} diff --git a/server/data/src/lib.rs b/server/data/src/lib.rs new file mode 100644 index 00000000..822d6997 --- /dev/null +++ b/server/data/src/lib.rs @@ -0,0 +1,357 @@ +/* + 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 . + +*/ + +pub mod book; +pub mod demands; +pub mod entities; +pub mod index; + +use anyhow::{Result, anyhow, bail}; +use clap::Parser; +use demands::generate_demands; +use hurrycurry_protocol::{ + Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex, + book::Book, + glam::{IVec2, Vec2}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::RwLock, + time::Duration, +}; + +use crate::entities::EntityDecl; + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] +#[serde(rename_all = "snake_case")] +pub enum RecipeDeclAction { + #[default] + Never, + Passive, + Active, + Instant, + Demand, +} + +#[rustfmt::skip] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RecipeDecl { + #[serde(default)] tile: Option, + #[serde(default)] inputs: Vec, + #[serde(default)] outputs: Vec, + #[serde(default)] action: RecipeDeclAction, + #[serde(default)] warn: bool, + #[serde(default)] revert_duration: Option, + #[serde(default)] duration: Option, + #[serde(default)] points: Option, +} + +#[rustfmt::skip] +#[derive(Debug, Clone, Deserialize)] +pub struct MapDecl { + map: Vec, + tiles: HashMap, + #[serde(default)] recipes: Option, + #[serde(default)] hand_count: Option, + #[serde(default)] entities: Vec, + #[serde(default)] score_baseline: i64, + #[serde(default)] default_timer: Option, + #[serde(default)] flags: ServerdataFlags, +} + +#[derive(Parser)] +struct TileArgs { + tile_name: String, + #[clap(short = 'c', long)] + collider: bool, + #[clap(short = 'x', long)] + exclusive: bool, + #[clap(short = 'w', long)] + walkable: bool, + #[clap(long)] + book: bool, + #[clap(long)] + chef_spawn: bool, + #[clap(long)] + customer_spawn: bool, + #[clap(short = 'i', long)] + item: Option, + #[clap(long)] + conveyor: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DemandDecl { + from: String, + to: Option, + duration: f32, + points: i64, +} + +#[derive(Debug, Clone, Default)] +#[rustfmt::skip] +pub struct Serverdata { + pub initial_map: HashMap)>, + pub chef_spawn: Vec2, + pub customer_spawn: Option, + pub score_baseline: i64, + pub default_timer: Option, + pub book: Book, + pub flags: ServerdataFlags, + pub entity_decls: Vec +} + +#[rustfmt::skip] +#[derive(Debug, Clone, Default, Deserialize)] +pub struct ServerdataFlags { + #[serde(default)] pub disable_unknown_orders: bool, +} + +fn build_data( + maps: &HashMap, + map_name: String, + map_in: MapDecl, + recipes_in: Vec, +) -> Result<(Gamedata, Serverdata)> { + let reg = ItemTileRegistry::default(); + let mut recipes = Vec::new(); + let mut entities = Vec::new(); + let mut raw_demands = Vec::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)); + 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 => raw_demands.push(( + inputs.next().ok_or(anyhow!("demand needs inputs"))?, + outputs.next(), + r.duration.unwrap_or(10.), + )), + } + 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") + } + + let mut tile_specs = BTreeMap::new(); + for (char, tile_spec_raw) in map_in.tiles { + let mut toks = shlex::split(&tile_spec_raw).ok_or(anyhow!("tile spec quoting invalid"))?; + toks.insert(0, "tile-spec".to_string()); // exe name + tile_specs.insert(char, TileArgs::try_parse_from(toks)?); + } + + let mut chef_spawn = None; + let mut customer_spawn = None; + let mut initial_map = HashMap::new(); + let mut tiles_used = HashSet::new(); + let mut items_used = HashSet::new(); + let mut tile_walkable = HashSet::new(); + let mut exclusive_tiles = BTreeMap::>::new(); + for (y, line) in map_in.map.iter().enumerate() { + for (x, char) in line.chars().enumerate() { + if char == ' ' { + continue; // space is empty space + } + let pos = IVec2::new(x as i32, y as i32); + + let tile_spec = tile_specs + .get(&char) + .ok_or(anyhow!("tile {char} is undefined"))?; + + let tile = reg.register_tile(tile_spec.tile_name.clone()); + tiles_used.insert(tile); + let item = tile_spec.item.clone().map(|i| reg.register_item(i)); + items_used.extend(item); + initial_map.insert(pos, (tile, item)); + + if tile_spec.chef_spawn { + chef_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); + } + if tile_spec.customer_spawn { + customer_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); + } + if tile_spec.walkable { + tile_walkable.insert(tile); + } + if tile_spec.walkable || tile_spec.collider || tile_spec.exclusive { + exclusive_tiles.entry(tile).or_default().extend(item); + } + if tile_spec.book { + entities.push(EntityDecl::Book { pos }); + } + if let Some(off) = &tile_spec.conveyor { + let (x, y) = off + .split_once(",") + .ok_or(anyhow!("conveyor offset invalid format"))?; + let dir = IVec2::new(x.parse()?, y.parse()?); + entities.push(EntityDecl::Conveyor { + from: pos, + speed: None, + to: pos + dir, + }); + } + } + } + + for tile in tile_specs.values() { + if !tiles_used.contains(®.register_tile(tile.tile_name.clone())) { + bail!("tile {:?} is unused", tile.tile_name) + } + } + + let chef_spawn = chef_spawn.ok_or(anyhow!("map has no chef spawn"))?; + + entities.extend(map_in.entities.clone()); + + let demands = generate_demands(&tiles_used, &items_used, &raw_demands, &recipes); + + let mut maps = maps + .iter() + .filter(|(_, v)| v.players > 0) + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>(); + maps.sort_unstable_by_key(|(_, m)| m.difficulty); + maps.sort_by_key(|(_, m)| m.players); + + let mut tile_placeable_items = BTreeMap::new(); + let mut tile_interactable_empty = HashSet::new(); + for (tile, used_items) in exclusive_tiles { + let whitelist = recipes + .iter() + .filter(|r| r.tile() == Some(tile)) + .flat_map(|e| e.inputs()) + .chain(used_items) + .collect(); + let int_empty = recipes + .iter() + .any(|r| r.tile() == Some(tile) && r.inputs().is_empty()); + tile_placeable_items.insert(tile, whitelist); + if int_empty { + tile_interactable_empty.insert(tile); + } + } + + for e in &entities { + e.run_register(®); + } + + let item_names = reg.items.into_inner().unwrap(); + let tile_names = reg.tiles.into_inner().unwrap(); + + let default_timer = if map_name.ends_with("lobby") { + None + } else { + Some(Duration::from_secs(map_in.default_timer.unwrap_or(420))) + }; + + Ok(( + Gamedata { + current_map: map_name, + maps, + tile_walkable, + tile_placeable_items, + tile_interactable_empty, + recipes, + item_names, + demands, + tile_names, + bot_algos: vec![ + "waiter".to_string(), + "simple".to_string(), + "dishwasher".to_string(), + "frank".to_string(), + ], + hand_count: map_in.hand_count.unwrap_or(1), + }, + Serverdata { + initial_map, + chef_spawn, + flags: map_in.flags, + customer_spawn, + default_timer, + book: Book::default(), + score_baseline: map_in.score_baseline, + entity_decls: entities, + }, + )) +} + +#[derive(Default)] +pub(crate) struct ItemTileRegistry { + tiles: RwLock>, + items: RwLock>, +} + +impl ItemTileRegistry { + pub fn register_tile(&self, name: String) -> TileIndex { + TileIndex(Self::register(&self.tiles, name)) + } + pub fn register_item(&self, name: String) -> ItemIndex { + ItemIndex(Self::register(&self.items, name)) + } + fn register(db: &RwLock>, name: String) -> usize { + let mut db = db.write().unwrap(); + if let Some(index) = db.iter().position(|e| e == &name) { + index + } else { + let index = db.len(); + db.push(name); + index + } + } +} diff --git a/server/src/data/demands.rs b/server/src/data/demands.rs deleted file mode 100644 index 77e187af..00000000 --- a/server/src/data/demands.rs +++ /dev/null @@ -1,88 +0,0 @@ -/* - 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 hurrycurry_protocol::{Demand, ItemIndex, Recipe, TileIndex}; -use std::collections::{HashMap, HashSet}; - -pub fn generate_demands( - tiles: &HashSet, - items: &HashSet, - raw_demands: &[(ItemIndex, Option, f32)], - recipes: &[Recipe], -) -> Vec { - let recipes = recipes - .iter() - .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) - .collect::>(); - - let mut producable = HashMap::new(); - - for i in items { - producable.insert(*i, 0.0); - } - - loop { - let prod_count = producable.len(); - - for r in &recipes { - let output_count = r.outputs().iter().filter(|o| !items.contains(o)).count(); - let Some(ingred_cost) = r - .inputs() - .iter() - .map(|i| producable.get(i).copied()) - .reduce(|a, b| { - if let (Some(a), Some(b)) = (a, b) { - Some(a + b) - } else { - None - } - }) - .unwrap_or(Some(0.)) - else { - continue; - }; - - let base_cost = match r { - Recipe::Passive { speed, .. } => 2. + (1. / speed) * 0.1, - Recipe::Active { speed, .. } => 2. + (1. / speed), - Recipe::Instant { .. } => 1., - }; - - let output_cost = (ingred_cost + base_cost) / output_count as f32; - for o in r.outputs() { - let cost = producable.entry(o).or_insert(f32::INFINITY); - *cost = cost.min(output_cost); - } - } - - if prod_count == producable.len() { - break; - } - } - - raw_demands - .iter() - .filter_map(|(i, o, d)| { - producable.get(i).map(|cost| Demand { - input: *i, - output: *o, - duration: *d, - points: *cost as i64, - }) - }) - .collect() -} diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs deleted file mode 100644 index 74fae62c..00000000 --- a/server/src/data/mod.rs +++ /dev/null @@ -1,434 +0,0 @@ -/* - 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 . - -*/ -pub mod demands; - -use crate::entity::{construct_entity, Entities, EntityDecl}; -use anyhow::{anyhow, bail, Context, Result}; -use clap::Parser; -use demands::generate_demands; -use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; -use hurrycurry_protocol::{ - book::Book, - glam::{IVec2, Vec2}, - Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - fs::{read_to_string, File}, - path::PathBuf, - str::FromStr, - sync::{Mutex, RwLock}, - time::Duration, -}; - -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] -#[serde(rename_all = "snake_case")] -pub enum RecipeDeclAction { - #[default] - Never, - Passive, - Active, - Instant, - Demand, -} - -#[rustfmt::skip] -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RecipeDecl { - #[serde(default)] tile: Option, - #[serde(default)] inputs: Vec, - #[serde(default)] outputs: Vec, - #[serde(default)] action: RecipeDeclAction, - #[serde(default)] warn: bool, - #[serde(default)] revert_duration: Option, - #[serde(default)] duration: Option, - #[serde(default)] points: Option, -} - -#[rustfmt::skip] -#[derive(Debug, Clone, Deserialize)] -pub struct MapDecl { - map: Vec, - tiles: HashMap, - #[serde(default)] recipes: Option, - #[serde(default)] hand_count: Option, - #[serde(default)] entities: Vec, - #[serde(default)] score_baseline: i64, - #[serde(default)] default_timer: Option, - #[serde(default)] flags: ServerdataFlags, -} - -#[derive(Parser)] -struct TileArgs { - tile_name: String, - #[clap(short = 'c', long)] - collider: bool, - #[clap(short = 'x', long)] - exclusive: bool, - #[clap(short = 'w', long)] - walkable: bool, - #[clap(long)] - book: bool, - #[clap(long)] - chef_spawn: bool, - #[clap(long)] - customer_spawn: bool, - #[clap(short = 'i', long)] - item: Option, - #[clap(long)] - conveyor: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DemandDecl { - from: String, - to: Option, - duration: f32, - points: i64, -} - -#[derive(Debug, Clone, Default)] -#[rustfmt::skip] -pub struct Serverdata { - pub initial_map: HashMap)>, - pub chef_spawn: Vec2, - pub customer_spawn: Option, - pub score_baseline: i64, - pub default_timer: Option, - pub book: Book, - pub flags: ServerdataFlags, -} - -#[rustfmt::skip] -#[derive(Debug, Clone, Default, Deserialize)] -pub struct ServerdataFlags { - #[serde(default)] pub disable_unknown_orders: bool, -} - -#[derive(Debug, Deserialize, Default)] -pub struct DataIndex { - pub maps: HashMap, - pub recipes: HashSet, -} - -pub static DATA_DIR: Mutex> = Mutex::new(None); -fn data_dir() -> PathBuf { - DATA_DIR - .lock() - .unwrap() - .to_owned() - .unwrap_or_else(|| PathBuf::from_str("data").unwrap()) -} - -impl DataIndex { - pub fn load() -> Result { - let mut s = Self::default(); - s.reload()?; - Ok(s) - } - pub fn reload(&mut self) -> Result<()> { - *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; - Ok(()) - } - - pub fn read_map(&self, name: &str) -> Result { - // Scary! - if name.contains("..") || name.starts_with("/") || name.contains("//") { - bail!("illegal map path"); - } - let path = data_dir().join(format!("maps/{name}.yaml")); - Ok(read_to_string(path)?) - } - pub fn read_recipes(&self, name: &str) -> Result { - if !self.recipes.contains(name) { - bail!("unknown recipes: {name:?}"); - } - let path = data_dir().join(format!("recipes/{name}.yaml")); - Ok(read_to_string(path)?) - } - pub fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> { - let map_in: MapDecl = serde_yml::from_str( - &self - .read_map(map) - .context(anyhow!("Failed to read map file ({map})"))?, - ) - .context(anyhow!("Failed to parse map file ({map})"))?; - let recipes_in = serde_yml::from_str( - &self - .read_recipes(map_in.recipes.as_deref().unwrap_or("default")) - .context("Failed read recipe file")?, - ) - .context("Failed to parse recipe file")?; - - build_data(&self.maps, map.to_string(), map_in, recipes_in) - } - pub fn generate_with_book(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> { - let (gd, mut sd, es) = self.generate(map)?; - sd.book = self.read_book()?; - Ok((gd, sd, es)) - } - pub fn read_book(&self) -> Result { - serde_json::from_str( - &read_to_string(data_dir().join("book.json")).context("Failed to read book file")?, - ) - .context("Failed to parse book file") - } -} - -pub fn build_data( - maps: &HashMap, - map_name: String, - map_in: MapDecl, - recipes_in: Vec, -) -> Result<(Gamedata, Serverdata, Entities)> { - let reg = ItemTileRegistry::default(); - let mut recipes = Vec::new(); - let mut entities = Vec::new(); - let mut raw_demands = Vec::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)); - 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 => raw_demands.push(( - inputs.next().ok_or(anyhow!("demand needs inputs"))?, - outputs.next(), - r.duration.unwrap_or(10.), - )), - } - 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") - } - - let mut tile_specs = BTreeMap::new(); - for (char, tile_spec_raw) in map_in.tiles { - let mut toks = shlex::split(&tile_spec_raw).ok_or(anyhow!("tile spec quoting invalid"))?; - toks.insert(0, "tile-spec".to_string()); // exe name - tile_specs.insert(char, TileArgs::try_parse_from(toks)?); - } - - let mut chef_spawn = None; - let mut customer_spawn = None; - let mut initial_map = HashMap::new(); - let mut tiles_used = HashSet::new(); - let mut items_used = HashSet::new(); - let mut tile_walkable = HashSet::new(); - let mut exclusive_tiles = BTreeMap::>::new(); - for (y, line) in map_in.map.iter().enumerate() { - for (x, char) in line.chars().enumerate() { - if char == ' ' { - continue; // space is empty space - } - let pos = IVec2::new(x as i32, y as i32); - - let tile_spec = tile_specs - .get(&char) - .ok_or(anyhow!("tile {char} is undefined"))?; - - let tile = reg.register_tile(tile_spec.tile_name.clone()); - tiles_used.insert(tile); - let item = tile_spec.item.clone().map(|i| reg.register_item(i)); - items_used.extend(item); - initial_map.insert(pos, (tile, item)); - - if tile_spec.chef_spawn { - chef_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); - } - if tile_spec.customer_spawn { - customer_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); - } - if tile_spec.walkable { - tile_walkable.insert(tile); - } - if tile_spec.walkable || tile_spec.collider || tile_spec.exclusive { - exclusive_tiles.entry(tile).or_default().extend(item); - } - if tile_spec.book { - entities.push(construct_entity(Some(pos), &EntityDecl::Book, ®)?); - } - if let Some(off) = &tile_spec.conveyor { - let (x, y) = off - .split_once(",") - .ok_or(anyhow!("conveyor offset invalid format"))?; - let dir = IVec2::new(x.parse()?, y.parse()?); - entities.push(construct_entity( - Some(pos), - &EntityDecl::Conveyor { - dir: Some(dir), - filter: None, - filter_dir: None, - from: None, - speed: None, - to: None, - }, - ®, - )?); - } - } - } - - for tile in tile_specs.values() { - if !tiles_used.contains(®.register_tile(tile.tile_name.clone())) { - bail!("tile {:?} is unused", tile.tile_name) - } - } - - let chef_spawn = chef_spawn.ok_or(anyhow!("map has no chef spawn"))?; - - entities.extend( - map_in - .entities - .iter() - .map(|decl| construct_entity(None, decl, ®)) - .try_collect::>()?, - ); - - let demands = generate_demands(&tiles_used, &items_used, &raw_demands, &recipes); - - let bot_algos = ALGO_CONSTRUCTORS - .iter() - .map(|(name, _)| (*name).to_owned()) - .collect::>(); - - let mut maps = maps - .iter() - .filter(|(_, v)| v.players > 0) - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::>(); - maps.sort_unstable_by_key(|(_, m)| m.difficulty); - maps.sort_by_key(|(_, m)| m.players); - - let mut tile_placeable_items = BTreeMap::new(); - let mut tile_interactable_empty = HashSet::new(); - for (tile, used_items) in exclusive_tiles { - let whitelist = recipes - .iter() - .filter(|r| r.tile() == Some(tile)) - .flat_map(|e| e.inputs()) - .chain(used_items) - .collect(); - let int_empty = recipes - .iter() - .any(|r| r.tile() == Some(tile) && r.inputs().is_empty()); - tile_placeable_items.insert(tile, whitelist); - if int_empty { - tile_interactable_empty.insert(tile); - } - } - - let item_names = reg.items.into_inner().unwrap(); - let tile_names = reg.tiles.into_inner().unwrap(); - - let default_timer = if map_name.ends_with("lobby") { - None - } else { - Some(Duration::from_secs(map_in.default_timer.unwrap_or(420))) - }; - - Ok(( - Gamedata { - bot_algos, - current_map: map_name, - maps, - tile_walkable, - tile_placeable_items, - tile_interactable_empty, - recipes, - item_names, - demands, - tile_names, - hand_count: map_in.hand_count.unwrap_or(1), - }, - Serverdata { - initial_map, - chef_spawn, - flags: map_in.flags, - customer_spawn, - default_timer, - book: Book::default(), - score_baseline: map_in.score_baseline, - }, - entities, - )) -} - -#[derive(Default)] -pub struct ItemTileRegistry { - tiles: RwLock>, - items: RwLock>, -} - -impl ItemTileRegistry { - pub fn register_tile(&self, name: String) -> TileIndex { - TileIndex(Self::register(&self.tiles, name)) - } - pub fn register_item(&self, name: String) -> ItemIndex { - ItemIndex(Self::register(&self.items, name)) - } - fn register(db: &RwLock>, name: String) -> usize { - let mut db = db.write().unwrap(); - if let Some(index) = db.iter().position(|e| e == &name) { - index - } else { - let index = db.len(); - db.push(name); - index - } - } -} diff --git a/server/src/entity/campaign.rs b/server/src/entity/campaign.rs index 53ea6582..fdc169d1 100644 --- a/server/src/entity/campaign.rs +++ b/server/src/entity/campaign.rs @@ -18,16 +18,16 @@ use super::{Entity, EntityContext}; use crate::{scoreboard::ScoreboardStore, server::GameServerExt}; use anyhow::Result; +use hurrycurry_data::entities::GateCondition; use hurrycurry_locale::{trm, TrError}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, Message, PacketC, PlayerID, TileIndex, }; -use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Clone)] pub struct Map { - pub location: Vec2, + pub pos: Vec2, pub name: String, } @@ -36,7 +36,7 @@ impl Entity for Map { let mut activate = false; c.game .players_spatial_index - .query(self.location, 0.5, |_, _| activate = true); + .query(self.pos, 0.5, |_, _| activate = true); if activate { *c.load_map = Some(self.name.clone()); @@ -46,19 +46,11 @@ impl Entity for Map { } } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum GateCondition { - All(Vec), - Any(Vec), - Stars(String, u8), -} - #[derive(Debug, Clone)] pub struct Gate { pub active: bool, pub unlocked: bool, - pub location: IVec2, + pub pos: IVec2, pub blocker_tile: TileIndex, pub condition: GateCondition, } @@ -69,7 +61,7 @@ impl Entity for Gate { self.unlocked = self.condition.check(c.scoreboard); if !self.unlocked { c.game - .set_tile(self.location, Some(self.blocker_tile), c.packet_out); + .set_tile(self.pos, Some(self.blocker_tile), c.packet_out); c.packet_out.push_back(PacketC::FlushMap); // TODO dont send too often } } @@ -81,7 +73,7 @@ impl Entity for Gate { pos: Option, _player: PlayerID, ) -> Result { - if !self.unlocked && pos == Some(self.location) { + if !self.unlocked && pos == Some(self.pos) { c.packet_out.push_back(PacketC::ServerMessage { message: trm!( "s.campaign.unlock_condition", @@ -95,7 +87,11 @@ impl Entity for Gate { } } -impl GateCondition { +trait GateConditionExt { + fn check(&self, scoreboard: &ScoreboardStore) -> bool; + fn show(&self, scoreboard: &ScoreboardStore) -> Message; +} +impl GateConditionExt for GateCondition { fn check(&self, scoreboard: &ScoreboardStore) -> bool { match self { GateCondition::All(cs) => cs.iter().all(|c| c.check(scoreboard)), @@ -105,7 +101,7 @@ impl GateCondition { .is_some_and(|s| s.best.first().is_some_and(|b| b.score.stars >= *thres)), } } - pub fn show(&self, scoreboard: &ScoreboardStore) -> Message { + fn show(&self, scoreboard: &ScoreboardStore) -> Message { match self { GateCondition::All(cs) => cs .iter() diff --git a/server/src/entity/conveyor.rs b/server/src/entity/conveyor.rs index 9c7f5e4d..e31410e3 100644 --- a/server/src/entity/conveyor.rs +++ b/server/src/entity/conveyor.rs @@ -18,14 +18,12 @@ use super::{Entity, EntityContext}; use crate::interaction::interact; use anyhow::{anyhow, bail, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation}; +use hurrycurry_protocol::{glam::IVec2, ItemLocation}; #[derive(Debug, Clone)] pub struct Conveyor { pub(super) from: IVec2, pub(super) to: IVec2, - pub(super) filter_tile: Option, - pub(super) filter_item: Option, pub(super) cooldown: f32, pub(super) max_cooldown: f32, } @@ -38,24 +36,7 @@ impl Entity for Conveyor { .get(&self.from) .ok_or(anyhow!("conveyor from missing"))?; - if let Some(from_item) = from.item.as_ref() { - let filter = if let Some(t) = &self.filter_tile { - let filter_tile = c - .game - .tiles - .get(t) - .ok_or(anyhow!("conveyor filter missing"))?; - filter_tile.item.as_ref().map(|e| e.kind) - } else { - self.filter_item.as_ref().map(|i| *i) - }; - - if let Some(filter) = filter { - if from_item.kind != filter { - return Ok(()); - } - } - + if from.item.is_some() { self.cooldown += c.dt; if self.cooldown < self.max_cooldown { return Ok(()); diff --git a/server/src/entity/customers.rs b/server/src/entity/customers.rs index e3a23830..22522e4e 100644 --- a/server/src/entity/customers.rs +++ b/server/src/entity/customers.rs @@ -29,13 +29,13 @@ pub struct Customers { } impl Customers { - pub fn new(scaling_factor: f32) -> Result { - Ok(Self { + pub fn new(scaling_factor: f32) -> Self { + Self { customers: Default::default(), spawn_cooldown: 0., chair_count: None, scaling_factor, - }) + } } } diff --git a/server/src/entity/environment_effect.rs b/server/src/entity/environment_effect.rs index 95040954..ba2c395e 100644 --- a/server/src/entity/environment_effect.rs +++ b/server/src/entity/environment_effect.rs @@ -16,23 +16,11 @@ */ use super::{Entity, EntityContext}; +use hurrycurry_data::entities::EnvironmentEffect; use hurrycurry_protocol::PacketC; use rand::random; -use serde::{Deserialize, Serialize}; use std::time::{Duration, Instant}; -#[derive(Clone, Debug, Deserialize, Serialize, Default)] -pub struct EnvironmentEffect { - name: String, - #[serde(default = "default_onoff")] - on: f32, - #[serde(default = "default_onoff")] - off: f32, -} -fn default_onoff() -> f32 { - 40. -} - #[derive(Clone, Debug)] pub struct EnvironmentEffectController { config: EnvironmentEffect, diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index 928910bc..47d37f3d 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -27,26 +27,19 @@ pub mod player_portal; pub mod tram; pub mod tutorial; -use crate::{ - data::{ItemTileRegistry, Serverdata}, - entity::pedestrians::Pedestrians, - scoreboard::ScoreboardStore, -}; -use anyhow::{anyhow, Result}; +use crate::{entity::pedestrians::Pedestrians, scoreboard::ScoreboardStore}; +use anyhow::Result; use book::Book; -use campaign::{Gate, GateCondition, Map}; +use campaign::{Gate, Map}; use conveyor::Conveyor; use customers::Customers; -use environment_effect::{EnvironmentController, EnvironmentEffect, EnvironmentEffectController}; +use environment_effect::{EnvironmentController, EnvironmentEffectController}; use hurrycurry_client_lib::Game; +use hurrycurry_data::{entities::EntityDecl, Serverdata}; use hurrycurry_locale::TrError; -use hurrycurry_protocol::{ - glam::{IVec2, Vec2}, - Character, PacketC, PacketS, PlayerID, -}; +use hurrycurry_protocol::{glam::IVec2, Character, Gamedata, PacketC, PacketS, PlayerID}; use item_portal::ItemPortal; use player_portal::PlayerPortal; -use serde::{Deserialize, Serialize}; use std::{ any::Any, collections::{HashMap, VecDeque}, @@ -87,135 +80,29 @@ pub trait Entity: Any { } } -// macro_rules! entities { -// ($($e:ident),*) => { -// pub enum DynEntity { $($e($e)),* } -// impl Entity for DynEntity { -// fn tick(&mut self, c: EntityContext<'_>) -> Result<()> { -// match self { $(DynEntity::$e(x) => x.tick(c)),*, } -// } -// fn destructor(&mut self, c: EntityContext<'_>) { -// match self { $(DynEntity::$e(x) => x.destructor(c)),*, } -// } -// } -// }; -// } -// entities!( -// Conveyor, -// ItemPortal, -// PlayerPortal, -// Customers, -// EnvironmentEffectController, -// EnvironmentController -// ); - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum EntityDecl { - Conveyor { - from: Option, - to: Option, - filter_dir: Option, - filter: Option, - dir: Option, - speed: Option, - }, - ItemPortal { - from: Option, - to: IVec2, - }, - PlayerPortal { - from: Option, - to: Vec2, - }, - Customers { - scaling_factor: Option, - }, - Map { - name: String, - location: Option, - }, - EnvironmentEffect(EnvironmentEffect), - Environment(Vec), - Gate { - location: Option, - condition: GateCondition, - }, - Tram { - length: usize, - color: Option, - points: Vec, - spacing: f32, - smoothing: f32, - }, - Book, - Pedestrians { - spawn_delay: f32, - spawn_delay_stdev: Option, - speed: Option, - points: Vec, - }, -} - -pub fn construct_entity( - pos: Option, - decl: &EntityDecl, - reg: &ItemTileRegistry, -) -> Result { - Ok(match decl.to_owned() { - EntityDecl::Book => Box::new(Book(pos.ok_or(anyhow!("book is tile entity"))?)), - EntityDecl::ItemPortal { from, to } => Box::new(ItemPortal { - from: from - .or(pos) - .ok_or(anyhow!("Item portal start without start"))?, - to, - }), - EntityDecl::PlayerPortal { from, to } => Box::new(PlayerPortal { - from: from - .or(pos.map(|v| v.as_vec2())) - .ok_or(anyhow!("Player portal without start"))?, - to, - }), - EntityDecl::Conveyor { +pub fn construct_entity(decl: &EntityDecl, data: &Gamedata) -> DynEntity { + match decl.to_owned() { + EntityDecl::Book { pos } => Box::new(Book(pos)), + EntityDecl::ItemPortal { from, to } => Box::new(ItemPortal { from, to }), + EntityDecl::PlayerPortal { from, to } => Box::new(PlayerPortal { from, to }), + EntityDecl::Conveyor { from, to, speed } => Box::new(Conveyor { from, to, - speed, - dir, - filter, - filter_dir, - } => { - let from = from.or(pos).ok_or(anyhow!("Conveyor has no start"))?; - let to = to - .or(dir.map(|s| s + from)) - .ok_or(anyhow!("Conveyor has no destination"))?; - Box::new(Conveyor { - from, - to, - max_cooldown: 1. / speed.unwrap_or(2.), - filter_tile: filter_dir.map(|o| to + o), - filter_item: filter.map(|name| reg.register_item(name)), - cooldown: 0., - }) - } - EntityDecl::Map { name, location } => Box::new(Map { - location: location - .or(pos.map(|p| p.as_vec2() + 0.5)) - .ok_or(anyhow!("no location"))?, - name, + max_cooldown: 1. / speed.unwrap_or(2.), + cooldown: 0., }), - EntityDecl::Gate { - condition, - location, - } => Box::new(Gate { + EntityDecl::Map { name, pos } => Box::new(Map { pos, name }), + EntityDecl::Gate { condition, pos } => Box::new(Gate { condition, unlocked: false, - location: location.or(pos).ok_or(anyhow!("no location"))?, - blocker_tile: reg.register_tile("fence".to_string()), + pos, + blocker_tile: data + .get_tile_by_name("fence") + .expect("asserted earlier (tm)"), active: true, }), EntityDecl::Customers { scaling_factor } => { - reg.register_item("unknown-order".to_owned()); - Box::new(Customers::new(scaling_factor.unwrap_or(0.5))?) + Box::new(Customers::new(scaling_factor.unwrap_or(0.5))) } EntityDecl::EnvironmentEffect(config) => Box::new(EnvironmentEffectController::new(config)), EntityDecl::Environment(names) => Box::new(EnvironmentController(names)), @@ -254,5 +141,5 @@ pub fn construct_entity( cooldown: 0., speed: speed.unwrap_or(0.6), }), - }) + } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 3e01ba36..da49f85d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -17,7 +17,6 @@ */ #![feature(if_let_guard, let_chains, iterator_try_collect, stmt_expr_attributes)] pub mod commands; -pub mod data; pub mod entity; pub mod interaction; pub mod network; diff --git a/server/src/main.rs b/server/src/main.rs index 5514e7d0..b1265420 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -18,9 +18,10 @@ use anyhow::{bail, Result}; use clap::Parser; use futures_util::{SinkExt, StreamExt}; +use hurrycurry_data::index::DATA_DIR; use hurrycurry_locale::trm; use hurrycurry_protocol::{PacketC, PacketS}; -use hurrycurry_server::{data::DATA_DIR, server::Server, ConnectionID}; +use hurrycurry_server::{server::Server, ConnectionID}; use log::{debug, info, trace, warn, LevelFilter}; use std::{ env::var, net::SocketAddr, path::PathBuf, process::exit, str::FromStr, sync::Arc, @@ -314,8 +315,9 @@ async fn run(args: Args) -> anyhow::Result<()> { #[cfg(test)] mod test { + use hurrycurry_data::index::DATA_DIR; use hurrycurry_protocol::{Character, PacketS, PlayerClass, PlayerID}; - use hurrycurry_server::{data::DATA_DIR, server::Server, ConnectionID}; + use hurrycurry_server::{server::Server, ConnectionID}; use std::future::Future; use tokio::sync::broadcast; diff --git a/server/src/server.rs b/server/src/server.rs index e16fdb61..c370f3c4 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -16,14 +16,14 @@ */ use crate::{ - data::{DataIndex, Serverdata}, - entity::{Entities, EntityContext}, + entity::{construct_entity, Entities, EntityContext}, interaction::{interact, tick_slot}, scoreboard::ScoreboardStore, ConnectionID, }; use anyhow::{Context, Result}; use hurrycurry_client_lib::{gamedata_index::GamedataIndex, Game, Involvement, Item, Player, Tile}; +use hurrycurry_data::{index::DataIndex, Serverdata}; use hurrycurry_locale::{tre, TrError}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, @@ -353,7 +353,7 @@ impl Server { impl Server { pub fn load( &mut self, - (gamedata, serverdata, entities): (Gamedata, Serverdata, Entities), + (gamedata, serverdata): (Gamedata, Serverdata), timer: Option, ) { for mut e in self.entities.drain(..) { @@ -369,7 +369,7 @@ impl Server { load_map: &mut None, }); } - self.tick(0.); + self.tick(0.); // TODO ? self.game.load( gamedata, &serverdata, @@ -377,8 +377,11 @@ impl Server { &mut self.packet_out, ); self.gamedata_index.update(&self.game.data); + for ed in &serverdata.entity_decls { + self.entities.push(construct_entity(ed, &self.game.data)); + } self.data = serverdata.into(); - self.entities = entities; + self.entities.clear(); for e in &mut self.entities { e.constructor(EntityContext { game: &mut self.game, diff --git a/server/tools/Cargo.toml b/server/tools/Cargo.toml index db1c5ebf..1c427c81 100644 --- a/server/tools/Cargo.toml +++ b/server/tools/Cargo.toml @@ -11,6 +11,7 @@ clap = { version = "4.5.47", features = ["derive"] } hurrycurry-protocol = { path = "../protocol" } hurrycurry-server = { path = ".." } hurrycurry-locale = { path = "../locale" } +hurrycurry-data = { path = "../data" } serde_json = "1.0.145" serde = { version = "1.0.225", features = ["derive"] } markup = "0.15.0" diff --git a/server/tools/src/book.rs b/server/tools/src/book.rs deleted file mode 100644 index bffbe836..00000000 --- a/server/tools/src/book.rs +++ /dev/null @@ -1,123 +0,0 @@ -/* - 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::{diagram_layout::diagram_layout, recipe_diagram::recipe_diagram}; -use anyhow::Result; -use hurrycurry_locale::trm; -use hurrycurry_protocol::{ - Gamedata, Message, - book::{Book, BookPage}, -}; -use hurrycurry_server::data::Serverdata; - -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 { - 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/tools/src/diagram_layout.rs b/server/tools/src/diagram_layout.rs deleted file mode 100644 index 0ea26a69..00000000 --- a/server/tools/src/diagram_layout.rs +++ /dev/null @@ -1,78 +0,0 @@ -/* - 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 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, - 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, - } - #[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::()?; - diagram.nodes[index].position = pos - } - - Ok(()) -} diff --git a/server/tools/src/graph.rs b/server/tools/src/graph.rs index 53f70d99..a65ffc97 100644 --- a/server/tools/src/graph.rs +++ b/server/tools/src/graph.rs @@ -16,8 +16,8 @@ */ use anyhow::Result; +use hurrycurry_data::index::DataIndex; use hurrycurry_protocol::{Demand, ItemIndex, Recipe, RecipeIndex}; -use hurrycurry_server::data::DataIndex; pub(crate) fn graph() -> Result<()> { let mut index = DataIndex::default(); @@ -25,7 +25,7 @@ pub(crate) fn graph() -> Result<()> { println!("digraph {{"); - let (data, _, _) = index.generate("5star")?; + let (data, _) = index.generate("5star")?; for i in 0..data.item_names.len() { println!("i{i} [label=\"{}\"]", data.item_name(ItemIndex(i))) } diff --git a/server/tools/src/graph_summary.rs b/server/tools/src/graph_summary.rs index be53e768..bfdcc955 100644 --- a/server/tools/src/graph_summary.rs +++ b/server/tools/src/graph_summary.rs @@ -15,9 +15,10 @@ along with this program. If not, see . */ + use anyhow::Result; +use hurrycurry_data::index::DataIndex; use hurrycurry_protocol::{ItemIndex, Recipe, TileIndex}; -use hurrycurry_server::data::DataIndex; use std::collections::HashSet; pub(crate) fn graph_summary() -> Result<()> { @@ -26,7 +27,7 @@ pub(crate) fn graph_summary() -> Result<()> { println!("digraph {{"); - let (data, sdata, _) = index.generate("5star")?; + let (data, sdata) = index.generate("5star")?; struct Node { inputs: Vec, diff --git a/server/tools/src/main.rs b/server/tools/src/main.rs index b550dabb..f70c5755 100644 --- a/server/tools/src/main.rs +++ b/server/tools/src/main.rs @@ -16,31 +16,28 @@ */ -pub mod book; pub mod book_html; pub mod diagram_dot; -pub mod diagram_layout; pub mod diagram_svg; pub mod graph; pub mod graph_summary; pub mod map_linter; -pub mod recipe_diagram; use crate::{ - book::{book, print_book}, book_html::render_html_book, diagram_dot::{diagram_dot, diagram_dot_svg}, - diagram_layout::diagram_layout, diagram_svg::diagram_svg, graph::graph, graph_summary::graph_summary, map_linter::check_map, - recipe_diagram::recipe_diagram, }; use anyhow::Result; use clap::Parser; +use hurrycurry_data::{ + book::{book, diagram_layout::diagram_layout, print_book, recipe_diagram::recipe_diagram}, + index::DataIndex, +}; use hurrycurry_locale::FALLBACK_LOCALE; -use hurrycurry_server::data::DataIndex; #[derive(Parser)] enum Action { @@ -83,7 +80,7 @@ fn main() -> Result<()> { } => { let mut index = DataIndex::default(); index.reload()?; - let (data, serverdata, _) = index.generate("5star")?; + let (data, serverdata) = index.generate("5star")?; let mut diagram = recipe_diagram(&data, &serverdata, &[&out])?; let out = if dot_out { diagram_dot(&data, &diagram, false)? @@ -99,20 +96,20 @@ fn main() -> Result<()> { Action::Book => { let mut index = DataIndex::default(); index.reload()?; - let (data, serverdata, _) = index.generate("5star")?; + let (data, serverdata) = index.generate("5star")?; print_book(&data, &serverdata)? } Action::BookHtml => { let mut index = DataIndex::default(); index.reload()?; - let (data, serverdata, _) = index.generate("5star")?; + let (data, serverdata) = index.generate("5star")?; let book = book(&data, &serverdata)?; println!("{}", render_html_book(&data, &book, &FALLBACK_LOCALE)); } Action::MapDemands { map } => { let mut index = DataIndex::default(); index.reload()?; - let (data, _, _) = index.generate(&map)?; + let (data, _) = index.generate(&map)?; for demand in &data.demands { println!("{}", data.item_name(demand.input)) } @@ -120,7 +117,7 @@ fn main() -> Result<()> { Action::MapItems { map } => { let mut index = DataIndex::default(); index.reload()?; - let (data, _, _) = index.generate(&map)?; + let (data, _) = index.generate(&map)?; for name in &data.item_names { println!("{name}") } @@ -128,7 +125,7 @@ fn main() -> Result<()> { Action::MapTiles { map } => { let mut index = DataIndex::default(); index.reload()?; - let (data, _, _) = index.generate(&map)?; + let (data, _) = index.generate(&map)?; for name in &data.tile_names { println!("{name}") } diff --git a/server/tools/src/map_linter.rs b/server/tools/src/map_linter.rs index 738a9e10..678f2930 100644 --- a/server/tools/src/map_linter.rs +++ b/server/tools/src/map_linter.rs @@ -17,6 +17,7 @@ */ use anyhow::Result; +use hurrycurry_data::{Serverdata, index::DataIndex}; use hurrycurry_locale::{ FALLBACK_LOCALE, message::{COLORED, MessageDisplayExt}, @@ -26,7 +27,6 @@ use hurrycurry_protocol::{ Gamedata, TileIndex, glam::{IVec2, ivec2}, }; -use hurrycurry_server::data::{DataIndex, Serverdata}; use std::{ collections::{BTreeSet, HashMap, HashSet}, sync::LazyLock, @@ -148,7 +148,7 @@ pub fn check_map(map: &str) -> Result<()> { let locale = &*FALLBACK_LOCALE; let mut index = DataIndex::default(); index.reload()?; - let (data, serverdata, _) = index.generate(map)?; + let (data, serverdata) = index.generate(map)?; let mut warnings = Vec::new(); diff --git a/server/tools/src/recipe_diagram.rs b/server/tools/src/recipe_diagram.rs deleted file mode 100644 index 0be75433..00000000 --- a/server/tools/src/recipe_diagram.rs +++ /dev/null @@ -1,239 +0,0 @@ -/* - 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 anyhow::Result; -use hurrycurry_protocol::{ - Gamedata, ItemIndex, Message, Recipe, RecipeIndex, - book::{Diagram, DiagramEdge, DiagramNode, NodeStyle}, - glam::Vec2, -}; -use hurrycurry_server::data::Serverdata; -use std::{ - cmp::Reverse, - collections::{BTreeMap, BTreeSet, HashSet}, -}; - -pub(crate) 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); - } -} -- cgit v1.2.3-70-g09d2