Temporarily move to rosu

This commit is contained in:
Natsu Kagami 2022-02-09 15:20:36 -05:00
parent a1975d5b54
commit 51db72acb2
Signed by: nki
GPG key ID: 7306B3D3C3AD6E51
10 changed files with 186 additions and 172 deletions

19
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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<BeatmapInfo>)>
})
@ -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<BeatmapInfoWithPP> {
let data = ctx.data.read().await;
let cache = data.get::<BeatmapCache>().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
}

View file

@ -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() {

View file

@ -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),

View file

@ -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))
}

View file

@ -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<CString>,
content: Arc<Beatmap>,
}
/// 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<f64> 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<OppaiAccuracy>,
mode: Option<oppai_rs::Mode>,
mods: impl Into<oppai_rs::Mods>,
) -> Result<f32> {
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<usize>, accuracy: Accuracy, mods: Mods) -> Result<f64> {
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<oppai_rs::Mode>,
mods: impl Into<oppai_rs::Mods>,
) -> Result<BeatmapInfo> {
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<BeatmapInfo> {
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<oppai_rs::Mode>,
mods: impl Into<oppai_rs::Mods>,
) -> Result<BeatmapInfoWithPP> {
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<BeatmapInfoWithPP> {
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::<Vec<_>>())?),
})
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<Option<BeatmapContent>> {
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<BeatmapContent> {
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,
}
}
}

View file

@ -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::<BeatmapCache>().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,
)

View file

@ -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<u8> for Mode {
}
}
impl From<Mode> 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<oppai_rs::Mode> {
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<Self> {
Some(match s {

View file

@ -136,9 +136,3 @@ impl fmt::Display for Mods {
Ok(())
}
}
impl From<Mods> for oppai_rs::Mods {
fn from(m: Mods) -> Self {
oppai_rs::Mods::from_bits_truncate(m.bits() as i32)
}
}