/*
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, MessageTimeout, 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((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),
})
}
}
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,
timeout,
player,
} => {
info!("{player:?} message {message:?}");
if let Some(timeout) = timeout {
if let Some(player) = self.game.players.get_mut(&player) {
player.communicate_persist = message.clone().map(|m| {
(
m,
MessageTimeout {
initial: timeout,
remaining: timeout,
},
)
});
}
}
packet_out.push_back(PacketC::Communicate {
player,
message,
timeout: timeout.map(|t| MessageTimeout {
initial: t,
remaining: t,
}),
})
}
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((_, 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.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),
});
}
}
}
}
}