Enable pp calculation for all modes

This commit is contained in:
Natsu Kagami 2023-10-22 17:19:32 +02:00
parent a04fcca1d6
commit 17e59e7135
Signed by: nki
GPG key ID: 55A032EB38B49ADB
7 changed files with 325 additions and 167 deletions

View file

@ -184,8 +184,7 @@ mod scores {
let beatmap = osu.get_beatmap(play.beatmap_id, mode).await?;
let info = {
let b = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?;
(if mode == Mode::Std { Some(mode) } else { None })
.and_then(|_| b.get_info_with(play.mods).ok())
b.get_info_with(mode, play.mods).ok()
};
Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)>
})
@ -199,10 +198,9 @@ mod scores {
Some(v) => Ok(v),
None => {
let b = beatmap_cache.get_beatmap(p.beatmap_id).await?;
let r: Result<_> =
Ok((if mode == Mode::Std { Some(mode) } else { None })
.and_then(|_| {
let r: Result<_> = Ok({
b.get_pp_from(
mode,
Some(p.max_combo as usize),
Accuracy::ByCount(
p.count_300,
@ -214,7 +212,7 @@ mod scores {
)
.ok()
.map(|pp| format!("{:.2}pp [?]", pp))
})
}
.unwrap_or_else(|| "-".to_owned()));
r
}
@ -389,24 +387,20 @@ mod beatmapset {
struct Paginate {
maps: Vec<Beatmap>,
infos: Vec<Option<Option<BeatmapInfoWithPP>>>,
infos: Vec<Option<BeatmapInfoWithPP>>,
mode: Option<Mode>,
mods: Mods,
message: String,
}
impl Paginate {
async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Option<BeatmapInfoWithPP> {
async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Result<BeatmapInfoWithPP> {
let data = ctx.data.read().await;
let cache = data.get::<BeatmapCache>().unwrap();
cache
.get_beatmap(b.beatmap_id)
.map(move |v| {
v.ok()
.filter(|_| b.mode == Mode::Std)
.and_then(move |v| v.get_possible_pp_with(self.mods).ok())
})
.await
.and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), self.mods))
}
}
@ -440,7 +434,7 @@ mod beatmapset {
let info = match &self.infos[page] {
Some(info) => *info,
None => {
let info = self.get_beatmap_info(ctx, map).await;
let info = self.get_beatmap_info(ctx, map).await?;
self.infos[page] = Some(info);
info
}

View file

@ -63,7 +63,7 @@ pub fn beatmap_offline_embed(
) -> Result<Box<dyn FnOnce(&mut CreateEmbed) -> &mut CreateEmbed + Send + Sync>> {
let bm = b.content.clone();
let metadata = b.metadata.clone();
let (info, pp) = b.get_possible_pp_with(mods)?;
let (info, pp) = b.get_possible_pp_with(m, mods)?;
let total_length = if bm.hit_objects.len() >= 1 {
Duration::from_millis(
@ -90,7 +90,7 @@ pub fn beatmap_offline_embed(
drain_length: total_length, // It's hard to calculate so maybe just skip...
total_length,
}
.apply_mods(mods, Some(info.stars));
.apply_mods(mods, info.stars);
Ok(Box::new(move |c: &mut CreateEmbed| {
c.title(beatmap_title(
&metadata.artist,
@ -145,12 +145,10 @@ pub fn beatmap_embed<'a>(
b: &'_ Beatmap,
m: Mode,
mods: Mods,
info: Option<BeatmapInfoWithPP>,
info: BeatmapInfoWithPP,
c: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed {
let diff = b
.difficulty
.apply_mods(mods, info.map(|(v, _)| v.stars as f64));
let diff = b.difficulty.apply_mods(mods, info.0.stars);
c.title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods))
.author(|a| {
a.name(&b.creator)
@ -160,24 +158,19 @@ pub fn beatmap_embed<'a>(
.url(b.link())
.image(b.cover_url())
.color(0xffb6c1)
.fields(info.map(|(_, pp)| {
(
.fields({
let pp = info.1;
std::iter::once((
"Calculated pp",
format!(
"95%: **{:.2}**pp, 98%: **{:.2}**pp, 99%: **{:.2}**pp, 100%: **{:.2}**pp",
pp[0], pp[1], pp[2], pp[3]
),
false,
)
}))
))
})
.field("Information", diff.format_info(m, mods, b), false)
.description(beatmap_description(b))
.footer(|f| {
if info.is_none() && mods != Mods::NOMOD {
f.text("Star difficulty not reflecting mods applied.");
}
f
})
}
const MAX_DIFFS: usize = 25 - 4;
@ -283,11 +276,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
let content = self.content;
let u = self.u;
let accuracy = s.accuracy(mode);
let info = if mode == Mode::Std {
content.get_info_with(s.mods).ok()
} else {
None
};
let info = content.get_info_with(mode, s.mods).ok();
let stars = info
.as_ref()
.map(|info| info.stars)
@ -312,34 +301,25 @@ impl<'a> ScoreEmbedBuilder<'a> {
),
};
let pp = s.pp.map(|pp| (pp, format!("{:.2}pp", pp))).or_else(|| {
(if mode == Mode::Std { Some(mode) } else { None })
.and_then(|_| {
content
.get_pp_from(
mode,
Some(s.max_combo as usize),
Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss),
s.mods,
)
.ok()
})
.map(|pp| (pp as f64, format!("{:.2}pp [?]", pp)))
});
let pp = if !s.perfect {
(if mode == Mode::Std { Some(mode) } else { None })
.and_then(|_| {
content
.get_pp_from(
mode,
None,
Accuracy::ByCount(
s.count_300 + s.count_miss,
s.count_100,
s.count_50,
0,
),
Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0),
s.mods,
)
.ok()
})
.filter(|&v| {
pp.as_ref()
.map(|&(origin, _)| origin < v as f64)
@ -382,7 +362,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
.world_record
.map(|v| format!("| #{} on Global Rankings!", v))
.unwrap_or_else(|| "".to_owned());
let diff = b.difficulty.apply_mods(s.mods, Some(stars));
let diff = b.difficulty.apply_mods(s.mods, stars);
let creator = if b.difficulty_name.contains("'s") {
"".to_owned()
} else {
@ -442,7 +422,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
pub(crate) fn user_embed(
u: User,
best: Option<(Score, BeatmapWithMode, Option<BeatmapInfo>)>,
best: Option<(Score, BeatmapWithMode, BeatmapInfo)>,
m: &mut CreateEmbed,
) -> &mut CreateEmbed {
m.title(u.username)
@ -521,7 +501,7 @@ pub(crate) fn user_embed(
.push(format!(
"> {}",
map.difficulty
.apply_mods(v.mods, info.map(|i| i.stars as f64))
.apply_mods(v.mods, info.stars as f64)
.format_info(mode, v.mods, &map)
.replace("\n", "\n> ")
))

View file

@ -163,7 +163,7 @@ pub fn hook<'a>(
}
enum EmbedType {
Beatmap(Beatmap, Option<BeatmapInfoWithPP>, Mods),
Beatmap(Beatmap, BeatmapInfoWithPP, Mods),
Beatmapset(Vec<Beatmap>),
}
@ -217,13 +217,12 @@ 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) {
Mode::Std => cache
let info = {
let mode = mode.unwrap_or(b.mode);
cache
.get_beatmap(b.beatmap_id)
.await
.and_then(|b| b.get_possible_pp_with(mods))
.pls_ok(),
_ => None,
.and_then(|b| b.get_possible_pp_with(mode, mods))?
};
Some(ToPrint {
embed: EmbedType::Beatmap(b, info, mods),
@ -287,13 +286,12 @@ 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) {
Mode::Std => cache
let info = {
let mode = mode.unwrap_or(beatmap.mode);
cache
.get_beatmap(beatmap.beatmap_id)
.await
.and_then(|b| b.get_possible_pp_with(mods))
.pls_ok(),
_ => None,
.and_then(|b| b.get_possible_pp_with(mode, mods))?
};
Some(ToPrint {
embed: EmbedType::Beatmap(beatmap, info, mods),
@ -353,13 +351,12 @@ 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) {
Mode::Std => cache
let info = {
let mode = mode.unwrap_or(beatmap.mode);
cache
.get_beatmap(beatmap.beatmap_id)
.await
.and_then(|b| b.get_possible_pp_with(mods))
.pls_ok(),
_ => None,
.and_then(|b| b.get_possible_pp_with(mode, mods))?
};
let r: Result<_> = Ok(ToPrint {
embed: EmbedType::Beatmap(beatmap, info, mods),
@ -383,7 +380,7 @@ fn handle_short_links<'a>(
async fn handle_beatmap<'a, 'b>(
ctx: &Context,
beatmap: &Beatmap,
info: Option<BeatmapInfoWithPP>,
info: BeatmapInfoWithPP,
link: &'_ str,
mode: Option<Mode>,
mods: Mods,

View file

@ -194,9 +194,15 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.into_iter()
.next()
.unwrap();
let info = data
.get::<BeatmapCache>()
.unwrap()
.get_beatmap(beatmap.beatmap_id)
.await?
.get_possible_pp_with(Mode::Std, Mods::NOMOD)?;
msg.await?
.edit(&ctx, |f| {
f.embed(|e| beatmap_embed(&beatmap, Mode::Std, Mods::NOMOD, None, e))
f.embed(|e| beatmap_embed(&beatmap, Mode::Std, Mods::NOMOD, info, e))
})
.await?;
return Ok(());
@ -478,8 +484,7 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.unwrap()
.get_beatmap(b.beatmap_id)
.await?
.get_possible_pp_with(mods)
.ok();
.get_possible_pp_with(m, mods)?;
msg.channel_id
.send_message(&ctx, |f| {
f.content("Here is the beatmap you requested!")
@ -646,15 +651,10 @@ 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 {
Mode::Std => Some(
oppai
let info = oppai
.get_beatmap(m.beatmap_id)
.await?
.get_info_with(m.mods)?,
),
_ => None,
};
.get_info_with(mode, m.mods)?;
Some((m, BeatmapWithMode(beatmap, mode), info))
}
None => None,

View file

@ -1,6 +1,10 @@
use crate::mods::Mods;
use crate::{models::Mode, mods::Mods};
use osuparse::MetadataSection;
use rosu_pp::{Beatmap, BeatmapExt};
use rosu_pp::catch::CatchDifficultyAttributes;
use rosu_pp::mania::ManiaDifficultyAttributes;
use rosu_pp::osu::OsuDifficultyAttributes;
use rosu_pp::taiko::TaikoDifficultyAttributes;
use rosu_pp::{AttributeProvider, Beatmap, CatchPP, DifficultyAttributes, ManiaPP, OsuPP, TaikoPP};
use std::io::Read;
use std::sync::Arc;
use youmubot_db_sql::{models::osu as models, Pool};
@ -21,6 +25,16 @@ pub struct BeatmapInfo {
pub stars: f64,
}
impl BeatmapInfo {
fn extract(beatmap: &Beatmap, attrs: DifficultyAttributes) -> Self {
BeatmapInfo {
objects: beatmap.hit_objects.len(),
max_combo: attrs.max_combo(),
stars: attrs.stars(),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Accuracy {
ByCount(u64, u64, u64, u64), // 300 / 100 / 50 / misses
@ -52,68 +66,240 @@ impl Accuracy {
/// Beatmap Info with attached 95/98/99/100% FC pp.
pub type BeatmapInfoWithPP = (BeatmapInfo, [f64; 4]);
impl BeatmapContent {
/// Get pp given the combo and accuracy.
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);
trait PPCalc<'a>: Sized {
type Attrs: rosu_pp::AttributeProvider + Clone;
fn new(beatmap: &'a Beatmap) -> Self;
fn mods(self, mods: u32) -> Self;
fn attributes(self, attrs: Self::Attrs) -> Self;
/* For pp calculation */
fn combo(self, combo: usize) -> Self;
fn accuracy(self, accuracy: f64) -> Self;
fn misses(self, misses: usize) -> Self;
fn get_pp(self) -> f64;
/* For difficulty calculation */
fn get_attrs(self) -> Self::Attrs;
fn combo_opt(self, combo: Option<usize>) -> Self {
match combo {
Some(c) => self.combo(c),
None => self,
}
if let Accuracy::ByCount(n300, n100, n50, _) = accuracy {
rosu = rosu
.n300(n300 as usize)
.n100(n100 as usize)
.n50(n50 as usize);
}
Ok(rosu
.n_misses(accuracy.misses())
.accuracy(accuracy.into())
.calculate()
.pp)
fn accuracy_from(self, accuracy: Accuracy) -> Self {
self.misses(accuracy.misses()).accuracy(accuracy.into())
}
/// Get info given mods.
pub fn get_info_with(&self, mods: Mods) -> Result<BeatmapInfo> {
let stars = self.content.stars().mods(mods.bits() as u32).calculate();
Ok(BeatmapInfo {
max_combo: stars.max_combo(),
objects: self.content.hit_objects.len(),
stars: stars.stars(),
fn map_attributes(beatmap: &'a Beatmap, mods: Mods) -> Self::Attrs {
Self::new(beatmap).mods(mods.bits() as u32).get_attrs()
}
fn map_pp(beatmap: &'a Beatmap, mods: Mods, combo: Option<usize>, accuracy: Accuracy) -> f64 {
Self::new(beatmap)
.mods(mods.bits() as u32)
.combo_opt(combo)
.accuracy_from(accuracy)
.get_pp()
}
fn map_info(beatmap: &'a Beatmap, mods: Mods) -> BeatmapInfo {
let attrs = Self::map_attributes(beatmap, mods).attributes();
BeatmapInfo::extract(beatmap, attrs)
}
fn map_info_with_pp(beatmap: &'a Beatmap, mods: Mods) -> BeatmapInfoWithPP {
let attrs = Self::map_attributes(beatmap, mods);
let nw = || {
Self::new(beatmap)
.mods(mods.bits() as u32)
.attributes(attrs.clone())
};
let pps = [
nw().accuracy_from(Accuracy::ByValue(95.0, 0)).get_pp(),
nw().accuracy_from(Accuracy::ByValue(98.0, 0)).get_pp(),
nw().accuracy_from(Accuracy::ByValue(99.0, 0)).get_pp(),
nw().accuracy_from(Accuracy::ByValue(100.0, 0)).get_pp(),
];
let info = BeatmapInfo::extract(beatmap, attrs.attributes());
(info, pps)
}
}
impl<'a> PPCalc<'a> for OsuPP<'a> {
type Attrs = OsuDifficultyAttributes;
fn new(beatmap: &'a Beatmap) -> Self {
Self::new(beatmap)
}
fn mods(self, mods: u32) -> Self {
self.mods(mods)
}
fn attributes(self, attrs: Self::Attrs) -> Self {
self.attributes(attrs)
}
fn combo(self, combo: usize) -> Self {
self.combo(combo)
}
fn accuracy(self, accuracy: f64) -> Self {
self.accuracy(accuracy)
}
fn misses(self, misses: usize) -> Self {
self.n_misses(misses)
}
fn get_pp(self) -> f64 {
self.calculate().pp()
}
fn get_attrs(self) -> Self::Attrs {
self.calculate().difficulty
}
}
impl<'a> PPCalc<'a> for TaikoPP<'a> {
type Attrs = TaikoDifficultyAttributes;
fn new(beatmap: &'a Beatmap) -> Self {
Self::new(beatmap)
}
fn mods(self, mods: u32) -> Self {
self.mods(mods)
}
fn attributes(self, attrs: Self::Attrs) -> Self {
self.attributes(attrs)
}
fn combo(self, combo: usize) -> Self {
self.combo(combo)
}
fn accuracy(self, accuracy: f64) -> Self {
self.accuracy(accuracy)
}
fn misses(self, misses: usize) -> Self {
self.n_misses(misses)
}
fn get_pp(self) -> f64 {
self.calculate().pp()
}
fn get_attrs(self) -> Self::Attrs {
self.calculate().difficulty
}
}
impl<'a> PPCalc<'a> for CatchPP<'a> {
type Attrs = CatchDifficultyAttributes;
fn new(beatmap: &'a Beatmap) -> Self {
Self::new(beatmap)
}
fn mods(self, mods: u32) -> Self {
self.mods(mods)
}
fn attributes(self, attrs: Self::Attrs) -> Self {
self.attributes(attrs)
}
fn combo(self, combo: usize) -> Self {
self.combo(combo)
}
fn accuracy(self, accuracy: f64) -> Self {
self.accuracy(accuracy)
}
fn misses(self, misses: usize) -> Self {
self.misses(misses)
}
fn get_pp(self) -> f64 {
self.calculate().pp()
}
fn get_attrs(self) -> Self::Attrs {
self.calculate().difficulty
}
}
impl<'a> PPCalc<'a> for ManiaPP<'a> {
type Attrs = ManiaDifficultyAttributes;
fn new(beatmap: &'a Beatmap) -> Self {
Self::new(beatmap)
}
fn mods(self, mods: u32) -> Self {
self.mods(mods)
}
fn attributes(self, attrs: Self::Attrs) -> Self {
self.attributes(attrs)
}
fn combo(self, _combo: usize) -> Self {
// Mania doesn't seem to care about combo?
self
}
fn accuracy(self, accuracy: f64) -> Self {
self.accuracy(accuracy)
}
fn misses(self, misses: usize) -> Self {
self.n_misses(misses)
}
fn get_pp(self) -> f64 {
self.calculate().pp()
}
fn get_attrs(self) -> Self::Attrs {
self.calculate().difficulty
}
}
impl BeatmapContent {
/// Get pp given the combo and accuracy.
pub fn get_pp_from(
&self,
mode: Mode,
combo: Option<usize>,
accuracy: Accuracy,
mods: Mods,
) -> Result<f64> {
let bm = self.content.as_ref();
Ok(match mode {
Mode::Std => OsuPP::map_pp(bm, mods, combo, accuracy),
Mode::Taiko => TaikoPP::map_pp(bm, mods, combo, accuracy),
Mode::Catch => CatchPP::map_pp(bm, mods, combo, accuracy),
Mode::Mania => ManiaPP::map_pp(bm, mods, combo, accuracy),
})
}
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 = [
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 max_combo = pp95.difficulty_attributes().max_combo();
let stars = pp95.difficulty_attributes().stars();
Ok((
BeatmapInfo {
objects: self.content.hit_objects.len(),
max_combo,
stars,
},
pp,
))
/// Get info given mods.
pub fn get_info_with(&self, mode: Mode, mods: Mods) -> Result<BeatmapInfo> {
let bm = self.content.as_ref();
Ok(match mode {
Mode::Std => OsuPP::map_info(bm, mods),
Mode::Taiko => TaikoPP::map_info(bm, mods),
Mode::Catch => CatchPP::map_info(bm, mods),
Mode::Mania => ManiaPP::map_info(bm, mods),
})
}
pub fn get_possible_pp_with(&self, mode: Mode, mods: Mods) -> Result<BeatmapInfoWithPP> {
let bm = self.content.as_ref();
Ok(match mode {
Mode::Std => OsuPP::map_info_with_pp(bm, mods),
Mode::Taiko => TaikoPP::map_info_with_pp(bm, mods),
Mode::Catch => CatchPP::map_info_with_pp(bm, mods),
Mode::Mania => ManiaPP::map_info_with_pp(bm, mods),
})
}
}

View file

@ -277,8 +277,9 @@ async fn show_leaderboard(
let oppai = data.get::<BeatmapCache>().unwrap();
let oppai_map = oppai.get_beatmap(bm.0.beatmap_id).await?;
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())
oppai_map
.get_pp_from(mode, Some(combo as usize), acc, mods)
.ok()
};
let guild = m.guild_id.expect("Guild-only command");

View file

@ -99,9 +99,9 @@ impl Difficulty {
}
/// Apply mods to the given difficulty.
/// Note that `stars`, `aim` and `speed` cannot be calculated from this alone.
pub fn apply_mods(&self, mods: Mods, updated_stars: Option<f64>) -> Difficulty {
pub fn apply_mods(&self, mods: Mods, updated_stars: f64) -> Difficulty {
let mut diff = Difficulty {
stars: updated_stars.unwrap_or(self.stars),
stars: updated_stars,
..self.clone()
};