Files
sdgb-utils-rs/music_db/src/lib.rs
2025-08-14 22:54:25 +08:00

118 lines
3.2 KiB
Rust

use std::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 achievement = achievement.min(1005000); // SSS+ case
let (rank, _, factor) = RANKS
.into_iter()
.rev()
.find(|&(_, threshold, _)| threshold <= achievement)
.unwrap(); // save here, due to zero threshold
let difficulty_rank: Decimal = self.difficulty.value;
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, i32, Decimal); 23] = [
("D", 0, dec!(0.0)),
("D", 100000, dec!(0.16)),
("D", 200000, dec!(0.32)),
("D", 300000, dec!(0.48)),
("D", 400000, dec!(0.64)),
("C", 500000, dec!(0.80)),
("B", 600000, dec!(0.96)),
("BB", 700000, dec!(0.112)),
("BBB", 750000, dec!(0.120)),
("BBB", 799999, dec!(0.128)),
("A", 800000, dec!(0.136)),
("AA", 900000, dec!(0.152)),
("AAA", 940000, dec!(0.168)),
("AAA", 969999, dec!(0.176)),
("S", 970000, dec!(0.200)),
("S+", 980000, dec!(0.203)),
("S+", 989999, dec!(0.206)),
("SS", 990000, dec!(0.208)),
("SS+", 995000, dec!(0.211)),
("SS+", 999999, dec!(0.214)),
("SSS", 1000000, dec!(0.216)),
("SSS", 1004999, dec!(0.222)),
("SSS+", 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));
}
}