/*
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, tutorial::Tutorial},
message::TrError,
server::Server,
tre, trm,
};
use anyhow::Result;
use clap::{Parser, ValueEnum};
use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
use hurrycurry_protocol::{
Character, DocumentElement, Menu, Message, PacketC, PlayerClass, 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
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,
/// 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", 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,
},
/// Return to the map editor
#[clap(alias = "e", alias = "editor")]
Edit,
#[clap(hide = true)]
SetEditorAddress { url: String },
}
#[derive(ValueEnum, Clone)]
enum DownloadType {
Map,
Recipes,
}
impl Server {
pub async 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,
)
.await?;
}
Ok(replies)
}
async fn handle_command(
&mut self,
player: PlayerID,
command: Command,
replies: &mut Vec,
) -> Result<(), TrError> {
match command {
Command::Start { spec, timer } => {
if !self.game.lobby {
self.tx
.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 data = self
.index
.generate(&spec)
.await
.map_err(|e| TrError::Plain(e.to_string()))?;
self.load(data, timer.map(Duration::from_secs));
}
Command::End => {
self.tx
.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(
self.index
.generate("lobby")
.await
.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(
self.index
.generate(&self.game.data.current_map)
.await
.map_err(|e| TrError::Plain(e.to_string()))?,
None,
);
}
Command::ReloadIndex => {
self.index
.reload()
.await
.map_err(|e| TrError::Plain(e.to_string()))?;
}
Command::Book => replies.push(PacketC::Menu(Menu::Document(self.data.book.clone()))),
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,
}
.map_err(|e| TrError::Plain(e.to_string()))?;
replies.push(PacketC::ServerMessage {
message: Message::Text(source),
error: false,
});
}
Command::List => replies.push(PacketC::ServerMessage {
message: Message::Text(format!(
"Maps: {:?}\nRecipes: {:?}",
self.index.maps.keys().collect::>(),
self.index.recipes
)),
error: false,
}),
Command::Effect { name } => {
self.tx.send(PacketC::Effect { name, player }).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.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(tre!("s.error.algo_not_found", s = algo))?;
let algo = cons();
self.entities.push(Box::new(BotDriver::new(
format!("{}-bot", name.unwrap_or((*aname).to_owned())),
Character {
color: 0,
hairstyle: 0,
headwear: 0,
},
PlayerClass::Bot,
algo,
)));
}
Command::Scoreboard { map, text } => {
let mapname = map.as_ref().unwrap_or(&self.game.data.current_map);
if let Some(board) = self.scoreboard.get(mapname) {
if text {
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();
}
replies.push(PacketC::ServerMessage {
message: Message::Text(o),
error: false,
});
} else {
replies.push(PacketC::Menu(Menu::Document(DocumentElement::Document {
es: vec![DocumentElement::Page {
es: vec![
DocumentElement::Par {
es: vec![DocumentElement::Text {
s: Message::Translation {
id: "c.menu.scoreboard".to_string(),
params: vec![],
},
size: 30.,
bold: false,
color: None,
font: None,
}],
},
DocumentElement::List {
es: board
.best
.iter()
.take(10)
.enumerate()
.map(|(place, entry)| DocumentElement::Text {
s: trm!(
"c.menu.scoreboard.entry",
s = (place + 1).to_string(),
s = entry.score.points.to_string(),
s = entry.players.clone().join(", ")
),
size: 15.,
bold: false,
color: None,
font: None,
})
.collect(),
},
],
background: None,
}],
})));
}
} 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
.index
.maps
.get(mapname)
.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))?;
#[cfg(not(test))] // TODO rust-analyser does not undestand trait upcasting
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 => {
#[cfg(not(test))] // TODO rust-analyser does not undestand trait upcasting
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::Edit => {
let addr = self
.editor_address
.clone()
.ok_or(tre!("s.error.not_editor_session"))?;
replies.push(PacketC::Redirect { uri: vec![addr] });
}
Command::SetEditorAddress { url } => {
self.editor_address = Some(url);
}
}
Ok(())
}
}