feat: DX RATING calculation
This commit is contained in:
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
] }
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user