feat: DX RATING calculation

This commit is contained in:
mokurin000
2025-08-01 03:31:16 +08:00
parent ef2df9052b
commit 68e8a6e005
5 changed files with 150 additions and 14 deletions

24
Cargo.lock generated
View File

@@ -1342,6 +1342,29 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rust_decimal"
version = "1.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d"
dependencies = [
"arrayvec",
"num-traits",
"rust_decimal_macros",
"serde",
"serde_json",
]
[[package]]
name = "rust_decimal_macros"
version = "1.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6268b74858287e1a062271b988a0c534bf85bbeb567fe09331bf40ed78113d5"
dependencies = [
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.26" version = "0.1.26"
@@ -1459,6 +1482,7 @@ dependencies = [
"hmac-sha256", "hmac-sha256",
"md5", "md5",
"nyquest", "nyquest",
"rust_decimal",
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -44,3 +44,7 @@ bincode = { version = "2.0.1", optional = true }
# magic macro # magic macro
crabtime = { git = "https://github.com/wdanilo/crabtime.git", rev = "2ed856f5" } crabtime = { git = "https://github.com/wdanilo/crabtime.git", rev = "2ed856f5" }
rustc-hash = "2.1.1" rustc-hash = "2.1.1"
rust_decimal = { version = "1.37.2", default-features = false, features = [
"serde-with-arbitrary-precision",
"macros",
] }

View File

@@ -11,4 +11,4 @@ pub fn level_name(level: u32) -> &'static str {
} }
mod music_db; mod music_db;
pub use music_db::{MUSIC_DB, MusicInfo,}; pub use music_db::{Level, MUSIC_DB, MusicInfo};

View File

@@ -1,15 +1,26 @@
use std::{fs::OpenOptions, sync::LazyLock}; use std::{fs::OpenOptions, sync::LazyLock};
use rust_decimal::{Decimal, dec, serde::DecimalFromString};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use spdlog::{info, warn}; use spdlog::{info, warn};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MusicInfo { pub struct MusicInfo {
pub id: u32, pub id: u32,
pub name: String, pub name: String,
pub version: i64, 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>; type MusicDB = FxHashMap<u32, MusicInfo>;
@@ -30,3 +41,65 @@ pub static MUSIC_DB: LazyLock<Option<MusicDB>> = LazyLock::new(|| {
Some(db.into_iter().map(|entry| (entry.id, entry)).collect()) 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) -> u32 {
let achievement = Decimal::new(achievement as _, 4);
let difficulty_rank: Decimal = self.difficulty.value;
let factor = match () {
// larger than best achievement
_ if achievement >= dec!(101.0) => return 0,
_ if achievement >= SSS_PLUS_THRESHOLD => SSS_PLUS_FACTOR,
_ if achievement >= SSS_THRESHOLD => SSS_FACTOR,
_ if achievement >= SS_PLUS_THRESHOLD => SS_PLUS_FACTOR,
_ if achievement >= SS_THRESHOLD => SS_FACTOR,
_ if achievement >= S_PLUS_THRESHOLD => S_PLUS_FACTOR,
_ if achievement >= S_THRESHOLD => S_FACTOR,
_ if achievement >= AAA_THRESHOLD => AAA_FACTOR,
_ if achievement >= AA_THRESHOLD => AA_FACTOR,
_ if achievement >= A_THRESHOLD => A_FACTOR,
// lower than A rank, does not get rating.
_ => return 0,
};
(factor * difficulty_rank * achievement)
.floor()
.try_into()
.unwrap_or_default()
}
}
const SSS_PLUS_THRESHOLD: Decimal = dec!(100.5);
const SSS_PLUS_FACTOR: Decimal = dec!(0.224);
const SSS_THRESHOLD: Decimal = dec!(100);
const SSS_FACTOR: Decimal = dec!(0.216);
const SS_PLUS_THRESHOLD: Decimal = dec!(99.5);
const SS_PLUS_FACTOR: Decimal = dec!(0.211);
const SS_THRESHOLD: Decimal = dec!(99);
const SS_FACTOR: Decimal = dec!(0.208);
const S_PLUS_THRESHOLD: Decimal = dec!(98);
const S_PLUS_FACTOR: Decimal = dec!(0.203);
const S_THRESHOLD: Decimal = dec!(97);
const S_FACTOR: Decimal = dec!(0.2);
const AAA_THRESHOLD: Decimal = dec!(94);
const AAA_FACTOR: Decimal = dec!(0.168);
const AA_THRESHOLD: Decimal = dec!(90);
const AA_FACTOR: Decimal = dec!(0.152);
const A_THRESHOLD: Decimal = dec!(80);
const A_FACTOR: Decimal = dec!(0.136);

View File

@@ -40,8 +40,6 @@ pub struct UserRating {
pub struct MusicRating { pub struct MusicRating {
/// Maimai music id /// Maimai music id
pub music_id: u32, pub music_id: u32,
/// difficulty
///
/// - 0: BASIC /// - 0: BASIC
/// - 1: ADVANCED /// - 1: ADVANCED
/// - 2: EXPERT /// - 2: EXPERT
@@ -92,17 +90,28 @@ pub struct Udemae {
impl Display for GetUserRatingApiResp { impl Display for GetUserRatingApiResp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let b35 = &self.user_rating.rating_list;
let b15 = &self.user_rating.new_rating_list;
f.write_fmt(format_args!("用户ID: {}\n", self.user_id))?; f.write_fmt(format_args!("用户ID: {}\n", self.user_id))?;
f.write_str("\n---------- B35 ----------\n")?; f.write_str("\n--------- B35 ---------\n")?;
for b35 in &self.user_rating.rating_list { for music in b35 {
f.write_fmt(format_args!("{b35}\n---\n"))?; f.write_fmt(format_args!("{music}\n---\n"))?;
} }
f.write_str("\n---------- B15 ----------\n")?; f.write_str("\n--------- B15 ---------\n")?;
for b15 in &self.user_rating.new_rating_list { for music in b15 {
f.write_fmt(format_args!("{b15}\n---\n"))?; f.write_fmt(format_args!("{music}\n---\n"))?;
} }
let b35_rating: u32 = b35.iter().filter_map(|m| m.dx_rating()).sum();
let b15_rating: u32 = b15.iter().filter_map(|m| m.dx_rating()).sum();
f.write_str("\n--------- Total ---------\n")?;
f.write_fmt(format_args!("B35 Rating: {b35_rating}\n"))?;
f.write_fmt(format_args!("B15 Rating: {b15_rating}\n"))?;
f.write_fmt(format_args!("总 Rating: {}\n", b35_rating + b15_rating))?;
Ok(()) Ok(())
} }
} }
@@ -110,10 +119,11 @@ impl Display for GetUserRatingApiResp {
impl Display for MusicRating { impl Display for MusicRating {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("歌曲ID: \t{}\n", self.music_id))?; f.write_fmt(format_args!("歌曲ID: \t{}\n", self.music_id))?;
if let Some(info) = MUSIC_DB.as_ref().map(|db| db.get(&self.music_id)).flatten() {
let title = &info.name; if let Some(title) = self.music_title() {
f.write_fmt(format_args!("曲目标题: \t{title}\n"))?; f.write_fmt(format_args!("曲目标题: \t{title}\n"))?;
} }
f.write_fmt(format_args!( f.write_fmt(format_args!(
"谱面版本: \t{}\n", "谱面版本: \t{}\n",
match (self.music_id / 10000) % 10 { match (self.music_id / 10000) % 10 {
@@ -125,11 +135,36 @@ impl Display for MusicRating {
f.write_fmt(format_args!("游玩难度: \t{}\n", level_name(self.level)))?; f.write_fmt(format_args!("游玩难度: \t{}\n", level_name(self.level)))?;
f.write_fmt(format_args!( f.write_fmt(format_args!(
"达成率: \t{}.{:04}%", "达成率: \t{}.{:04}%\n",
self.achievement / 10000, self.achievement / 10000,
self.achievement % 10000 self.achievement % 10000
))?; ))?;
if let Some(dx_rating) = self.dx_rating() {
f.write_fmt(format_args!("DX RATING: \t{dx_rating}"))?;
}
Ok(()) Ok(())
} }
} }
impl MusicRating {
pub fn music_title(&self) -> Option<String> {
MUSIC_DB
.as_ref()?
.get(&self.music_id)
.map(|music_info| music_info.name.clone())
}
pub fn dx_rating(&self) -> Option<u32> {
Some(
MUSIC_DB
.as_ref()?
.get(&self.music_id)?
.levels
.iter()
.find(|d| d.level == self.level)?
.dx_rating(self.achievement),
)
}
}