summaryrefslogtreecommitdiff
path: root/server/src/server.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/server.rs')
-rw-r--r--server/src/server.rs706
1 files changed, 706 insertions, 0 deletions
diff --git a/server/src/server.rs b/server/src/server.rs
new file mode 100644
index 00000000..f4ccbf35
--- /dev/null
+++ b/server/src/server.rs
@@ -0,0 +1,706 @@
+/*
+ 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 <https://www.gnu.org/licenses/>.
+
+*/
+use crate::{
+ data::Serverdata,
+ entity::{Entity, EntityContext, EntityT},
+ 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, 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<Serverdata>,
+ entities: Vec<Entity>,
+ pub lobby: bool,
+ pub player_id_counter: PlayerID,
+ pub score_changed: bool,
+}
+
+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<PacketC>);
+ fn load(
+ &mut self,
+ gamedata: Gamedata,
+ serverdata: &Serverdata,
+ timer: Option<Duration>,
+ packet_out: &mut VecDeque<PacketC>,
+ );
+ fn prime_client(&self) -> Vec<PacketC>;
+}
+impl GameServerExt for Game {
+ fn unload(&mut self, packet_out: &mut VecDeque<PacketC>) {
+ 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<Duration>,
+ packet_out: &mut VecDeque<PacketC>,
+ ) {
+ let players = self
+ .players
+ .iter()
+ .filter(|(_, p)| p.character >= 0)
+ .map(|(id, p)| (*id, (p.name.to_owned(), p.character)))
+ .collect::<HashMap<_, _>>();
+
+ 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<PacketC> {
+ 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(c) = &player.communicate_persist {
+ out.push(PacketC::Communicate {
+ player: id,
+ message: Some(c.to_owned()),
+ persist: true,
+ })
+ }
+ }
+ 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,
+ }
+ }
+}
+impl Server<'_> {
+ pub fn load(
+ &mut self,
+ (gamedata, serverdata): (Gamedata, Serverdata),
+ timer: Option<Duration>,
+ packet_out: &mut VecDeque<PacketC>,
+ ) {
+ self.game.load(gamedata, &serverdata, timer, packet_out);
+ self.state.data = serverdata.into();
+ }
+
+ pub fn join_player(
+ &mut self,
+ name: String,
+ character: i32,
+ packet_out: &mut VecDeque<PacketC>,
+ ) -> PlayerID {
+ let id = self.state.player_id_counter;
+ self.state.player_id_counter.0 += 1;
+ 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,
+ });
+ id
+ }
+
+ pub fn packet_in(
+ &mut self,
+ packet: PacketS,
+ replies: &mut Vec<PacketC>,
+ packet_out: &mut VecDeque<PacketC>,
+ ) -> Result<()> {
+ match packet {
+ PacketS::Join { name, character } => {
+ let id = self.join_player(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);
+
+ let _ = pos; // TODO
+
+ // if let Some(pos) = pos {
+ // let dt = pd.last_position_update.elapsed();
+ // pd.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,
+ persist,
+ player,
+ } => {
+ info!("{player:?} message {message:?}");
+ if persist {
+ if let Some(player) = self.game.players.get_mut(&player) {
+ player.communicate_persist = message.clone()
+ }
+ }
+ packet_out.push_back(PacketC::Communicate {
+ player,
+ message,
+ persist,
+ })
+ }
+ 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::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<PacketC>) -> 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(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,
+ dt,
+ }) {
+ warn!("entity tick failed: {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<Item>,
+ this_loc: ItemLocation,
+ other: &mut Option<Item>,
+ other_loc: ItemLocation,
+ this_tile_kind: Option<TileIndex>,
+ packet_out: &mut VecDeque<PacketC>,
+ 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),
+ });
+ }
+ }
+ }
+ }
+}