diff options
Diffstat (limited to 'server/src/data')
| -rw-r--r-- | server/src/data/demands.rs | 94 | ||||
| -rw-r--r-- | server/src/data/mod.rs | 371 | 
2 files changed, 465 insertions, 0 deletions
| diff --git a/server/src/data/demands.rs b/server/src/data/demands.rs new file mode 100644 index 00000000..2501e225 --- /dev/null +++ b/server/src/data/demands.rs @@ -0,0 +1,94 @@ +/* +    Hurry Curry! - 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 <https://www.gnu.org/licenses/>. + +*/ +use super::Demand; +use crate::interaction::Recipe; +use hurrycurry_protocol::{ItemIndex, TileIndex}; +use std::collections::{HashMap, HashSet}; + +pub fn generate_demands( +    tiles: HashSet<TileIndex>, +    items: HashSet<ItemIndex>, +    raw_demands: &[(ItemIndex, Option<ItemIndex>, f32)], +    recipes: &[Recipe], +) -> Vec<Demand> { +    let recipes = recipes +        .iter() +        .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) +        .collect::<Vec<_>>(); + +    let mut producable = HashMap::new(); + +    for i in &items { +        producable.insert(*i, 0.0); +    } + +    loop { +        let prod_count = producable.len(); + +        for r in &recipes { +            let output_count = r.outputs().iter().filter(|o| !items.contains(&o)).count(); +            let Some(ingred_cost) = r +                .inputs() +                .iter() +                .map(|i| producable.get(i).copied()) +                .reduce(|a, b| { +                    if let (Some(a), Some(b)) = (a, b) { +                        Some(a + b) +                    } else { +                        None +                    } +                }) +                .unwrap_or(Some(0.)) +            else { +                continue; +            }; + +            let base_cost = match r { +                Recipe::Passive { duration, .. } => 2. + duration * 0.1, +                Recipe::Active { duration, .. } => 2. + duration, +                Recipe::Instant { .. } => 1., +            }; + +            let output_cost = (ingred_cost + base_cost) / output_count as f32; +            for o in r.outputs() { +                let cost = producable.entry(o).or_insert(f32::INFINITY); +                *cost = cost.min(output_cost); +            } +        } + +        if prod_count == producable.len() { +            break; +        } +    } + +    raw_demands +        .iter() +        .filter_map(|(i, o, d)| { +            if let Some(cost) = producable.get(i) { +                Some(Demand { +                    from: *i, +                    to: *o, +                    duration: *d, +                    points: *cost as i64, +                }) +            } else { +                None +            } +        }) +        .collect() +} diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs new file mode 100644 index 00000000..6df60535 --- /dev/null +++ b/server/src/data/mod.rs @@ -0,0 +1,371 @@ +/* +    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/>. + +*/ +use crate::{ +    entity::{construct_entity, Entity, EntityDecl}, +    interaction::Recipe, +}; +use anyhow::{anyhow, bail, Result}; +use demands::generate_demands; +use hurrycurry_protocol::{ +    glam::{IVec2, Vec2}, +    DemandIndex, ItemIndex, MapMetadata, RecipeIndex, 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; + +pub mod demands; + +#[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>, +} + +#[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, Serialize, Deserialize, Default)] +#[rustfmt::skip] +pub struct Gamedata { +    pub spec: String, +    pub item_names: Vec<String>, +    pub tile_names: Vec<String>, +    pub tile_collide: Vec<bool>, +    pub tile_interact: Vec<bool>, +    pub map: HashMap<String, MapMetadata>, +    #[serde(skip)] pub recipes: Vec<Recipe>, +    #[serde(skip)] pub demands: Vec<Demand>, +    #[serde(skip)] pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>, +    #[serde(skip)] pub chef_spawn: Vec2, +    #[serde(skip)] pub customer_spawn: Vec2, +    #[serde(skip)] pub entities: Vec<Entity>, +} + +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { +    pub maps: HashMap<String, MapMetadata>, +    pub demands: HashSet<String>, +    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_yaml::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_demands(&self, name: &str) -> Result<String> { +        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<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> { +        let (map, recipes) = spec.split_once("-").unwrap_or((spec.as_str(), "default")); + +        let map_in = serde_yaml::from_str(&self.read_map(map).await?)?; +        let recipes_in = serde_yaml::from_str(&self.read_recipes(recipes).await?)?; + +        let mut gd = Gamedata::build(spec, map_in, recipes_in)?; +        gd.map = self.maps.clone(); +        Ok(gd) +    } +} + +impl Gamedata { +    pub fn build(spec: String, map_in: InitialMap, recipes_in: Vec<RecipeDecl>) -> Result<Self> { +        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.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 = 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)); +            } +        } + +        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 { +            spec, +            demands, +            tile_collide, +            tile_interact, +            recipes, +            map: HashMap::new(), +            initial_map, +            item_names, +            entities, +            tile_names, +            chef_spawn, +            customer_spawn, +        }) +    } +} + +#[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 +        } +    } +} + +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<TileIndex> { +        self.tile_names +            .iter() +            .position(|t| t == name) +            .map(TileIndex) +    } +    pub fn get_item_by_name(&self, name: &str) -> Option<ItemIndex> { +        self.item_names +            .iter() +            .position(|t| t == name) +            .map(ItemIndex) +    } +    pub fn recipes(&self) -> impl Iterator<Item = (RecipeIndex, &Recipe)> { +        self.recipes +            .iter() +            .enumerate() +            .map(|(i, e)| (RecipeIndex(i), e)) +    } +} | 
