diff options
Diffstat (limited to 'server/data/src/lib.rs')
-rw-r--r-- | server/data/src/lib.rs | 357 |
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(®.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(®); + } + + 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 + } + } +} |