/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors 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, tutorial::Tutorial}, server::{AnnounceState, Server}, }; use anyhow::Result; use clap::{Parser, ValueEnum}; use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; use hurrycurry_data::build_gamedata; use hurrycurry_locale::{TrError, tre, trm}; use hurrycurry_protocol::{ Character, Hand, ItemLocation, Menu, Message, PacketC, PlayerClass, PlayerID, }; use std::fmt::Write; #[cfg(feature = "cheats")] use std::time::Duration; #[derive(Parser)] #[clap(multicall = true)] enum Command { /// Start a new game #[clap(alias = "s")] Start { /// Gamedata specification #[arg(default_value = "junior")] name: String, /// Skip announement and pause at game start #[arg(short = 's', long)] skip_announce: bool, /// Number of hands per player #[arg(short = 'c', long)] hand_count: Option, /// Duration in seconds #[cfg(feature = "cheats")] timer: Option, }, /// Shows the best entries of the scoreboard for this map. #[clap(alias = "top", alias = "top5")] Scoreboard { /// Name of the map, default: current map: Option, /// Send text instead of document #[arg(short, long)] text: bool, }, #[clap(alias = "mapinfo")] Info { /// Name of the map, default: current map: Option, }, /// Abort the current game End, /// Send an effect Effect { name: String }, /// Send an item message Item { name: String }, /// Send a tile message Tile { name: String }, /// Show all possible demands for this map Demands, /// Ready yourself Ready { #[arg(short, long)] force: bool, #[arg(short, long)] unready: bool, }, #[clap(alias = "summon", alias = "bot")] CreateBot { algo: String, name: Option }, /// Reload the current map #[clap(alias = "r")] Reload, /// Shows the recipe book Book, /// Start an interactive tutorial for some item #[clap(alias = "tutorial")] StartTutorial { item: String }, /// End the tutorial unfinished EndTutorial, #[clap(alias = "tr")] /// Manually send a translated message TranslateMessage { message_id: String, arguments: Vec, }, /// Set your players hand item (CHEAT) #[cfg(feature = "cheats")] #[clap(alias = "give")] SetItem { name: String }, /// Set the tile under your player (CHEAT) #[cfg(feature = "cheats")] #[clap(alias = "setblock")] SetTile { name: String }, } #[derive(ValueEnum, Clone)] enum DownloadType { Map, Recipes, } impl Server { pub fn handle_command_parse( &mut self, player: PlayerID, command: &str, ) -> Result, TrError> { let mut replies = Vec::new(); for line in command.split("\n") { self.handle_command( player, Command::try_parse_from( shlex::split(line) .ok_or(tre!("s.error.quoting_invalid"))? .into_iter(), ) .map_err(|e| TrError::Plain(e.to_string()))?, &mut replies, )?; } Ok(replies) } fn handle_command( &mut self, player: PlayerID, command: Command, replies: &mut Vec, ) -> Result<(), TrError> { match command { Command::Start { name, #[cfg(feature = "cheats")] timer, skip_announce, hand_count, } => { if !self.game.lobby { self.broadcast .send(PacketC::ServerMessage { message: trm!( "s.state.game_aborted", s = self .game .players .get(&player) .ok_or(tre!("s.error.no_player"))? .name .clone() ), error: false, }) .ok(); } let mut data = build_gamedata(&self.config.data_path, &name, true) .map_err(|e| TrError::Plain(e.to_string()))?; if let Some(hand_count) = hand_count { data.0.hand_count = hand_count; } #[cfg(feature = "cheats")] self.load(data, timer.map(Duration::from_secs)); #[cfg(not(feature = "cheats"))] self.load(data, None); if skip_announce { self.announce_state = AnnounceState::Done } } Command::End => { self.broadcast .send(PacketC::ServerMessage { message: trm!( "s.state.game_aborted", s = self .game .players .get(&player) .ok_or(tre!("s.error.no_player"))? .name .clone() ), error: false, }) .ok(); self.load( build_gamedata(&self.config.data_path, &self.config.lobby, true) .map_err(|e| TrError::Plain(e.to_string()))?, None, ); } Command::Reload => { if self.count_chefs() > 1 { return Err(tre!("s.error.must_be_alone")); } self.load( build_gamedata(&self.config.data_path, &self.game.data.current_map, true) .map_err(|e| TrError::Plain(e.to_string()))?, None, ); self.announce_state = AnnounceState::Done } Command::Ready { force, unready } => { for c in self.connections.values_mut() { if c.players.contains(&player) || force { c.ready = true; } if unready && c.players.contains(&player) { c.ready = false; } } self.update_paused(); } Command::Book => { replies.push(PacketC::Menu(Menu::Book(self.priv_gamedata.book.clone()))) } Command::Effect { name } => { self.broadcast .send(PacketC::Effect2 { name, location: ItemLocation::Player(player, Hand(0)), }) .ok(); } Command::Item { name } => { let item = self .game .data .get_item_by_name(&name) .ok_or(tre!("s.error.item_not_found", s = name))?; self.broadcast .send(PacketC::Communicate { player, message: Some(Message::Item(item)), timeout: None, }) .ok(); } Command::Tile { name } => { let tile = self .game .data .get_tile_by_name(&name) .ok_or(tre!("s.error.no_tile", s = name))?; self.broadcast .send(PacketC::Communicate { player, message: Some(Message::Tile(tile)), timeout: None, }) .ok(); } Command::CreateBot { algo, name } => { let (aname, cons) = ALGO_CONSTRUCTORS .iter() .find(|(name, _)| *name == algo.as_str()) .ok_or(tre!("s.error.algo_not_found", s = algo))?; self.entities.push(Box::new(BotDriver::new( format!("{}-bot", name.unwrap_or((*aname).to_owned())), Character { color: 0, hairstyle: 0, headwear: 0, }, PlayerClass::Bot, cons, ))); } Command::Scoreboard { map, text } => { let mapname = map.as_ref().unwrap_or(&self.game.data.current_map); let mapname_pretty = &self .data .maps .iter() .find(|(n, _)| n == mapname) .map(|(_, m)| &m.name) .ok_or(tre!("s.error.scoreboard_disabled"))?; if let Some(board) = self.scoreboard.get(mapname) { if text { let mut o = format!("Scoreboard for {mapname_pretty}:\n"); for (i, entry) in board.best.iter().take(5).enumerate() { writeln!( o, " {i}. {} points by {}", entry.score.points, entry.players.clone().join(", ") ) .unwrap(); } replies.push(PacketC::ServerMessage { message: Message::Text(o), error: false, }); } else { let mut board = board.to_owned(); board.map = Some(mapname_pretty.to_string()); replies.push(PacketC::Menu(Menu::Scoreboard(board))); } } else { replies.push(PacketC::ServerMessage { message: trm!("c.menu.scoreboard.no_finish", s = mapname.to_string()), error: false, }); } } Command::Info { map } => { let mapname = map.as_ref().unwrap_or(&self.game.data.current_map); let info = &self .data .maps .iter() .find(|(n, _)| n == mapname) .map(|(_, m)| m) .ok_or(tre!("s.error.no_info"))?; replies.push(PacketC::ServerMessage { message: Message::Text(format!( "{:?}\nRecommended player count: {}\nDifficulty: {}", info.name, info.difficulty, info.players )), error: false, }); } Command::StartTutorial { item } => { let item = self .game .data .get_item_by_name(&item) .ok_or(tre!("s.error.item_not_found", s = item))?; if self.entities.iter().any(|e| { ::downcast_ref::(e.as_ref()) .is_some_and(|t| t.player == player) }) { return Err(tre!("s.error.tutorial_already_running")); } self.entities.push(Box::new(Tutorial::new(player, item))); } Command::EndTutorial => { if let Some(tutorial) = self .entities .iter_mut() .find_map(|e| ::downcast_mut::(e.as_mut())) { tutorial.ended = true; } else { return Err(tre!("s.error.tutorial_no_running")); } } Command::TranslateMessage { message_id, arguments, } => { self.packet_out.push_back(PacketC::Communicate { player, message: Some(Message::Translation { id: message_id, params: arguments.into_iter().map(Message::Text).collect(), }), timeout: None, }); } Command::Demands => { replies.push(PacketC::ServerMessage { message: Message::Text( self.game .data .demands .iter() .map(|d| self.game.data.item_name(d.input).to_owned()) .collect::>() .join("\n"), ), error: false, }); } #[cfg(feature = "cheats")] Command::SetItem { name } => { use hurrycurry_game_core::Item; use hurrycurry_protocol::{Hand, ItemLocation}; let item = self .game .data .get_item_by_name(&name) .ok_or(tre!("s.error.item_not_found", s = name))?; let player_data = self .game .players .get_mut(&player) .ok_or(tre!("s.error.no_player"))?; player_data.items[0] = Some(Item { active: None, kind: item, }); self.game.events.push_back(PacketC::SetItem { location: ItemLocation::Player(player, Hand(0)), item: Some(item), }); } #[cfg(feature = "cheats")] Command::SetTile { name } => { let tile = self .game .data .get_tile_by_name(&name) .ok_or(tre!("s.error.no_tile", s = name))?; let pos = self .game .players .get(&player) .ok_or(tre!("s.error.no_player"))? .movement .position .as_ivec2(); self.game.add_tile_part(pos, tile); } } Ok(()) } }