From 16ff78180669411326d42ea32d4a9260c018236c Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 13 Aug 2024 12:48:31 +0200 Subject: refactor server to use client-lib data model (breaks customers) --- server/src/bin/graph.rs | 2 +- server/src/data.rs | 369 +++++++--------- server/src/entity/conveyor.rs | 29 +- server/src/entity/customers/mod.rs | 368 ++++++++-------- server/src/entity/environment_effect.rs | 38 +- server/src/entity/item_portal.rs | 25 +- server/src/entity/mod.rs | 16 +- server/src/entity/player_portal.rs | 24 +- server/src/game.rs | 737 -------------------------------- server/src/interaction.rs | 12 +- server/src/lib.rs | 2 +- server/src/main.rs | 2 +- server/src/server.rs | 706 ++++++++++++++++++++++++++++++ server/src/state.rs | 70 ++- 14 files changed, 1167 insertions(+), 1233 deletions(-) delete mode 100644 server/src/game.rs create mode 100644 server/src/server.rs (limited to 'server/src') 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, - pub tile_names: Vec, - pub tile_collide: Vec, - pub tile_interact: Vec, - pub map: HashMap, - pub recipes: Vec, pub initial_map: HashMap)>, 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 { + 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, - ) -> 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"))?, +pub fn build_data( + maps: HashMap, + spec: String, + map_name: String, + map_in: InitialMap, + recipes_in: Vec, +) -> 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, + 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, - 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(); - - 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)); + // 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(); + + 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::>()?, - ); - - let item_names = reg.items.into_inner().unwrap(); - let tile_names = reg.tiles.into_inner().unwrap(); - let tile_collide = tile_names + entities.extend( + map_in + .entities .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, + .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 { + 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 { - 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/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 . */ -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, 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, 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.; - let id = game.join_player( - faker::name::fr_fr::Name().fake(), - -1 - (random::() 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::() * 10.; + // let id = game.join_player( + // faker::name::fr_fr::Name().fake(), + // -1 - (random::() 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::() 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::() * 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::() 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::() * 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 . */ -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, - _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::())); 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::())); 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); impl EntityT for EnvironmentController { - fn tick( - &mut self, - game: &mut Game, - packet_out: &mut VecDeque, - _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 . */ -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, - _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, + pub score_changed: &'a mut bool, + pub dt: f32, +} + pub trait EntityT: Clone { - fn tick(&mut self, game: &mut Game, packet_out: &mut VecDeque, 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, 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 . */ -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, - _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/game.rs b/server/src/game.rs deleted file mode 100644 index 39cd61dc..00000000 --- a/server/src/game.rs +++ /dev/null @@ -1,737 +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 crate::{ - data::Gamedata, - entity::{Entity, EntityT}, - interaction::{interact, tick_slot, InteractEffect, TickEffect}, - spatial_index::SpatialIndex, -}; -use anyhow::{anyhow, bail, Result}; -use hurrycurry_protocol::{ - glam::{IVec2, Vec2}, - movement::MovementBase, - ClientGamedata, ItemIndex, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID, - RecipeIndex, Score, TileIndex, -}; -use log::{info, warn}; -use std::{ - collections::{HashMap, HashSet, VecDeque}, - sync::{Arc, RwLock}, - 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, -} - -pub struct Tile { - pub kind: TileIndex, - pub item: Option, -} - -pub struct Player { - pub name: String, - pub character: i32, - pub interacting: Option, - pub item: Option, - pub communicate_persist: Option, - - pub movement: MovementBase, - pub last_position_update: Instant, -} - -pub struct Game { - pub data: Arc, - pub tiles: HashMap, - pub walkable: HashSet, - pub players: HashMap, - pub players_spatial_index: SpatialIndex, - entities: Arc>>, - end: Option, - pub lobby: bool, - - pub environment_effects: HashSet, - pub score_changed: bool, - pub score: Score, - - pub player_id_counter: PlayerID, -} - -impl Default for Game { - 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), - } - } - - fn unload(&mut self, packet_out: &mut VecDeque) { - packet_out.push_back(PacketC::SetIngame { - state: false, - lobby: false, - }); - for (id, _) in self.players.drain() { - packet_out.push_back(PacketC::RemovePlayer { id }) - } - for (pos, _) in self.tiles.drain() { - packet_out.push_back(PacketC::UpdateMap { - tile: pos, - kind: None, - neighbors: [None, None, None, None], - }) - } - self.score = Score::default(); - self.end = None; - self.environment_effects.clear(); - self.walkable.clear(); - } - pub fn load( - &mut self, - gamedata: Gamedata, - timer: Option, - packet_out: &mut VecDeque, - ) { - let players = self - .players - .iter() - .filter(|(_, p)| p.character >= 0) - .map(|(id, p)| (*id, (p.name.to_owned(), p.character))) - .collect::>(); - - self.unload(packet_out); - - self.lobby = gamedata.map_name == "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 { - self.tiles.insert( - p, - Tile { - kind: *tile, - item: item.map(|i| Item { - kind: i, - active: None, - }), - }, - ); - if !self.data.tile_collide[tile.0] { - self.walkable.insert(p); - } - } - for (id, (name, character)) in players { - self.players.insert( - id, - Player { - item: None, - character, - movement: MovementBase { - position: if character < 0 { - self.data.customer_spawn - } else { - self.data.chef_spawn - }, - input_direction: Vec2::ZERO, - input_boost: false, - facing: Vec2::X, - rotation: 0., - velocity: Vec2::ZERO, - boosting: false, - stamina: 0., - }, - last_position_update: Instant::now(), - communicate_persist: None, - interacting: None, - name: name.clone(), - }, - ); - } - - packet_out.extend(self.prime_client()); - } - - pub fn prime_client(&self) -> Vec { - 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(), - }, - }); - out.push(PacketC::Environment { - effects: self.environment_effects.clone(), - }); - for (&id, player) in &self.players { - out.push(PacketC::AddPlayer { - id, - position: player.movement.position, - character: player.character, - name: player.name.clone(), - }); - if let Some(item) = &player.item { - out.push(PacketC::SetItem { - location: ItemLocation::Player(id), - item: Some(item.kind), - }) - } - if let Some(c) = &player.communicate_persist { - out.push(PacketC::Communicate { - player: id, - message: Some(c.to_owned()), - persist: true, - }) - } - } - for (&tile, tdata) in &self.tiles { - out.push(PacketC::UpdateMap { - tile, - neighbors: [ - self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind), - self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind), - self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind), - self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind), - ], - kind: Some(tdata.kind), - }); - if let Some(item) = &tdata.item { - out.push(PacketC::SetItem { - location: ItemLocation::Tile(tile), - item: Some(item.kind), - }) - } - } - out.push(PacketC::Score(self.score.clone())); - out.push(PacketC::SetIngame { - state: true, - lobby: self.lobby, - }); - out - } - - pub fn join_player( - &mut self, - name: String, - character: i32, - packet_out: &mut VecDeque, - ) -> PlayerID { - let id = self.player_id_counter; - self.player_id_counter.0 += 1; - let position = if id.0 < 0 { - self.data.customer_spawn - } else { - self.data.chef_spawn - }; - self.players.insert( - id, - Player { - item: None, - character, - movement: MovementBase { - position: if character < 0 { - self.data.customer_spawn - } else { - self.data.chef_spawn - }, - input_direction: Vec2::ZERO, - input_boost: false, - facing: Vec2::X, - rotation: 0., - velocity: Vec2::ZERO, - 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()); - packet_out.push_back(PacketC::AddPlayer { - id, - name, - position, - character, - }); - id - } - - pub fn packet_in( - &mut self, - packet: PacketS, - replies: &mut Vec, - packet_out: &mut VecDeque, - ) -> Result<()> { - match packet { - PacketS::Join { name, character } => { - let id = self.join_player(name, character, packet_out); - replies.push(PacketC::Joined { id }) - } - PacketS::Leave { player } => { - let p = self - .players - .remove(&player) - .ok_or(anyhow!("player does not exist"))?; - - self.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 tile.item.is_none() { - packet_out.push_back(PacketC::SetItem { - location: ItemLocation::Tile(pos), - item: Some(item.kind), - }); - tile.item = Some(item); - } - } - } - packet_out.push_back(PacketC::RemovePlayer { id: player }) - } - PacketS::Movement { - pos, - boost, - dir: direction, - player, - } => { - let pd = self - .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()); - - if diff.length() > 1. { - replies.push(PacketC::MovementSync { player }); - } - } - } - PacketS::Interact { pos, player } => { - let pid = player; - let player = self - .players - .get_mut(&pid) - .ok_or(anyhow!("player does not exist"))?; - - let (pos, edge) = match (pos, player.interacting) { - (None, None) => return Ok(()), // this is silent because of auto release - (None, Some(pos)) => (pos, false), - (Some(pos), None) => (pos, true), - (Some(_), Some(_)) => bail!("already interacting"), - }; - - let entpos = pos.as_vec2() + Vec2::splat(0.5); - if edge && entpos.distance(player.movement.position) > 2. { - bail!("interacting too far from player"); - } - - let tile = self - .tiles - .get_mut(&pos) - .ok_or(anyhow!("tile does not exist"))?; - - // No going back from here on - - player.interacting = if edge { Some(pos) } else { None }; - - let other_pid = if !self.data.is_tile_interactable(tile.kind) { - self.players - .iter() - .find(|(id, p)| **id != pid && p.movement.position.distance(entpos) < 0.7) - .map(|(&id, _)| id) - } else { - None - }; - - if let Some(base_pid) = other_pid { - let [other, this] = self - .players - .get_many_mut([&pid, &base_pid]) - .ok_or(anyhow!("interacting with yourself. this is impossible"))?; - - if this.character < 0 || other.character < 0 { - bail!("You shall not interact with customers.") - } - - interact_effect( - &self.data, - edge, - &mut this.item, - ItemLocation::Player(base_pid), - &mut other.item, - ItemLocation::Player(pid), - None, - packet_out, - &mut self.score, - &mut self.score_changed, - false, - ) - } else { - let player = self - .players - .get_mut(&pid) - .ok_or(anyhow!("player does not exist"))?; - - interact_effect( - &self.data, - edge, - &mut tile.item, - ItemLocation::Tile(pos), - &mut player.item, - ItemLocation::Player(pid), - Some(tile.kind), - packet_out, - &mut self.score, - &mut self.score_changed, - false, - ) - } - } - PacketS::Communicate { - message, - persist, - player, - } => { - info!("{player:?} message {message:?}"); - if persist { - if let Some(player) = self.players.get_mut(&player) { - player.communicate_persist = message.clone() - } - } - packet_out.push_back(PacketC::Communicate { - player, - message, - persist, - }) - } - PacketS::ReplaceHand { item, player } => { - let pdata = self - .players - .get_mut(&player) - .ok_or(anyhow!("player does not exist"))?; - pdata.item = item.map(|i| Item { - kind: i, - active: None, - }); - packet_out.push_back(PacketC::SetItem { - location: ItemLocation::Player(player), - item, - }) - } - PacketS::ReplayTick { .. } => bail!("packet not supported in this session"), - } - Ok(()) - } - - /// Returns true if the game should end - pub fn tick(&mut self, dt: f32, packet_out: &mut VecDeque) -> bool { - if self.score_changed { - self.score_changed = false; - packet_out.push_back(PacketC::Score(self.score.clone())); - } - - for (&pos, tile) in &mut self.tiles { - if let Some(effect) = tick_slot( - dt, - &self.data, - Some(tile.kind), - &mut tile.item, - &mut self.score, - ) { - match effect { - TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { - warn, - item: ItemLocation::Tile(pos), - progress: tile - .item - .as_ref() - .unwrap() - .active - .as_ref() - .map(|i| i.progress), - }), - TickEffect::Produce => { - packet_out.push_back(PacketC::SetProgress { - warn: false, - item: ItemLocation::Tile(pos), - progress: None, - }); - packet_out.push_back(PacketC::SetItem { - location: ItemLocation::Tile(pos), - item: tile.item.as_ref().map(|i| i.kind), - }); - } - } - } - } - - for (&pid, player) in &mut self.players { - player.movement.update(&self.walkable, dt); - - self.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) - } - }) - }); - - for (&pid, player) in &mut self.players { - packet_out.push_back(PacketC::Movement { - player: pid, - pos: player.movement.position, - dir: player.movement.input_direction, - boost: player.movement.boosting, - rot: player.movement.rotation, - }); - - if let Some(effect) = tick_slot(dt, &self.data, None, &mut player.item, &mut self.score) - { - match effect { - TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { - warn, - item: ItemLocation::Player(pid), - progress: player - .item - .as_ref() - .unwrap() - .active - .as_ref() - .map(|i| i.progress), - }), - TickEffect::Produce => { - packet_out.push_back(PacketC::SetProgress { - warn: false, - item: ItemLocation::Player(pid), - progress: None, - }); - packet_out.push_back(PacketC::SetItem { - location: ItemLocation::Player(pid), - item: player.item.as_ref().map(|i| i.kind), - }); - } - } - } - } - - let mut players_auto_release = Vec::new(); - for (pid, player) in &mut self.players { - if let Some(pos) = player.interacting { - if let Some(tile) = self.tiles.get(&pos) { - if let Some(item) = &tile.item { - if let Some(involvement) = &item.active { - if involvement.progress >= 1. { - players_auto_release.push(*pid); - } - } - } - } - } - } - for player in players_auto_release.drain(..) { - let _ = self.packet_in( - PacketS::Interact { pos: None, player }, - &mut vec![], - packet_out, - ); - } - - for entity in self.entities.clone().write().unwrap().iter_mut() { - if let Err(e) = entity.tick(self, packet_out, 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 end < now { - let relative_score = (self.score.points * 100) / self.data.score_baseline.max(1); - self.score.stars = match relative_score { - 100.. => 3, - 70.. => 2, - 40.. => 1, - _ => 0, - }; - packet_out.push_back(PacketC::Menu(Menu::Score(self.score.clone()))); - true - } else { - false - } - } else { - false - } - } - - pub fn count_chefs(&self) -> usize { - self.players - .values() - .map(|p| if p.character >= 0 { 1 } else { 0 }) - .sum() - } -} - -impl From for Tile { - fn from(kind: TileIndex) -> Self { - Self { kind, item: None } - } -} - -pub fn interact_effect( - data: &Gamedata, - edge: bool, - this: &mut Option, - this_loc: ItemLocation, - other: &mut Option, - other_loc: ItemLocation, - this_tile_kind: Option, - packet_out: &mut VecDeque, - score: &mut Score, - score_changed: &mut bool, - automated: bool, -) { - let this_had_item = this.is_some(); - let other_had_item = other.is_some(); - - if let Some(effect) = interact(data, edge, this_tile_kind, this, other, score, automated) { - match effect { - InteractEffect::Put => { - info!("put {this_loc} <- {other_loc}"); - packet_out.push_back(PacketC::MoveItem { - from: other_loc, - to: this_loc, - }) - } - InteractEffect::Take => { - info!("take {this_loc} -> {other_loc}"); - packet_out.push_back(PacketC::MoveItem { - from: this_loc, - to: other_loc, - }) - } - InteractEffect::Produce => { - info!("produce {this_loc} <~ {other_loc}"); - *score_changed = true; - if this_had_item { - packet_out.push_back(PacketC::SetProgress { - item: this_loc, - progress: None, - warn: false, - }); - packet_out.push_back(PacketC::SetItem { - location: this_loc, - item: None, - }); - } - if other_had_item { - packet_out.push_back(PacketC::MoveItem { - from: other_loc, - to: this_loc, - }); - packet_out.push_back(PacketC::SetItem { - location: this_loc, - item: None, - }); - } - if let Some(i) = &other { - packet_out.push_back(PacketC::SetItem { - location: this_loc, - item: Some(i.kind), - }); - packet_out.push_back(PacketC::MoveItem { - from: this_loc, - to: other_loc, - }) - } - if let Some(i) = &this { - packet_out.push_back(PacketC::SetItem { - location: this_loc, - item: Some(i.kind), - }); - } - } - } - } -} - -impl Player { - pub fn position(&self) -> Vec2 { - self.movement.position - } -} 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 . */ -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/server.rs b/server/src/server.rs new file mode 100644 index 00000000..f4ccbf35 --- /dev/null +++ b/server/src/server.rs @@ -0,0 +1,706 @@ +/* + 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 crate::{ + data::Serverdata, + entity::{Entity, EntityContext, EntityT}, + interaction::{interact, tick_slot, InteractEffect, TickEffect}, +}; +use anyhow::{anyhow, bail, Result}; +use hurrycurry_client_lib::{Game, Item, Player, Tile}; +use hurrycurry_protocol::{ + glam::{IVec2, Vec2}, + movement::MovementBase, + Gamedata, ItemLocation, Menu, PacketC, PacketS, PlayerID, Score, TileIndex, +}; +use log::{info, warn}; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, + time::{Duration, Instant}, +}; + +pub struct ServerState { + pub data: Arc, + entities: Vec, + pub lobby: bool, + pub player_id_counter: PlayerID, + pub score_changed: bool, +} + +pub struct Server<'a> { + pub game: &'a mut Game, + pub state: &'a mut ServerState, +} + +impl Default for ServerState { + fn default() -> Self { + Self::new() + } +} + +pub trait GameServerExt { + fn unload(&mut self, packet_out: &mut VecDeque); + fn load( + &mut self, + gamedata: Gamedata, + serverdata: &Serverdata, + timer: Option, + packet_out: &mut VecDeque, + ); + fn prime_client(&self) -> Vec; +} +impl GameServerExt for Game { + fn unload(&mut self, packet_out: &mut VecDeque) { + packet_out.push_back(PacketC::SetIngame { + state: false, + lobby: false, + }); + for (id, _) in self.players.drain() { + packet_out.push_back(PacketC::RemovePlayer { id }) + } + for (pos, _) in self.tiles.drain() { + packet_out.push_back(PacketC::UpdateMap { + tile: pos, + kind: None, + neighbors: [None, None, None, None], + }) + } + self.score = Score::default(); + self.end = None; + self.environment_effects.clear(); + self.walkable.clear(); + } + fn load( + &mut self, + gamedata: Gamedata, + serverdata: &Serverdata, + timer: Option, + packet_out: &mut VecDeque, + ) { + let players = self + .players + .iter() + .filter(|(_, p)| p.character >= 0) + .map(|(id, p)| (*id, (p.name.to_owned(), p.character))) + .collect::>(); + + self.unload(packet_out); + + 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); + + for (&p, (tile, item)) in &serverdata.initial_map { + self.tiles.insert( + p, + Tile { + kind: *tile, + item: item.map(|i| Item { + kind: i, + active: None, + }), + }, + ); + if !self.data.tile_collide[tile.0] { + self.walkable.insert(p); + } + } + for (id, (name, character)) in players { + self.players.insert( + id, + Player { + item: None, + character, + movement: MovementBase { + position: if character < 0 { + serverdata.customer_spawn + } else { + serverdata.chef_spawn + }, + input_direction: Vec2::ZERO, + input_boost: false, + facing: Vec2::X, + rotation: 0., + velocity: Vec2::ZERO, + boosting: false, + stamina: 0., + }, + communicate_persist: None, + interacting: None, + name: name.clone(), + }, + ); + } + + packet_out.extend(self.prime_client()); + } + + fn prime_client(&self) -> Vec { + let mut out = Vec::new(); + out.push(PacketC::Data { + data: self.data.as_ref().to_owned(), + }); + out.push(PacketC::Environment { + effects: self.environment_effects.clone(), + }); + for (&id, player) in &self.players { + out.push(PacketC::AddPlayer { + id, + position: player.movement.position, + character: player.character, + name: player.name.clone(), + }); + if let Some(item) = &player.item { + out.push(PacketC::SetItem { + location: ItemLocation::Player(id), + item: Some(item.kind), + }) + } + if let Some(c) = &player.communicate_persist { + out.push(PacketC::Communicate { + player: id, + message: Some(c.to_owned()), + persist: true, + }) + } + } + for (&tile, tdata) in &self.tiles { + out.push(PacketC::UpdateMap { + tile, + neighbors: [ + self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind), + self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind), + self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind), + self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind), + ], + kind: Some(tdata.kind), + }); + if let Some(item) = &tdata.item { + out.push(PacketC::SetItem { + location: ItemLocation::Tile(tile), + item: Some(item.kind), + }) + } + } + out.push(PacketC::Score(self.score.clone())); + out.push(PacketC::SetIngame { + state: true, + lobby: self.lobby, + }); + 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, + packet_out: &mut VecDeque, + ) { + self.game.load(gamedata, &serverdata, timer, packet_out); + self.state.data = serverdata.into(); + } + + pub fn join_player( + &mut self, + name: String, + character: i32, + packet_out: &mut VecDeque, + ) -> PlayerID { + 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.state.data.chef_spawn + }; + self.game.players.insert( + id, + Player { + item: None, + character, + movement: MovementBase { + position, + input_direction: Vec2::ZERO, + input_boost: false, + facing: Vec2::X, + rotation: 0., + velocity: Vec2::ZERO, + boosting: false, + stamina: 0., + }, + + communicate_persist: None, + interacting: None, + name: name.clone(), + }, + ); + self.game.score.players = self.game.score.players.max(self.game.players.len()); + packet_out.push_back(PacketC::AddPlayer { + id, + name, + position, + character, + }); + id + } + + pub fn packet_in( + &mut self, + packet: PacketS, + replies: &mut Vec, + packet_out: &mut VecDeque, + ) -> Result<()> { + match packet { + PacketS::Join { name, character } => { + let id = self.join_player(name, character, packet_out); + replies.push(PacketC::Joined { id }) + } + PacketS::Leave { player } => { + let p = self + .game + .players + .remove(&player) + .ok_or(anyhow!("player does not exist"))?; + + 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.game.tiles.get_mut(&pos) { + if tile.item.is_none() { + packet_out.push_back(PacketC::SetItem { + location: ItemLocation::Tile(pos), + item: Some(item.kind), + }); + tile.item = Some(item); + } + } + } + packet_out.push_back(PacketC::RemovePlayer { id: player }) + } + PacketS::Movement { + pos, + boost, + dir: direction, + player, + } => { + let pd = self + .game + .players + .get_mut(&player) + .ok_or(anyhow!("player does not exist"))?; + + pd.movement.input(direction, boost); + + let _ = pos; // TODO + + // 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"))?; + + let (pos, edge) = match (pos, player.interacting) { + (None, None) => return Ok(()), // this is silent because of auto release + (None, Some(pos)) => (pos, false), + (Some(pos), None) => (pos, true), + (Some(_), Some(_)) => bail!("already interacting"), + }; + + let entpos = pos.as_vec2() + Vec2::splat(0.5); + if edge && entpos.distance(player.movement.position) > 2. { + bail!("interacting too far from player"); + } + + let tile = self + .game + .tiles + .get_mut(&pos) + .ok_or(anyhow!("tile does not exist"))?; + + // No going back from here on + + player.interacting = if edge { Some(pos) } else { None }; + + 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) + } else { + None + }; + + 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"))?; + + if this.character < 0 || other.character < 0 { + bail!("You shall not interact with customers.") + } + + interact_effect( + &self.game.data, + edge, + &mut this.item, + ItemLocation::Player(base_pid), + &mut other.item, + ItemLocation::Player(pid), + None, + packet_out, + &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.game.data, + edge, + &mut tile.item, + ItemLocation::Tile(pos), + &mut player.item, + ItemLocation::Player(pid), + Some(tile.kind), + packet_out, + &mut self.game.score, + &mut self.state.score_changed, + false, + ) + } + } + PacketS::Communicate { + message, + persist, + player, + } => { + info!("{player:?} message {message:?}"); + if persist { + if let Some(player) = self.game.players.get_mut(&player) { + player.communicate_persist = message.clone() + } + } + packet_out.push_back(PacketC::Communicate { + player, + message, + persist, + }) + } + PacketS::ReplaceHand { item, player } => { + let pdata = self + .game + .players + .get_mut(&player) + .ok_or(anyhow!("player does not exist"))?; + pdata.item = item.map(|i| Item { + kind: i, + active: None, + }); + packet_out.push_back(PacketC::SetItem { + location: ItemLocation::Player(player), + item, + }) + } + PacketS::ReplayTick { .. } => bail!("packet not supported in this session"), + } + Ok(()) + } + + /// Returns true if the game should end + pub fn tick(&mut self, dt: f32, packet_out: &mut VecDeque) -> bool { + 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.game.tiles { + if let Some(effect) = tick_slot( + dt, + &self.game.data, + Some(tile.kind), + &mut tile.item, + &mut self.game.score, + ) { + match effect { + TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { + warn, + item: ItemLocation::Tile(pos), + progress: tile + .item + .as_ref() + .unwrap() + .active + .as_ref() + .map(|i| i.progress), + }), + TickEffect::Produce => { + packet_out.push_back(PacketC::SetProgress { + warn: false, + item: ItemLocation::Tile(pos), + progress: None, + }); + packet_out.push_back(PacketC::SetItem { + location: ItemLocation::Tile(pos), + item: tile.item.as_ref().map(|i| i.kind), + }); + } + } + } + } + + for (&pid, player) in &mut self.game.players { + player.movement.update(&self.game.walkable, dt); + + self.game + .players_spatial_index + .update_entry(pid, player.movement.position); + } + + 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.game.players { + packet_out.push_back(PacketC::Movement { + player: pid, + pos: player.movement.position, + dir: player.movement.input_direction, + boost: player.movement.boosting, + rot: player.movement.rotation, + }); + + 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, + item: ItemLocation::Player(pid), + progress: player + .item + .as_ref() + .unwrap() + .active + .as_ref() + .map(|i| i.progress), + }), + TickEffect::Produce => { + packet_out.push_back(PacketC::SetProgress { + warn: false, + item: ItemLocation::Player(pid), + progress: None, + }); + packet_out.push_back(PacketC::SetItem { + location: ItemLocation::Player(pid), + item: player.item.as_ref().map(|i| i.kind), + }); + } + } + } + } + + let mut players_auto_release = Vec::new(); + for (pid, player) in &mut self.game.players { + if let Some(pos) = player.interacting { + 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. { + players_auto_release.push(*pid); + } + } + } + } + } + } + for player in players_auto_release.drain(..) { + let _ = self.packet_in( + PacketS::Interact { pos: None, player }, + &mut vec![], + packet_out, + ); + } + + 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.game.end { + self.game.score.time_remaining = (end - now).as_secs_f64(); + if end < now { + 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.game.score.clone()))); + true + } else { + false + } + } else { + false + } + } + + pub fn count_chefs(&self) -> usize { + self.game + .players + .values() + .map(|p| if p.character >= 0 { 1 } else { 0 }) + .sum() + } +} + +pub fn interact_effect( + data: &Gamedata, + edge: bool, + this: &mut Option, + this_loc: ItemLocation, + other: &mut Option, + other_loc: ItemLocation, + this_tile_kind: Option, + packet_out: &mut VecDeque, + score: &mut Score, + score_changed: &mut bool, + automated: bool, +) { + let this_had_item = this.is_some(); + let other_had_item = other.is_some(); + + if let Some(effect) = interact(data, edge, this_tile_kind, this, other, score, automated) { + match effect { + InteractEffect::Put => { + info!("put {this_loc} <- {other_loc}"); + packet_out.push_back(PacketC::MoveItem { + from: other_loc, + to: this_loc, + }) + } + InteractEffect::Take => { + info!("take {this_loc} -> {other_loc}"); + packet_out.push_back(PacketC::MoveItem { + from: this_loc, + to: other_loc, + }) + } + InteractEffect::Produce => { + info!("produce {this_loc} <~ {other_loc}"); + *score_changed = true; + if this_had_item { + packet_out.push_back(PacketC::SetProgress { + item: this_loc, + progress: None, + warn: false, + }); + packet_out.push_back(PacketC::SetItem { + location: this_loc, + item: None, + }); + } + if other_had_item { + packet_out.push_back(PacketC::MoveItem { + from: other_loc, + to: this_loc, + }); + packet_out.push_back(PacketC::SetItem { + location: this_loc, + item: None, + }); + } + if let Some(i) = &other { + packet_out.push_back(PacketC::SetItem { + location: this_loc, + item: Some(i.kind), + }); + packet_out.push_back(PacketC::MoveItem { + from: this_loc, + to: other_loc, + }) + } + if let Some(i) = &this { + packet_out.push_back(PacketC::SetItem { + location: this_loc, + item: Some(i.kind), + }); + } + } + } + } +} 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 . */ -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, tx: Sender, connections: HashMap>, + 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) -- cgit v1.2.3-70-g09d2