/* 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::DataIndex, server::{Server, ServerState}, ConnectionID, }; use anyhow::{anyhow, bail, Result}; use clap::{Parser, ValueEnum}; use hurrycurry_client_lib::Game; use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID}; use log::{debug, trace}; use std::{ collections::{HashMap, HashSet, VecDeque}, time::Duration, }; use tokio::sync::broadcast::Sender; pub struct State { index: DataIndex, packet_out: VecDeque, tx: Sender, connections: HashMap>, pub server: ServerState, pub game: Game, } #[derive(Parser)] #[clap(multicall = true)] enum Command { /// Start a new game Start { /// Gamedata specification #[arg(default_value = "junior")] spec: String, /// Duration in seconds #[arg(default_value = "420")] timer: u64, }, /// Abort the current game End, /// Download recipe/map's source declaration Download { /// Resource kind #[arg(value_enum)] r#type: DownloadType, /// Name name: String, }, /// List all recipes and maps List, /// Send an effect Effect { name: String }, /// Send an item Item { name: String }, /// Reload the resource index ReloadIndex, /// Reload the current map #[clap(alias = "r")] Reload, } #[derive(ValueEnum, Clone)] enum DownloadType { Map, Recipes, } impl State { pub async fn new(tx: Sender) -> Result { let mut index = DataIndex::default(); index.reload()?; let mut packet_out = VecDeque::new(); let mut game = Game::default(); let mut server = ServerState::default(); { Server { game: &mut game, state: &mut server, } .load( index.generate("lobby-none".to_string()).await?, None, &mut packet_out, ); } Ok(Self { game, server, index, tx, packet_out, connections: HashMap::new(), }) } pub async fn tick(&mut self, dt: f32) -> anyhow::Result<()> { let mut server = Server { game: &mut self.game, state: &mut self.server, }; if server.tick(dt, &mut self.packet_out) { server.load( self.index.generate("lobby-none".to_string()).await?, None, &mut self.packet_out, ); } while let Some(p) = self.packet_out.pop_front() { if matches!(p, PacketC::UpdateMap { .. } | PacketC::Movement { .. }) { trace!("-> {p:?}"); } else { debug!("-> {p:?}"); } self.tx.send(p).unwrap(); } Ok(()) } pub async fn packet_in(&mut self, conn: ConnectionID, packet: PacketS) -> Result> { if let Some(p) = get_packet_player(&packet) { if !self.connections.entry(conn).or_default().contains(&p) { bail!("Packet sent to player that is not owned by this connection."); } } let mut replies = Vec::new(); match &packet { PacketS::Communicate { message: Some(Message::Text(text)), persist: false, player, } if let Some(command) = text.strip_prefix("/") => { match self.handle_command_parse(*player, command).await { Ok(()) => return Ok(vec![]), Err(e) => { return Ok(vec![PacketC::ServerMessage { text: format!("{e}"), }]); } } } PacketS::Leave { player } => { self.connections.entry(conn).or_default().remove(player); } PacketS::Join { .. } => { if self.connections.entry(conn).or_default().len() > 8 { bail!("Players per connection limit exceeded.") } } _ => (), } let mut server = Server { game: &mut self.game, state: &mut self.server, }; server.packet_in(packet, &mut replies, &mut self.packet_out)?; for p in &replies { match p { PacketC::Joined { id } => { self.connections.entry(conn).or_default().insert(*id); } _ => (), } } if server.count_chefs() <= 0 && !server.state.lobby { self.tx .send(PacketC::ServerMessage { text: "Game was aborted automatically due to a lack of players".to_string(), }) .ok(); server.load( self.index.generate("lobby-none".to_string()).await?, None, &mut self.packet_out, ); } Ok(replies) } pub async fn disconnect(&mut self, conn: ConnectionID) { if let Some(players) = self.connections.get(&conn) { for player in players.to_owned() { let _ = self.packet_in(conn, PacketS::Leave { player }).await; } } self.connections.remove(&conn); } async fn handle_command_parse(&mut self, player: PlayerID, command: &str) -> Result<()> { self.handle_command( player, Command::try_parse_from( shlex::split(command) .ok_or(anyhow!("quoting invalid"))? .into_iter(), )?, ) .await?; Ok(()) } async fn handle_command(&mut self, player: PlayerID, command: Command) -> Result<()> { let mut server = Server { game: &mut self.game, state: &mut self.server, }; match command { Command::Start { spec, timer } => { let data = self.index.generate(spec).await?; server.load(data, Some(Duration::from_secs(timer)), &mut self.packet_out); } Command::End => { self.tx .send(PacketC::ServerMessage { text: format!( "Game was aborted by {}.", server .game .players .get(&player) .ok_or(anyhow!("player missing"))? .name ), }) .ok(); server.load( self.index.generate("lobby-none".to_string()).await?, None, &mut self.packet_out, ); } Command::Reload => { if server.count_chefs() > 1 { bail!("must be at most one player to reload"); } server.load( self.index .generate(server.state.data.spec.to_string()) .await?, None, &mut self.packet_out, ); } Command::ReloadIndex => { self.index.reload()?; } Command::Download { r#type, name } => { let source = match r#type { DownloadType::Map => self.index.read_map(&name).await, DownloadType::Recipes => self.index.read_recipes(&name).await, }?; bail!("{source}"); } Command::List => { bail!( "Maps: {:?}\nRecipes: {:?}", self.index.maps.keys().collect::>(), self.index.recipes ) } Command::Effect { name } => { self.tx .send(PacketC::Communicate { player, message: Some(Message::Effect(name)), persist: false, }) .ok(); } Command::Item { name } => { let item = server .game .data .get_item_by_name(&name) .ok_or(anyhow!("unknown item"))?; self.tx .send(PacketC::Communicate { player, message: Some(Message::Item(item)), persist: false, }) .ok(); } } Ok(()) } } fn get_packet_player(packet: &PacketS) -> Option { match packet { PacketS::Join { .. } => None, PacketS::Leave { player } => Some(*player), PacketS::Movement { player, .. } => Some(*player), PacketS::Interact { player, .. } => Some(*player), PacketS::Communicate { player, .. } => Some(*player), PacketS::ReplaceHand { player, .. } => Some(*player), PacketS::ApplyScore(_) => None, PacketS::ReplayTick { .. } => None, } }