/*
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::{KEEPALIVE_INTERVAL, Menu, Message, PacketC, PacketS, PlayerID, VERSION};
use log::{debug, info, trace, warn};
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 keepalive_kick = Vec::new();
let mut inactivity_kick = Vec::new();
for (cid, conn) in &mut self.connections {
conn.keepalive_timer += dt;
if conn.keepalive_timer > KEEPALIVE_INTERVAL + 10. {
keepalive_kick.push(*cid);
}
for pid in &conn.players {
if let Some(t) = self.player_inactivity_timers.get_mut(pid) {
*t -= dt;
if *t < 0. {
inactivity_kick.push((*cid, *pid));
}
}
}
}
for cid in keepalive_kick {
info!("{cid} Client did not send keepalive in time");
self.disconnect(cid, Some(trm!("s.disconnect_reason.keepalive_timer")));
}
for (cid, pid) in inactivity_kick {
info!("{cid} Removing {pid} because of inactivity");
if let Some(conn) = self.connections.get(&cid) {
let _ = conn.replies.try_send(PacketC::ServerMessage {
message: trm!("s.leave_inactivity"),
error: false,
});
}
if let Err(e) = self.packet_in_outer(cid, PacketS::Leave { player: pid }) {
warn!("error when auto-leaving player: {e}")
}
}
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(),
keepalive_timer: 0.,
replies: replies_tx,
},
);
self.update_paused();
(init, broadcast_rx, replies_rx)
}
pub fn disconnect(&mut self, conn: ConnectionID, reason: Option) {
if let Some(cd) = self.connections.get(&conn) {
if let Some(reason) = reason {
let _ = cd.replies.try_send(PacketC::Disconnect { reason });
}
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> {
let Some(conn_data) = self.connections.get_mut(&conn) else {
return Ok(vec![]);
};
if let Some(p) = get_packet_player(&packet)
&& !conn_data.players.contains(&p)
{
return Ok(vec![]);
}
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 => {
conn_data.ready = true;
self.update_paused();
}
PacketS::Idle { paused } => {
conn_data.idle = *paused;
self.update_paused();
}
PacketS::Leave { player } => {
self.last_movement_update.remove(player);
self.player_inactivity_timers.remove(player);
self.connections
.get_mut(&conn)
.unwrap()
.players
.remove(player);
}
PacketS::Join { .. } => {
if conn_data.players.len() > 8 {
return Err(tre!("s.error.conn_too_many_players"));
}
}
PacketS::Keepalive => {
conn_data.keepalive_timer = 0.;
}
PacketS::Movement {
player, dir, boost, ..
} if *boost || dir.length() > 0.5 => {
self.player_inactivity_timers
.insert(*player, self.config.inactivity_timeout);
}
PacketS::Interact { player, .. } | PacketS::Communicate { player, .. } => {
self.player_inactivity_timers
.insert(*player, self.config.inactivity_timeout);
}
_ => (),
}
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);
self.player_inactivity_timers
.insert(*id, self.config.inactivity_timeout);
}
}
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(_)
| PacketS::Keepalive => None,
}
}