/* 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::{entity::bot::BotDriver, server::Server}; use anyhow::{anyhow, bail, Result}; use clap::{Parser, ValueEnum}; use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; use hurrycurry_protocol::{Menu, Message, PacketC, PlayerID}; use std::{fmt::Write, time::Duration}; #[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, }, /// Shows the best entries of the scoreboard for this map. #[clap(alias = "top5")] Top { /// Name of the map, default: current map: Option, }, #[clap(alias = "mapinfo")] Info { /// Name of the map, default: current map: Option, }, /// 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, #[clap(alias = "summon-bot", alias = "spawn-bot")] CreateBot { algo: String, name: Option }, /// Reload the current map #[clap(alias = "r")] Reload, /// Shows the recipe book Book, } #[derive(ValueEnum, Clone)] enum DownloadType { Map, Recipes, } impl Server { pub async fn handle_command_parse( &mut self, player: PlayerID, command: &str, ) -> Result> { let mut replies = Vec::new(); for line in command.split("\n") { self.handle_command( player, Command::try_parse_from( shlex::split(line) .ok_or(anyhow!("quoting invalid"))? .into_iter(), )?, &mut replies, ) .await?; } Ok(replies) } async fn handle_command( &mut self, player: PlayerID, command: Command, replies: &mut Vec, ) -> Result<()> { match command { Command::Start { spec, timer } => { let data = self.index.generate(&spec).await?; self.load(data, Some(Duration::from_secs(timer))); } Command::End => { self.tx .send(PacketC::ServerMessage { text: format!( "Game was aborted by {}.", self.game .players .get(&player) .ok_or(anyhow!("player missing"))? .name ), }) .ok(); self.load(self.index.generate("lobby").await?, None); } Command::Reload => { if self.count_chefs() > 1 { bail!("must be at most one player to reload"); } self.load( self.index.generate(&self.game.data.current_map).await?, None, ); } Command::ReloadIndex => { self.index.reload().await?; } Command::Book => replies.push(PacketC::Menu(Menu::Book)), 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)), timeout: None, }) .ok(); } Command::Item { name } => { let item = self .game .data .get_item_by_name(&name) .ok_or(anyhow!("unknown item"))?; self.tx .send(PacketC::Communicate { player, message: Some(Message::Item(item)), timeout: None, }) .ok(); } Command::CreateBot { algo, name } => { let (aname, cons) = ALGO_CONSTRUCTORS .iter() .find(|(name, _)| *name == algo.as_str()) .ok_or(anyhow!("algo name unknown"))?; let algo = cons(); self.entities.push(Box::new(BotDriver::new( format!("{}-bot", name.unwrap_or((*aname).to_owned())), 51, algo, ))); } Command::Top { map } => { let mapname = map.as_ref().unwrap_or(&self.game.data.current_map); if let Some(board) = self.scoreboard.get(mapname) { let mut o = format!("Scoreboard for {}:\n", mapname); for (i, entry) in board.best.iter().take(5).enumerate() { writeln!( o, " {i}. {} points by {}", entry.score.points, entry.players.clone().join(", ") ) .unwrap(); } bail!("{o}"); } } Command::Info { map } => { let mapname = map.as_ref().unwrap_or(&self.game.data.current_map); let info = self .index .maps .get(mapname) .ok_or(anyhow!("no information available"))?; bail!( "{:?}\nRecommended player count: {}\nDifficulty: {}", info.name, info.difficulty, info.players ) } } Ok(()) } }