diff options
Diffstat (limited to 'server/src/data/mod.rs')
-rw-r--r-- | server/src/data/mod.rs | 434 |
1 files changed, 0 insertions, 434 deletions
diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs deleted file mode 100644 index 74fae62c..00000000 --- a/server/src/data/mod.rs +++ /dev/null @@ -1,434 +0,0 @@ -/* - Hurry Curry! - a game about cooking - Copyright (C) 2025 Hurry Curry! Contributors - - 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, Context, Result}; -use clap::Parser; -use demands::generate_demands; -use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; -use hurrycurry_protocol::{ - book::Book, - glam::{IVec2, Vec2}, - Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - fs::{read_to_string, File}, - path::PathBuf, - str::FromStr, - sync::{Mutex, RwLock}, - time::Duration, -}; - -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] -#[serde(rename_all = "snake_case")] -pub enum RecipeDeclAction { - #[default] - Never, - Passive, - Active, - Instant, - Demand, -} - -#[rustfmt::skip] -#[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: RecipeDeclAction, - #[serde(default)] warn: bool, - #[serde(default)] revert_duration: Option<f32>, - #[serde(default)] duration: Option<f32>, - #[serde(default)] points: Option<i64>, -} - -#[rustfmt::skip] -#[derive(Debug, Clone, Deserialize)] -pub struct MapDecl { - map: Vec<String>, - tiles: HashMap<char, String>, - #[serde(default)] recipes: Option<String>, - #[serde(default)] hand_count: Option<usize>, - #[serde(default)] entities: Vec<EntityDecl>, - #[serde(default)] score_baseline: i64, - #[serde(default)] default_timer: Option<u64>, - #[serde(default)] flags: ServerdataFlags, -} - -#[derive(Parser)] -struct TileArgs { - tile_name: String, - #[clap(short = 'c', long)] - collider: bool, - #[clap(short = 'x', long)] - exclusive: bool, - #[clap(short = 'w', long)] - walkable: bool, - #[clap(long)] - book: bool, - #[clap(long)] - chef_spawn: bool, - #[clap(long)] - customer_spawn: bool, - #[clap(short = 'i', long)] - item: Option<String>, - #[clap(long)] - conveyor: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DemandDecl { - from: String, - to: Option<String>, - duration: f32, - points: i64, -} - -#[derive(Debug, Clone, Default)] -#[rustfmt::skip] -pub struct Serverdata { - pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>, - pub chef_spawn: Vec2, - pub customer_spawn: Option<Vec2>, - pub score_baseline: i64, - pub default_timer: Option<Duration>, - pub book: Book, - pub flags: ServerdataFlags, -} - -#[rustfmt::skip] -#[derive(Debug, Clone, Default, Deserialize)] -pub struct ServerdataFlags { - #[serde(default)] pub disable_unknown_orders: bool, -} - -#[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 load() -> Result<Self> { - let mut s = Self::default(); - s.reload()?; - Ok(s) - } - pub fn reload(&mut self) -> Result<()> { - *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; - Ok(()) - } - - pub fn read_map(&self, name: &str) -> Result<String> { - // Scary! - if name.contains("..") || name.starts_with("/") || name.contains("//") { - bail!("illegal map path"); - } - let path = data_dir().join(format!("maps/{name}.yaml")); - Ok(read_to_string(path)?) - } - pub 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)?) - } - pub fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> { - let map_in: MapDecl = serde_yml::from_str( - &self - .read_map(map) - .context(anyhow!("Failed to read map file ({map})"))?, - ) - .context(anyhow!("Failed to parse map file ({map})"))?; - let recipes_in = serde_yml::from_str( - &self - .read_recipes(map_in.recipes.as_deref().unwrap_or("default")) - .context("Failed read recipe file")?, - ) - .context("Failed to parse recipe file")?; - - build_data(&self.maps, map.to_string(), map_in, recipes_in) - } - pub fn generate_with_book(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> { - let (gd, mut sd, es) = self.generate(map)?; - sd.book = self.read_book()?; - Ok((gd, sd, es)) - } - pub fn read_book(&self) -> Result<Book> { - serde_json::from_str( - &read_to_string(data_dir().join("book.json")).context("Failed to read book file")?, - ) - .context("Failed to parse book file") - } -} - -pub fn build_data( - maps: &HashMap<String, MapMetadata>, - map_name: String, - map_in: MapDecl, - 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 { - #[cfg(feature = "fast_recipes")] - match r.action { - RecipeDeclAction::Passive | RecipeDeclAction::Active => { - if !r.warn { - r.duration = Some(0.5) - } - } - _ => (), - } - - 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 { - RecipeDeclAction::Never => {} - RecipeDeclAction::Passive => recipes.push(Recipe::Passive { - speed: 1. / r.duration.ok_or(anyhow!("duration for passive missing"))?, - warn: r.warn, - tile, - revert_speed: r.revert_duration.map(|d| 1. / d), - input: inputs - .next() - .ok_or(anyhow!("passive recipe without input"))?, - output: outputs.next(), - }), - RecipeDeclAction::Active => recipes.push(Recipe::Active { - speed: 1. / 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()], - }), - RecipeDeclAction::Instant => { - recipes.push(Recipe::Instant { - points: r.points.take().unwrap_or(0), - tile, - inputs: [inputs.next(), inputs.next()], - outputs: [outputs.next(), outputs.next()], - }); - } - RecipeDeclAction::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") - } - - let mut tile_specs = BTreeMap::new(); - for (char, tile_spec_raw) in map_in.tiles { - let mut toks = shlex::split(&tile_spec_raw).ok_or(anyhow!("tile spec quoting invalid"))?; - toks.insert(0, "tile-spec".to_string()); // exe name - tile_specs.insert(char, TileArgs::try_parse_from(toks)?); - } - - let mut chef_spawn = None; - let mut customer_spawn = None; - let mut initial_map = HashMap::new(); - let mut tiles_used = HashSet::new(); - let mut items_used = HashSet::new(); - let mut tile_walkable = HashSet::new(); - let mut exclusive_tiles = BTreeMap::<TileIndex, HashSet<ItemIndex>>::new(); - for (y, line) in map_in.map.iter().enumerate() { - for (x, char) in line.chars().enumerate() { - if char == ' ' { - continue; // space is empty space - } - let pos = IVec2::new(x as i32, y as i32); - - let tile_spec = tile_specs - .get(&char) - .ok_or(anyhow!("tile {char} is undefined"))?; - - let tile = reg.register_tile(tile_spec.tile_name.clone()); - tiles_used.insert(tile); - let item = tile_spec.item.clone().map(|i| reg.register_item(i)); - items_used.extend(item); - initial_map.insert(pos, (tile, item)); - - if tile_spec.chef_spawn { - chef_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); - } - if tile_spec.customer_spawn { - customer_spawn = Some(pos.as_vec2() + Vec2::splat(0.5)); - } - if tile_spec.walkable { - tile_walkable.insert(tile); - } - if tile_spec.walkable || tile_spec.collider || tile_spec.exclusive { - exclusive_tiles.entry(tile).or_default().extend(item); - } - if tile_spec.book { - entities.push(construct_entity(Some(pos), &EntityDecl::Book, ®)?); - } - if let Some(off) = &tile_spec.conveyor { - let (x, y) = off - .split_once(",") - .ok_or(anyhow!("conveyor offset invalid format"))?; - let dir = IVec2::new(x.parse()?, y.parse()?); - entities.push(construct_entity( - Some(pos), - &EntityDecl::Conveyor { - dir: Some(dir), - filter: None, - filter_dir: None, - from: None, - speed: None, - to: None, - }, - ®, - )?); - } - } - } - - for tile in tile_specs.values() { - if !tiles_used.contains(®.register_tile(tile.tile_name.clone())) { - bail!("tile {:?} is unused", tile.tile_name) - } - } - - let chef_spawn = chef_spawn.ok_or(anyhow!("map has no chef spawn"))?; - - 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 bot_algos = ALGO_CONSTRUCTORS - .iter() - .map(|(name, _)| (*name).to_owned()) - .collect::<Vec<String>>(); - - let mut maps = maps - .iter() - .filter(|(_, v)| v.players > 0) - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::<Vec<(String, MapMetadata)>>(); - maps.sort_unstable_by_key(|(_, m)| m.difficulty); - maps.sort_by_key(|(_, m)| m.players); - - let mut tile_placeable_items = BTreeMap::new(); - let mut tile_interactable_empty = HashSet::new(); - for (tile, used_items) in exclusive_tiles { - let whitelist = recipes - .iter() - .filter(|r| r.tile() == Some(tile)) - .flat_map(|e| e.inputs()) - .chain(used_items) - .collect(); - let int_empty = recipes - .iter() - .any(|r| r.tile() == Some(tile) && r.inputs().is_empty()); - tile_placeable_items.insert(tile, whitelist); - if int_empty { - tile_interactable_empty.insert(tile); - } - } - - let item_names = reg.items.into_inner().unwrap(); - let tile_names = reg.tiles.into_inner().unwrap(); - - let default_timer = if map_name.ends_with("lobby") { - None - } else { - Some(Duration::from_secs(map_in.default_timer.unwrap_or(420))) - }; - - Ok(( - Gamedata { - bot_algos, - current_map: map_name, - maps, - tile_walkable, - tile_placeable_items, - tile_interactable_empty, - recipes, - item_names, - demands, - tile_names, - hand_count: map_in.hand_count.unwrap_or(1), - }, - Serverdata { - initial_map, - chef_spawn, - flags: map_in.flags, - customer_spawn, - default_timer, - book: Book::default(), - 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 - } - } -} |