/*
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::{index::GamedataIndex, DataIndex, Serverdata},
entity::{Entities, EntityContext},
interaction::{interact, tick_slot},
message::TrError,
scoreboard::ScoreboardStore,
tre, ConnectionID,
};
use anyhow::{Context, Result};
use hurrycurry_client_lib::{Game, Involvement, Item, Player, Tile};
use hurrycurry_protocol::{
glam::{IVec2, Vec2},
movement::MovementBase,
Gamedata, 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;
pub struct Server {
pub tx: Sender,
pub connections: HashMap>,
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 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 join_player(
&mut self,
id: PlayerID,
name: String,
character: i32,
class: PlayerClass,
serverdata: &Serverdata,
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.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, class)) in players {
self.join_player(id, name, character, class, serverdata, 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(),
});
if let Some(item) = &player.item {
out.push(PacketC::SetItem {
location: ItemLocation::Player(id),
item: Some(item.kind),
});
if let Some(Involvement {
player,
position,
speed,
warn,
..
}) = item.active
{
out.push(PacketC::SetProgress {
player,
item: ItemLocation::Player(id),
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: i32,
class: PlayerClass,
serverdata: &Serverdata,
packet_out: Option<&mut VecDeque>,
) {
let position = match class {
PlayerClass::Customer => serverdata.customer_spawn,
PlayerClass::Bot | PlayerClass::Chef => serverdata.chef_spawn,
} + (Vec2::new(random(), random()) - 0.5);
self.players.insert(
id,
Player {
item: None,
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.is_tile_colliding(kind) {
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 async fn new(tx: Sender) -> Result {
Ok(Self {
game: Game::default(),
index: DataIndex::load()
.await
.context("Failed to load data index")?,
tx,
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()
.await
.context("Failed to load scoreboards")?,
})
}
}
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.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;
}
pub fn packet_in(
&mut self,
packet: PacketS,
replies: &mut Vec,
) -> Result<(), TrError> {
match packet {
PacketS::Join {
name,
character,
id,
class,
} => {
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,
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);
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() {
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 } => {
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, 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) } 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 {
if pid == base_pid {
return Err(tre!("s.error.self_interact"));
}
let [Some(other), Some(this)] =
self.game.players.get_many_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"));
}
interact(
&self.game.data,
edge,
None,
Some(pid),
&mut this.item,
ItemLocation::Player(base_pid),
&mut other.item,
ItemLocation::Player(pid),
&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"))?;
interact(
&self.game.data,
edge,
Some(tile.kind),
Some(pid),
&mut tile.item,
ItemLocation::Tile(pos),
&mut player.item,
ItemLocation::Player(pid),
&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 } => {
let pdata = self.game.players.get_mut(&player).ok_or(tre!(""))?;
pdata.item = item.map(|i| Item {
kind: i,
active: None,
});
self.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.score_changed = true;
}
PacketS::ReplayTick { .. } => return Err(tre!("s.error.packet_not_supported")),
}
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_many_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,
});
tick_slot(
dt,
&self.game.data,
&self.gamedata_index,
None,
&mut player.item,
ItemLocation::Player(pid),
&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) = 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);
}
}
}
}
}
}
for player in players_auto_release.drain(..) {
let _ = self.packet_in(PacketS::Interact { pos: None, player }, &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()
}
}