/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors 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::{DataIndex, Serverdata}, entity::{Entities, EntityContext}, interaction::{interact, tick_slot}, message::TrError, scoreboard::ScoreboardStore, tre, ConnectionID, }; use anyhow::{Context, Result}; use hurrycurry_client_lib::{gamedata_index::GamedataIndex, Game, Involvement, Item, Player, Tile}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, movement::MovementBase, Character, Gamedata, Hand, ItemLocation, Menu, MessageTimeout, PacketC, PacketS, PlayerClass, PlayerID, Score, TileIndex, }; use log::{info, warn}; use rand::random; use std::{ collections::{HashMap, HashSet, VecDeque}, sync::Arc, time::{Duration, Instant}, }; use tokio::sync::broadcast::Sender; #[derive(Debug, Default)] pub struct ConnectionData { pub players: HashSet, pub idle: bool, pub ready: bool, } pub enum AnnounceState { Queued, Running(f32), Done, } pub struct Server { pub tx: Sender, pub connections: HashMap, pub paused: bool, pub announce_state: AnnounceState, pub game: Game, pub data: Arc, pub entities: Entities, pub player_id_counter: PlayerID, pub score_changed: bool, pub packet_loopback: VecDeque, pub last_movement_update: HashMap, pub index: DataIndex, pub packet_out: VecDeque, pub scoreboard: ScoreboardStore, pub gamedata_index: GamedataIndex, pub editor_address: Option, } 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, ); #[allow(clippy::too_many_arguments)] fn join_player( &mut self, id: PlayerID, name: String, character: Character, class: PlayerClass, serverdata: &Serverdata, custom_position: Option, packet_out: Option<&mut VecDeque>, ); fn prime_client(&self) -> Vec; fn set_tile(&mut self, p: IVec2, t: Option, packet_out: &mut VecDeque); } 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, ) { // TODO cleanup let players = self .players .iter() .filter(|(_, p)| p.class == PlayerClass::Chef) .map(|(id, p)| (*id, (p.name.to_owned(), p.character, p.class))) .collect::>(); self.unload(packet_out); self.lobby = gamedata.current_map == "lobby"; self.data = gamedata.into(); self.data_index.update(&self.data); 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_index.tile_collide[tile.0] { self.walkable.insert(p); } } for (id, (name, character, class)) in players { self.join_player(id, name, character, class, serverdata, None, None); } 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, class: player.class, position: player.movement.position, character: player.character, name: player.name.clone(), }); for (i, item) in player.items.iter().enumerate() { if let Some(item) = &item { out.push(PacketC::SetItem { location: ItemLocation::Player(id, Hand(i)), item: Some(item.kind), }); if let Some(Involvement { player, position, speed, warn, .. }) = item.active { out.push(PacketC::SetProgress { player, item: ItemLocation::Player(id, Hand(i)), position, speed, warn, }); } } } if let Some((message, timeout)) = &player.communicate_persist { out.push(PacketC::Communicate { player: id, message: Some(message.to_owned()), timeout: Some(*timeout), }) } } 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), }); if let Some(Involvement { player, position, speed, warn, .. }) = item.active { out.push(PacketC::SetProgress { player, item: ItemLocation::Tile(tile), position, speed, warn, }); } } } out.push(PacketC::FlushMap); out.push(PacketC::Score(self.score.clone())); out.push(PacketC::SetIngame { state: true, lobby: self.lobby, }); out } fn join_player( &mut self, id: PlayerID, name: String, character: Character, class: PlayerClass, serverdata: &Serverdata, custom_position: Option, packet_out: Option<&mut VecDeque>, ) { let position = custom_position.unwrap_or(match class { PlayerClass::Customer => serverdata.customer_spawn, PlayerClass::Bot | PlayerClass::Chef => serverdata.chef_spawn, PlayerClass::Tram => Vec2::ZERO, // should always have custom location }) + (Vec2::new(random(), random()) - 0.5); self.players.insert( id, Player { items: (0..self.data.hand_count).map(|_| None).collect(), character, class, movement: MovementBase::new(position), communicate_persist: None, interacting: None, name: name.clone(), }, ); self.score.players = self.score.players.max(self.players.len()); if let Some(packet_out) = packet_out { packet_out.push_back(PacketC::AddPlayer { id, name, class, position, character, }); } } /// Dont forget to flush fn set_tile( &mut self, tile: IVec2, kind: Option, packet_out: &mut VecDeque, ) { if let Some(kind) = kind { self.tiles.insert(tile, Tile::from(kind)); if self.data_index.tile_collide[kind.0] { self.walkable.remove(&tile); } else { self.walkable.insert(tile); } } else { self.tiles.remove(&tile); self.walkable.remove(&tile); } packet_out.push_back(PacketC::UpdateMap { tile, kind, 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), ], }); } } impl Server { pub fn new(tx: Sender) -> Result { Ok(Self { game: Game::default(), index: DataIndex::load().context("Failed to load data index")?, tx, announce_state: AnnounceState::Done, packet_out: VecDeque::new(), connections: HashMap::new(), data: Serverdata::default().into(), gamedata_index: GamedataIndex::default(), entities: Vec::new(), player_id_counter: PlayerID(1), score_changed: false, packet_loopback: VecDeque::new(), last_movement_update: HashMap::default(), scoreboard: ScoreboardStore::load().context("Failed to load scoreboards")?, editor_address: None, paused: false, }) } } impl Server { pub fn load( &mut self, (gamedata, serverdata, entities): (Gamedata, Serverdata, Entities), timer: Option, ) { for mut e in self.entities.drain(..) { e.destructor(EntityContext { game: &mut self.game, packet_out: &mut self.packet_out, packet_in: &mut self.packet_loopback, score_changed: &mut self.score_changed, scoreboard: &self.scoreboard, serverdata: self.data.as_ref(), replies: None, dt: 0., load_map: &mut None, }); } self.tick(0.); self.game.load( gamedata, &serverdata, timer.or(serverdata.default_timer), &mut self.packet_out, ); self.gamedata_index.update(&self.game.data); self.data = serverdata.into(); self.entities = entities; for e in &mut self.entities { e.constructor(EntityContext { game: &mut self.game, packet_out: &mut self.packet_out, packet_in: &mut self.packet_loopback, score_changed: &mut self.score_changed, load_map: &mut None, serverdata: &self.data, scoreboard: &self.scoreboard, replies: None, dt: 0., }); } self.connections.values_mut().for_each(|c| c.ready = false); self.announce_state = if self.game.lobby { AnnounceState::Done } else { AnnounceState::Queued }; self.update_paused(); } pub fn packet_in( &mut self, packet: PacketS, replies: &mut Vec, ) -> Result<(), TrError> { match packet { PacketS::Join { name, character, id, class, position, } => { if name.chars().count() > 32 || name.len() > 64 { return Err(tre!( "s.error.username_length_limit", s = "32".to_string(), s = "64".to_string() )); } if self.game.players.len() > 64 { return Err(tre!("s.error.too_many_players")); } let id = id.unwrap_or_else(|| { let id = self.player_id_counter; self.player_id_counter.0 += 1; id }); self.game.join_player( id, name, character, class, &self.data, position, Some(&mut self.packet_out), ); replies.push(PacketC::Joined { id }) } PacketS::Leave { player } => { let p = self .game .players .remove(&player) .ok_or(tre!("s.error.no_player"))?; self.game.players_spatial_index.remove_entry(player); // TODO if holding two, one is destroyed for item in p.items.into_iter().flatten() { let pos = p.movement.position.floor().as_ivec2(); if let Some(tile) = self.game.tiles.get_mut(&pos) { if tile.item.is_none() { self.packet_out.push_back(PacketC::SetItem { location: ItemLocation::Tile(pos), item: Some(item.kind), }); tile.item = Some(item); } } } self.packet_out .push_back(PacketC::RemovePlayer { id: player }) } PacketS::Effect { player, name } => { self.packet_out.push_back(PacketC::Effect { name, player }); } PacketS::Movement { pos, boost, dir, player, } => { let pd = self .game .players .get_mut(&player) .ok_or(tre!("s.error.no_player"))?; pd.movement.input(dir, boost); if let Some(pos) = pos { let last_position_update = self .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, hand } => { for e in &mut self.entities { if e.interact( EntityContext { game: &mut self.game, packet_out: &mut self.packet_out, packet_in: &mut self.packet_loopback, score_changed: &mut self.score_changed, load_map: &mut None, serverdata: &self.data, scoreboard: &self.scoreboard, replies: Some(replies), dt: 0., }, pos, player, )? { return Ok(()); } } let pid = player; let player = self .game .players .get_mut(&pid) .ok_or(tre!("s.error.no_player"))?; let pos = pos.map(|p| (p, hand)); let ((pos, hand), 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(_)) => return Err(tre!("s.error.already_interacting")), }; let entpos = pos.as_vec2() + Vec2::splat(0.5); if edge && entpos.distance(player.movement.position) > 2. { return Err(tre!("s.error.interacting_too_far")); } let tile = self .game .tiles .get_mut(&pos) .ok_or(tre!("s.error.no_tile"))?; // No going back from here on player.interacting = if edge { Some((pos, hand)) } else { None }; // Dont try interacting with player it tile is interactable let other_pid = if !self .game .data .tile_placeable_items .get(&tile.kind) .map_or(false, |p| !p.is_empty()) // TODO check for hand item { 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 { if pid == base_pid { return Err(tre!("s.error.self_interact")); } let [Some(other), Some(this)] = self.game.players.get_disjoint_mut([&pid, &base_pid]) else { return Err(tre!("s.error.self_interact")); }; if this.class == PlayerClass::Customer || other.class == PlayerClass::Customer { return Err(tre!("s.error.customer_interact")); } let this_hslot = this.items.get_mut(hand.0).ok_or(tre!("s.error.no_hand"))?; let other_hslot = other.items.get_mut(hand.0).ok_or(tre!("s.error.no_hand"))?; interact( &self.game.data, edge, None, Some(pid), this_hslot, ItemLocation::Player(base_pid, hand), other_hslot, ItemLocation::Player(pid, hand), &mut self.game.score, &mut self.score_changed, false, &mut self.packet_out, ) } else { let player = self .game .players .get_mut(&pid) .ok_or(tre!("s.error.no_player"))?; let hslot = player .items .get_mut(hand.0) .ok_or(tre!("s.error.no_hand"))?; interact( &self.game.data, edge, Some(tile.kind), Some(pid), &mut tile.item, ItemLocation::Tile(pos), hslot, ItemLocation::Player(pid, hand), &mut self.game.score, &mut self.score_changed, false, &mut self.packet_out, ) } } PacketS::Communicate { message, timeout, player, pin, } => { info!("{player:?} message {message:?}"); let pin = pin.unwrap_or(false); let timeout = if let Some(timeout) = timeout { if let Some(player) = self.game.players.get_mut(&player) { let mut timeout = MessageTimeout { initial: timeout, remaining: timeout, pinned: pin, }; if let Some((_, t)) = &player.communicate_persist { timeout.initial = t.initial; }; player.communicate_persist = message.clone().map(|m| (m, timeout)); Some(timeout) } else { None } } else { None }; self.packet_out.push_back(PacketC::Communicate { player, message, timeout, }); } PacketS::ReplaceHand { item, player, hand } => { let pdata = self.game.players.get_mut(&player).ok_or(tre!(""))?; if let Some(slot) = pdata.items.get_mut(hand.0) { *slot = item.map(|i| Item { kind: i, active: None, }); } self.packet_out.push_back(PacketC::SetItem { location: ItemLocation::Player(player, hand), 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.score_changed = true; } PacketS::ReplayTick { .. } => return Err(tre!("s.error.packet_not_supported")), PacketS::Idle { .. } | PacketS::Ready => (), } Ok(()) } /// Returns true if the game should end pub fn tick(&mut self, dt: f32) -> Option<(String, Option)> { if self.score_changed { self.score_changed = false; self.packet_out .push_back(PacketC::Score(self.game.score.clone())); } for (&pos, tile) in &mut self.game.tiles { tick_slot( dt, &self.game.data, &self.gamedata_index, Some(tile.kind), &mut tile.item, ItemLocation::Tile(pos), &mut self.game.score, &mut self.score_changed, &mut self.packet_out, ); } 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 p1 != p2 { if let [Some(a), Some(b)] = self.game.players.get_disjoint_mut([&p1, &p2]) { a.movement.collide(&mut b.movement, dt) } } }) }); for (&pid, player) in &mut self.game.players { self.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, }); for (i, item) in player.items.iter_mut().enumerate() { tick_slot( dt, &self.game.data, &self.gamedata_index, None, item, ItemLocation::Player(pid, Hand(i)), &mut self.game.score, &mut self.score_changed, &mut self.packet_out, ); } } let mut players_auto_release = Vec::new(); for (pid, player) in &mut self.game.players { if let Some((_, timeout)) = &mut player.communicate_persist { timeout.remaining -= dt; if timeout.remaining < 0. { player.communicate_persist = None; } } if let Some((pos, hand)) = 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.position >= 1. { players_auto_release.push((*pid, hand)); } } } } } } for (player, hand) in players_auto_release.drain(..) { let _ = self.packet_in( PacketS::Interact { pos: None, player, hand, }, &mut vec![], ); } let mut load_map = None; for entity in self.entities.iter_mut() { if let Err(e) = entity.tick(EntityContext { game: &mut self.game, load_map: &mut load_map, packet_out: &mut self.packet_out, score_changed: &mut self.score_changed, packet_in: &mut self.packet_loopback, scoreboard: &self.scoreboard, serverdata: &self.data, replies: None, dt, }) { warn!("Entity tick failed: {e}") } } self.entities.retain_mut(|e| { if e.finished() { e.destructor(EntityContext { game: &mut self.game, load_map: &mut load_map, packet_out: &mut self.packet_out, score_changed: &mut self.score_changed, packet_in: &mut self.packet_loopback, scoreboard: &self.scoreboard, serverdata: &self.data, replies: None, dt: 0., }); false } else { true } }); if let Some(map) = load_map { return Some((map, None)); } while let Some(p) = self.packet_loopback.pop_front() { if let Err(e) = self.packet_in(p, &mut vec![]) { 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.data.score_baseline.max(1); self.game.score.stars = match relative_score { 100.. => 3, 70.. => 2, 40.. => 1, _ => 0, }; self.packet_out .push_back(PacketC::Menu(Menu::Score(self.game.score.clone()))); self.scoreboard.insert( &self.game.data.current_map, self.game .players .values() .filter(|m| m.class == PlayerClass::Chef) .map(|m| m.name.clone()) .collect(), self.game.score.clone(), ); Some(("lobby".to_string(), None)) } else { None } } else { None } } pub fn count_chefs(&self) -> usize { self.game .players .values() .map(|p| if p.class == PlayerClass::Chef { 1 } else { 0 }) .sum() } }