/* 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 anyhow::Result; use directories::ProjectDirs; use hurrycurry_protocol::{Score, Scoreboard, ScoreboardEntry}; use log::warn; use serde::{Deserialize, Serialize}; use std::{ cmp::Reverse, collections::HashMap, fs::{create_dir_all, read_to_string, rename, File}, io::Write, }; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct ScoreboardStore { maps: HashMap, } fn project_dirs() -> Option { ProjectDirs::from("org", "hurrycurry", "hurrycurry") } impl ScoreboardStore { pub 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())?; ScoreboardStore::default().save()?; } let s = read_to_string(path)?; Ok(serde_json::from_str(&s)?) } pub 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)?.write_all(serde_json::to_string(self)?.as_bytes())?; rename(buffer_path, path)?; 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(); } } }