diff options
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/bin/graph.rs | 2 | ||||
-rw-r--r-- | server/src/bin/graph_summary.rs | 2 | ||||
-rw-r--r-- | server/src/commands.rs | 168 | ||||
-rw-r--r-- | server/src/data/mod.rs | 7 | ||||
-rw-r--r-- | server/src/lib.rs | 2 | ||||
-rw-r--r-- | server/src/scoreboard.rs | 65 | ||||
-rw-r--r-- | server/src/server.rs | 11 | ||||
-rw-r--r-- | server/src/state.rs | 151 |
8 files changed, 252 insertions, 156 deletions
diff --git a/server/src/bin/graph.rs b/server/src/bin/graph.rs index 0bc3702f..c6f1ee11 100644 --- a/server/src/bin/graph.rs +++ b/server/src/bin/graph.rs @@ -22,7 +22,7 @@ use hurrycurry_server::data::DataIndex; #[tokio::main] async fn main() -> Result<()> { let mut index = DataIndex::default(); - index.reload()?; + index.reload().await?; println!("digraph {{"); diff --git a/server/src/bin/graph_summary.rs b/server/src/bin/graph_summary.rs index 2c97799a..374d9521 100644 --- a/server/src/bin/graph_summary.rs +++ b/server/src/bin/graph_summary.rs @@ -23,7 +23,7 @@ use std::collections::HashSet; #[tokio::main] async fn main() -> Result<()> { let mut index = DataIndex::default(); - index.reload()?; + index.reload().await?; println!("digraph {{"); diff --git a/server/src/commands.rs b/server/src/commands.rs new file mode 100644 index 00000000..e2d40f05 --- /dev/null +++ b/server/src/commands.rs @@ -0,0 +1,168 @@ +/* + 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::{entity::bot::BotDriver, server::Server}; +use anyhow::{anyhow, bail, Result}; +use clap::{Parser, ValueEnum}; +use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; +use hurrycurry_protocol::{Message, PacketC, PlayerID}; +use std::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, + }, + /// 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<String> }, + /// Reload the current map + #[clap(alias = "r")] + Reload, +} + +#[derive(ValueEnum, Clone)] +enum DownloadType { + Map, + Recipes, +} + +impl Server { + pub async fn handle_command_parse(&mut self, player: PlayerID, command: &str) -> Result<()> { + for line in command.split("\n") { + self.handle_command( + player, + Command::try_parse_from( + shlex::split(line) + .ok_or(anyhow!("quoting invalid"))? + .into_iter(), + )?, + ) + .await?; + } + Ok(()) + } + async fn handle_command(&mut self, player: PlayerID, command: Command) -> 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::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::<Vec<_>>(), + 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, + ))); + } + } + Ok(()) + } +} diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs index 7ec0707d..53414178 100644 --- a/server/src/data/mod.rs +++ b/server/src/data/mod.rs @@ -120,7 +120,12 @@ fn data_dir() -> PathBuf { } impl DataIndex { - pub fn reload(&mut self) -> Result<()> { + pub async fn load() -> Result<Self> { + let mut s = Self::default(); + s.reload().await?; + Ok(s) + } + pub async fn reload(&mut self) -> Result<()> { *self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?; Ok(()) } diff --git a/server/src/lib.rs b/server/src/lib.rs index 93218a9e..89822133 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -21,6 +21,8 @@ pub mod entity; pub mod server; pub mod interaction; pub mod state; +pub mod scoreboard; +pub mod commands; use hurrycurry_protocol::glam::Vec2; diff --git a/server/src/scoreboard.rs b/server/src/scoreboard.rs new file mode 100644 index 00000000..e7b97b8d --- /dev/null +++ b/server/src/scoreboard.rs @@ -0,0 +1,65 @@ +/* + 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 anyhow::Result; +use hurrycurry_protocol::Score; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tokio::{ + fs::{read_to_string, rename, File}, + io::AsyncWriteExt, +}; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct ScoreboardStore { + maps: HashMap<String, Scoreboard>, +} +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Scoreboard { + plays: usize, + best: Vec<ScoreboardEntry>, +} +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ScoreboardEntry { + players: Vec<String>, + score: Score, +} + +impl ScoreboardStore { + pub async fn load() -> Result<Self> { + let path = + xdg::BaseDirectories::with_prefix("hurrycurry")?.place_data_file("scoreboards.json")?; + // TOCTOU because its easier that way + if !path.exists() { + ScoreboardStore::default().save().await?; + } + let s = read_to_string(path).await?; + Ok(serde_json::from_str(&s)?) + } + pub async fn save(&self) -> Result<()> { + let path = + xdg::BaseDirectories::with_prefix("hurrycurry")?.place_data_file("scoreboards.json")?; + let buffer_path = xdg::BaseDirectories::with_prefix("hurrycurry")? + .place_data_file("scoreboards.json~")?; + File::create(&buffer_path) + .await? + .write_all(serde_json::to_string(self)?.as_bytes()) + .await?; + rename(buffer_path, path).await?; + Ok(()) + } +} diff --git a/server/src/server.rs b/server/src/server.rs index 12eca299..6693d0fb 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -19,9 +19,10 @@ use crate::{ data::{DataIndex, Serverdata}, entity::{Entities, EntityContext}, interaction::{interact, tick_slot, InteractEffect, TickEffect}, + scoreboard::ScoreboardStore, ConnectionID, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Context, Result}; use hurrycurry_client_lib::{Game, Item, Player, Tile}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, @@ -50,6 +51,7 @@ pub struct Server { pub packet_out: VecDeque<PacketC>, pub tx: Sender<PacketC>, pub connections: HashMap<ConnectionID, HashSet<PlayerID>>, + pub scoreboard: ScoreboardStore, } pub trait GameServerExt { @@ -231,11 +233,9 @@ impl GameServerExt for Game { impl Server { pub async fn new(tx: Sender<PacketC>) -> Result<Self> { - let mut index = DataIndex::default(); - index.reload()?; Ok(Self { game: Game::default(), - index, + index: DataIndex::load().await.context("loading data index")?, tx, packet_out: VecDeque::new(), connections: HashMap::new(), @@ -245,6 +245,9 @@ impl Server { score_changed: false, packet_loopback: VecDeque::new(), last_movement_update: HashMap::default(), + scoreboard: ScoreboardStore::load() + .await + .context("loading scoreboards")?, }) } } diff --git a/server/src/state.rs b/server/src/state.rs index ad9eeb1c..ed1a7488 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -15,56 +15,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{entity::bot::BotDriver, server::Server, ConnectionID}; -use anyhow::{anyhow, bail, Result}; -use clap::{Parser, ValueEnum}; -use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; +use crate::{server::Server, ConnectionID}; +use anyhow::{bail, Result}; use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID}; use log::{debug, trace}; -use std::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, - }, - /// 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<String> }, - /// Reload the current map - #[clap(alias = "r")] - Reload, -} - -#[derive(ValueEnum, Clone)] -enum DownloadType { - Map, - Recipes, -} impl Server { pub async fn tick_outer(&mut self, dt: f32) -> anyhow::Result<()> { @@ -148,107 +102,6 @@ impl Server { } self.connections.remove(&conn); } - - async fn handle_command_parse(&mut self, player: PlayerID, command: &str) -> Result<()> { - for line in command.split("\n") { - self.handle_command( - player, - Command::try_parse_from( - shlex::split(line) - .ok_or(anyhow!("quoting invalid"))? - .into_iter(), - )?, - ) - .await?; - } - Ok(()) - } - - async fn handle_command(&mut self, player: PlayerID, command: Command) -> 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()?; - } - 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::<Vec<_>>(), - 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, - ))); - } - } - Ok(()) - } } fn get_packet_player(packet: &PacketS) -> Option<PlayerID> { |