diff options
Diffstat (limited to 'server/src/server.rs')
| -rw-r--r-- | server/src/server.rs | 706 | 
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), +                    }); +                } +            } +        } +    } +} | 
