/* Undercooked - a game about cooking Copyright 2024 metamuffin 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::{ entity::{construct_entity, Entity, EntityDecl}, interaction::Recipe, protocol::{DemandIndex, ItemIndex, RecipeIndex, TileIndex}, }; use anyhow::{anyhow, bail, Result}; use glam::{IVec2, Vec2}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs::File, path::PathBuf, str::FromStr, sync::{Mutex, RwLock}, }; use tokio::fs::read_to_string; #[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] #[serde(rename_all = "snake_case")] pub enum Action { #[default] Never, Passive, Active, Instant, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct RecipeDecl { #[serde(default)] tile: Option, #[serde(default)] inputs: Vec, #[serde(default)] outputs: Vec, #[serde(default)] action: Action, #[serde(default)] warn: bool, #[serde(default)] revert_duration: Option, #[serde(default)] duration: Option, #[serde(default)] points: Option, } #[derive(Debug, Clone, Deserialize)] pub struct InitialMap { 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, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DemandDecl { from: String, to: Option, duration: f32, points: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Demand { pub from: ItemIndex, pub to: Option, pub duration: f32, pub points: i64, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[rustfmt::skip] pub struct Gamedata { pub item_names: Vec, pub tile_names: Vec, pub tile_collide: Vec, pub tile_interact: Vec, pub map_names: HashSet, #[serde(skip)] pub recipes: Vec, #[serde(skip)] pub demands: Vec, #[serde(skip)] pub initial_map: HashMap)>, #[serde(skip)] pub chef_spawn: Vec2, #[serde(skip)] pub customer_spawn: Vec2, #[serde(skip)] pub entities: Vec, } #[derive(Debug, Deserialize, Default)] pub struct DataIndex { pub maps: HashSet, pub demands: HashSet, 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 reload(&mut self) -> Result<()> { *self = serde_yaml::from_reader(File::open(data_dir().join("index.yaml"))?)?; Ok(()) } pub async fn read_map(&self, name: &str) -> Result { if !self.maps.contains(name) { bail!("unknown map: {name:?}"); } let path = data_dir().join(format!("maps/{name}.yaml")); Ok(read_to_string(path).await?) } pub async fn read_demands(&self, name: &str) -> Result { if !self.demands.contains(name) { bail!("unknown demands: {name:?}"); } let path = data_dir().join(format!("demands/{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, spec: String) -> Result { let (map, rest) = spec.split_once("-").unwrap_or((spec.as_str(), "default")); let (demands, recipes) = rest.split_once("-").unwrap_or((rest, "default")); let map_in = serde_yaml::from_str(&self.read_map(map).await?)?; let demands_in = serde_yaml::from_str(&self.read_demands(demands).await?)?; let recipes_in = serde_yaml::from_str(&self.read_recipes(recipes).await?)?; let mut gd = Gamedata::build(map_in, demands_in, recipes_in)?; gd.map_names = self.maps.clone(); Ok(gd) } } impl Gamedata { pub fn build( map_in: InitialMap, demands_in: Vec, recipes_in: Vec, ) -> Result { let item_names = RwLock::new(Vec::new()); let tile_names = RwLock::new(Vec::new()); let mut recipes = Vec::new(); let mut demands = Vec::new(); let mut entities = Vec::new(); for mut r in recipes_in { let r2 = r.clone(); let mut inputs = r .inputs .into_iter() .map(|i| ItemIndex(register(&item_names, i))); let mut outputs = r .outputs .into_iter() .map(|o| ItemIndex(register(&item_names, o))); let tile = r.tile.map(|t| TileIndex(register(&tile_names, t))); match r.action { Action::Never => {} Action::Passive => recipes.push(Recipe::Passive { duration: r.duration.expect("duration for passive missing"), warn: r.warn, tile, revert_duration: r.revert_duration, input: inputs.next().expect("passive recipe without input"), output: outputs.next(), }), Action::Active => recipes.push(Recipe::Active { duration: r.duration.expect("duration for active missing"), tile, input: inputs.next().expect("active recipe without input"), outputs: [outputs.next(), outputs.next()], }), Action::Instant => { recipes.push(Recipe::Instant { points: r.points.take().unwrap_or(0), tile, inputs: [inputs.next(), inputs.next()], outputs: [outputs.next(), outputs.next()], }); } } 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") } for d in demands_in { demands.push(Demand { from: ItemIndex(register(&item_names, d.from)), to: d.to.map(|to| ItemIndex(register(&item_names, to))), duration: d.duration, points: d.points, }) } let mut chef_spawn = Vec2::new(0., 0.); let mut customer_spawn = Vec2::new(0., 0.); let mut initial_map = HashMap::new(); 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 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(); if let Some(ent) = map_in.tile_entities.get(&tile) { entities.push(construct_entity(Some(pos), ent)?); } let itemname = map_in.items.get(&tile).cloned(); let tile = TileIndex(register(&tile_names, tilename)); let item = itemname.map(|i| ItemIndex(register(&item_names, i))); initial_map.insert(pos, (tile, item)); } } let item_names = item_names.into_inner().unwrap(); let tile_names = tile_names.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(); entities.extend( map_in .entities .iter() .map(|decl| construct_entity(None, decl)) .try_collect::>()?, ); Ok(Gamedata { demands, tile_collide, tile_interact, recipes, map_names: HashSet::new(), initial_map, item_names, entities, tile_names, chef_spawn, customer_spawn, }) } } 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 } } impl Gamedata { pub fn tile_name(&self, index: TileIndex) -> &String { &self.tile_names[index.0] } pub fn is_tile_colliding(&self, index: TileIndex) -> bool { self.tile_collide[index.0] } pub fn is_tile_interactable(&self, index: TileIndex) -> bool { self.tile_interact[index.0] } pub fn item_name(&self, index: ItemIndex) -> &String { &self.item_names[index.0] } pub fn recipe(&self, index: RecipeIndex) -> &Recipe { &self.recipes[index.0] } pub fn demand(&self, index: DemandIndex) -> &Demand { &self.demands[index.0] } pub fn get_tile_by_name(&self, name: &str) -> Option { self.tile_names .iter() .position(|t| t == name) .map(TileIndex) } pub fn get_item_by_name(&self, name: &str) -> Option { self.item_names .iter() .position(|t| t == name) .map(ItemIndex) } pub fn recipes(&self) -> impl Iterator { self.recipes .iter() .enumerate() .map(|(i, e)| (RecipeIndex(i), e)) } }