From 5f883c80e7fc63c97910d003c44aea814ab8a5d6 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Thu, 18 Jul 2024 15:42:11 +0200 Subject: reimplement customers as entity --- server/src/bin/graph.rs | 44 ++-- server/src/customer/mod.rs | 290 --------------------- server/src/customer/pathfinding.rs | 109 -------- server/src/data.rs | 405 +++++++++++++++++++++++++++++ server/src/data/demands.rs | 94 ------- server/src/data/mod.rs | 388 --------------------------- server/src/entity/conveyor.rs | 34 +-- server/src/entity/customers/demands.rs | 94 +++++++ server/src/entity/customers/mod.rs | 274 +++++++++++++++++++ server/src/entity/customers/pathfinding.rs | 98 +++++++ server/src/entity/mod.rs | 43 +-- server/src/entity/portal.rs | 26 +- server/src/game.rs | 105 ++++---- server/src/lib.rs | 1 - server/src/spatial_index.rs | 17 ++ server/src/state.rs | 9 + 16 files changed, 1011 insertions(+), 1020 deletions(-) delete mode 100644 server/src/customer/mod.rs delete mode 100644 server/src/customer/pathfinding.rs create mode 100644 server/src/data.rs delete mode 100644 server/src/data/demands.rs delete mode 100644 server/src/data/mod.rs create mode 100644 server/src/entity/customers/demands.rs create mode 100644 server/src/entity/customers/mod.rs create mode 100644 server/src/entity/customers/pathfinding.rs (limited to 'server/src') diff --git a/server/src/bin/graph.rs b/server/src/bin/graph.rs index 49ad4716..58cc1763 100644 --- a/server/src/bin/graph.rs +++ b/server/src/bin/graph.rs @@ -17,10 +17,7 @@ */ use anyhow::{anyhow, Result}; use hurrycurry_protocol::{ItemIndex, RecipeIndex}; -use hurrycurry_server::{ - data::{DataIndex, Demand}, - interaction::Recipe, -}; +use hurrycurry_server::{data::DataIndex, interaction::Recipe}; #[tokio::main] async fn main() -> Result<()> { @@ -60,25 +57,26 @@ async fn main() -> Result<()> { } } - for ( - di, - Demand { - duration, - from: ItemIndex(from), - to, - points, - }, - ) in data.demands.iter().enumerate() - { - let color = "#c4422b"; - println!( - "d{di} [label=\"Demand\\ntakes {duration}s\\n{points} points\" shape=box color={color:?} fillcolor={color:?} style=filled]", - ); - println!("i{from} -> d{di}"); - if let Some(ItemIndex(to)) = to { - println!("d{di} -> i{to}"); - } - } + // TODO + // for ( + // di, + // Demand { + // duration, + // from: ItemIndex(from), + // to, + // points, + // }, + // ) in data.demands.iter().enumerate() + // { + // let color = "#c4422b"; + // println!( + // "d{di} [label=\"Demand\\ntakes {duration}s\\n{points} points\" shape=box color={color:?} fillcolor={color:?} style=filled]", + // ); + // println!("i{from} -> d{di}"); + // if let Some(ItemIndex(to)) = to { + // println!("d{di} -> i{to}"); + // } + // } println!("}}"); Ok(()) diff --git a/server/src/customer/mod.rs b/server/src/customer/mod.rs deleted file mode 100644 index bf385927..00000000 --- a/server/src/customer/mod.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - 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 . - -*/ -mod pathfinding; - -use crate::{data::Gamedata, game::Tile}; -use anyhow::{anyhow, Result}; -use fake::{faker, Fake}; -use hurrycurry_protocol::{ - glam::IVec2, movement::MovementBase, DemandIndex, Message, PacketS, PlayerID, -}; -use log::info; -use pathfinding::{find_path, Path}; -use rand::{random, thread_rng}; -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; - -pub struct DemandState { - data: Arc, - walkable: HashSet, - chairs: HashMap, - customer_id_counter: PlayerID, - customers: HashMap, - spawn_cooldown: f32, - - pub completed: usize, - pub failed: usize, - pub score_changed: bool, -} - -enum CustomerState { - Entering { - path: Path, - chair: IVec2, - }, - Waiting { - demand: DemandIndex, - chair: IVec2, - timeout: f32, - }, - Eating { - demand: DemandIndex, - target: IVec2, - progress: f32, - chair: IVec2, - }, - Exiting { - path: Path, - }, -} - -pub struct Customer { - movement: MovementBase, - state: CustomerState, -} - -impl DemandState { - pub fn new(data: Arc, map: &HashMap) -> Self { - let chair = data.get_tile_by_name("chair"); - Self { - score_changed: true, - completed: 0, - failed: 0, - walkable: map - .iter() - .filter(|(_, v)| !data.is_tile_colliding(v.kind)) - .map(|(e, _)| *e) - .collect(), - chairs: map - .iter() - .filter(|(_, v)| Some(v.kind) == chair) - .map(|(e, _)| (*e, true)) - .collect(), - customer_id_counter: PlayerID(0), - customers: Default::default(), - data, - spawn_cooldown: 0., - } - } -} - -impl DemandState { - pub fn tick( - &mut self, - packets_out: &mut Vec<(PlayerID, PacketS)>, - tiles: &mut HashMap, - data: &Gamedata, - dt: f32, - points: &mut i64, - ) -> Result<()> { - self.spawn_cooldown -= dt; - self.spawn_cooldown = self.spawn_cooldown.max(0.); - if self.customers.len() < 5 && self.spawn_cooldown <= 0. { - self.spawn_cooldown = 10. + random::() * 10.; - self.customer_id_counter.0 -= 1; - let id = self.customer_id_counter; - packets_out.push(( - id, - PacketS::Join { - name: faker::name::fr_fr::Name().fake(), - character: -1 - (random::() as i32), - }, - )); - let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?; - let from = data.customer_spawn.as_ivec2(); - let path = find_path(&self.walkable, from, chair) - .ok_or(anyhow!("no path from {from} to {chair}"))?; - info!("{id:?} -> entering"); - self.customers.insert( - id, - Customer { - movement: MovementBase::new(data.customer_spawn), - state: CustomerState::Entering { path, chair }, - }, - ); - } - let mut customers_to_remove = Vec::new(); - for (&id, p) in &mut self.customers { - match &mut p.state { - CustomerState::Entering { path, chair } => { - packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt))); - if path.is_done() { - let demand = DemandIndex(random::() % self.data.demands.len()); - packets_out.push(( - id, - PacketS::Communicate { - message: Some(Message::Item(data.demand(demand).from)), - persist: true, - }, - )); - info!("{id:?} -> waiting"); - p.state = CustomerState::Waiting { - chair: *chair, - timeout: 90. + random::() * 60., - demand, - }; - } - } - CustomerState::Waiting { - chair, - demand, - timeout, - } => { - *timeout -= dt; - if *timeout <= 0. { - packets_out.push(( - id, - PacketS::Communicate { - message: None, - persist: true, - }, - )); - packets_out.push(( - id, - PacketS::Communicate { - message: Some(Message::Effect("angry".to_string())), - persist: false, - }, - )); - let path = find_path( - &self.walkable, - p.movement.position.as_ivec2(), - data.customer_spawn.as_ivec2(), - ) - .expect("no path to exit"); - *self.chairs.get_mut(&chair).unwrap() = true; - self.failed += 1; - *points -= 1; - self.score_changed = true; - info!("{id:?} -> exiting"); - p.state = CustomerState::Exiting { path } - } else { - let demand_data = &data.demand(*demand); - let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] - .into_iter() - .find_map(|off| { - let pos = *chair + off; - if tiles - .get(&pos) - .map(|t| { - t.item - .as_ref() - .map(|i| i.kind == demand_data.from) - .unwrap_or_default() - }) - .unwrap_or_default() - { - Some(pos) - } else { - None - } - }); - if let Some(pos) = demand_pos { - packets_out.push(( - id, - PacketS::Communicate { - persist: true, - message: None, - }, - )); - packets_out.push(( - id, - PacketS::Communicate { - message: Some(Message::Effect("satisfied".to_string())), - persist: false, - }, - )); - packets_out.push((id, PacketS::Interact { pos: Some(pos) })); - packets_out.push((id, PacketS::Interact { pos: None })); - info!("{id:?} -> eating"); - p.state = CustomerState::Eating { - demand: *demand, - target: pos, - progress: 0., - chair: *chair, - } - } - } - } - CustomerState::Eating { - demand, - target, - progress, - chair, - } => { - let demand = data.demand(*demand); - *progress += dt / demand.duration; - if *progress >= 1. { - packets_out.push((id, PacketS::ReplaceHand { item: demand.to })); - if demand.to.is_some() { - packets_out.push((id, PacketS::Interact { pos: Some(*target) })); - packets_out.push((id, PacketS::Interact { pos: None })); - } - let path = find_path( - &self.walkable, - p.movement.position.as_ivec2(), - data.customer_spawn.as_ivec2(), - ) - .ok_or(anyhow!("no path to exit"))?; - *self.chairs.get_mut(&chair).unwrap() = true; - self.completed += 1; - *points += demand.points; - self.score_changed = true; - info!("{id:?} -> exiting"); - p.state = CustomerState::Exiting { path } - } - } - CustomerState::Exiting { path } => { - packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt))); - if path.is_done() { - info!("{id:?} -> leave"); - packets_out.push((id, PacketS::Leave)); - customers_to_remove.push(id); - } - } - } - } - for c in customers_to_remove { - self.customers.remove(&c).unwrap(); - } - Ok(()) - } - - fn select_chair(&mut self) -> Option { - use rand::seq::IteratorRandom; - let (chosen, free) = self - .chairs - .iter_mut() - .filter(|(_p, free)| **free) - .choose(&mut thread_rng())?; - *free = false; - Some(*chosen) - } -} diff --git a/server/src/customer/pathfinding.rs b/server/src/customer/pathfinding.rs deleted file mode 100644 index d1e1e997..00000000 --- a/server/src/customer/pathfinding.rs +++ /dev/null @@ -1,109 +0,0 @@ -/* - 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 . - -*/ -use hurrycurry_protocol::{ - glam::{IVec2, Vec2}, movement::MovementBase, PacketS -}; -use log::trace; -use std::{ - cmp::Ordering, - collections::{BinaryHeap, HashMap, HashSet}, -}; - -pub struct Path(Vec); - -impl Path { - pub fn execute_tick( - &mut self, - player: &mut MovementBase, - walkable: &HashSet, - dt: f32, - ) -> PacketS { - if let Some(next) = self.0.last().copied() { - trace!("next {next}"); - if next.distance(player.position) < if self.0.len() == 1 { 0.1 } else { 0.6 } { - self.0.pop(); - } - player.update( - &walkable, - (next - player.position).normalize_or_zero() * 0.5, - false, - dt, - ) - } else { - player.update(&walkable, Vec2::ZERO, false, dt) - } - } - pub fn is_done(&self) -> bool { - self.0.is_empty() - } -} - -pub fn find_path(walkable: &HashSet, from: IVec2, to: IVec2) -> Option { - #[derive(Debug, PartialEq, Eq)] - struct Open(i32, IVec2, IVec2, i32); - impl PartialOrd for Open { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } - } - impl Ord for Open { - fn cmp(&self, other: &Self) -> Ordering { - self.0.cmp(&other.0) - } - } - - let mut visited = HashMap::new(); - let mut open = BinaryHeap::new(); - open.push(Open(1, from, from, 0)); - - loop { - let Some(Open(_, pos, f, distance)) = open.pop() else { - return None; - }; - if visited.contains_key(&pos) { - continue; - } - visited.insert(pos, f); - if pos == to { - break; - } - for dir in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { - let next = pos + dir; - if walkable.contains(&next) { - open.push(Open( - -(distance + next.distance_squared(to).isqrt()), - next, - pos, - distance + 1, - )); - } - } - } - - let mut path = Vec::new(); - let mut c = to; - loop { - path.push(c.as_vec2() + 0.5); - let cn = visited[&c]; - if cn == c { - break; - } - c = cn - } - Some(Path(path)) -} 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 . + +*/ +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, + #[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, + #[serde(default)] + points: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct InitialMap { + map: Vec, + tiles: HashMap, + #[serde(default)] + items: HashMap, + collider: Vec, + walkable: Vec, + chef_spawn: char, + customer_spawn: char, + #[serde(default)] + entities: Vec, + #[serde(default)] + tile_entities: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DemandDecl { + from: String, + to: Option, + duration: f32, + points: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Demand { + pub from: ItemIndex, + pub to: Option, + pub duration: f32, + pub points: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[rustfmt::skip] +pub struct Gamedata { + pub spec: String, + pub item_names: Vec, + pub tile_names: Vec, + pub tile_collide: Vec, + pub tile_interact: Vec, + pub map: HashMap, + #[serde(skip)] pub recipes: Vec, + #[serde(skip)] pub initial_map: HashMap)>, + #[serde(skip)] pub chef_spawn: Vec2, + #[serde(skip)] pub customer_spawn: Vec2, + #[serde(skip)] pub entities: Vec, +} + +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { + pub maps: HashMap, + 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) -> Result<()> { + *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; + Ok(()) + } + + pub async fn read_map(&self, name: &str) -> Result { + 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 { + 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 { + 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 { + 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) -> Result { + 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, + ®, + &tiles_used, + &items_used, + &raw_demands, + &recipes, + &initial_map, + )?); + } + } + } + + entities.extend( + map_in + .entities + .iter() + .map(|decl| { + construct_entity( + None, + decl, + ®, + &tiles_used, + &items_used, + &raw_demands, + &recipes, + &initial_map, + ) + }) + .try_collect::>()?, + ); + + 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>, + items: RwLock>, +} + +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>, 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 { + 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)) + } +} +/* + 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 . + +*/ diff --git a/server/src/data/demands.rs b/server/src/data/demands.rs deleted file mode 100644 index 2501e225..00000000 --- a/server/src/data/demands.rs +++ /dev/null @@ -1,94 +0,0 @@ -/* - 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 . - -*/ -use super::Demand; -use crate::interaction::Recipe; -use hurrycurry_protocol::{ItemIndex, TileIndex}; -use std::collections::{HashMap, HashSet}; - -pub fn generate_demands( - tiles: HashSet, - items: HashSet, - raw_demands: &[(ItemIndex, Option, f32)], - recipes: &[Recipe], -) -> Vec { - let recipes = recipes - .iter() - .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) - .collect::>(); - - 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 deleted file mode 100644 index 28347a25..00000000 --- a/server/src/data/mod.rs +++ /dev/null @@ -1,388 +0,0 @@ -/* - 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 . - -*/ -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, - #[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, - #[serde(default)] - points: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct InitialMap { - map: Vec, - tiles: HashMap, - #[serde(default)] - items: HashMap, - collider: Vec, - walkable: Vec, - chef_spawn: char, - customer_spawn: char, - #[serde(default)] - entities: Vec, - #[serde(default)] - tile_entities: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DemandDecl { - from: String, - to: Option, - duration: f32, - points: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Demand { - pub from: ItemIndex, - pub to: Option, - pub duration: f32, - pub points: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[rustfmt::skip] -pub struct Gamedata { - pub spec: String, - pub item_names: Vec, - pub tile_names: Vec, - pub tile_collide: Vec, - pub tile_interact: Vec, - pub map: HashMap, - #[serde(skip)] pub recipes: Vec, - #[serde(skip)] pub demands: Vec, - #[serde(skip)] pub initial_map: HashMap)>, - #[serde(skip)] pub chef_spawn: Vec2, - #[serde(skip)] pub customer_spawn: Vec2, - #[serde(skip)] pub entities: Vec, -} - -#[derive(Debug, Deserialize, Default)] -pub struct DataIndex { - pub maps: HashMap, - 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) -> Result<()> { - *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; - Ok(()) - } - - pub async fn read_map(&self, name: &str) -> Result { - 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 { - 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 { - 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 { - 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) -> Result { - 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::>()?, - ); - - 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>, - items: RwLock>, -} - -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>, 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)) - } -} -/* - 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 . - -*/ \ No newline at end of file diff --git a/server/src/entity/conveyor.rs b/server/src/entity/conveyor.rs index 2d56c144..d1594ce7 100644 --- a/server/src/entity/conveyor.rs +++ b/server/src/entity/conveyor.rs @@ -16,13 +16,9 @@ */ use super::EntityT; -use crate::{ - data::Gamedata, - game::{interact_effect, Tile}, -}; +use crate::game::{interact_effect, Game}; use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation, PacketC}; -use std::collections::{HashMap, VecDeque}; +use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation}; #[derive(Debug, Clone)] pub struct Conveyor { @@ -35,21 +31,18 @@ pub struct Conveyor { } impl EntityT for Conveyor { - fn tick( - &mut self, - data: &Gamedata, - points: &mut i64, - packet_out: &mut VecDeque, - tiles: &mut HashMap, - dt: f32, - ) -> Result<()> { - let from = tiles + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()> { + let from = game + .tiles .get(&self.from) .ok_or(anyhow!("conveyor from missing"))?; if let Some(from_item) = from.item.as_ref() { let filter = if let Some(t) = &self.filter_tile { - let filter_tile = tiles.get(t).ok_or(anyhow!("conveyor filter missing"))?; + let filter_tile = game + .tiles + .get(t) + .ok_or(anyhow!("conveyor filter missing"))?; filter_tile.item.as_ref().map(|e| e.kind) } else if let Some(i) = &self.filter_item { Some(*i) @@ -69,20 +62,21 @@ impl EntityT for Conveyor { } self.cooldown = 0.; - let [from, to] = tiles + let [from, to] = game + .tiles .get_many_mut([&self.from, &self.to]) .ok_or(anyhow!("conveyor does ends in itself"))?; interact_effect( - data, + &game.data, true, &mut to.item, ItemLocation::Tile(self.to), &mut from.item, ItemLocation::Tile(self.from), Some(to.kind), - packet_out, - points, + &mut game.packet_out, + &mut game.points, true, ); } diff --git a/server/src/entity/customers/demands.rs b/server/src/entity/customers/demands.rs new file mode 100644 index 00000000..fa7e0dbf --- /dev/null +++ b/server/src/entity/customers/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 . + +*/ +use super::Demand; +use crate::interaction::Recipe; +use hurrycurry_protocol::{ItemIndex, TileIndex}; +use std::collections::{HashMap, HashSet}; + +pub fn generate_demands( + tiles: &HashSet, + items: &HashSet, + raw_demands: &[(ItemIndex, Option, f32)], + recipes: &[Recipe], +) -> Vec { + let recipes = recipes + .iter() + .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) + .collect::>(); + + 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/entity/customers/mod.rs b/server/src/entity/customers/mod.rs new file mode 100644 index 00000000..7f0b0c22 --- /dev/null +++ b/server/src/entity/customers/mod.rs @@ -0,0 +1,274 @@ +/* + 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 . + +*/ +pub mod demands; +mod pathfinding; + +use super::EntityT; +use crate::{data::Demand, game::Game}; +use anyhow::{anyhow, Result}; +use fake::{faker, Fake}; +use hurrycurry_protocol::{glam::IVec2, DemandIndex, Message, PacketS, PlayerID}; +use log::{info, warn}; +use pathfinding::{find_path, Path}; +use rand::{random, thread_rng}; +use std::collections::{HashMap, VecDeque}; + +#[derive(Debug, Clone)] +pub struct Customers { + demands: Vec, + cpackets: VecDeque<(PlayerID, PacketS)>, + chairs: HashMap, + customer_id_counter: PlayerID, + customers: HashMap, + spawn_cooldown: f32, +} + +#[derive(Debug, Clone)] +enum CustomerState { + Entering { + path: Path, + chair: IVec2, + }, + Waiting { + demand: DemandIndex, + chair: IVec2, + timeout: f32, + }, + Eating { + demand: DemandIndex, + target: IVec2, + progress: f32, + chair: IVec2, + }, + Exiting { + path: Path, + }, +} + +impl Customers { + pub fn new(chairs: HashMap, demands: Vec) -> Self { + Self { + chairs, + customer_id_counter: PlayerID(0), + customers: Default::default(), + demands, + spawn_cooldown: 0., + cpackets: VecDeque::new(), + } + } +} + +impl EntityT for Customers { + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()> { + self.spawn_cooldown -= dt; + self.spawn_cooldown = self.spawn_cooldown.max(0.); + if self.customers.len() < 5 && self.spawn_cooldown <= 0. { + self.spawn_cooldown = 10. + random::() * 10.; + self.customer_id_counter.0 -= 1; + let id = self.customer_id_counter; + self.cpackets.push_back(( + id, + PacketS::Join { + name: faker::name::fr_fr::Name().fake(), + character: -1 - (random::() as i32), + }, + )); + let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?; + let from = game.data.customer_spawn.as_ivec2(); + let path = find_path(&game.walkable, from, chair) + .ok_or(anyhow!("no path from {from} to {chair}"))?; + info!("{id:?} -> entering"); + self.customers + .insert(id, CustomerState::Entering { path, chair }); + } + let mut customers_to_remove = Vec::new(); + for (&id, state) in &mut self.customers { + let Some(player) = game.players.get_mut(&id) else { + continue; + }; + + match state { + CustomerState::Entering { path, chair } => { + player.direction = path.next_direction(player.position()); + if path.is_done() { + let demand = DemandIndex(random::() % self.demands.len()); + self.cpackets.push_back(( + id, + PacketS::Communicate { + message: Some(Message::Item(self.demands[demand.0].from)), + persist: true, + }, + )); + info!("{id:?} -> waiting"); + *state = CustomerState::Waiting { + chair: *chair, + timeout: 90. + random::() * 60., + demand, + }; + } + } + CustomerState::Waiting { + chair, + demand, + timeout, + } => { + player.direction *= 0.; + *timeout -= dt; + if *timeout <= 0. { + self.cpackets.push_back(( + id, + PacketS::Communicate { + message: None, + persist: true, + }, + )); + self.cpackets.push_back(( + id, + PacketS::Communicate { + message: Some(Message::Effect("angry".to_string())), + persist: false, + }, + )); + let path = find_path( + &game.walkable, + player.position().as_ivec2(), + game.data.customer_spawn.as_ivec2(), + ) + .expect("no path to exit"); + *self.chairs.get_mut(&chair).unwrap() = true; + game.demands_failed += 1; + game.points -= 1; + game.score_changed = true; + info!("{id:?} -> exiting"); + *state = CustomerState::Exiting { path } + } else { + let demand_data = &self.demands[demand.0]; + let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] + .into_iter() + .find_map(|off| { + let pos = *chair + off; + if game + .tiles + .get(&pos) + .map(|t| { + t.item + .as_ref() + .map(|i| i.kind == demand_data.from) + .unwrap_or_default() + }) + .unwrap_or_default() + { + Some(pos) + } else { + None + } + }); + if let Some(pos) = demand_pos { + self.cpackets.push_back(( + id, + PacketS::Communicate { + persist: true, + message: None, + }, + )); + self.cpackets.push_back(( + id, + PacketS::Communicate { + message: Some(Message::Effect("satisfied".to_string())), + persist: false, + }, + )); + self.cpackets + .push_back((id, PacketS::Interact { pos: Some(pos) })); + self.cpackets + .push_back((id, PacketS::Interact { pos: None })); + info!("{id:?} -> eating"); + *state = CustomerState::Eating { + demand: *demand, + target: pos, + progress: 0., + chair: *chair, + } + } + } + } + CustomerState::Eating { + demand, + target, + progress, + chair, + } => { + player.direction *= 0.; + let demand = &self.demands[demand.0]; + *progress += dt / demand.duration; + if *progress >= 1. { + self.cpackets + .push_back((id, PacketS::ReplaceHand { item: demand.to })); + if demand.to.is_some() { + self.cpackets + .push_back((id, PacketS::Interact { pos: Some(*target) })); + self.cpackets + .push_back((id, PacketS::Interact { pos: None })); + } + let path = find_path( + &game.walkable, + player.position().as_ivec2(), + game.data.customer_spawn.as_ivec2(), + ) + .ok_or(anyhow!("no path to exit"))?; + *self.chairs.get_mut(&chair).unwrap() = true; + game.demands_completed += 1; + game.points += demand.points; + game.score_changed = true; + info!("{id:?} -> exiting"); + *state = CustomerState::Exiting { path } + } + } + CustomerState::Exiting { path } => { + player.direction = path.next_direction(player.position()); + if path.is_done() { + info!("{id:?} -> leave"); + self.cpackets.push_back((id, PacketS::Leave)); + customers_to_remove.push(id); + } + } + } + } + for c in customers_to_remove { + self.customers.remove(&c).unwrap(); + } + for (player, packet) in self.cpackets.drain(..) { + if let Err(err) = game.packet_in(player, packet) { + warn!("demand packet {err}"); + } + } + Ok(()) + } +} +impl Customers { + fn select_chair(&mut self) -> Option { + use rand::seq::IteratorRandom; + let (chosen, free) = self + .chairs + .iter_mut() + .filter(|(_p, free)| **free) + .choose(&mut thread_rng())?; + *free = false; + Some(*chosen) + } +} diff --git a/server/src/entity/customers/pathfinding.rs b/server/src/entity/customers/pathfinding.rs new file mode 100644 index 00000000..97bd8328 --- /dev/null +++ b/server/src/entity/customers/pathfinding.rs @@ -0,0 +1,98 @@ +/* + 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 . + +*/ +use hurrycurry_protocol::glam::{IVec2, Vec2}; +use log::trace; +use std::{ + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet}, +}; + +#[derive(Debug, Clone)] +pub struct Path(Vec); + +impl Path { + pub fn next_direction(&mut self, position: Vec2) -> Vec2 { + if let Some(next) = self.0.last().copied() { + trace!("next {next}"); + if next.distance(position) < if self.0.len() == 1 { 0.1 } else { 0.6 } { + self.0.pop(); + } + (next - position).normalize_or_zero() * 0.5 + } else { + Vec2::ZERO + } + } + pub fn is_done(&self) -> bool { + self.0.is_empty() + } +} + +pub fn find_path(walkable: &HashSet, from: IVec2, to: IVec2) -> Option { + #[derive(Debug, PartialEq, Eq)] + struct Open(i32, IVec2, IVec2, i32); + impl PartialOrd for Open { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } + } + impl Ord for Open { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } + } + + let mut visited = HashMap::new(); + let mut open = BinaryHeap::new(); + open.push(Open(1, from, from, 0)); + + loop { + let Some(Open(_, pos, f, distance)) = open.pop() else { + return None; + }; + if visited.contains_key(&pos) { + continue; + } + visited.insert(pos, f); + if pos == to { + break; + } + for dir in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { + let next = pos + dir; + if walkable.contains(&next) { + open.push(Open( + -(distance + next.distance_squared(to).isqrt()), + next, + pos, + distance + 1, + )); + } + } + } + + let mut path = Vec::new(); + let mut c = to; + loop { + path.push(c.as_vec2() + 0.5); + let cn = visited[&c]; + if cn == c { + break; + } + c = cn + } + Some(Path(path)) +} diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index a1f690a3..beee9309 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -16,27 +16,20 @@ */ pub mod conveyor; +pub mod customers; pub mod portal; -use crate::{ - data::{Gamedata, ItemTileRegistry}, - game::Tile, -}; +use std::collections::{HashMap, HashSet}; + +use crate::{data::ItemTileRegistry, game::Game, interaction::Recipe}; use anyhow::{anyhow, Result}; use conveyor::Conveyor; -use hurrycurry_protocol::{glam::IVec2, PacketC}; +use customers::{demands::generate_demands, Customers}; +use hurrycurry_protocol::{glam::IVec2, ItemIndex, TileIndex}; use portal::Portal; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, VecDeque}; pub trait EntityT: Clone { - fn tick( - &mut self, - data: &Gamedata, - points: &mut i64, - packet_out: &mut VecDeque, - tiles: &mut HashMap, - dt: f32, - ) -> Result<()>; + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()>; } macro_rules! entities { @@ -44,14 +37,14 @@ macro_rules! entities { #[derive(Debug, Clone)] pub enum Entity { $($e($e)),* } impl EntityT for Entity { - fn tick(&mut self, data: &Gamedata, points: &mut i64, packet_out: &mut VecDeque, tiles: &mut HashMap, dt: f32) -> Result<()> { - match self { $(Entity::$e(x) => x.tick(data, points, packet_out, tiles, dt)),*, } + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()> { + match self { $(Entity::$e(x) => x.tick(game, dt)),*, } } } }; } -entities!(Conveyor, Portal); +entities!(Conveyor, Portal, Customers); #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] @@ -68,12 +61,18 @@ pub enum EntityDecl { from: Option, to: IVec2, }, + Customers {}, } pub fn construct_entity( pos: Option, decl: &EntityDecl, reg: &ItemTileRegistry, + tiles_used: &HashSet, + items_used: &HashSet, + raw_demands: &[(ItemIndex, Option, f32)], + recipes: &[Recipe], + initial_map: &HashMap)>, ) -> Result { Ok(match decl.to_owned() { EntityDecl::Portal { from, to } => Entity::Portal(Portal { @@ -101,5 +100,15 @@ pub fn construct_entity( cooldown: 0., }) } + EntityDecl::Customers {} => { + let demands = generate_demands(tiles_used, items_used, &raw_demands, &recipes); + let chair = reg.register_tile("chair".to_string()); + let chairs = initial_map + .iter() + .filter(|(_, (tile, _))| *tile == chair) + .map(|(e, _)| (*e, true)) + .collect(); + Entity::Customers(Customers::new(chairs, demands)) + } }) } diff --git a/server/src/entity/portal.rs b/server/src/entity/portal.rs index 092a8da5..3aed35ac 100644 --- a/server/src/entity/portal.rs +++ b/server/src/entity/portal.rs @@ -16,13 +16,9 @@ */ use super::EntityT; -use crate::{ - data::Gamedata, - game::{interact_effect, Tile}, -}; +use crate::game::{interact_effect, Game}; use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemLocation, PacketC}; -use std::collections::{HashMap, VecDeque}; +use hurrycurry_protocol::{glam::IVec2, ItemLocation}; #[derive(Debug, Default, Clone)] pub struct Portal { @@ -31,29 +27,23 @@ pub struct Portal { } impl EntityT for Portal { - fn tick( - &mut self, - data: &Gamedata, - points: &mut i64, - packet_out: &mut VecDeque, - tiles: &mut HashMap, - _dt: f32, - ) -> Result<()> { - let [from, to] = tiles + fn tick(&mut self, game: &mut Game, _dt: f32) -> Result<()> { + let [from, to] = game + .tiles .get_many_mut([&self.from, &self.to]) .ok_or(anyhow!("conveyor does ends in itself"))?; if from.item.is_some() { interact_effect( - data, + &game.data, true, &mut to.item, ItemLocation::Tile(self.to), &mut from.item, ItemLocation::Tile(self.from), Some(to.kind), - packet_out, - points, + &mut game.packet_out, + &mut game.points, true, ); } diff --git a/server/src/game.rs b/server/src/game.rs index b3b23ce0..370c2e8f 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -16,7 +16,6 @@ */ use crate::{ - customer::DemandState, data::Gamedata, entity::{Entity, EntityT}, interaction::{interact, tick_slot, InteractEffect, TickEffect}, @@ -32,7 +31,7 @@ use hurrycurry_protocol::{ use log::{info, warn}; use std::{ collections::{HashMap, HashSet, VecDeque}, - sync::Arc, + sync::{Arc, RwLock}, time::{Duration, Instant}, }; @@ -62,37 +61,44 @@ pub struct Player { pub communicate_persist: Option, movement: MovementBase, - direction: Vec2, - boost: bool, - last_position_update: Instant, + pub direction: Vec2, + pub boost: bool, + pub last_position_update: Instant, } pub struct Game { pub data: Arc, - tiles: HashMap, - walkable: HashSet, + pub tiles: HashMap, + pub walkable: HashSet, pub players: HashMap, players_spatial_index: SpatialIndex, - packet_out: VecDeque, - demand: Option, - pub points: i64, - entities: Vec, + pub packet_out: VecDeque, + entities: Arc>>, end: Option, + pub lobby: bool, + + pub score_changed: bool, + pub points: i64, + pub demands_failed: usize, + pub demands_completed: usize, } impl Game { pub fn new() -> Self { Self { + lobby: false, data: Gamedata::default().into(), packet_out: Default::default(), players: HashMap::new(), tiles: HashMap::new(), walkable: HashSet::new(), - demand: None, end: None, - entities: vec![], + entities: Arc::new(RwLock::new(vec![])), players_spatial_index: SpatialIndex::default(), points: 0, + demands_failed: 0, + demands_completed: 0, + score_changed: false, } } @@ -111,7 +117,7 @@ impl Game { neighbors: [None, None, None, None], }) } - self.demand = None; + self.walkable.clear(); } pub fn load(&mut self, gamedata: Gamedata, timer: Option) { let players = self @@ -126,7 +132,7 @@ impl Game { self.data = gamedata.into(); self.points = 0; self.end = timer.map(|dur| Instant::now() + dur); - self.entities = self.data.entities.clone(); + self.entities = Arc::new(RwLock::new(self.data.entities.clone())); for (&p, (tile, item)) in &self.data.initial_map { self.tiles.insert( @@ -150,7 +156,11 @@ impl Game { item: None, character, movement: MovementBase { - position: self.data.chef_spawn, + position: if character < 0 { + self.data.customer_spawn + } else { + self.data.chef_spawn + }, facing: Vec2::X, rotation: 0., velocity: Vec2::ZERO, @@ -167,17 +177,9 @@ impl Game { ); } - if !self.data.demands.is_empty() { - self.demand = Some(DemandState::new(self.data.clone(), &self.tiles)) - } - self.packet_out.extend(self.prime_client()); } - pub fn tiles(&self) -> &HashMap { - &self.tiles - } - pub fn packet_out(&mut self) -> Option { self.packet_out.pop_front() } @@ -249,7 +251,7 @@ impl Game { out.push(self.score()); out.push(PacketC::SetIngame { state: true, - lobby: self.demand.is_none(), + lobby: self.lobby, }); out } @@ -258,12 +260,8 @@ impl Game { PacketC::Score { time_remaining: self.end.map(|t| (t - Instant::now()).as_secs_f32()), points: self.points, - demands_failed: self.demand.as_ref().map(|d| d.failed).unwrap_or_default(), - demands_completed: self - .demand - .as_ref() - .map(|d| d.completed) - .unwrap_or_default(), + demands_failed: self.demands_failed, + demands_completed: self.demands_completed, } } pub fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<()> { @@ -285,7 +283,11 @@ impl Game { item: None, character, movement: MovementBase { - position: self.data.chef_spawn, + position: if character < 0 { + self.data.customer_spawn + } else { + self.data.chef_spawn + }, facing: Vec2::X, rotation: 0., velocity: Vec2::ZERO, @@ -471,26 +473,9 @@ impl Game { /// Returns true if the game should end pub fn tick(&mut self, dt: f32) -> bool { - if let Some(demand) = &mut self.demand { - let mut packet_out = Vec::new(); - if let Err(err) = demand.tick( - &mut packet_out, - &mut self.tiles, - &self.data, - dt, - &mut self.points, - ) { - warn!("demand tick {err}"); - } - if demand.score_changed { - demand.score_changed = false; - self.packet_out.push_back(self.score()); - } - for (player, packet) in packet_out { - if let Err(err) = self.packet_in(player, packet) { - warn!("demand packet {err}"); - } - } + if self.score_changed { + self.score_changed = false; + self.packet_out.push_back(self.score()); } for (&pos, tile) in &mut self.tiles { @@ -583,14 +568,8 @@ impl Game { let _ = self.packet_in(pid, PacketS::Interact { pos: None }); } - for entity in &mut self.entities { - if let Err(e) = entity.tick( - &self.data, - &mut self.points, - &mut self.packet_out, - &mut self.tiles, - dt, - ) { + for entity in self.entities.clone().write().unwrap().iter_mut() { + if let Err(e) = entity.tick(self, dt) { warn!("entity tick failed: {e}") } } @@ -686,3 +665,9 @@ pub fn interact_effect( } } } + +impl Player { + pub fn position(&self) -> Vec2 { + self.movement.position + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 0339b535..a59aad11 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -16,7 +16,6 @@ */ #![feature(if_let_guard, map_many_mut, let_chains, iterator_try_collect, isqrt)] -pub mod customer; pub mod data; pub mod entity; pub mod game; diff --git a/server/src/spatial_index.rs b/server/src/spatial_index.rs index 4395f0f5..a62c80e0 100644 --- a/server/src/spatial_index.rs +++ b/server/src/spatial_index.rs @@ -1,3 +1,20 @@ +/* + 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 . + +*/ use hurrycurry_protocol::glam::Vec2; use std::{collections::HashMap, hash::Hash}; diff --git a/server/src/state.rs b/server/src/state.rs index 347e2a92..e637b323 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -118,6 +118,15 @@ impl State { _ => (), } self.game.packet_in(player, packet)?; + if self.game.players.is_empty() && !self.game.lobby { + self.tx + .send(PacketC::ServerMessage { + text: "Game was aborted automatically due to a lack of players".to_string(), + }) + .ok(); + self.game + .load(self.index.generate("lobby-none".to_string()).await?, None); + } Ok(vec![]) } -- cgit v1.2.3-70-g09d2