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 --- 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 +++++++++++++++++++++++++++++++++ 7 files changed, 1083 insertions(+) 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 (limited to 'server/data/src') 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 + } + } +} -- cgit v1.2.3-70-g09d2