From 17e59e71356abaa2ff3f8b870b7dd9ad70d48074 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 22 Oct 2023 17:19:32 +0200 Subject: [PATCH] Enable pp calculation for all modes --- youmubot-osu/src/discord/display.rs | 48 ++-- youmubot-osu/src/discord/embeds.rs | 78 +++--- youmubot-osu/src/discord/hook.rs | 31 ++- youmubot-osu/src/discord/mod.rs | 24 +- youmubot-osu/src/discord/oppai_cache.rs | 302 +++++++++++++++++++----- youmubot-osu/src/discord/server_rank.rs | 5 +- youmubot-osu/src/models/mod.rs | 4 +- 7 files changed, 325 insertions(+), 167 deletions(-) diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 9421dee..1aa90b3 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -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)> }) @@ -199,23 +198,22 @@ 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(|_| { - 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())); + let r: Result<_> = Ok({ + b.get_pp_from( + mode, + 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 } } @@ -389,24 +387,20 @@ mod beatmapset { struct Paginate { maps: Vec, - infos: Vec>>, + infos: Vec>, mode: Option, mods: Mods, message: String, } impl Paginate { - async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Option { + async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Result { let data = ctx.data.read().await; let cache = data.get::().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 } diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index 9b6702d..3c5255c 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -63,7 +63,7 @@ pub fn beatmap_offline_embed( ) -> Result &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, + 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( - Some(s.max_combo as usize), - Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss), - s.mods, - ) - .ok() - }) + 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( - None, - Accuracy::ByCount( - s.count_300 + s.count_miss, - s.count_100, - s.count_50, - 0, - ), - s.mods, - ) - .ok() - }) + content + .get_pp_from( + mode, + None, + 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)>, + 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> ") )) diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index e323f0d..0e6ae3d 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -163,7 +163,7 @@ pub fn hook<'a>( } enum EmbedType { - Beatmap(Beatmap, Option, Mods), + Beatmap(Beatmap, BeatmapInfoWithPP, Mods), Beatmapset(Vec), } @@ -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, + info: BeatmapInfoWithPP, link: &'_ str, mode: Option, mods: Mods, diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 1cbfd02..56cef4c 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -194,9 +194,15 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .into_iter() .next() .unwrap(); + let info = data + .get::() + .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 - .get_beatmap(m.beatmap_id) - .await? - .get_info_with(m.mods)?, - ), - _ => None, - }; + let info = oppai + .get_beatmap(m.beatmap_id) + .await? + .get_info_with(mode, m.mods)?; Some((m, BeatmapWithMode(beatmap, mode), info)) } None => None, diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index b2c64d2..978c5b1 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -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, 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); +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) -> 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 { - 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, 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, + accuracy: Accuracy, + mods: Mods, + ) -> Result { + 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 { - 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 { + 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 { + 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), + }) } } diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 4c3b8b2..cbcf681 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -277,8 +277,9 @@ async fn show_leaderboard( let oppai = data.get::().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"); diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 5fa7077..932eee4 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -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) -> 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() };