/* Hurry Curry! - a game about cooking Copyright 2024 metamuffin Copyright 2024 nokoe 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; pub mod index; use crate::entity::{construct_entity, Entities, EntityDecl}; use anyhow::{anyhow, bail, Context, Result}; use demands::generate_demands; use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, DocumentElement, Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex, }; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs::File, path::PathBuf, str::FromStr, sync::{Mutex, RwLock}, time::Duration, }; use tokio::fs::read_to_string; #[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 { #[serde(default)] recipes: Option, map: Vec, tiles: HashMap, #[serde(default)] items: HashMap, collider: Vec, walkable: Vec, chef_spawn: char, customer_spawn: char, #[serde(default)] entities: Vec, #[serde(default)] tile_entities: HashMap, #[serde(default)] score_baseline: i64, #[serde(default)] default_timer: Option, #[serde(default)] flags: ServerdataFlags, } #[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: Vec2, pub score_baseline: i64, pub default_timer: Option, pub book: DocumentElement, 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 async fn load() -> Result { let mut s = Self::default(); s.reload().await?; Ok(s) } pub async fn reload(&mut self) -> Result<()> { *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; Ok(()) } pub async 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).await?) } pub async 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).await?) } pub async fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> { let map_in: MapDecl = serde_yml::from_str(&self.read_map(map).await?)?; let recipes_in = serde_yml::from_str( &self .read_recipes(map_in.recipes.as_deref().unwrap_or("default")) .await?, )?; let book = serde_json::from_str( &read_to_string(data_dir().join("book.json")) .await .context("loading book")?, ) .context("invalid book")?; build_data(&self.maps, map.to_string(), map_in, recipes_in, book) } } pub fn build_data( maps: &HashMap, map_name: String, map_in: MapDecl, recipes_in: Vec, book: DocumentElement, ) -> 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 { 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 chef_spawn = Vec2::new(0., 0.); let mut customer_spawn = Vec2::new(0., 0.); let mut initial_map = HashMap::new(); let mut tiles_used = HashSet::new(); let mut items_used = HashSet::new(); for (y, line) in map_in.map.iter().enumerate() { for (x, tile) in line.chars().enumerate() { if tile == ' ' { continue; // space is empty space } let pos = IVec2::new(x as i32, y as i32); if tile == map_in.chef_spawn { chef_spawn = pos.as_vec2() + Vec2::splat(0.5); } if tile == map_in.customer_spawn { customer_spawn = pos.as_vec2() + Vec2::splat(0.5); } let tilename = map_in .tiles .get(&tile) .ok_or(anyhow!("tile {tile} is undefined"))? .clone(); let itemname = map_in.items.get(&tile).cloned(); let tile = reg.register_tile(tilename); let item = itemname.map(|i| reg.register_item(i)); tiles_used.insert(tile); if let Some(i) = item { items_used.insert(i); }; initial_map.insert(pos, (tile, item)); } } for (y, line) in map_in.map.iter().enumerate() { for (x, tile) in line.trim().chars().enumerate() { let pos = IVec2::new(x as i32, y as i32); if let Some(ent) = map_in.tile_entities.get(&tile) { entities.push(construct_entity(Some(pos), ent, ®)?); } } } 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 item_names = reg.items.into_inner().unwrap(); let tile_names = reg.tiles.into_inner().unwrap(); let tile_collide = tile_names .iter() .map(|i| !map_in.walkable.contains(i)) .collect(); let tile_interact = tile_names .iter() .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) .collect(); 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_collide, tile_interact, recipes, item_names, demands, tile_names, }, Serverdata { initial_map, chef_spawn, flags: map_in.flags, customer_spawn, default_timer, book, 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 } } }