diff --git a/Cargo.lock b/Cargo.lock index f7980f0..f535d7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1342,6 +1342,29 @@ dependencies = [ "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]] name = "rustc-demangle" version = "0.1.26" @@ -1459,6 +1482,7 @@ dependencies = [ "hmac-sha256", "md5", "nyquest", + "rust_decimal", "rustc-hash", "serde", "serde_json", diff --git a/sdgb-api/Cargo.toml b/sdgb-api/Cargo.toml index f5b8dbf..2260fe5 100644 --- a/sdgb-api/Cargo.toml +++ b/sdgb-api/Cargo.toml @@ -44,3 +44,7 @@ bincode = { version = "2.0.1", optional = true } # magic macro crabtime = { git = "https://github.com/wdanilo/crabtime.git", rev = "2ed856f5" } rustc-hash = "2.1.1" +rust_decimal = { version = "1.37.2", default-features = false, features = [ + "serde-with-arbitrary-precision", + "macros", +] } diff --git a/sdgb-api/src/helper/mod.rs b/sdgb-api/src/helper/mod.rs index 6ff2a5a..faead71 100644 --- a/sdgb-api/src/helper/mod.rs +++ b/sdgb-api/src/helper/mod.rs @@ -11,4 +11,4 @@ pub fn level_name(level: u32) -> &'static str { } mod music_db; -pub use music_db::{MUSIC_DB, MusicInfo,}; +pub use music_db::{Level, MUSIC_DB, MusicInfo}; diff --git a/sdgb-api/src/helper/music_db/mod.rs b/sdgb-api/src/helper/music_db/mod.rs index 06ec456..c21d5c5 100644 --- a/sdgb-api/src/helper/music_db/mod.rs +++ b/sdgb-api/src/helper/music_db/mod.rs @@ -1,15 +1,26 @@ use std::{fs::OpenOptions, sync::LazyLock}; +use rust_decimal::{Decimal, dec, serde::DecimalFromString}; use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use spdlog::{info, warn}; -#[derive(Default, Debug, Clone, PartialEq, Serialize, 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; @@ -30,3 +41,65 @@ pub static MUSIC_DB: LazyLock> = LazyLock::new(|| { 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); diff --git a/sdgb-api/src/title/model/get_user_rating_api/mod.rs b/sdgb-api/src/title/model/get_user_rating_api/mod.rs index 7bfba50..dc08583 100644 --- a/sdgb-api/src/title/model/get_user_rating_api/mod.rs +++ b/sdgb-api/src/title/model/get_user_rating_api/mod.rs @@ -40,8 +40,6 @@ pub struct UserRating { pub struct MusicRating { /// Maimai music id pub music_id: u32, - /// difficulty - /// /// - 0: BASIC /// - 1: ADVANCED /// - 2: EXPERT @@ -92,17 +90,28 @@ pub struct Udemae { impl Display for GetUserRatingApiResp { 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_str("\n---------- B35 ----------\n")?; - for b35 in &self.user_rating.rating_list { - f.write_fmt(format_args!("{b35}\n---\n"))?; + f.write_str("\n--------- B35 ---------\n")?; + for music in b35 { + f.write_fmt(format_args!("{music}\n---\n"))?; } - f.write_str("\n---------- B15 ----------\n")?; - for b15 in &self.user_rating.new_rating_list { - f.write_fmt(format_args!("{b15}\n---\n"))?; + f.write_str("\n--------- B15 ---------\n")?; + for music in b15 { + 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(()) } } @@ -110,10 +119,11 @@ impl Display for GetUserRatingApiResp { impl Display for MusicRating { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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{}\n", 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{}.{:04}%", + "达成率: \t{}.{:04}%\n", 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(()) } } + +impl MusicRating { + pub fn music_title(&self) -> Option { + MUSIC_DB + .as_ref()? + .get(&self.music_id) + .map(|music_info| music_info.name.clone()) + } + + pub fn dx_rating(&self) -> Option { + Some( + MUSIC_DB + .as_ref()? + .get(&self.music_id)? + .levels + .iter() + .find(|d| d.level == self.level)? + .dx_rating(self.achievement), + ) + } +}