/* 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 . */ pub mod movement; mod pathfinding; use crate::{ data::Gamedata, game::Game, protocol::{DemandIndex, ItemIndex, Message, PacketC, PacketS, PlayerID}, state::State, }; use anyhow::{anyhow, Result}; use glam::{IVec2, Vec2}; use log::{debug, error, warn}; 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, walkable: HashSet, chairs: HashMap, items: HashMap, customers: HashMap, customer_id_counter: PlayerID, demand: DemandState, } struct DemandState { data: Gamedata, } enum CustomerState { Entering { path: Path, chair: IVec2, }, Waiting { demand: DemandIndex, chair: IVec2, timeout: f32, }, Eating { demand: DemandIndex, target: IVec2, progress: f32, chair: IVec2, }, Exiting { path: Path, }, } 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 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() { self.customer_id_counter.0 -= 1; let id = self.customer_id_counter; packets_out.push(( id, PacketS::Join { name: "George".to_string(), 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}"))?; self.customers.insert( id, Customer { movement: MovementBase { position: self.demand.data.customer_spawn, facing: Vec2::X, vel: Vec2::ZERO, }, state: CustomerState::Entering { path, chair }, }, ); } let mut customers_to_remove = Vec::new(); for (&id, p) in &mut self.customers { match &mut p.state { CustomerState::Entering { path, chair } => { 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(); packets_out.push(( id, PacketS::Communicate { message: Some(Message::Item(self.demand.data.demand(demand).from)), }, )); p.state = CustomerState::Waiting { chair: *chair, timeout: 60., demand, }; } } CustomerState::Waiting { chair, demand, timeout, } => { debug!("{id:?} waiting"); *timeout -= dt; if *timeout <= 0. { packets_out.push((id, PacketS::Communicate { message: None })); let path = find_path( &self.walkable, p.movement.position.as_ivec2(), self.demand.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_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) { Some(pos) } else { None } }); if let Some(pos) = demand_pos { packets_out.push((id, PacketS::Communicate { message: None })); for edge in [true, false] { packets_out.push((id, PacketS::Interact { pos, edge })) } p.state = CustomerState::Eating { demand: *demand, target: pos, progress: 0., chair: *chair, } } } } CustomerState::Eating { demand, target, progress, chair, } => { debug!("{id:?} eating"); let demand = self.demand.data.demand(*demand); *progress += dt / demand.duration; if *progress >= 1. { packets_out.push(( id, PacketS::ReplaceHand { item: Some(demand.to), }, )); for edge in [true, false] { packets_out.push((id, PacketS::Interact { pos: *target, edge })) } let path = find_path( &self.walkable, p.movement.position.as_ivec2(), self.demand.data.customer_spawn.as_ivec2(), ) .ok_or(anyhow!("no path to exit"))?; *self.chairs.get_mut(&chair).unwrap() = true; p.state = CustomerState::Exiting { path } } } CustomerState::Exiting { path } => { debug!("{id:?} exiting"); packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt))); if path.is_done() { packets_out.push((id, PacketS::Leave)); customers_to_remove.push(id); } } } } for c in customers_to_remove { self.customers.remove(&c).unwrap(); } 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) }