/* 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 anyhow::Result; use directories::ProjectDirs; use hurrycurry_protocol::Score; use log::warn; use serde::{Deserialize, Serialize}; use std::{cmp::Reverse, collections::HashMap}; use tokio::{ fs::{create_dir_all, read_to_string, rename, File}, io::AsyncWriteExt, }; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct ScoreboardStore { maps: HashMap, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Scoreboard { pub plays: usize, pub best: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ScoreboardEntry { pub players: Vec, pub score: Score, } fn project_dirs() -> Option { ProjectDirs::from("org", "hurrycurry", "hurrycurry") } impl ScoreboardStore { pub async fn load() -> Result { let Some(dir) = project_dirs() else { warn!("scoreboard load skipped; no data dir for this platform"); return Ok(Self::default()); }; // TOCTOU because its easier that way let path = dir.data_dir().join("scoreboards.json"); if !path.exists() { create_dir_all(dir.data_dir()).await?; ScoreboardStore::default().save().await?; } let s = read_to_string(path).await?; Ok(serde_json::from_str(&s)?) } pub async fn save(&self) -> Result<()> { let Some(dir) = project_dirs() else { warn!("scoreboard save skipped; no data dir for this platform"); return Ok(()); }; let path = dir.data_dir().join("scoreboards.json"); let buffer_path = dir.data_dir().join("scoreboards.json~"); File::create(&buffer_path) .await? .write_all(serde_json::to_string(self)?.as_bytes()) .await?; rename(buffer_path, path).await?; Ok(()) } pub fn get<'a>(&'a self, map: &str) -> Option<&'a Scoreboard> { self.maps.get(map) } pub fn insert(&mut self, map: &str, players: Vec, score: Score) { let b = self.maps.entry(map.to_owned()).or_default(); b.plays += 1; b.best.push(ScoreboardEntry { players, score }); b.best.sort_by_key(|e| Reverse(e.score.points)); while b.best.len() > 16 { b.best.pop(); } } }