/* 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::{Entities, EntityContext}, 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, pub entities: Entities, pub lobby: bool, pub player_id_counter: PlayerID, pub score_changed: bool, pub packet_loopback: VecDeque, pub last_movement_update: HashMap, } 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, packet_loopback: VecDeque::new(), last_movement_update: HashMap::default(), } } } impl Server<'_> { pub fn load( &mut self, (gamedata, serverdata, entities): (Gamedata, Serverdata, Entities), timer: Option, packet_out: &mut VecDeque, ) { self.game.load(gamedata, &serverdata, timer, packet_out); self.state.data = serverdata.into(); for mut e in self.state.entities.drain(..) { e.destructor(EntityContext { game: self.game, packet_out, packet_in: &mut self.state.packet_loopback, score_changed: &mut self.state.score_changed, dt: 0., }); } self.state.entities = entities; } pub fn join_player( &mut self, id: PlayerID, name: String, character: i32, packet_out: &mut VecDeque, ) { 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, }); } pub fn packet_in( &mut self, packet: PacketS, replies: &mut Vec, packet_out: &mut VecDeque, ) -> Result<()> { match packet { PacketS::Join { name, character, id, } => { let id = id.unwrap_or_else(|| { let id = self.state.player_id_counter; self.state.player_id_counter.0 += 1; id }); self.join_player(id, 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); if let Some(pos) = pos { let last_position_update = self .state .last_movement_update .entry(player) .or_insert_with(|| Instant::now()); let dt = last_position_update.elapsed(); *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::ApplyScore(score) => { self.game.score.demands_completed += score.demands_completed; self.game.score.demands_failed += score.demands_failed; self.game.score.points += score.points; self.state.score_changed = true; } 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, packet_in: &mut self.state.packet_loopback, dt, }) { warn!("entity tick failed: {e}") } } while let Some(p) = self.state.packet_loopback.pop_front() { if let Err(e) = self.packet_in(p, &mut vec![], packet_out) { warn!("internal packet errored: {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), }); } } } } }