124 lines
3.6 KiB
Rust
124 lines
3.6 KiB
Rust
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<Level>,
|
|
}
|
|
|
|
#[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<u32, MusicInfo>;
|
|
|
|
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<Option<MusicDB>> = LazyLock::new(|| {
|
|
let db: Vec<MusicInfo> = 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<i32>, 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));
|
|
}
|
|
}
|