/* 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::{ ConnectionID, server::{AnnounceState, ConnectionData, GameServerExt, Server}, }; use anyhow::Result; use hurrycurry_locale::{TrError, tre, trm}; use hurrycurry_protocol::{Menu, Message, PacketC, PacketS, PlayerID, VERSION}; use log::{debug, info, trace}; use std::{ collections::HashSet, time::{Duration, Instant}, }; use tokio::sync::{broadcast, mpsc}; impl Server { pub fn tick_outer(&mut self, dt: f32) -> anyhow::Result<()> { let mut idle_kick = Vec::new(); for (cid, conn) in &mut self.connections { conn.last_player_input += dt; if conn.last_player_input > self.config.inactivity_kick_timeout { idle_kick.push(*cid); } } for cid in idle_kick { self.disconnect(cid); } if !self.paused { let start = Instant::now(); let r = self.tick(dt); self.tick_perf.0 += start.elapsed(); self.tick_perf.1 += 1; if self.tick_perf.1 >= 500 { debug!("tick perf {:?}", self.tick_perf.0 / 500); self.tick_perf = (Duration::ZERO, 0); } if let Some((name, timer)) = r { self.scoreboard.save()?; self.load(self.index.generate_with_book(&name)?, timer); } } match &mut self.announce_state { AnnounceState::Queued if !self.paused => { self.announce_state = AnnounceState::Running(3.5); self.packet_out .push_back(PacketC::Menu(Menu::AnnounceStart)); self.update_paused(); } AnnounceState::Running(timer) => { *timer -= dt; if *timer <= 0. { self.announce_state = AnnounceState::Done; self.update_paused(); } } _ => (), } self.packet_out.extend(self.game.events.drain(..)); while let Some(p) = self.packet_out.pop_front() { if matches!(p, PacketC::UpdateMap { .. } | PacketC::Movement { .. }) { trace!("-> {p:?}"); } else { debug!("-> {p:?}"); } let _ = self.broadcast.send(p); } Ok(()) } pub async fn connect( &mut self, id: ConnectionID, ) -> ( Vec, broadcast::Receiver, mpsc::Receiver, ) { let mut init = self.game.prime_client(); let (replies_tx, replies_rx) = mpsc::channel(1024); let broadcast_rx = self.broadcast.subscribe(); init.insert( 0, PacketC::Version { major: VERSION.0, minor: VERSION.1, supports_bincode: true, }, ); self.connections.insert( id, ConnectionData { idle: false, ready: false, players: HashSet::new(), last_player_input: 0., replies: replies_tx, }, ); self.update_paused(); (init, broadcast_rx, replies_rx) } pub fn disconnect(&mut self, conn: ConnectionID) { if let Some(cd) = self.connections.get(&conn) { for player in cd.players.clone() { let _ = self.packet_in_outer(conn, PacketS::Leave { player }); } self.connections.remove(&conn); self.update_paused(); } } pub fn packet_in_outer( &mut self, conn: ConnectionID, packet: PacketS, ) -> Result, TrError> { if let Some(p) = get_packet_player(&packet) && !self.connections.get(&conn).unwrap().players.contains(&p) { return Err(tre!("s.error.packet_sender_invalid")); } let mut replies = Vec::new(); match &packet { PacketS::Communicate { message: Some(Message::Text(text)), timeout: None, player, .. } if let Some(command) = text.strip_prefix("/") => { match self.handle_command_parse(*player, command) { Ok(packets) => return Ok(packets), Err(e) => { return Ok(vec![PacketC::ServerMessage { message: e.into(), error: true, }]); } } } PacketS::Ready => { self.connections.get_mut(&conn).unwrap().ready = true; self.update_paused(); } PacketS::Idle { paused } => { self.connections.get_mut(&conn).unwrap().idle = *paused; self.update_paused(); } PacketS::Leave { player } => { self.connections .get_mut(&conn) .unwrap() .players .remove(player); } PacketS::Join { .. } => { if self.connections.get_mut(&conn).unwrap().players.len() > 8 { return Err(tre!("s.error.conn_too_many_players")); } } PacketS::Interact { .. } | PacketS::Communicate { .. } | PacketS::Movement { boost: true, .. } => { self.connections.get_mut(&conn).unwrap().last_player_input = 0.; } _ => (), } self.packet_in(Some(conn), packet, &mut replies)?; for p in &replies { if let PacketC::Joined { id } = p { self.connections.get_mut(&conn).unwrap().players.insert(*id); } } if self.count_chefs() == 0 && !self.game.lobby { self.broadcast .send(PacketC::ServerMessage { message: trm!("s.state.abort_no_players"), error: false, }) .ok(); self.load( self.index .generate_with_book(&self.config.lobby) .map_err(|m| tre!("s.error.map_load", s = format!("{m}")))?, None, ); } Ok(replies) } pub fn update_paused(&mut self) { let all_idle = self .connections .values() .all(|c| c.idle && !c.players.is_empty()); let mut not_ready = self .connections .values() .filter(|c| !c.ready && !c.players.is_empty()) .count(); let announcing = matches!(self.announce_state, AnnounceState::Running(_)); if self.game.lobby { not_ready = 0; // not waiting for players in lobby } let should_pause = all_idle || not_ready > 0 || announcing; let reason = if not_ready > 0 { info!("Game paused: {not_ready} player are not ready"); Some(trm!( "s.state.paused.any_not_ready", s = not_ready.to_string() )) } else if all_idle { if self.connections.is_empty() { info!("Game paused: Server empty"); } else { info!("Game paused: All players idle"); } Some(trm!("s.state.paused.all_idle")) } else if announcing { info!("Game paused: Waiting for announcement"); None } else { if self.paused { info!("Game unpaused"); } None }; self.paused = should_pause; for p in self.game.players.keys() { self.packet_out .push_back(PacketC::Pause { state: self.paused }); self.packet_out.push_back(PacketC::ServerHint { position: None, message: reason.clone(), player: *p, }); } } } fn get_packet_player(packet: &PacketS) -> Option { match packet { PacketS::Leave { player } | PacketS::Movement { player, .. } | PacketS::Interact { player, .. } | PacketS::Communicate { player, .. } | PacketS::ReplaceHand { player, .. } | PacketS::Effect { player, .. } => Some(*player), PacketS::Join { .. } | PacketS::Idle { .. } | PacketS::Ready | PacketS::ApplyScore(_) | PacketS::ReplayTick { .. } | PacketS::Debug(_) => None, } }