diff --git a/Cargo.lock b/Cargo.lock index b0aae03..ce4456f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,17 +1029,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "oppai-rs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e060483dec5ed6590103a045f0a0fe09c06e185d582ec66efaa87e24cc1e877e" -dependencies = [ - "bitflags", - "cc", - "libc", -] - [[package]] name = "parking_lot" version = "0.11.1" @@ -1349,6 +1338,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rosu-pp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4efb6f419a910e96683aada6e12c14f4a4b1da286f1a205a1edc3eca3b8fa69e" + [[package]] name = "rustbreak" version = "2.0.0" @@ -2308,9 +2303,9 @@ dependencies = [ "chrono", "dashmap", "lazy_static", - "oppai-rs", "regex", "reqwest", + "rosu-pp", "serde", "serde_json", "serenity", diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index 7307aee..5e92c14 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] } bitflags = "1" lazy_static = "1" regex = "1" -oppai-rs = "0.2" +rosu-pp = "0.4" dashmap = "4" bincode = "1" diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 4f6d882..9421dee 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -122,6 +122,7 @@ mod scores { } pub mod table { + use crate::discord::oppai_cache::Accuracy; use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache}; use crate::models::{Mode, Score}; use serenity::{framework::standard::CommandResult, model::channel::Message}; @@ -183,8 +184,8 @@ mod scores { let beatmap = osu.get_beatmap(play.beatmap_id, mode).await?; let info = { let b = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?; - mode.to_oppai_mode() - .and_then(|mode| b.get_info_with(Some(mode), play.mods).ok()) + (if mode == Mode::Std { Some(mode) } else { None }) + .and_then(|_| b.get_info_with(play.mods).ok()) }; Ok((beatmap, info)) as Result<(Beatmap, Option)> }) @@ -198,25 +199,23 @@ mod scores { Some(v) => Ok(v), None => { let b = beatmap_cache.get_beatmap(p.beatmap_id).await?; - let r: Result<_> = Ok(mode - .to_oppai_mode() - .and_then(|op| { - b.get_pp_from( - oppai_rs::Combo::NonFC { - max_combo: p.max_combo as u32, - misses: p.count_miss as u32, - }, - oppai_rs::Accuracy::from_hits( - p.count_100 as u32, - p.count_50 as u32, - ), - Some(op), - p.mods, - ) - .ok() - .map(|pp| format!("{:.2}pp [?]", pp)) - }) - .unwrap_or_else(|| "-".to_owned())); + let r: Result<_> = + Ok((if mode == Mode::Std { Some(mode) } else { None }) + .and_then(|_| { + b.get_pp_from( + Some(p.max_combo as usize), + Accuracy::ByCount( + p.count_300, + p.count_100, + p.count_50, + p.count_miss, + ), + p.mods, + ) + .ok() + .map(|pp| format!("{:.2}pp [?]", pp)) + }) + .unwrap_or_else(|| "-".to_owned())); r } } @@ -325,7 +324,7 @@ mod scores { page + 1, self.total_pages() )); - if self.mode.to_oppai_mode().is_none() { + if self.mode != Mode::Std { m.push_line("Note: star difficulty doesn't reflect mods applied."); } else { m.push_line("[?] means pp was predicted by oppai-rs."); @@ -400,12 +399,12 @@ mod beatmapset { async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Option { let data = ctx.data.read().await; let cache = data.get::().unwrap(); - let mode = self.mode.unwrap_or(b.mode).to_oppai_mode(); cache .get_beatmap(b.beatmap_id) .map(move |v| { v.ok() - .and_then(move |v| v.get_possible_pp_with(Some(mode?), self.mods).ok()) + .filter(|_| b.mode == Mode::Std) + .and_then(move |v| v.get_possible_pp_with(self.mods).ok()) }) .await } diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index 32e1096..cc8cbde 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -1,6 +1,6 @@ use super::BeatmapWithMode; use crate::{ - discord::oppai_cache::{BeatmapContent, BeatmapInfo, BeatmapInfoWithPP, OppaiAccuracy}, + discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfo, BeatmapInfoWithPP}, models::{Beatmap, Mode, Mods, Rank, Score, User}, }; use serenity::{builder::CreateEmbed, utils::MessageBuilder}; @@ -212,12 +212,14 @@ impl<'a> ScoreEmbedBuilder<'a> { let content = self.content; let u = self.u; let accuracy = s.accuracy(mode); - let info = mode - .to_oppai_mode() - .and_then(|mode| content.get_info_with(Some(mode), s.mods).ok()); + let info = if mode == Mode::Std { + content.get_info_with(s.mods).ok() + } else { + None + }; let stars = info .as_ref() - .map(|info| info.stars as f64) + .map(|info| info.stars) .unwrap_or(b.difficulty.stars); let score_line = match s.rank { Rank::SS | Rank::SSH => "SS".to_string(), @@ -239,13 +241,12 @@ impl<'a> ScoreEmbedBuilder<'a> { ), }; let pp = s.pp.map(|pp| (pp, format!("{:.2}pp", pp))).or_else(|| { - mode.to_oppai_mode() - .and_then(|op| { + (if mode == Mode::Std { Some(mode) } else { None }) + .and_then(|_| { content .get_pp_from( - oppai_rs::Combo::non_fc(s.max_combo as u32, s.count_miss as u32), - OppaiAccuracy::from_hits(s.count_100 as u32, s.count_50 as u32), - Some(op), + Some(s.max_combo as usize), + Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss), s.mods, ) .ok() @@ -253,13 +254,17 @@ impl<'a> ScoreEmbedBuilder<'a> { .map(|pp| (pp as f64, format!("{:.2}pp [?]", pp))) }); let pp = if !s.perfect { - mode.to_oppai_mode() - .and_then(|op| { + (if mode == Mode::Std { Some(mode) } else { None }) + .and_then(|_| { content .get_pp_from( - oppai_rs::Combo::FC(0), - OppaiAccuracy::from_hits(s.count_100 as u32, s.count_50 as u32), - Some(op), + None, + Accuracy::ByCount( + s.count_300 + s.count_miss, + s.count_100, + s.count_50, + 0, + ), s.mods, ) .ok() @@ -354,7 +359,7 @@ impl<'a> ScoreEmbedBuilder<'a> { ) .field("Map stats", diff.format_info(mode, s.mods, b), false); let mut footer = self.footer.take().unwrap_or_else(String::new); - if mode.to_oppai_mode().is_none() && s.mods != Mods::NOMOD { + if mode != Mode::Std && s.mods != Mods::NOMOD { footer += " Star difficulty does not reflect game mods."; } if !footer.is_empty() { diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 89ce2c6..d511ffb 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -122,13 +122,13 @@ fn handle_old_links<'a>( .map(|v| Mods::from_str(v.as_str()).pls_ok()) .flatten() .unwrap_or(Mods::NOMOD); - let info = match mode.unwrap_or(b.mode).to_oppai_mode() { - Some(mode) => cache + let info = match mode.unwrap_or(b.mode) { + Mode::Std => cache .get_beatmap(b.beatmap_id) .await - .and_then(|b| b.get_possible_pp_with(Some(mode), mods)) + .and_then(|b| b.get_possible_pp_with(mods)) .pls_ok(), - None => None, + _ => None, }; Some(ToPrint { embed: EmbedType::Beatmap(b, info, mods), @@ -192,13 +192,13 @@ fn handle_new_links<'a>( .name("mods") .and_then(|v| Mods::from_str(v.as_str()).pls_ok()) .unwrap_or(Mods::NOMOD); - let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() { - Some(mode) => cache + let info = match mode.unwrap_or(beatmap.mode) { + Mode::Std => cache .get_beatmap(beatmap.beatmap_id) .await - .and_then(|b| b.get_possible_pp_with(Some(mode), mods)) + .and_then(|b| b.get_possible_pp_with(mods)) .pls_ok(), - None => None, + _ => None, }; Some(ToPrint { embed: EmbedType::Beatmap(beatmap, info, mods), @@ -258,13 +258,13 @@ fn handle_short_links<'a>( .name("mods") .and_then(|v| Mods::from_str(v.as_str()).pls_ok()) .unwrap_or(Mods::NOMOD); - let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() { - Some(mode) => cache + let info = match mode.unwrap_or(beatmap.mode) { + Mode::Std => cache .get_beatmap(beatmap.beatmap_id) .await - .and_then(|b| b.get_possible_pp_with(Some(mode), mods)) + .and_then(|b| b.get_possible_pp_with(mods)) .pls_ok(), - None => None, + _ => None, }; let r: Result<_> = Ok(ToPrint { embed: EmbedType::Beatmap(beatmap, info, mods), diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index e803301..1372f3e 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -421,7 +421,7 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .unwrap() .get_beatmap(b.beatmap_id) .await? - .get_possible_pp_with(m.to_oppai_mode(), mods) + .get_possible_pp_with(mods) .ok(); msg.channel_id .send_message(&ctx, |f| { @@ -589,14 +589,14 @@ async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> C { Some(m) => { let beatmap = cache.get_beatmap(m.beatmap_id, mode).await?; - let info = match mode.to_oppai_mode() { - Some(mode) => Some( + let info = match mode { + Mode::Std => Some( oppai .get_beatmap(m.beatmap_id) .await? - .get_info_with(Some(mode), m.mods)?, + .get_info_with(m.mods)?, ), - None => None, + _ => None, }; Some((m, BeatmapWithMode(beatmap, mode), info)) } diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index ad86518..0a0208c 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -1,77 +1,106 @@ -use std::{ffi::CString, sync::Arc}; +use crate::mods::Mods; +use rosu_pp::{Beatmap, BeatmapExt}; +use std::sync::Arc; use youmubot_db_sql::{models::osu as models, Pool}; use youmubot_prelude::*; -pub use oppai_rs::Accuracy as OppaiAccuracy; - /// the information collected from a download/Oppai request. #[derive(Debug)] pub struct BeatmapContent { id: u64, - content: Arc, + content: Arc, } /// the output of "one" oppai run. #[derive(Clone, Copy, Debug)] pub struct BeatmapInfo { - pub objects: u32, - pub stars: f32, + pub objects: usize, + pub stars: f64, +} + +#[derive(Clone, Copy, Debug)] +pub enum Accuracy { + ByCount(u64, u64, u64, u64), // 300 / 100 / 50 / misses + #[allow(dead_code)] + ByValue(f64, u64), +} + +impl Into for Accuracy { + fn into(self) -> f64 { + match self { + Accuracy::ByValue(v, _) => v, + Accuracy::ByCount(n300, n100, n50, nmiss) => { + ((6 * n300 + 2 * n100 + n50) as f64) / ((6 * (n300 + n100 + n50 + nmiss)) as f64) + } + } + } +} + +impl Accuracy { + pub fn misses(&self) -> usize { + (match self { + Accuracy::ByCount(_, _, _, nmiss) => *nmiss, + Accuracy::ByValue(_, nmiss) => *nmiss, + }) as usize + } } /// Beatmap Info with attached 95/98/99/100% FC pp. -pub type BeatmapInfoWithPP = (BeatmapInfo, [f32; 4]); +pub type BeatmapInfoWithPP = (BeatmapInfo, [f64; 4]); impl BeatmapContent { /// Get pp given the combo and accuracy. - pub fn get_pp_from( - &self, - combo: oppai_rs::Combo, - accuracy: impl Into, - mode: Option, - mods: impl Into, - ) -> Result { - let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; - oppai.combo(combo)?.accuracy(accuracy)?.mods(mods.into()); - if let Some(mode) = mode { - oppai.mode(mode)?; + pub fn get_pp_from(&self, combo: Option, accuracy: Accuracy, mods: Mods) -> Result { + let bm = self.content.as_ref(); + let mut rosu = rosu_pp::OsuPP::new(bm).mods(mods.bits() as u32); + if let Some(combo) = combo { + rosu = rosu.combo(combo); } - Ok(oppai.pp()) + if let Accuracy::ByCount(n300, n100, n50, _) = accuracy { + rosu = rosu + .n300(n300 as usize) + .n100(n100 as usize) + .n50(n50 as usize); + } + Ok(rosu + .misses(accuracy.misses()) + .accuracy(accuracy.into()) + .calculate() + .pp) } /// Get info given mods. - pub fn get_info_with( - &self, - mode: Option, - mods: impl Into, - ) -> Result { - let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; - if let Some(mode) = mode { - oppai.mode(mode)?; - } - oppai.mods(mods.into()); - let objects = oppai.num_objects(); - let stars = oppai.stars(); - Ok(BeatmapInfo { objects, stars }) + pub fn get_info_with(&self, mods: Mods) -> Result { + let stars = self.content.stars(mods.bits() as u32, None); + Ok(BeatmapInfo { + objects: stars.max_combo().unwrap_or(0), + stars: stars.stars(), + }) } - pub fn get_possible_pp_with( - &self, - mode: Option, - mods: impl Into, - ) -> Result { - let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; - if let Some(mode) = mode { - oppai.mode(mode)?; - } - oppai.mods(mods.into()).combo(oppai_rs::Combo::PERFECT)?; + pub fn get_possible_pp_with(&self, mods: Mods) -> Result { + let rosu = || self.content.pp().mods(mods.bits() as u32); + let pp95 = rosu().accuracy(95.0).calculate(); let pp = [ - oppai.accuracy(95.0)?.pp(), - oppai.accuracy(98.0)?.pp(), - oppai.accuracy(99.0)?.pp(), - oppai.accuracy(100.0)?.pp(), + pp95.pp(), + rosu() + .attributes(pp95.clone()) + .accuracy(98.0) + .calculate() + .pp(), + rosu() + .attributes(pp95.clone()) + .accuracy(99.0) + .calculate() + .pp(), + rosu() + .attributes(pp95.clone()) + .accuracy(100.0) + .calculate() + .pp(), ]; - let objects = oppai.num_objects(); - let stars = oppai.stars(); + let objects = pp95.difficulty_attributes().max_combo().unwrap_or(0); + let stars = pp95.difficulty_attributes().stars(); Ok((BeatmapInfo { objects, stars }, pp)) } } @@ -99,40 +128,37 @@ impl BeatmapCache { .await? .bytes() .await?; - Ok(BeatmapContent { + let bm = BeatmapContent { id, - content: Arc::new(CString::new(content.into_iter().collect::>())?), - }) + content: Arc::new(Beatmap::parse(content.as_ref())?), + }; + + let mut bc = models::CachedBeatmapContent { + beatmap_id: id as i64, + cached_at: chrono::Utc::now(), + content: content.as_ref().to_owned(), + }; + bc.store(&self.pool).await?; + Ok(bm) } async fn get_beatmap_db(&self, id: u64) -> Result> { Ok(models::CachedBeatmapContent::by_id(id as i64, &self.pool) .await? - .map(|v| BeatmapContent { - id, - content: Arc::new(CString::new(v.content).unwrap()), - })) - } - - async fn save_beatmap(&self, b: &BeatmapContent) -> Result<()> { - let mut bc = models::CachedBeatmapContent { - beatmap_id: b.id as i64, - cached_at: chrono::Utc::now(), - content: b.content.as_ref().clone().into_bytes(), - }; - bc.store(&self.pool).await?; - Ok(()) + .map(|v| { + Ok(BeatmapContent { + id, + content: Arc::new(Beatmap::parse(&v.content[..])?), + }) as Result<_> + }) + .transpose()?) } /// Get a beatmap from the cache. pub async fn get_beatmap(&self, id: u64) -> Result { match self.get_beatmap_db(id).await? { Some(v) => Ok(v), - None => { - let m = self.download_beatmap(id).await?; - self.save_beatmap(&m).await?; - Ok(m) - } + None => self.download_beatmap(id).await, } } } diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index ab0bb5b..9d8992b 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -6,7 +6,7 @@ use super::{ use crate::{ discord::{ display::ScoreListStyle, - oppai_cache::{BeatmapCache, OppaiAccuracy}, + oppai_cache::{Accuracy, BeatmapCache}, BeatmapWithMode, }, models::{Mode, Mods, Score}, @@ -275,18 +275,9 @@ async fn show_leaderboard( let mode = bm.1; let oppai = data.get::().unwrap(); let oppai_map = oppai.get_beatmap(bm.0.beatmap_id).await?; - let get_oppai_pp = move |combo: u64, misses: u64, acc: OppaiAccuracy, mods: Mods| { - mode.to_oppai_mode().and_then(|mode| { - oppai_map - .get_pp_from( - oppai_rs::Combo::non_fc(combo as u32, misses as u32), - acc, - Some(mode), - mods, - ) - .ok() - .map(|v| v as f64) - }) + let get_oppai_pp = move |combo: u64, acc: Accuracy, mods: Mods| { + (if mode == Mode::Std { Some(mode) } else { None }) + .and_then(|_| oppai_map.get_pp_from(Some(combo as usize), acc, mods).ok()) }; let guild = m.guild_id.expect("Guild-only command"); @@ -321,10 +312,11 @@ async fn show_leaderboard( .or_else(|| { get_oppai_pp( score.max_combo, - score.count_miss, - OppaiAccuracy::from_hits( - score.count_100 as u32, - score.count_50 as u32, + Accuracy::ByCount( + score.count_300, + score.count_100, + score.count_50, + score.count_miss, ), score.mods, ) diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index d93fbe9..dfb93c9 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; use regex::Regex; +use rosu_pp::GameMode; use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; @@ -258,6 +259,17 @@ impl From for Mode { } } +impl From for GameMode { + fn from(n: Mode) -> Self { + match n { + Mode::Std => GameMode::STD, + Mode::Taiko => GameMode::TKO, + Mode::Catch => GameMode::CTB, + Mode::Mania => GameMode::MNA, + } + } +} + impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Mode::*; @@ -275,15 +287,6 @@ impl fmt::Display for Mode { } impl Mode { - /// Convert to oppai mode. - pub fn to_oppai_mode(self) -> Option { - Some(match self { - Mode::Std => oppai_rs::Mode::Std, - Mode::Taiko => oppai_rs::Mode::Taiko, - _ => return None, - }) - } - /// Parse from the display output of the enum itself. pub fn parse_from_display(s: &str) -> Option { Some(match s { diff --git a/youmubot-osu/src/models/mods.rs b/youmubot-osu/src/models/mods.rs index d6c2660..aef87f1 100644 --- a/youmubot-osu/src/models/mods.rs +++ b/youmubot-osu/src/models/mods.rs @@ -136,9 +136,3 @@ impl fmt::Display for Mods { Ok(()) } } - -impl From for oppai_rs::Mods { - fn from(m: Mods) -> Self { - oppai_rs::Mods::from_bits_truncate(m.bits() as i32) - } -}