aboutsummaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-09-02 01:41:33 +0200
committermetamuffin <metamuffin@disroot.org>2024-09-02 01:41:37 +0200
commit6c4fb2a9d5bf0fde77a6633cb244828852559b04 (patch)
treee39555afb74c2fdc561679cac9567ef0a4519b03 /server/src
parent52e7384c955d6fcfe5d522c3c4d5258de38f3507 (diff)
downloadhurrycurry-6c4fb2a9d5bf0fde77a6633cb244828852559b04.tar
hurrycurry-6c4fb2a9d5bf0fde77a6633cb244828852559b04.tar.bz2
hurrycurry-6c4fb2a9d5bf0fde77a6633cb244828852559b04.tar.zst
move things around, add scoreboards load/save module
Diffstat (limited to 'server/src')
-rw-r--r--server/src/bin/graph.rs2
-rw-r--r--server/src/bin/graph_summary.rs2
-rw-r--r--server/src/commands.rs168
-rw-r--r--server/src/data/mod.rs7
-rw-r--r--server/src/lib.rs2
-rw-r--r--server/src/scoreboard.rs65
-rw-r--r--server/src/server.rs11
-rw-r--r--server/src/state.rs151
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> {