use std::{ops::RangeInclusive, sync::LazyLock}; use rust_decimal::{Decimal, dec, serde::DecimalFromString}; use rustc_hash::FxHashMap; use serde::Deserialize; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct MusicInfo { pub id: u32, pub name: String, pub version: i64, pub levels: Vec, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Level { /// 0, 1, 2, 3, 4, 5 pub level: u32, /// for example: "13.7" pub difficulty: DecimalFromString, } type MusicDB = FxHashMap; pub fn preload_db() { _ = &*MUSIC_DB; } pub fn query_music(music_id: u32) -> Option<&'static MusicInfo> { MUSIC_DB.as_ref()?.get(&music_id) } pub fn query_music_level(music_id: u32, level: u32) -> Option<&'static Level> { MUSIC_DB .as_ref()? .get(&music_id)? .levels .iter() .find(|d| d.level == level) } pub static MUSIC_DB: LazyLock> = LazyLock::new(|| { let db: Vec = serde_json::from_slice(include_bytes!("musicDB.json")) .inspect_err(|_e| { #[cfg(feature = "log")] spdlog::warn!("failed to load musicDB: {_e}") }) .ok()?; Some(db.into_iter().map(|entry| (entry.id, entry)).collect()) }); impl Level { /// achievement: xxx.xxxx% * 10000 /// /// This will **NOT** ignore utage level, you can calculate a in-theory DX Rating. /// /// On invalid input, it returns 0. pub fn dx_rating(&self, achievement: i32) -> (&'static str, u32) { let difficulty_rank: Decimal = self.difficulty.value; let achievement = achievement.min(1005000); // SSS+ case dx_rating(difficulty_rank, achievement) } } pub fn dx_rating(difficulty_rank: Decimal, achievement: i32) -> (&'static str, u32) { let (rank, _, factor) = RANKS .into_iter() .rev() .find(|(_, threshold, _)| threshold.contains(&achievement)) .unwrap(); // save here, due to zero threshold let achievement = Decimal::new(achievement as _, 4); #[cfg(feature = "log")] spdlog::info!("factor: {factor}, achievement: {achievement}"); // when ach > 100.5%, calculate as 100.5% let rating: u32 = (factor * difficulty_rank * achievement) .floor() .try_into() .unwrap_or_default(); (rank, rating) } const RANKS: [(&'static str, RangeInclusive, Decimal); 23] = [ ("D", 0..=99999, dec!(0.0)), ("D", 100000..=199999, dec!(0.016)), ("D", 200000..=299999, dec!(0.032)), ("D", 300000..=399999, dec!(0.048)), ("D", 400000..=499999, dec!(0.064)), ("C", 500000..=599999, dec!(0.080)), ("B", 600000..=699999, dec!(0.096)), ("BB", 700000..=749999, dec!(0.112)), ("BBB", 750000..=799998, dec!(0.120)), ("BBB", 799999..=799999, dec!(0.128)), ("A", 800000..=899999, dec!(0.136)), ("AA", 900000..=939999, dec!(0.152)), ("AAA", 940000..=969998, dec!(0.168)), ("AAA", 969999..=969999, dec!(0.176)), ("S", 970000..=979999, dec!(0.200)), ("S+", 980000..=989998, dec!(0.203)), ("S+", 989999..=989999, dec!(0.206)), ("SS", 990000..=994999, dec!(0.208)), ("SS+", 995000..=999998, dec!(0.211)), ("SS+", 999999..=999999, dec!(0.214)), ("SSS", 1000000..=1004998, dec!(0.216)), ("SSS", 1004999..=1004999, dec!(0.222)), ("SSS+", 1005000..=1005000, dec!(0.224)), ]; #[cfg(test)] mod tests { use crate::query_music_level; #[test] fn test_rating_calculate() { let level = query_music_level(11696, 3).expect("not found"); assert_eq!(level.dx_rating(953184), ("AAA", 184)); } }