diff options
Diffstat (limited to 'server/src/data/mod.rs')
-rw-r--r-- | server/src/data/mod.rs | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs new file mode 100644 index 00000000..6555fbc4 --- /dev/null +++ b/server/src/data/mod.rs @@ -0,0 +1,341 @@ +/* + 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 <https://www.gnu.org/licenses/>. + +*/ +pub mod demands; + +use crate::entity::{construct_entity, Entities, EntityDecl}; +use anyhow::{anyhow, bail, Result}; +use demands::generate_demands; +use hurrycurry_protocol::{ + glam::{IVec2, Vec2}, + Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex, +}; +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, + Demand, +} + +#[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: Action, + #[serde(default)] + warn: bool, + #[serde(default)] + revert_duration: Option<f32>, + #[serde(default)] + duration: Option<f32>, + #[serde(default)] + points: Option<i64>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct InitialMap { + map: Vec<String>, + tiles: HashMap<char, String>, + #[serde(default)] + items: HashMap<char, String>, + collider: Vec<String>, + walkable: Vec<String>, + chef_spawn: char, + customer_spawn: char, + #[serde(default)] + entities: Vec<EntityDecl>, + #[serde(default)] + tile_entities: HashMap<char, EntityDecl>, + #[serde(default)] + score_baseline: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DemandDecl { + from: String, + to: Option<String>, + duration: f32, + points: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Demand { + pub from: ItemIndex, + pub to: Option<ItemIndex>, + pub duration: f32, + pub points: i64, +} + +#[derive(Debug,Clone, Default)] +#[rustfmt::skip] +pub struct Serverdata { + pub demands: Vec<Demand>, + pub spec: String, + pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>, + pub chef_spawn: Vec2, + pub customer_spawn: Vec2, + pub score_baseline: i64, +} + +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { + pub maps: HashMap<String, MapMetadata>, + pub recipes: HashSet<String>, +} + +pub static DATA_DIR: Mutex<Option<PathBuf>> = 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_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; + Ok(()) + } + + pub async fn read_map(&self, name: &str) -> Result<String> { + if !self.maps.contains_key(name) { + bail!("unknown map: {name:?}"); + } + let path = data_dir().join(format!("maps/{name}.yaml")); + Ok(read_to_string(path).await?) + } + pub async fn read_recipes(&self, name: &str) -> Result<String> { + 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<(Gamedata, Serverdata, Entities)> { + let (map, recipes) = spec.split_once("-").unwrap_or((spec.as_str(), "default")); + + let map_in = serde_yml::from_str(&self.read_map(map).await?)?; + let recipes_in = serde_yml::from_str(&self.read_recipes(recipes).await?)?; + + Ok(build_data( + self.maps.clone(), + spec.clone(), + map.to_string(), + map_in, + recipes_in, + )?) + } +} + +pub fn build_data( + maps: HashMap<String, MapMetadata>, + spec: String, + map_name: String, + map_in: InitialMap, + recipes_in: Vec<RecipeDecl>, +) -> 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 { + Action::Never => {} + Action::Passive => recipes.push(Recipe::Passive { + duration: r.duration.ok_or(anyhow!("duration for passive missing"))?, + warn: r.warn, + tile, + revert_duration: r.revert_duration, + input: inputs + .next() + .ok_or(anyhow!("passive recipe without input"))?, + output: outputs.next(), + }), + Action::Active => recipes.push(Recipe::Active { + duration: 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()], + }), + Action::Instant => { + recipes.push(Recipe::Instant { + points: r.points.take().unwrap_or(0), + tile, + inputs: [inputs.next(), inputs.next()], + outputs: [outputs.next(), outputs.next()], + }); + } + Action::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") + } + + // TODO + // for d in demands_in { + // demands.push(Demand { + // from: reg.register_item(d.from), + // to: d.to.map(|to| reg.register_item(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(); + 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::<Vec<_>>()?, + ); + + let demands = generate_demands(&tiles_used, &items_used, &raw_demands, &recipes); + + 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(); + + Ok(( + Gamedata { + current_map: map_name, + maps, + tile_collide, + tile_interact, + recipes, + item_names, + tile_names, + }, + Serverdata { + spec, + demands, + initial_map, + chef_spawn, + customer_spawn, + score_baseline: map_in.score_baseline, + }, + entities, + )) +} + +#[derive(Default)] +pub 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 + } + } +} |