aboutsummaryrefslogtreecommitdiff
path: root/server/data/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/data/src/lib.rs')
-rw-r--r--server/data/src/lib.rs357
1 files changed, 357 insertions, 0 deletions
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 <https://www.gnu.org/licenses/>.
+
+*/
+
+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<String>,
+ #[serde(default)] inputs: Vec<String>,
+ #[serde(default)] outputs: Vec<String>,
+ #[serde(default)] action: RecipeDeclAction,
+ #[serde(default)] warn: bool,
+ #[serde(default)] revert_duration: Option<f32>,
+ #[serde(default)] duration: Option<f32>,
+ #[serde(default)] points: Option<i64>,
+}
+
+#[rustfmt::skip]
+#[derive(Debug, Clone, Deserialize)]
+pub struct MapDecl {
+ map: Vec<String>,
+ tiles: HashMap<char, String>,
+ #[serde(default)] recipes: Option<String>,
+ #[serde(default)] hand_count: Option<usize>,
+ #[serde(default)] entities: Vec<EntityDecl>,
+ #[serde(default)] score_baseline: i64,
+ #[serde(default)] default_timer: Option<u64>,
+ #[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<String>,
+ #[clap(long)]
+ conveyor: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DemandDecl {
+ from: String,
+ to: Option<String>,
+ duration: f32,
+ points: i64,
+}
+
+#[derive(Debug, Clone, Default)]
+#[rustfmt::skip]
+pub struct Serverdata {
+ pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>,
+ pub chef_spawn: Vec2,
+ pub customer_spawn: Option<Vec2>,
+ pub score_baseline: i64,
+ pub default_timer: Option<Duration>,
+ pub book: Book,
+ pub flags: ServerdataFlags,
+ pub entity_decls: Vec<EntityDecl>
+}
+
+#[rustfmt::skip]
+#[derive(Debug, Clone, Default, Deserialize)]
+pub struct ServerdataFlags {
+ #[serde(default)] pub disable_unknown_orders: bool,
+}
+
+fn build_data(
+ maps: &HashMap<String, MapMetadata>,
+ map_name: String,
+ map_in: MapDecl,
+ recipes_in: Vec<RecipeDecl>,
+) -> 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::<TileIndex, HashSet<ItemIndex>>::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(&reg.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::<Vec<(String, MapMetadata)>>();
+ 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(&reg);
+ }
+
+ 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<Vec<String>>,
+ items: RwLock<Vec<String>>,
+}
+
+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<Vec<String>>, 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
+ }
+ }
+}