diff options
-rw-r--r-- | Cargo.lock | 30 | ||||
-rw-r--r-- | pixel-client/src/game.rs | 6 | ||||
-rw-r--r-- | server/Cargo.toml | 2 | ||||
-rw-r--r-- | server/bot/Cargo.toml | 2 | ||||
-rw-r--r-- | server/bot/src/lib.rs | 17 | ||||
-rw-r--r-- | server/bot/src/main.rs | 2 | ||||
-rw-r--r-- | server/client-lib/src/lib.rs | 35 | ||||
-rw-r--r-- | server/protocol/src/lib.rs | 41 | ||||
-rw-r--r-- | server/src/bin/graph.rs | 2 | ||||
-rw-r--r-- | server/src/data.rs | 363 | ||||
-rw-r--r-- | server/src/entity/conveyor.rs | 29 | ||||
-rw-r--r-- | server/src/entity/customers/mod.rs | 368 | ||||
-rw-r--r-- | server/src/entity/environment_effect.rs | 38 | ||||
-rw-r--r-- | server/src/entity/item_portal.rs | 25 | ||||
-rw-r--r-- | server/src/entity/mod.rs | 16 | ||||
-rw-r--r-- | server/src/entity/player_portal.rs | 24 | ||||
-rw-r--r-- | server/src/interaction.rs | 12 | ||||
-rw-r--r-- | server/src/lib.rs | 2 | ||||
-rw-r--r-- | server/src/main.rs | 2 | ||||
-rw-r--r-- | server/src/server.rs (renamed from server/src/game.rs) | 311 | ||||
-rw-r--r-- | server/src/state.rs | 70 |
21 files changed, 702 insertions, 695 deletions
@@ -277,20 +277,6 @@ dependencies = [ ] [[package]] -name = "bot" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger", - "hurrycurry-client-lib", - "hurrycurry-protocol", - "log", - "rand 0.9.0-alpha.2", - "rustls", -] - -[[package]] name = "built" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -816,6 +802,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] +name = "hurrycurry-bot" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "hurrycurry-client-lib", + "hurrycurry-protocol", + "log", + "rand 0.9.0-alpha.2", + "rustls", +] + +[[package]] name = "hurrycurry-client-lib" version = "0.1.0" dependencies = [ @@ -868,6 +868,8 @@ dependencies = [ "env_logger", "fake", "futures-util", + "hurrycurry-bot", + "hurrycurry-client-lib", "hurrycurry-protocol", "log", "pollster", diff --git a/pixel-client/src/game.rs b/pixel-client/src/game.rs index 29b1ba30..dca84d77 100644 --- a/pixel-client/src/game.rs +++ b/pixel-client/src/game.rs @@ -29,7 +29,7 @@ use hurrycurry_client_lib::{network::sync::Network, spatial_index::SpatialIndex} use hurrycurry_protocol::{ glam::{IVec2, Vec2}, movement::MovementBase, - ClientGamedata, ItemIndex, ItemLocation, PacketC, PacketS, PlayerID, Score, TileIndex, + Gamedata, ItemIndex, ItemLocation, PacketC, PacketS, PlayerID, Score, TileIndex, }; use log::{info, warn}; use sdl2::{ @@ -41,7 +41,7 @@ use std::collections::{HashMap, HashSet}; pub struct Game { network: Network, - data: ClientGamedata, + data: Gamedata, tiles: HashMap<IVec2, Tile>, tilemap: Tilemap, walkable: HashSet<IVec2>, @@ -94,7 +94,7 @@ impl Game { players: HashMap::new(), tilemap: Tilemap::default(), my_id: PlayerID(0), - data: ClientGamedata::default(), + data: Gamedata::default(), walkable: HashSet::new(), movement_send_cooldown: 0., misc_textures: MiscTextures::init(layout), diff --git a/server/Cargo.toml b/server/Cargo.toml index 789bb76a..72e9dc45 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -22,3 +22,5 @@ pollster = "0.3.0" bincode = "2.0.0-rc.3" hurrycurry-protocol = { path = "protocol" } +hurrycurry-client-lib = { path = "client-lib" } +hurrycurry-bot = { path = "bot" } diff --git a/server/bot/Cargo.toml b/server/bot/Cargo.toml index ee9706d8..3143ab55 100644 --- a/server/bot/Cargo.toml +++ b/server/bot/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bot" +name = "hurrycurry-bot" version = "0.1.0" edition = "2021" diff --git a/server/bot/src/lib.rs b/server/bot/src/lib.rs index e8f05fd6..385e2334 100644 --- a/server/bot/src/lib.rs +++ b/server/bot/src/lib.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 <https://www.gnu.org/licenses/>. + +*/ #![feature(isqrt)] pub mod algos; pub mod pathfinding; diff --git a/server/bot/src/main.rs b/server/bot/src/main.rs index e9798b75..9ecd0a84 100644 --- a/server/bot/src/main.rs +++ b/server/bot/src/main.rs @@ -16,8 +16,8 @@ */ use anyhow::Result; -use bot::{algos::ALGO_CONSTRUCTORS, BotAlgo, BotInput}; use clap::Parser; +use hurrycurry_bot::{algos::ALGO_CONSTRUCTORS, BotAlgo, BotInput}; use hurrycurry_client_lib::{network::sync::Network, Game}; use hurrycurry_protocol::{PacketC, PacketS, PlayerID}; use log::warn; diff --git a/server/client-lib/src/lib.rs b/server/client-lib/src/lib.rs index d949ac6e..b04f46f7 100644 --- a/server/client-lib/src/lib.rs +++ b/server/client-lib/src/lib.rs @@ -20,8 +20,8 @@ pub mod network; pub mod spatial_index; use hurrycurry_protocol::{ - glam::IVec2, movement::MovementBase, ClientGamedata, ItemIndex, ItemLocation, Message, PacketC, - PlayerID, Score, TileIndex, + glam::IVec2, movement::MovementBase, Gamedata, ItemIndex, ItemLocation, Message, PacketC, + PlayerID, RecipeIndex, Score, TileIndex, }; use spatial_index::SpatialIndex; use std::{ @@ -31,9 +31,17 @@ use std::{ }; #[derive(Debug, PartialEq)] +pub struct Involvement { + pub progress: f32, + pub recipe: RecipeIndex, + pub working: usize, + pub warn: bool, +} + +#[derive(Debug, PartialEq)] pub struct Item { pub kind: ItemIndex, - pub progress: Option<(f32, bool)>, + pub active: Option<Involvement>, } pub struct Tile { @@ -52,7 +60,7 @@ pub struct Player { } pub struct Game { - pub data: Arc<ClientGamedata>, + pub data: Arc<Gamedata>, pub tiles: HashMap<IVec2, Tile>, pub walkable: HashSet<IVec2>, pub players: HashMap<PlayerID, Player>, @@ -125,17 +133,20 @@ impl Game { *self.get_item(to) = self.get_item(from).take(); } PacketC::SetItem { location, item } => { - *self.get_item(location) = item.map(|kind| Item { - kind, - progress: None, - }); + *self.get_item(location) = item.map(|kind| Item { kind, active: None }); } PacketC::SetProgress { item, progress, warn, } => { - self.get_item(item).as_mut().unwrap().progress = progress.map(|s| (s, warn)); + self.get_item(item).as_mut().unwrap().active = + progress.map(|progress| Involvement { + working: 1, + warn, + progress, + recipe: RecipeIndex(0), + }); } PacketC::UpdateMap { tile, @@ -204,3 +215,9 @@ impl Game { } } } + +impl From<TileIndex> for Tile { + fn from(kind: TileIndex) -> Self { + Self { kind, item: None } + } +} diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index 2c165a92..05561a12 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -71,13 +71,12 @@ pub struct MapMetadata { #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Default)] #[rustfmt::skip] -pub struct ClientGamedata { +pub struct Gamedata { pub current_map: String, pub item_names: Vec<String>, pub tile_names: Vec<String>, pub tile_collide: Vec<bool>, pub tile_interact: Vec<bool>, - pub map_names: HashSet<String>, // for compat with game jam version pub maps: HashMap<String, MapMetadata>, pub recipes: Vec<Recipe>, } @@ -144,7 +143,7 @@ pub enum PacketC { id: PlayerID, }, Data { - data: ClientGamedata, + data: Gamedata, }, AddPlayer { id: PlayerID, @@ -257,6 +256,42 @@ pub enum Recipe { }, } +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)) + } +} + impl Recipe { pub fn tile(&self) -> Option<TileIndex> { match self { diff --git a/server/src/bin/graph.rs b/server/src/bin/graph.rs index 03a59e37..62dc47a2 100644 --- a/server/src/bin/graph.rs +++ b/server/src/bin/graph.rs @@ -30,7 +30,7 @@ async fn main() -> Result<()> { .nth(1) .ok_or(anyhow!("first arg should be recipe set name"))?; - let data = index.generate(format!("sushibar-{rn}")).await?; + let (data, _) = index.generate(format!("sushibar-{rn}")).await?; for i in 0..data.item_names.len() { println!("i{i} [label=\"{}\"]", data.item_name(ItemIndex(i))) diff --git a/server/src/data.rs b/server/src/data.rs index 99cbaf9f..f0a85cca 100644 --- a/server/src/data.rs +++ b/server/src/data.rs @@ -20,7 +20,7 @@ use crate::entity::{construct_entity, Entity, EntityDecl}; use anyhow::{anyhow, bail, Result}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, - ItemIndex, MapMetadata, Recipe, RecipeIndex, TileIndex, + Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex, }; use serde::{Deserialize, Serialize}; use std::{ @@ -99,15 +99,8 @@ pub struct Demand { #[derive(Debug, Clone, Default)] #[rustfmt::skip] -pub struct Gamedata { +pub struct Serverdata { pub spec: String, - pub map_name: 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>, - pub recipes: Vec<Recipe>, pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>, pub chef_spawn: Vec2, pub customer_spawn: Vec2, @@ -151,182 +144,189 @@ impl DataIndex { Ok(read_to_string(path).await?) } - pub async fn generate(&self, spec: String) -> Result<Gamedata> { + pub async fn generate(&self, spec: String) -> Result<(Gamedata, Serverdata)> { 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.clone(), map.to_string(), map_in, recipes_in)?; - gd.map = self.maps.clone(); - Ok(gd) + Ok(build_data( + self.maps.clone(), + spec.clone(), + map.to_string(), + map_in, + recipes_in, + )?) } } -impl Gamedata { - pub fn build( - spec: String, - map_name: 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(); +pub fn build_data( + maps: HashMap<String, MapMetadata>, + spec: String, + map_name: String, + map_in: InitialMap, + recipes_in: Vec<RecipeDecl>, +) -> Result<(Gamedata, Serverdata)> { + 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, + 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, - 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"))?, + inputs: [inputs.next(), inputs.next()], 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") + Action::Demand => raw_demands.push(( + inputs.next().ok_or(anyhow!("demand needs inputs"))?, + outputs.next(), + r.duration.unwrap_or(10.), + )), } + assert_eq!(inputs.next(), None, "{r2:?} inputs left over"); + assert_eq!(outputs.next(), None, "{r2:?} outputs left over"); + assert_eq!(r.points, None, "points specified where not possible") + } - // TODO - // for d in demands_in { - // demands.push(Demand { - // from: reg.register_item(d.from), - // to: d.to.map(|to| reg.register_item(to)), - // duration: d.duration, - // points: d.points, - // }) - // } - - let mut chef_spawn = Vec2::new(0., 0.); - let mut customer_spawn = Vec2::new(0., 0.); - let mut initial_map = HashMap::new(); - let mut tiles_used = HashSet::new(); - let mut items_used = HashSet::new(); - for (y, line) in map_in.map.iter().enumerate() { - for (x, tile) in line.chars().enumerate() { - if tile == ' ' { - continue; // space is empty space - } - let pos = IVec2::new(x as i32, y as i32); - if tile == map_in.chef_spawn { - chef_spawn = pos.as_vec2() + Vec2::splat(0.5); - } - if tile == map_in.customer_spawn { - customer_spawn = pos.as_vec2() + Vec2::splat(0.5); - } - let tilename = map_in - .tiles - .get(&tile) - .ok_or(anyhow!("tile {tile} is undefined"))? - .clone(); + // 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 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)); + let mut chef_spawn = Vec2::new(0., 0.); + let mut customer_spawn = Vec2::new(0., 0.); + let mut initial_map = HashMap::new(); + let mut tiles_used = HashSet::new(); + let mut items_used = HashSet::new(); + for (y, line) in map_in.map.iter().enumerate() { + for (x, tile) in line.chars().enumerate() { + if tile == ' ' { + continue; // space is empty space + } + let pos = IVec2::new(x as i32, y as i32); + if tile == map_in.chef_spawn { + chef_spawn = pos.as_vec2() + Vec2::splat(0.5); } + if tile == map_in.customer_spawn { + customer_spawn = pos.as_vec2() + Vec2::splat(0.5); + } + let tilename = map_in + .tiles + .get(&tile) + .ok_or(anyhow!("tile {tile} is undefined"))? + .clone(); + + let itemname = map_in.items.get(&tile).cloned(); + let tile = reg.register_tile(tilename); + let item = itemname.map(|i| reg.register_item(i)); + tiles_used.insert(tile); + if let Some(i) = item { + items_used.insert(i); + }; + initial_map.insert(pos, (tile, item)); } + } - for (y, line) in map_in.map.iter().enumerate() { - for (x, tile) in line.trim().chars().enumerate() { - let pos = IVec2::new(x as i32, y as i32); - if let Some(ent) = map_in.tile_entities.get(&tile) { - entities.push(construct_entity( - Some(pos), - ent, - ®, - &tiles_used, - &items_used, - &raw_demands, - &recipes, - &initial_map, - )?); - } + 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::<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 + entities.extend( + map_in + .entities .iter() - .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) - .collect(); + .map(|decl| { + construct_entity( + None, + decl, + ®, + &tiles_used, + &items_used, + &raw_demands, + &recipes, + &initial_map, + ) + }) + .try_collect::<Vec<_>>()?, + ); - Ok(Gamedata { - spec, + let item_names = reg.items.into_inner().unwrap(); + let tile_names = reg.tiles.into_inner().unwrap(); + let tile_collide = tile_names + .iter() + .map(|i| !map_in.walkable.contains(i)) + .collect(); + let tile_interact = tile_names + .iter() + .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) + .collect(); + + Ok(( + Gamedata { + current_map: map_name, + maps, tile_collide, - map_name, tile_interact, recipes, - score_baseline: map_in.score_baseline, - map: HashMap::new(), - initial_map, item_names, - entities, tile_names, + }, + Serverdata { + spec, + initial_map, chef_spawn, customer_spawn, - }) - } + score_baseline: map_in.score_baseline, + entities, + }, + )) } #[derive(Default)] @@ -353,56 +353,3 @@ impl ItemTileRegistry { } } } - -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/>. - -*/ diff --git a/server/src/entity/conveyor.rs b/server/src/entity/conveyor.rs index b370356c..f7f091c7 100644 --- a/server/src/entity/conveyor.rs +++ b/server/src/entity/conveyor.rs @@ -1,5 +1,3 @@ -use std::collections::VecDeque; - /* Hurry Curry! - a game about cooking Copyright 2024 metamuffin @@ -17,10 +15,10 @@ use std::collections::VecDeque; along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use super::EntityT; -use crate::game::{interact_effect, Game}; +use super::{EntityContext, EntityT}; +use crate::server::interact_effect; use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation, PacketC}; +use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation}; #[derive(Debug, Clone)] pub struct Conveyor { @@ -33,15 +31,17 @@ pub struct Conveyor { } impl EntityT for Conveyor { - fn tick(&mut self, game: &mut Game, packet_out: &mut VecDeque<PacketC>, dt: f32) -> Result<()> { - let from = game + fn tick(&mut self, c: EntityContext) -> Result<()> { + let from = c + .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 = game + let filter_tile = c + .game .tiles .get(t) .ok_or(anyhow!("conveyor filter missing"))?; @@ -56,28 +56,29 @@ impl EntityT for Conveyor { } } - self.cooldown += dt; + self.cooldown += c.dt; if self.cooldown < self.max_cooldown { return Ok(()); } self.cooldown = 0.; - let [from, to] = game + let [from, to] = c + .game .tiles .get_many_mut([&self.from, &self.to]) .ok_or(anyhow!("conveyor does ends in itself"))?; interact_effect( - &game.data, + &c.game.data, true, &mut to.item, ItemLocation::Tile(self.to), &mut from.item, ItemLocation::Tile(self.from), Some(to.kind), - packet_out, - &mut game.score, - &mut game.score_changed, + c.packet_out, + &mut c.game.score, + c.score_changed, true, ); } diff --git a/server/src/entity/customers/mod.rs b/server/src/entity/customers/mod.rs index 221351a5..85da2c07 100644 --- a/server/src/entity/customers/mod.rs +++ b/server/src/entity/customers/mod.rs @@ -18,8 +18,8 @@ pub mod demands; mod pathfinding; -use super::EntityT; -use crate::{data::Demand, game::Game}; +use super::{EntityContext, EntityT}; +use crate::{data::Demand, server::Server}; use anyhow::{anyhow, bail, Result}; use fake::{faker, Fake}; use hurrycurry_protocol::{glam::IVec2, DemandIndex, Message, PacketC, PacketS, PlayerID}; @@ -75,190 +75,190 @@ impl Customers { } impl EntityT for Customers { - fn tick(&mut self, game: &mut Game, packet_out: &mut VecDeque<PacketC>, 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::<f32>() * 10.; - let id = game.join_player( - faker::name::fr_fr::Name().fake(), - -1 - (random::<u16>() as i32), - packet_out, - ); + fn tick(&mut self, c: EntityContext) -> 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::<f32>() * 10.; + // let id = game.join_player( + // faker::name::fr_fr::Name().fake(), + // -1 - (random::<u16>() as i32), + // packet_out, + // ); - 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 (&player, state) in &mut self.customers { - let Some(playerdata) = game.players.get_mut(&player) else { - continue; - }; + // 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 (&player, state) in &mut self.customers { + // let Some(playerdata) = game.players.get_mut(&player) else { + // continue; + // }; - match state { - CustomerState::Entering { path, chair } => { - playerdata - .movement - .input(path.next_direction(playerdata.position()), false); - if path.is_done() { - let demand = DemandIndex(random::<u32>() as usize % self.demands.len()); - self.cpackets.push_back(PacketS::Communicate { - message: Some(Message::Item(self.demands[demand.0].from)), - persist: true, - player, - }); - info!("{player:?} -> waiting"); - *state = CustomerState::Waiting { - chair: *chair, - timeout: 90. + random::<f32>() * 60., - demand, - }; - } - } - CustomerState::Waiting { - chair, - demand, - timeout, - } => { - playerdata - .movement - .input((chair.as_vec2() + 0.5) - playerdata.position(), false); - *timeout -= dt; - if *timeout <= 0. { - self.cpackets.push_back(PacketS::Communicate { - message: None, - persist: true, - player, - }); - self.cpackets.push_back(PacketS::Communicate { - message: Some(Message::Effect("angry".to_string())), - persist: false, - player, - }); - let path = find_path( - &game.walkable, - playerdata.position().as_ivec2(), - game.data.customer_spawn.as_ivec2(), - ) - .expect("no path to exit"); - *self.chairs.get_mut(chair).unwrap() = true; - game.score.demands_failed += 1; - game.score.points -= 1; - game.score_changed = true; - info!("{player:?} -> 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(PacketS::Communicate { - persist: true, - message: None, - player, - }); - self.cpackets.push_back(PacketS::Communicate { - message: Some(Message::Effect("satisfied".to_string())), - persist: false, - player, - }); - self.cpackets.push_back(PacketS::Interact { - pos: Some(pos), - player, - }); - self.cpackets - .push_back(PacketS::Interact { pos: None, player }); - info!("{player:?} -> eating"); - *state = CustomerState::Eating { - demand: *demand, - target: pos, - progress: 0., - chair: *chair, - } - } - } - } - CustomerState::Eating { - demand, - target, - progress, - chair, - } => { - playerdata - .movement - .input((chair.as_vec2() + 0.5) - playerdata.position(), false); - let demand = &self.demands[demand.0]; - *progress += dt / demand.duration; - if *progress >= 1. { - self.cpackets.push_back(PacketS::ReplaceHand { - player, - item: demand.to, - }); - if demand.to.is_some() { - self.cpackets.push_back(PacketS::Interact { - player, - pos: Some(*target), - }); - self.cpackets - .push_back(PacketS::Interact { player, pos: None }); - } - let path = find_path( - &game.walkable, - playerdata.position().as_ivec2(), - game.data.customer_spawn.as_ivec2(), - ) - .ok_or(anyhow!("no path to exit"))?; - *self.chairs.get_mut(chair).unwrap() = true; - game.score.demands_completed += 1; - game.score.points += demand.points; - game.score_changed = true; - info!("{player:?} -> exiting"); - *state = CustomerState::Exiting { path } - } - } - CustomerState::Exiting { path } => { - playerdata - .movement - .input(path.next_direction(playerdata.position()), false); - if path.is_done() { - info!("{player:?} -> leave"); - self.cpackets.push_back(PacketS::Leave { player }); - customers_to_remove.push(player); - } - } - } - } - for c in customers_to_remove { - self.customers.remove(&c).unwrap(); - } - for packet in self.cpackets.drain(..) { - if let Err(err) = game.packet_in(packet, &mut vec![], packet_out) { - warn!("demand packet {err}"); - } - } + // match state { + // CustomerState::Entering { path, chair } => { + // playerdata + // .movement + // .input(path.next_direction(playerdata.position()), false); + // if path.is_done() { + // let demand = DemandIndex(random::<u32>() as usize % self.demands.len()); + // self.cpackets.push_back(PacketS::Communicate { + // message: Some(Message::Item(self.demands[demand.0].from)), + // persist: true, + // player, + // }); + // info!("{player:?} -> waiting"); + // *state = CustomerState::Waiting { + // chair: *chair, + // timeout: 90. + random::<f32>() * 60., + // demand, + // }; + // } + // } + // CustomerState::Waiting { + // chair, + // demand, + // timeout, + // } => { + // playerdata + // .movement + // .input((chair.as_vec2() + 0.5) - playerdata.position(), false); + // *timeout -= dt; + // if *timeout <= 0. { + // self.cpackets.push_back(PacketS::Communicate { + // message: None, + // persist: true, + // player, + // }); + // self.cpackets.push_back(PacketS::Communicate { + // message: Some(Message::Effect("angry".to_string())), + // persist: false, + // player, + // }); + // let path = find_path( + // &game.walkable, + // playerdata.position().as_ivec2(), + // game.data.customer_spawn.as_ivec2(), + // ) + // .expect("no path to exit"); + // *self.chairs.get_mut(chair).unwrap() = true; + // game.score.demands_failed += 1; + // game.score.points -= 1; + // game.score_changed = true; + // info!("{player:?} -> 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(PacketS::Communicate { + // persist: true, + // message: None, + // player, + // }); + // self.cpackets.push_back(PacketS::Communicate { + // message: Some(Message::Effect("satisfied".to_string())), + // persist: false, + // player, + // }); + // self.cpackets.push_back(PacketS::Interact { + // pos: Some(pos), + // player, + // }); + // self.cpackets + // .push_back(PacketS::Interact { pos: None, player }); + // info!("{player:?} -> eating"); + // *state = CustomerState::Eating { + // demand: *demand, + // target: pos, + // progress: 0., + // chair: *chair, + // } + // } + // } + // } + // CustomerState::Eating { + // demand, + // target, + // progress, + // chair, + // } => { + // playerdata + // .movement + // .input((chair.as_vec2() + 0.5) - playerdata.position(), false); + // let demand = &self.demands[demand.0]; + // *progress += dt / demand.duration; + // if *progress >= 1. { + // self.cpackets.push_back(PacketS::ReplaceHand { + // player, + // item: demand.to, + // }); + // if demand.to.is_some() { + // self.cpackets.push_back(PacketS::Interact { + // player, + // pos: Some(*target), + // }); + // self.cpackets + // .push_back(PacketS::Interact { player, pos: None }); + // } + // let path = find_path( + // &game.walkable, + // playerdata.position().as_ivec2(), + // game.data.customer_spawn.as_ivec2(), + // ) + // .ok_or(anyhow!("no path to exit"))?; + // *self.chairs.get_mut(chair).unwrap() = true; + // game.score.demands_completed += 1; + // game.score.points += demand.points; + // game.score_changed = true; + // info!("{player:?} -> exiting"); + // *state = CustomerState::Exiting { path } + // } + // } + // CustomerState::Exiting { path } => { + // playerdata + // .movement + // .input(path.next_direction(playerdata.position()), false); + // if path.is_done() { + // info!("{player:?} -> leave"); + // self.cpackets.push_back(PacketS::Leave { player }); + // customers_to_remove.push(player); + // } + // } + // } + // } + // for c in customers_to_remove { + // self.customers.remove(&c).unwrap(); + // } + // for packet in self.cpackets.drain(..) { + // if let Err(err) = game.packet_in(packet, &mut vec![], packet_out) { + // warn!("demand packet {err}"); + // } + // } Ok(()) } } diff --git a/server/src/entity/environment_effect.rs b/server/src/entity/environment_effect.rs index f369bce4..8f54d29c 100644 --- a/server/src/entity/environment_effect.rs +++ b/server/src/entity/environment_effect.rs @@ -15,15 +15,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use super::EntityT; -use crate::game::Game; +use super::{EntityContext, EntityT}; use hurrycurry_protocol::PacketC; use rand::random; use serde::{Deserialize, Serialize}; -use std::{ - collections::VecDeque, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; #[derive(Clone, Debug, Deserialize, Serialize, Default)] pub struct EnvironmentEffect { @@ -54,26 +50,21 @@ impl EnvironmentEffectController { } } impl EntityT for EnvironmentEffectController { - fn tick( - &mut self, - game: &mut Game, - packet_out: &mut VecDeque<PacketC>, - _dt: f32, - ) -> anyhow::Result<()> { + fn tick(&mut self, c: EntityContext) -> anyhow::Result<()> { if self.next_transition < Instant::now() { if self.active { self.next_transition += Duration::from_secs_f32(self.config.on + (0.5 + random::<f32>())); self.active = false; - game.environment_effects.remove(&self.config.name); + c.game.environment_effects.remove(&self.config.name); } else { self.next_transition += Duration::from_secs_f32(self.config.off * (0.5 + random::<f32>())); self.active = true; - game.environment_effects.insert(self.config.name.clone()); + c.game.environment_effects.insert(self.config.name.clone()); } - packet_out.push_back(PacketC::Environment { - effects: game.environment_effects.clone(), + c.packet_out.push_back(PacketC::Environment { + effects: c.game.environment_effects.clone(), }) } Ok(()) @@ -83,16 +74,11 @@ impl EntityT for EnvironmentEffectController { #[derive(Debug, Clone)] pub struct EnvironmentController(pub Vec<String>); impl EntityT for EnvironmentController { - fn tick( - &mut self, - game: &mut Game, - packet_out: &mut VecDeque<PacketC>, - _dt: f32, - ) -> anyhow::Result<()> { - if game.environment_effects.is_empty() { - game.environment_effects.extend(self.0.clone()); - packet_out.push_back(PacketC::Environment { - effects: game.environment_effects.clone(), + fn tick(&mut self, c: EntityContext) -> anyhow::Result<()> { + if c.game.environment_effects.is_empty() { + c.game.environment_effects.extend(self.0.clone()); + c.packet_out.push_back(PacketC::Environment { + effects: c.game.environment_effects.clone(), }) } Ok(()) diff --git a/server/src/entity/item_portal.rs b/server/src/entity/item_portal.rs index f63ff274..be6acd06 100644 --- a/server/src/entity/item_portal.rs +++ b/server/src/entity/item_portal.rs @@ -15,11 +15,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use super::EntityT; -use crate::game::{interact_effect, Game}; +use super::{EntityContext, EntityT}; +use crate::server::interact_effect; use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemLocation, PacketC}; -use std::collections::VecDeque; +use hurrycurry_protocol::{glam::IVec2, ItemLocation}; #[derive(Debug, Default, Clone)] pub struct ItemPortal { @@ -28,29 +27,25 @@ pub struct ItemPortal { } impl EntityT for ItemPortal { - fn tick( - &mut self, - game: &mut Game, - packet_out: &mut VecDeque<PacketC>, - _dt: f32, - ) -> Result<()> { - let [from, to] = game + fn tick(&mut self, c: EntityContext) -> Result<()> { + let [from, to] = c + .game .tiles .get_many_mut([&self.from, &self.to]) .ok_or(anyhow!("conveyor does ends in itself"))?; if from.item.is_some() { interact_effect( - &game.data, + &c.game.data, true, &mut to.item, ItemLocation::Tile(self.to), &mut from.item, ItemLocation::Tile(self.from), Some(to.kind), - packet_out, - &mut game.score, - &mut game.score_changed, + c.packet_out, + &mut c.game.score, + c.score_changed, true, ); } diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index 94718dd1..10d0c155 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -21,11 +21,12 @@ pub mod environment_effect; pub mod item_portal; pub mod player_portal; -use crate::{data::ItemTileRegistry, game::Game}; +use crate::data::ItemTileRegistry; use anyhow::{anyhow, Result}; use conveyor::Conveyor; use customers::{demands::generate_demands, Customers}; use environment_effect::{EnvironmentController, EnvironmentEffect, EnvironmentEffectController}; +use hurrycurry_client_lib::Game; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, ItemIndex, PacketC, Recipe, TileIndex, @@ -35,8 +36,15 @@ use player_portal::PlayerPortal; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet, VecDeque}; +pub struct EntityContext<'a> { + pub game: &'a mut Game, + pub packet_out: &'a mut VecDeque<PacketC>, + pub score_changed: &'a mut bool, + pub dt: f32, +} + pub trait EntityT: Clone { - fn tick(&mut self, game: &mut Game, packet_out: &mut VecDeque<PacketC>, dt: f32) -> Result<()>; + fn tick(&mut self, c: EntityContext<'_>) -> Result<()>; } macro_rules! entities { @@ -44,8 +52,8 @@ macro_rules! entities { #[derive(Debug, Clone)] pub enum Entity { $($e($e)),* } impl EntityT for Entity { - fn tick(&mut self, game: &mut Game, packet_out: &mut VecDeque<PacketC>, dt: f32) -> Result<()> { - match self { $(Entity::$e(x) => x.tick(game, packet_out, dt)),*, } + fn tick(&mut self, c: EntityContext<'_>) -> Result<()> { + match self { $(Entity::$e(x) => x.tick(c)),*, } } } }; diff --git a/server/src/entity/player_portal.rs b/server/src/entity/player_portal.rs index 6ee04f15..11442677 100644 --- a/server/src/entity/player_portal.rs +++ b/server/src/entity/player_portal.rs @@ -15,11 +15,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use super::EntityT; -use crate::game::Game; +use super::{EntityContext, EntityT}; use anyhow::{anyhow, Result}; use hurrycurry_protocol::{glam::Vec2, PacketC}; -use std::collections::VecDeque; #[derive(Debug, Default, Clone)] pub struct PlayerPortal { @@ -28,19 +26,21 @@ pub struct PlayerPortal { } impl EntityT for PlayerPortal { - fn tick( - &mut self, - game: &mut Game, - _packet_out: &mut VecDeque<PacketC>, - _dt: f32, - ) -> Result<()> { + fn tick(&mut self, c: EntityContext) -> Result<()> { let mut players = Vec::new(); - game.players_spatial_index + c.game + .players_spatial_index .query(self.from, 0.5, |pid, _| players.push(pid)); - for p in players { - let p = game.players.get_mut(&p).ok_or(anyhow!("player missing"))?; + for pid in players { + let p = c + .game + .players + .get_mut(&pid) + .ok_or(anyhow!("player missing"))?; p.movement.position = self.to; + c.packet_out + .push_back(PacketC::MovementSync { player: pid }); } Ok(()) diff --git a/server/src/interaction.rs b/server/src/interaction.rs index 4630b536..2f58ed8f 100644 --- a/server/src/interaction.rs +++ b/server/src/interaction.rs @@ -1,3 +1,4 @@ +use hurrycurry_client_lib::{Involvement, Item}; /* Hurry Curry! - a game about cooking Copyright 2024 metamuffin @@ -15,11 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{ - data::Gamedata, - game::{Involvement, Item}, -}; -use hurrycurry_protocol::{Recipe, Score, TileIndex}; +use hurrycurry_protocol::{Gamedata, Recipe, Score, TileIndex}; use log::info; pub enum InteractEffect { @@ -80,6 +77,7 @@ pub fn interact( recipe: ri, working: 1, progress: 0., + warn: false, }); } } @@ -96,6 +94,7 @@ pub fn interact( recipe: ri, working: 1, progress: 0., + warn: false, }); } *this = Some(item); @@ -180,11 +179,12 @@ pub fn tick_slot( } else { for (ri, recipe) in data.recipes() { if recipe.supports_tile(tile) { - if let Recipe::Passive { input, .. } = recipe { + if let Recipe::Passive { input, warn, .. } = recipe { if *input == item.kind { item.active = Some(Involvement { recipe: ri, progress: 0., + warn: *warn, working: 1, }); return Some(TickEffect::Progress(recipe.warn())); diff --git a/server/src/lib.rs b/server/src/lib.rs index c8f7af8c..b87f1cb9 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -18,7 +18,7 @@ #![feature(if_let_guard, map_many_mut, let_chains, iterator_try_collect, isqrt)] pub mod data; pub mod entity; -pub mod game; +pub mod server; pub mod interaction; pub mod spatial_index; pub mod state; diff --git a/server/src/main.rs b/server/src/main.rs index 0f38aa2b..40ea3433 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -19,7 +19,7 @@ use anyhow::{anyhow, Result}; use clap::Parser; use futures_util::{SinkExt, StreamExt}; use hurrycurry_protocol::{PacketC, PacketS, BINCODE_CONFIG, VERSION}; -use hurrycurry_server::{data::DATA_DIR, state::State, ConnectionID}; +use hurrycurry_server::{data::DATA_DIR, server::GameServerExt, state::State, ConnectionID}; use log::{debug, info, trace, warn, LevelFilter}; use std::{ net::SocketAddr, diff --git a/server/src/game.rs b/server/src/server.rs index 39cd61dc..f4ccbf35 100644 --- a/server/src/game.rs +++ b/server/src/server.rs @@ -16,95 +16,55 @@ */ use crate::{ - data::Gamedata, - entity::{Entity, EntityT}, + data::Serverdata, + entity::{Entity, EntityContext, EntityT}, interaction::{interact, tick_slot, InteractEffect, TickEffect}, - spatial_index::SpatialIndex, }; use anyhow::{anyhow, bail, Result}; +use hurrycurry_client_lib::{Game, Item, Player, Tile}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, movement::MovementBase, - ClientGamedata, ItemIndex, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID, - RecipeIndex, Score, TileIndex, + Gamedata, ItemLocation, Menu, PacketC, PacketS, PlayerID, Score, TileIndex, }; use log::{info, warn}; use std::{ - collections::{HashMap, HashSet, VecDeque}, - sync::{Arc, RwLock}, + collections::{HashMap, VecDeque}, + sync::Arc, time::{Duration, Instant}, }; -#[derive(Debug, PartialEq)] -pub struct Involvement { - pub recipe: RecipeIndex, - pub progress: f32, - pub working: usize, -} - -#[derive(Debug, PartialEq)] -pub struct Item { - pub kind: ItemIndex, - pub active: Option<Involvement>, -} - -pub struct Tile { - pub kind: TileIndex, - pub item: Option<Item>, -} - -pub struct Player { - pub name: String, - pub character: i32, - pub interacting: Option<IVec2>, - pub item: Option<Item>, - pub communicate_persist: Option<Message>, - - pub movement: MovementBase, - pub last_position_update: Instant, -} - -pub struct Game { - pub data: Arc<Gamedata>, - pub tiles: HashMap<IVec2, Tile>, - pub walkable: HashSet<IVec2>, - pub players: HashMap<PlayerID, Player>, - pub players_spatial_index: SpatialIndex<PlayerID>, - entities: Arc<RwLock<Vec<Entity>>>, - end: Option<Instant>, +pub struct ServerState { + pub data: Arc<Serverdata>, + entities: Vec<Entity>, pub lobby: bool, - - pub environment_effects: HashSet<String>, + pub player_id_counter: PlayerID, pub score_changed: bool, - pub score: Score, +} - pub player_id_counter: PlayerID, +pub struct Server<'a> { + pub game: &'a mut Game, + pub state: &'a mut ServerState, } -impl Default for Game { +impl Default for ServerState { fn default() -> Self { Self::new() } } -impl Game { - pub fn new() -> Self { - Self { - lobby: false, - data: Gamedata::default().into(), - players: HashMap::new(), - tiles: HashMap::new(), - walkable: HashSet::new(), - end: None, - entities: Arc::new(RwLock::new(vec![])), - players_spatial_index: SpatialIndex::default(), - score: Score::default(), - environment_effects: HashSet::default(), - score_changed: false, - player_id_counter: PlayerID(1), - } - } - +pub trait GameServerExt { + fn unload(&mut self, packet_out: &mut VecDeque<PacketC>); + fn load( + &mut self, + gamedata: Gamedata, + serverdata: &Serverdata, + timer: Option<Duration>, + packet_out: &mut VecDeque<PacketC>, + ); + fn prime_client(&self) -> Vec<PacketC>; +} +impl GameServerExt for Game { fn unload(&mut self, packet_out: &mut VecDeque<PacketC>) { packet_out.push_back(PacketC::SetIngame { state: false, @@ -125,9 +85,10 @@ impl Game { self.environment_effects.clear(); self.walkable.clear(); } - pub fn load( + fn load( &mut self, gamedata: Gamedata, + serverdata: &Serverdata, timer: Option<Duration>, packet_out: &mut VecDeque<PacketC>, ) { @@ -140,16 +101,15 @@ impl Game { self.unload(packet_out); - self.lobby = gamedata.map_name == "lobby"; + self.lobby = gamedata.current_map == "lobby"; self.data = gamedata.into(); self.score = Score { time_remaining: timer.map(|dur| dur.as_secs_f64()).unwrap_or(0.), ..Default::default() }; self.end = timer.map(|dur| Instant::now() + dur); - self.entities = Arc::new(RwLock::new(self.data.entities.clone())); - for (&p, (tile, item)) in &self.data.initial_map { + for (&p, (tile, item)) in &serverdata.initial_map { self.tiles.insert( p, Tile { @@ -172,9 +132,9 @@ impl Game { character, movement: MovementBase { position: if character < 0 { - self.data.customer_spawn + serverdata.customer_spawn } else { - self.data.chef_spawn + serverdata.chef_spawn }, input_direction: Vec2::ZERO, input_boost: false, @@ -184,7 +144,6 @@ impl Game { boosting: false, stamina: 0., }, - last_position_update: Instant::now(), communicate_persist: None, interacting: None, name: name.clone(), @@ -195,32 +154,10 @@ impl Game { packet_out.extend(self.prime_client()); } - pub fn prime_client(&self) -> Vec<PacketC> { + fn prime_client(&self) -> Vec<PacketC> { let mut out = Vec::new(); out.push(PacketC::Data { - data: ClientGamedata { - recipes: self.data.recipes.clone(), - item_names: self.data.item_names.clone(), - tile_names: self.data.tile_names.clone(), - tile_collide: self.data.tile_collide.clone(), - tile_interact: self.data.tile_interact.clone(), - current_map: self.data.map_name.clone(), - map_names: self - .data - .map - .clone() - .keys() - .filter(|n| n.as_str() != "lobby") - .map(|s| s.to_owned()) - .collect(), - maps: self - .data - .map - .clone() - .into_iter() - .filter(|(n, _)| n != "lobby") - .collect(), - }, + data: self.data.as_ref().to_owned(), }); out.push(PacketC::Environment { effects: self.environment_effects.clone(), @@ -271,6 +208,29 @@ impl Game { }); out } +} + +impl ServerState { + pub fn new() -> Self { + Self { + lobby: false, + data: Serverdata::default().into(), + entities: vec![], + player_id_counter: PlayerID(1), + score_changed: false, + } + } +} +impl Server<'_> { + pub fn load( + &mut self, + (gamedata, serverdata): (Gamedata, Serverdata), + timer: Option<Duration>, + packet_out: &mut VecDeque<PacketC>, + ) { + self.game.load(gamedata, &serverdata, timer, packet_out); + self.state.data = serverdata.into(); + } pub fn join_player( &mut self, @@ -278,24 +238,20 @@ impl Game { character: i32, packet_out: &mut VecDeque<PacketC>, ) -> PlayerID { - let id = self.player_id_counter; - self.player_id_counter.0 += 1; - let position = if id.0 < 0 { - self.data.customer_spawn + let id = self.state.player_id_counter; + self.state.player_id_counter.0 += 1; + let position = if character < 0 { + self.state.data.customer_spawn } else { - self.data.chef_spawn + self.state.data.chef_spawn }; - self.players.insert( + self.game.players.insert( id, Player { item: None, character, movement: MovementBase { - position: if character < 0 { - self.data.customer_spawn - } else { - self.data.chef_spawn - }, + position, input_direction: Vec2::ZERO, input_boost: false, facing: Vec2::X, @@ -304,13 +260,13 @@ impl Game { boosting: false, stamina: 0., }, - last_position_update: Instant::now(), + communicate_persist: None, interacting: None, name: name.clone(), }, ); - self.score.players = self.score.players.max(self.players.len()); + self.game.score.players = self.game.score.players.max(self.game.players.len()); packet_out.push_back(PacketC::AddPlayer { id, name, @@ -333,15 +289,16 @@ impl Game { } PacketS::Leave { player } => { let p = self + .game .players .remove(&player) .ok_or(anyhow!("player does not exist"))?; - self.players_spatial_index.remove_entry(player); + self.game.players_spatial_index.remove_entry(player); if let Some(item) = p.item { let pos = p.movement.position.floor().as_ivec2(); - if let Some(tile) = self.tiles.get_mut(&pos) { + if let Some(tile) = self.game.tiles.get_mut(&pos) { if tile.item.is_none() { packet_out.push_back(PacketC::SetItem { location: ItemLocation::Tile(pos), @@ -360,26 +317,30 @@ impl Game { player, } => { let pd = self + .game .players .get_mut(&player) .ok_or(anyhow!("player does not exist"))?; pd.movement.input(direction, boost); - if let Some(pos) = pos { - let dt = pd.last_position_update.elapsed(); - pd.last_position_update += dt; - let diff = pos - pd.movement.position; - pd.movement.position += diff.clamp_length_max(dt.as_secs_f32()); + let _ = pos; // TODO - if diff.length() > 1. { - replies.push(PacketC::MovementSync { player }); - } - } + // if let Some(pos) = pos { + // let dt = pd.last_position_update.elapsed(); + // pd.last_position_update += dt; + // let diff = pos - pd.movement.position; + // pd.movement.position += diff.clamp_length_max(dt.as_secs_f32()); + + // if diff.length() > 1. { + // replies.push(PacketC::MovementSync { player }); + // } + // } } PacketS::Interact { pos, player } => { let pid = player; let player = self + .game .players .get_mut(&pid) .ok_or(anyhow!("player does not exist"))?; @@ -397,6 +358,7 @@ impl Game { } let tile = self + .game .tiles .get_mut(&pos) .ok_or(anyhow!("tile does not exist"))?; @@ -405,8 +367,9 @@ impl Game { player.interacting = if edge { Some(pos) } else { None }; - let other_pid = if !self.data.is_tile_interactable(tile.kind) { - self.players + let other_pid = if !self.game.data.is_tile_interactable(tile.kind) { + self.game + .players .iter() .find(|(id, p)| **id != pid && p.movement.position.distance(entpos) < 0.7) .map(|(&id, _)| id) @@ -416,6 +379,7 @@ impl Game { if let Some(base_pid) = other_pid { let [other, this] = self + .game .players .get_many_mut([&pid, &base_pid]) .ok_or(anyhow!("interacting with yourself. this is impossible"))?; @@ -425,7 +389,7 @@ impl Game { } interact_effect( - &self.data, + &self.game.data, edge, &mut this.item, ItemLocation::Player(base_pid), @@ -433,18 +397,19 @@ impl Game { ItemLocation::Player(pid), None, packet_out, - &mut self.score, - &mut self.score_changed, + &mut self.game.score, + &mut self.state.score_changed, false, ) } else { let player = self + .game .players .get_mut(&pid) .ok_or(anyhow!("player does not exist"))?; interact_effect( - &self.data, + &self.game.data, edge, &mut tile.item, ItemLocation::Tile(pos), @@ -452,8 +417,8 @@ impl Game { ItemLocation::Player(pid), Some(tile.kind), packet_out, - &mut self.score, - &mut self.score_changed, + &mut self.game.score, + &mut self.state.score_changed, false, ) } @@ -465,7 +430,7 @@ impl Game { } => { info!("{player:?} message {message:?}"); if persist { - if let Some(player) = self.players.get_mut(&player) { + if let Some(player) = self.game.players.get_mut(&player) { player.communicate_persist = message.clone() } } @@ -477,6 +442,7 @@ impl Game { } PacketS::ReplaceHand { item, player } => { let pdata = self + .game .players .get_mut(&player) .ok_or(anyhow!("player does not exist"))?; @@ -496,18 +462,18 @@ impl Game { /// Returns true if the game should end pub fn tick(&mut self, dt: f32, packet_out: &mut VecDeque<PacketC>) -> bool { - if self.score_changed { - self.score_changed = false; - packet_out.push_back(PacketC::Score(self.score.clone())); + if self.state.score_changed { + self.state.score_changed = false; + packet_out.push_back(PacketC::Score(self.game.score.clone())); } - for (&pos, tile) in &mut self.tiles { + for (&pos, tile) in &mut self.game.tiles { if let Some(effect) = tick_slot( dt, - &self.data, + &self.game.data, Some(tile.kind), &mut tile.item, - &mut self.score, + &mut self.game.score, ) { match effect { TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { @@ -536,22 +502,25 @@ impl Game { } } - for (&pid, player) in &mut self.players { - player.movement.update(&self.walkable, dt); + for (&pid, player) in &mut self.game.players { + player.movement.update(&self.game.walkable, dt); - self.players_spatial_index + self.game + .players_spatial_index .update_entry(pid, player.movement.position); } - self.players_spatial_index.all(|p1, pos1| { - self.players_spatial_index.query(pos1, 2., |p2, _pos2| { - if let Some([a, b]) = self.players.get_many_mut([&p1, &p2]) { - a.movement.collide(&mut b.movement, dt) - } - }) + self.game.players_spatial_index.all(|p1, pos1| { + self.game + .players_spatial_index + .query(pos1, 2., |p2, _pos2| { + if let Some([a, b]) = self.game.players.get_many_mut([&p1, &p2]) { + a.movement.collide(&mut b.movement, dt) + } + }) }); - for (&pid, player) in &mut self.players { + for (&pid, player) in &mut self.game.players { packet_out.push_back(PacketC::Movement { player: pid, pos: player.movement.position, @@ -560,8 +529,13 @@ impl Game { rot: player.movement.rotation, }); - if let Some(effect) = tick_slot(dt, &self.data, None, &mut player.item, &mut self.score) - { + if let Some(effect) = tick_slot( + dt, + &self.game.data, + None, + &mut player.item, + &mut self.game.score, + ) { match effect { TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { warn, @@ -590,9 +564,9 @@ impl Game { } let mut players_auto_release = Vec::new(); - for (pid, player) in &mut self.players { + for (pid, player) in &mut self.game.players { if let Some(pos) = player.interacting { - if let Some(tile) = self.tiles.get(&pos) { + if let Some(tile) = self.game.tiles.get(&pos) { if let Some(item) = &tile.item { if let Some(involvement) = &item.active { if involvement.progress >= 1. { @@ -611,25 +585,31 @@ impl Game { ); } - for entity in self.entities.clone().write().unwrap().iter_mut() { - if let Err(e) = entity.tick(self, packet_out, dt) { + for entity in self.state.entities.iter_mut() { + if let Err(e) = entity.tick(EntityContext { + game: self.game, + packet_out, + score_changed: &mut self.state.score_changed, + dt, + }) { warn!("entity tick failed: {e}") } } let now = Instant::now(); - if let Some(end) = self.end { - self.score.time_remaining = (end - now).as_secs_f64(); + if let Some(end) = self.game.end { + self.game.score.time_remaining = (end - now).as_secs_f64(); if end < now { - let relative_score = (self.score.points * 100) / self.data.score_baseline.max(1); - self.score.stars = match relative_score { + let relative_score = + (self.game.score.points * 100) / self.state.data.score_baseline.max(1); + self.game.score.stars = match relative_score { 100.. => 3, 70.. => 2, 40.. => 1, _ => 0, }; - packet_out.push_back(PacketC::Menu(Menu::Score(self.score.clone()))); + packet_out.push_back(PacketC::Menu(Menu::Score(self.game.score.clone()))); true } else { false @@ -640,19 +620,14 @@ impl Game { } pub fn count_chefs(&self) -> usize { - self.players + self.game + .players .values() .map(|p| if p.character >= 0 { 1 } else { 0 }) .sum() } } -impl From<TileIndex> for Tile { - fn from(kind: TileIndex) -> Self { - Self { kind, item: None } - } -} - pub fn interact_effect( data: &Gamedata, edge: bool, @@ -729,9 +704,3 @@ pub fn interact_effect( } } } - -impl Player { - pub fn position(&self) -> Vec2 { - self.movement.position - } -} diff --git a/server/src/state.rs b/server/src/state.rs index 526f70aa..ecfa0fcb 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -15,9 +15,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{data::DataIndex, game::Game, ConnectionID}; +use crate::{ + data::DataIndex, + server::{Server, ServerState}, + ConnectionID, +}; use anyhow::{anyhow, bail, Result}; use clap::{Parser, ValueEnum}; +use hurrycurry_client_lib::Game; use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID}; use log::{debug, trace}; use std::{ @@ -31,6 +36,7 @@ pub struct State { packet_out: VecDeque<PacketC>, tx: Sender<PacketC>, connections: HashMap<ConnectionID, HashSet<PlayerID>>, + pub server: ServerState, pub game: Game, } @@ -81,15 +87,24 @@ impl State { index.reload()?; let mut packet_out = VecDeque::new(); - let mut game = Game::new(); - game.load( - index.generate("lobby-none".to_string()).await?, - None, - &mut packet_out, - ); + let mut game = Game::default(); + let mut server = ServerState::default(); + + { + Server { + game: &mut game, + state: &mut server, + } + .load( + index.generate("lobby-none".to_string()).await?, + None, + &mut packet_out, + ); + } Ok(Self { game, + server, index, tx, packet_out, @@ -98,8 +113,12 @@ impl State { } pub async fn tick(&mut self, dt: f32) -> anyhow::Result<()> { - if self.game.tick(dt, &mut self.packet_out) { - self.game.load( + let mut server = Server { + game: &mut self.game, + state: &mut self.server, + }; + if server.tick(dt, &mut self.packet_out) { + server.load( self.index.generate("lobby-none".to_string()).await?, None, &mut self.packet_out, @@ -147,8 +166,11 @@ impl State { } _ => (), } - self.game - .packet_in(packet, &mut replies, &mut self.packet_out)?; + let mut server = Server { + game: &mut self.game, + state: &mut self.server, + }; + server.packet_in(packet, &mut replies, &mut self.packet_out)?; for p in &replies { match p { @@ -159,13 +181,13 @@ impl State { } } - if self.game.count_chefs() <= 0 && !self.game.lobby { + if server.count_chefs() <= 0 && !server.state.lobby { self.tx .send(PacketC::ServerMessage { text: "Game was aborted automatically due to a lack of players".to_string(), }) .ok(); - self.game.load( + server.load( self.index.generate("lobby-none".to_string()).await?, None, &mut self.packet_out, @@ -197,18 +219,22 @@ impl State { } async fn handle_command(&mut self, player: PlayerID, command: Command) -> Result<()> { + let mut server = Server { + game: &mut self.game, + state: &mut self.server, + }; match command { Command::Start { spec, timer } => { let data = self.index.generate(spec).await?; - self.game - .load(data, Some(Duration::from_secs(timer)), &mut self.packet_out); + server.load(data, Some(Duration::from_secs(timer)), &mut self.packet_out); } Command::End => { self.tx .send(PacketC::ServerMessage { text: format!( "Game was aborted by {}.", - self.game + server + .game .players .get(&player) .ok_or(anyhow!("player missing"))? @@ -216,18 +242,20 @@ impl State { ), }) .ok(); - self.game.load( + server.load( self.index.generate("lobby-none".to_string()).await?, None, &mut self.packet_out, ); } Command::Reload => { - if self.game.count_chefs() > 1 { + if server.count_chefs() > 1 { bail!("must be at most one player to reload"); } - self.game.load( - self.index.generate(self.game.data.spec.to_string()).await?, + server.load( + self.index + .generate(server.state.data.spec.to_string()) + .await?, None, &mut self.packet_out, ); @@ -259,7 +287,7 @@ impl State { .ok(); } Command::Item { name } => { - let item = self + let item = server .game .data .get_item_by_name(&name) |