From 16ff78180669411326d42ea32d4a9260c018236c Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 13 Aug 2024 12:48:31 +0200 Subject: refactor server to use client-lib data model (breaks customers) --- server/src/server.rs | 706 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) create mode 100644 server/src/server.rs (limited to 'server/src/server.rs') 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 . + +*/ +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, + entities: Vec, + 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); + 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(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, + packet_out: &mut VecDeque, + ) { + 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, + ) -> 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, + packet_out: &mut VecDeque, + ) -> 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) -> 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, + 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), + }); + } + } + } + } +} -- cgit v1.2.3-70-g09d2