aboutsummaryrefslogtreecommitdiff
path: root/server/src/game.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-08-13 12:48:31 +0200
committermetamuffin <metamuffin@disroot.org>2024-08-13 16:03:38 +0200
commit16ff78180669411326d42ea32d4a9260c018236c (patch)
treed7c6a7ab498bb1b4f9a3b3db99d54e8781216e05 /server/src/game.rs
parent11ff74f034aeec58c06dbe15a3f1ee650ef18c9f (diff)
downloadhurrycurry-16ff78180669411326d42ea32d4a9260c018236c.tar
hurrycurry-16ff78180669411326d42ea32d4a9260c018236c.tar.bz2
hurrycurry-16ff78180669411326d42ea32d4a9260c018236c.tar.zst
refactor server to use client-lib data model (breaks customers)
Diffstat (limited to 'server/src/game.rs')
-rw-r--r--server/src/game.rs737
1 files changed, 0 insertions, 737 deletions
diff --git a/server/src/game.rs b/server/src/game.rs
deleted file mode 100644
index 39cd61dc..00000000
--- a/server/src/game.rs
+++ /dev/null
@@ -1,737 +0,0 @@
-/*
- 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::Gamedata,
- entity::{Entity, EntityT},
- interaction::{interact, tick_slot, InteractEffect, TickEffect},
- spatial_index::SpatialIndex,
-};
-use anyhow::{anyhow, bail, Result};
-use hurrycurry_protocol::{
- glam::{IVec2, Vec2},
- movement::MovementBase,
- ClientGamedata, ItemIndex, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID,
- RecipeIndex, Score, TileIndex,
-};
-use log::{info, warn};
-use std::{
- collections::{HashMap, HashSet, VecDeque},
- sync::{Arc, RwLock},
- time::{Duration, Instant},
-};
-
-#[derive(Debug, PartialEq)]
-pub struct Involvement {
- pub recipe: RecipeIndex,
- pub progress: f32,
- pub working: usize,
-}
-
-#[derive(Debug, PartialEq)]
-pub struct Item {
- pub kind: ItemIndex,
- pub active: Option<Involvement>,
-}
-
-pub struct Tile {
- pub kind: TileIndex,
- pub item: Option<Item>,
-}
-
-pub struct Player {
- pub name: String,
- pub character: i32,
- pub interacting: Option<IVec2>,
- pub item: Option<Item>,
- pub communicate_persist: Option<Message>,
-
- pub movement: MovementBase,
- pub last_position_update: Instant,
-}
-
-pub struct Game {
- pub data: Arc<Gamedata>,
- pub tiles: HashMap<IVec2, Tile>,
- pub walkable: HashSet<IVec2>,
- pub players: HashMap<PlayerID, Player>,
- pub players_spatial_index: SpatialIndex<PlayerID>,
- entities: Arc<RwLock<Vec<Entity>>>,
- end: Option<Instant>,
- pub lobby: bool,
-
- pub environment_effects: HashSet<String>,
- pub score_changed: bool,
- pub score: Score,
-
- pub player_id_counter: PlayerID,
-}
-
-impl Default for Game {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl Game {
- pub fn new() -> Self {
- Self {
- lobby: false,
- data: Gamedata::default().into(),
- players: HashMap::new(),
- tiles: HashMap::new(),
- walkable: HashSet::new(),
- end: None,
- entities: Arc::new(RwLock::new(vec![])),
- players_spatial_index: SpatialIndex::default(),
- score: Score::default(),
- environment_effects: HashSet::default(),
- score_changed: false,
- player_id_counter: PlayerID(1),
- }
- }
-
- 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();
- }
- pub fn load(
- &mut self,
- gamedata: Gamedata,
- 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.map_name == "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);
- self.entities = Arc::new(RwLock::new(self.data.entities.clone()));
-
- for (&p, (tile, item)) in &self.data.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 {
- self.data.customer_spawn
- } else {
- self.data.chef_spawn
- },
- input_direction: Vec2::ZERO,
- input_boost: false,
- facing: Vec2::X,
- rotation: 0.,
- velocity: Vec2::ZERO,
- boosting: false,
- stamina: 0.,
- },
- last_position_update: Instant::now(),
- communicate_persist: None,
- interacting: None,
- name: name.clone(),
- },
- );
- }
-
- packet_out.extend(self.prime_client());
- }
-
- pub fn prime_client(&self) -> Vec<PacketC> {
- let mut out = Vec::new();
- out.push(PacketC::Data {
- data: ClientGamedata {
- recipes: self.data.recipes.clone(),
- item_names: self.data.item_names.clone(),
- tile_names: self.data.tile_names.clone(),
- tile_collide: self.data.tile_collide.clone(),
- tile_interact: self.data.tile_interact.clone(),
- current_map: self.data.map_name.clone(),
- map_names: self
- .data
- .map
- .clone()
- .keys()
- .filter(|n| n.as_str() != "lobby")
- .map(|s| s.to_owned())
- .collect(),
- maps: self
- .data
- .map
- .clone()
- .into_iter()
- .filter(|(n, _)| n != "lobby")
- .collect(),
- },
- });
- 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
- }
-
- pub fn join_player(
- &mut self,
- name: String,
- character: i32,
- packet_out: &mut VecDeque<PacketC>,
- ) -> PlayerID {
- let id = self.player_id_counter;
- self.player_id_counter.0 += 1;
- let position = if id.0 < 0 {
- self.data.customer_spawn
- } else {
- self.data.chef_spawn
- };
- self.players.insert(
- id,
- Player {
- item: None,
- character,
- movement: MovementBase {
- position: if character < 0 {
- self.data.customer_spawn
- } else {
- self.data.chef_spawn
- },
- input_direction: Vec2::ZERO,
- input_boost: false,
- facing: Vec2::X,
- rotation: 0.,
- velocity: Vec2::ZERO,
- boosting: false,
- stamina: 0.,
- },
- last_position_update: Instant::now(),
- communicate_persist: None,
- interacting: None,
- name: name.clone(),
- },
- );
- self.score.players = self.score.players.max(self.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
- .players
- .remove(&player)
- .ok_or(anyhow!("player does not exist"))?;
-
- self.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.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
- .players
- .get_mut(&player)
- .ok_or(anyhow!("player does not exist"))?;
-
- pd.movement.input(direction, boost);
-
- 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
- .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
- .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.data.is_tile_interactable(tile.kind) {
- self.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
- .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.data,
- edge,
- &mut this.item,
- ItemLocation::Player(base_pid),
- &mut other.item,
- ItemLocation::Player(pid),
- None,
- packet_out,
- &mut self.score,
- &mut self.score_changed,
- false,
- )
- } else {
- let player = self
- .players
- .get_mut(&pid)
- .ok_or(anyhow!("player does not exist"))?;
-
- interact_effect(
- &self.data,
- edge,
- &mut tile.item,
- ItemLocation::Tile(pos),
- &mut player.item,
- ItemLocation::Player(pid),
- Some(tile.kind),
- packet_out,
- &mut self.score,
- &mut self.score_changed,
- false,
- )
- }
- }
- PacketS::Communicate {
- message,
- persist,
- player,
- } => {
- info!("{player:?} message {message:?}");
- if persist {
- if let Some(player) = self.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
- .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.score_changed {
- self.score_changed = false;
- packet_out.push_back(PacketC::Score(self.score.clone()));
- }
-
- for (&pos, tile) in &mut self.tiles {
- if let Some(effect) = tick_slot(
- dt,
- &self.data,
- Some(tile.kind),
- &mut tile.item,
- &mut self.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.players {
- player.movement.update(&self.walkable, dt);
-
- self.players_spatial_index
- .update_entry(pid, player.movement.position);
- }
-
- self.players_spatial_index.all(|p1, pos1| {
- self.players_spatial_index.query(pos1, 2., |p2, _pos2| {
- if let Some([a, b]) = self.players.get_many_mut([&p1, &p2]) {
- a.movement.collide(&mut b.movement, dt)
- }
- })
- });
-
- for (&pid, player) in &mut self.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.data, None, &mut player.item, &mut self.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.players {
- if let Some(pos) = player.interacting {
- if let Some(tile) = self.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.entities.clone().write().unwrap().iter_mut() {
- if let Err(e) = entity.tick(self, packet_out, dt) {
- warn!("entity tick failed: {e}")
- }
- }
-
- let now = Instant::now();
-
- if let Some(end) = self.end {
- self.score.time_remaining = (end - now).as_secs_f64();
- if end < now {
- let relative_score = (self.score.points * 100) / self.data.score_baseline.max(1);
- self.score.stars = match relative_score {
- 100.. => 3,
- 70.. => 2,
- 40.. => 1,
- _ => 0,
- };
- packet_out.push_back(PacketC::Menu(Menu::Score(self.score.clone())));
- true
- } else {
- false
- }
- } else {
- false
- }
- }
-
- pub fn count_chefs(&self) -> usize {
- self.players
- .values()
- .map(|p| if p.character >= 0 { 1 } else { 0 })
- .sum()
- }
-}
-
-impl From<TileIndex> for Tile {
- fn from(kind: TileIndex) -> Self {
- Self { kind, item: None }
- }
-}
-
-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),
- });
- }
- }
- }
- }
-}
-
-impl Player {
- pub fn position(&self) -> Vec2 {
- self.movement.position
- }
-}