aboutsummaryrefslogtreecommitdiff
path: root/server/src/data.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/data.rs')
-rw-r--r--server/src/data.rs405
1 files changed, 405 insertions, 0 deletions
diff --git a/server/src/data.rs b/server/src/data.rs
new file mode 100644
index 00000000..2d190f3b
--- /dev/null
+++ b/server/src/data.rs
@@ -0,0 +1,405 @@
+/*
+ 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 hurrycurry_protocol::{
+ glam::{IVec2, Vec2},
+ 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;
+
+#[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 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_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_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_yml::from_str(&self.read_map(map).await?)?;
+ let recipes_in = serde_yml::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();
+
+ 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,
+ &reg,
+ &tiles_used,
+ &items_used,
+ &raw_demands,
+ &recipes,
+ &initial_map,
+ )?);
+ }
+ }
+ }
+
+ entities.extend(
+ map_in
+ .entities
+ .iter()
+ .map(|decl| {
+ construct_entity(
+ None,
+ decl,
+ &reg,
+ &tiles_used,
+ &items_used,
+ &raw_demands,
+ &recipes,
+ &initial_map,
+ )
+ })
+ .try_collect::<Vec<_>>()?,
+ );
+
+ 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,
+ 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 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))
+ }
+}
+/*
+ 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/>.
+
+*/