/* 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::{ interaction::Recipe, protocol::{DemandIndex, ItemIndex, RecipeIndex, TileIndex}, }; use anyhow::{anyhow, bail, Context, Result}; use glam::{IVec2, Vec2}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs::File, path::PathBuf, str::FromStr, sync::{Mutex, RwLock}, }; #[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, } #[derive(Debug, Clone, Deserialize)] pub struct InitialMap { map: Vec, tiles: HashMap, items: HashMap, collider: Vec, walkable: Vec, chef_spawn: char, customer_spawn: char, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DemandDecl { from: String, to: String, duration: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Demand { pub from: ItemIndex, pub to: ItemIndex, pub duration: f32, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Gamedata { #[serde(skip)] pub recipes: Vec, #[serde(skip)] pub demands: Vec, pub item_names: Vec, pub tile_names: Vec, pub tile_collide: Vec, pub tile_interact: Vec, #[serde(skip)] pub initial_map: HashMap)>, pub chef_spawn: Vec2, pub customer_spawn: Vec2, } #[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) -> anyhow::Result<()> { *self = serde_yaml::from_reader(File::open(data_dir().join("index.yaml"))?)?; Ok(()) } pub fn generate(&self, spec: String) -> anyhow::Result { let [demands, map, recipes] = spec .split("-") .collect::>() .try_into() .map_err(|_| anyhow!("data specification malformed"))?; if !self.demands.contains(demands) { bail!("unknown demands: {demands:?}"); } if !self.maps.contains(map) { bail!("unknown map: {map:?}"); } if !self.recipes.contains(recipes) { bail!("unknown recipes: {recipes:?}"); } let demands_path = data_dir().join(format!("demands/{demands}.yaml")); let map_path = data_dir().join(format!("maps/{map}.yaml")); let recipes_path = data_dir().join(format!("recipes/{recipes}.yaml")); let demands_in = serde_yaml::from_reader(File::open(demands_path).context("opening demands failed")?)?; let map_in = serde_yaml::from_reader(File::open(map_path).context("opening map failed")?)?; let recipes_in = serde_yaml::from_reader( File::open(recipes_path).context("opening recipes failed. are they generated yet?")?, )?; Ok(Gamedata::build(recipes_in, map_in, demands_in)?) } } impl Gamedata { pub fn build( recipes_in: Vec, map_in: InitialMap, demands_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(); for 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 { tile, inputs: [inputs.next(), inputs.next()], outputs: [outputs.next(), outputs.next()], }); } } assert_eq!(inputs.next(), None, "{r2:?}"); assert_eq!(outputs.next(), None, "{r2:?}"); } for d in demands_in { demands.push(Demand { from: ItemIndex(register(&item_names, d.from)), to: ItemIndex(register(&item_names, d.to)), duration: d.duration, }) } 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(); 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(); Ok(Gamedata { demands, tile_collide, tile_interact, recipes, initial_map, item_names, 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)) } }