From fd2d5455339a61d8b38bcb47fa40e094ef61856c Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 26 Jun 2024 15:43:47 +0200 Subject: make demand state part of the game state --- server/src/customer/mod.rs | 205 ++++++++++++------------------------- server/src/customer/pathfinding.rs | 22 ++-- server/src/game.rs | 24 ++++- server/src/main.rs | 3 - server/src/protocol.rs | 4 + 5 files changed, 100 insertions(+), 158 deletions(-) (limited to 'server') diff --git a/server/src/customer/mod.rs b/server/src/customer/mod.rs index 7f32b094..e6f999e6 100644 --- a/server/src/customer/mod.rs +++ b/server/src/customer/mod.rs @@ -20,39 +20,27 @@ mod pathfinding; use crate::{ data::Gamedata, - game::Game, - protocol::{DemandIndex, ItemIndex, Message, PacketC, PacketS, PlayerID}, - state::State, + game::Tile, + protocol::{DemandIndex, Message, PacketS, PlayerID}, }; use anyhow::{anyhow, Result}; use fake::{faker, Fake}; use glam::{IVec2, Vec2}; -use log::{debug, error, warn}; +use log::debug; use movement::MovementBase; use pathfinding::{find_path, Path}; use rand::{random, thread_rng}; use std::{ collections::{HashMap, HashSet}, sync::Arc, - time::Duration, -}; -use tokio::{ - sync::{broadcast, RwLock}, - time::interval, }; -struct CustomerManager { - disabled: bool, +pub struct DemandState { + data: Arc, walkable: HashSet, chairs: HashMap, - items: HashMap, - customers: HashMap, customer_id_counter: PlayerID, - demand: DemandState, -} - -struct DemandState { - data: Gamedata, + customers: HashMap, } enum CustomerState { @@ -76,119 +64,41 @@ enum CustomerState { }, } -struct Customer { +pub struct Customer { movement: MovementBase, state: CustomerState, } -pub async fn customer(gstate: Arc>, mut grx: broadcast::Receiver) { - let mut state = CustomerManager { - disabled: true, - customer_id_counter: PlayerID(0), - walkable: Default::default(), - chairs: Default::default(), - items: Default::default(), - customers: Default::default(), - demand: DemandState { - data: Gamedata::default(), - }, - }; - let initial = gstate.write().await.game.prime_client(); - for packet in initial { - state.packet(packet); - } - - let mut interval = interval(Duration::from_millis(40)); - let mut packets_out = Vec::new(); - loop { - tokio::select! { - packet = grx.recv() => { - let packet = match packet { - Ok(p) => p, - Err(e) => { - warn!("{e}"); - continue; - } - }; - match packet { - PacketC::PutItem { .. } - | PacketC::TakeItem { .. } - | PacketC::SetTileItem { .. } => { - let g = gstate.read().await; - update_items(&mut state, &g.game) - }, - _ => () - } - state.packet(packet); - } - _ = interval.tick() => { - if !state.disabled { - if let Err(e) = state.tick(&mut packets_out, 0.04) { - warn!("error caught: {e}") - } - for (player,packet) in packets_out.drain(..) { - if let Err(e) = gstate.write().await.packet_in(player, packet).await { - error!("customer misbehaved: {e}") - } - } - } - } - } - } -} - -// TODO very inefficient, please do that incrementally -fn update_items(state: &mut CustomerManager, game: &Game) { - state.items.clear(); - for (&pos, tile) in game.tiles() { - if let Some(item) = &tile.item { - state.items.insert(pos, item.kind); +impl DemandState { + pub fn new(data: Arc, map: &HashMap) -> Self { + let chair = data.get_tile_by_name("chair"); + Self { + walkable: map + .iter() + .filter(|(_, v)| !data.is_tile_colliding(v.kind)) + .map(|(e, _)| *e) + .collect(), + chairs: map + .iter() + .filter(|(_, v)| Some(v.kind) == chair) + .map(|(e, _)| (*e, true)) + .collect(), + customer_id_counter: PlayerID(0), + customers: Default::default(), + data, } } } impl DemandState { - pub fn target_customer_count(&self) -> usize { - // TODO insert sofa magic formula - 5 - } - pub fn generate_demand(&self) -> DemandIndex { - // TODO insert sofa magic formula - DemandIndex(random::() % self.data.demands.len()) - } -} - -impl CustomerManager { - pub fn packet(&mut self, packet: PacketC) { - match packet { - PacketC::Data { data } => { - self.disabled = data.demands.is_empty(); - self.demand.data = data; - } - PacketC::RemovePlayer { id } => { - self.customers.remove(&id); - } - PacketC::UpdateMap { - tile: pos, kind, .. - } => { - if let Some(kind) = kind { - let tilename = self.demand.data.tile_name(kind); - if !self.demand.data.is_tile_colliding(kind) { - self.walkable.insert(pos); - } - if tilename == "chair" { - self.chairs.insert(pos, true); - } - } else { - self.chairs.remove(&pos); - self.walkable.remove(&pos); - } - } - _ => (), - } - } - pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) -> Result<()> { - if self.customers.len() < self.demand.target_customer_count() { + pub fn tick( + &mut self, + packets_out: &mut Vec<(PlayerID, PacketS)>, + tiles: &mut HashMap, + data: &Gamedata, + dt: f32, + ) -> Result<()> { + if self.customers.len() < 5 { self.customer_id_counter.0 -= 1; let id = self.customer_id_counter; packets_out.push(( @@ -198,14 +108,15 @@ impl CustomerManager { character: -2, }, )); - let chair = select_chair(&mut self.chairs).ok_or(anyhow!("no free chair found"))?; - let to = self.demand.data.customer_spawn.as_ivec2(); - let path = find_path(&self.walkable, to, chair).ok_or(anyhow!("no path to {to}"))?; + let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?; + let from = data.customer_spawn.as_ivec2(); + let path = find_path(&self.walkable, from, chair) + .ok_or(anyhow!("no path from {from} to {chair}"))?; self.customers.insert( id, Customer { movement: MovementBase { - position: self.demand.data.customer_spawn, + position: data.customer_spawn, facing: Vec2::X, vel: Vec2::ZERO, }, @@ -220,11 +131,11 @@ impl CustomerManager { debug!("{id:?} entering"); packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt))); if path.is_done() { - let demand = self.demand.generate_demand(); + let demand = DemandIndex(random::() % self.data.demands.len()); packets_out.push(( id, PacketS::Communicate { - message: Some(Message::Item(self.demand.data.demand(demand).from)), + message: Some(Message::Item(data.demand(demand).from)), }, )); p.state = CustomerState::Waiting { @@ -246,18 +157,27 @@ impl CustomerManager { let path = find_path( &self.walkable, p.movement.position.as_ivec2(), - self.demand.data.customer_spawn.as_ivec2(), + data.customer_spawn.as_ivec2(), ) .expect("no path to exit"); *self.chairs.get_mut(&chair).unwrap() = true; p.state = CustomerState::Exiting { path } } else { - let demand_data = &self.demand.data.demand(*demand); + let demand_data = &data.demand(*demand); let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] .into_iter() .find_map(|off| { let pos = *chair + off; - if self.items.get(&pos) == Some(&demand_data.from) { + if tiles + .get(&pos) + .map(|t| { + t.item + .as_ref() + .map(|i| i.kind == demand_data.from) + .unwrap_or_default() + }) + .unwrap_or_default() + { Some(pos) } else { None @@ -284,7 +204,7 @@ impl CustomerManager { chair, } => { debug!("{id:?} eating"); - let demand = self.demand.data.demand(*demand); + let demand = data.demand(*demand); *progress += dt / demand.duration; if *progress >= 1. { packets_out.push(( @@ -299,7 +219,7 @@ impl CustomerManager { let path = find_path( &self.walkable, p.movement.position.as_ivec2(), - self.demand.data.customer_spawn.as_ivec2(), + data.customer_spawn.as_ivec2(), ) .ok_or(anyhow!("no path to exit"))?; *self.chairs.get_mut(&chair).unwrap() = true; @@ -321,14 +241,15 @@ impl CustomerManager { } Ok(()) } -} -pub fn select_chair(chairs: &mut HashMap) -> Option { - use rand::seq::IteratorRandom; - let (chosen, free) = chairs - .iter_mut() - .filter(|(_p, free)| **free) - .choose(&mut thread_rng())?; - *free = false; - Some(*chosen) + pub fn select_chair(&mut self) -> Option { + use rand::seq::IteratorRandom; + let (chosen, free) = self + .chairs + .iter_mut() + .filter(|(_p, free)| **free) + .choose(&mut thread_rng())?; + *free = false; + Some(*chosen) + } } diff --git a/server/src/customer/pathfinding.rs b/server/src/customer/pathfinding.rs index 5354407f..5056975e 100644 --- a/server/src/customer/pathfinding.rs +++ b/server/src/customer/pathfinding.rs @@ -1,19 +1,19 @@ /* Undercooked - a game about cooking Copyright 2024 metamuffin - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - + */ use super::movement::MovementBase; use crate::protocol::PacketS; @@ -29,22 +29,22 @@ pub struct Path(Vec); impl Path { pub fn execute_tick( &mut self, - customer: &mut MovementBase, + player: &mut MovementBase, walkable: &HashSet, dt: f32, ) -> PacketS { if let Some(next) = self.0.last().copied() { debug!("next {next}"); - if next.distance(customer.position) < if self.0.len() == 1 { 0.1 } else { 0.6 } { + if next.distance(player.position) < if self.0.len() == 1 { 0.1 } else { 0.6 } { self.0.pop(); } - customer.update( + player.update( &walkable, - (next - customer.position).normalize_or_zero() * 0.5, + (next - player.position).normalize_or_zero() * 0.5, dt, ) } else { - customer.update(&walkable, Vec2::ZERO, dt) + player.update(&walkable, Vec2::ZERO, dt) } } pub fn is_done(&self) -> bool { @@ -52,7 +52,7 @@ impl Path { } } -pub fn find_path(map: &HashSet, from: IVec2, to: IVec2) -> Option { +pub fn find_path(walkable: &HashSet, from: IVec2, to: IVec2) -> Option { #[derive(Debug, PartialEq, Eq)] struct Open(i32, IVec2, IVec2); impl PartialOrd for Open { @@ -84,7 +84,7 @@ pub fn find_path(map: &HashSet, from: IVec2, to: IVec2) -> Option { } for d in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { let n = p + d; - if map.contains(&n) { + if walkable.contains(&n) { open.push(Open(-d.distance_squared(to), n, p)); } } diff --git a/server/src/game.rs b/server/src/game.rs index defaf7bf..595816e1 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -16,14 +16,14 @@ */ use crate::{ - customer::movement::PLAYER_SPEED_LIMIT, + customer::{movement::PLAYER_SPEED_LIMIT, DemandState}, data::Gamedata, interaction::{interact, tick_tile, InteractEffect, TickEffect}, protocol::{ItemIndex, Message, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex}, }; use anyhow::{anyhow, bail, Result}; use glam::{IVec2, Vec2}; -use log::info; +use log::{info, warn}; use std::{ collections::{HashMap, VecDeque}, ops::Deref, @@ -64,6 +64,7 @@ pub struct Game { tiles: HashMap, players: HashMap, packet_out: VecDeque, + demand: Option, } impl Game { @@ -73,6 +74,7 @@ impl Game { packet_out: Default::default(), players: Default::default(), tiles: Default::default(), + demand: None, } } @@ -87,6 +89,7 @@ impl Game { neighbors: [None, None, None, None], }) } + self.demand = None; } pub fn load(&mut self, gamedata: Gamedata) { let players = self @@ -99,6 +102,7 @@ impl Game { self.unload(); self.data = gamedata.into(); + for (&p, (tile, item)) in &self.data.initial_map { self.tiles.insert( p, @@ -126,6 +130,10 @@ impl Game { ); } + if !self.data.demands.is_empty() { + self.demand = Some(DemandState::new(self.data.clone(), &self.tiles)) + } + self.packet_out.extend(self.prime_client()); } @@ -375,6 +383,18 @@ impl Game { } pub fn tick(&mut self, dt: f32) { + if let Some(demand) = &mut self.demand { + let mut packet_out = Vec::new(); + if let Err(err) = demand.tick(&mut packet_out, &mut self.tiles, &self.data, dt) { + warn!("demand tick {err}"); + } + for (player, packet) in packet_out { + if let Err(err) = self.packet_in(player, packet) { + warn!("demand packet {err}"); + } + } + } + for (&pos, tile) in &mut self.tiles { if let Some(effect) = tick_tile(dt, &self.data, tile) { match effect { diff --git a/server/src/main.rs b/server/src/main.rs index b3e6334d..c294478e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -27,7 +27,6 @@ use tokio::{ }; use tokio_tungstenite::tungstenite::Message; use undercooked::{ - customer::customer, protocol::{PacketC, PacketS, PlayerID}, state::State, }; @@ -53,8 +52,6 @@ async fn main() -> Result<()> { }); } - spawn(customer(state.clone(), rx.resubscribe())); - for id in (1..).map(PlayerID) { let (sock, addr) = ws_listener.accept().await?; let Ok(sock) = tokio_tungstenite::accept_async(sock).await else { diff --git a/server/src/protocol.rs b/server/src/protocol.rs index 262fad8d..daa5c8af 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -130,6 +130,10 @@ pub enum PacketC { player: PlayerID, message: Option, }, + Score { + demands_failed: usize, + demands_complete: usize, + }, Error { message: String, }, -- cgit v1.2.3-70-g09d2