From ae4d6cb10eef829e741937d8e6a807232d270445 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Fri, 28 Jun 2024 21:32:20 +0200 Subject: [PATCH 1/5] use rosu-mods --- Cargo.toml | 1 + src/any/difficulty/inspect.rs | 8 +- src/any/difficulty/mod.rs | 64 ++++++---- src/any/performance/mod.rs | 9 +- src/catch/performance/mod.rs | 26 ++-- src/lib.rs | 5 +- src/mania/performance/mod.rs | 24 ++-- src/model/beatmap/attributes.rs | 111 ++++++++--------- src/model/beatmap/mod.rs | 2 +- src/model/mod.rs | 2 + src/model/mods.rs | 156 ++++++++++++++++++++++++ src/osu/difficulty/gradual.rs | 1 - src/osu/difficulty/mod.rs | 5 +- src/osu/difficulty/skills/flashlight.rs | 5 +- src/osu/difficulty/skills/mod.rs | 7 +- src/osu/performance/mod.rs | 24 ++-- src/taiko/performance/mod.rs | 24 ++-- src/util/mod.rs | 1 - src/util/mods.rs | 53 -------- 19 files changed, 345 insertions(+), 183 deletions(-) create mode 100644 src/model/mods.rs delete mode 100644 src/util/mods.rs diff --git a/Cargo.toml b/Cargo.toml index 732afb4e..6bb0d113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ tracing = ["rosu-map/tracing"] [dependencies] rosu-map = { version = "0.1.1" } +rosu-mods = { path = "../rosu-mods", default-features = false } [dev-dependencies] proptest = "1.4.0" diff --git a/src/any/difficulty/inspect.rs b/src/any/difficulty/inspect.rs index 0021a712..1f76ac94 100644 --- a/src/any/difficulty/inspect.rs +++ b/src/any/difficulty/inspect.rs @@ -1,14 +1,12 @@ -use crate::Difficulty; +use crate::{model::mods::GameMods, Difficulty}; use super::ModsDependent; /// [`Difficulty`] but all fields are public for inspection. #[derive(Clone, Debug, Default, PartialEq)] pub struct InspectDifficulty { - /// Specify mods through their bit values. - /// - /// See - pub mods: u32, + /// Specify mods. + pub mods: GameMods, /// Amount of passed objects for partial plays, e.g. a fail. pub passed_objects: Option, /// Adjust the clock rate used in the calculation. diff --git a/src/any/difficulty/mod.rs b/src/any/difficulty/mod.rs index 51575a58..a9c81304 100644 --- a/src/any/difficulty/mod.rs +++ b/src/any/difficulty/mod.rs @@ -9,7 +9,10 @@ use rosu_map::section::general::GameMode; use crate::{ catch::Catch, mania::Mania, - model::beatmap::{Beatmap, Converted}, + model::{ + beatmap::{Beatmap, Converted}, + mods::GameMods, + }, osu::Osu, taiko::Taiko, GradualDifficulty, GradualPerformance, @@ -25,7 +28,7 @@ pub mod inspect; pub mod object; pub mod skills; -use crate::{model::mode::IGameMode, util::mods::Mods}; +use crate::model::mode::IGameMode; /// Difficulty calculator on maps of any mode. /// @@ -43,7 +46,7 @@ use crate::{model::mode::IGameMode, util::mods::Mods}; #[derive(Clone, PartialEq)] #[must_use] pub struct Difficulty { - mods: u32, + mods: GameMods, passed_objects: Option, /// Clock rate will be clamped internally between 0.01 and 100.0. /// @@ -86,7 +89,7 @@ impl Difficulty { /// Create a new difficulty calculator. pub const fn new() -> Self { Self { - mods: 0, + mods: GameMods::DEFAULT, passed_objects: None, clock_rate: None, ar: None, @@ -122,7 +125,7 @@ impl Difficulty { InspectDifficulty { mods, passed_objects, - clock_rate: clock_rate.map(non_zero_u32_to_f64), + clock_rate: clock_rate.map(non_zero_u32_to_f32).map(f64::from), ar, cs, hp, @@ -131,21 +134,30 @@ impl Difficulty { } } - /// Specify mods through their bit values. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) /// /// See - pub const fn mods(self, mods: u32) -> Self { - Self { mods, ..self } - } - - /// Amount of passed objects for partial plays, e.g. a fail. - pub const fn passed_objects(self, passed_objects: u32) -> Self { + pub fn mods(self, mods: impl Into) -> Self { Self { - passed_objects: Some(passed_objects), + mods: mods.into(), ..self } } + /// Amount of passed objects for partial plays, e.g. a fail. + pub const fn passed_objects(mut self, passed_objects: u32) -> Self { + self.passed_objects = Some(passed_objects); + + self + } + /// Adjust the clock rate used in the calculation. /// /// If none is specified, it will take the clock rate based on the mods @@ -250,11 +262,10 @@ impl Difficulty { /// Adjust patterns as if the HR mod is enabled. /// /// Only relevant for osu!catch. - pub const fn hardrock_offsets(self, hardrock_offsets: bool) -> Self { - Self { - hardrock_offsets: Some(hardrock_offsets), - ..self - } + pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self { + self.hardrock_offsets = Some(hardrock_offsets); + + self } /// Perform the difficulty calculation. @@ -300,13 +311,16 @@ impl Difficulty { GradualPerformance::new(self, map) } - pub(crate) const fn get_mods(&self) -> u32 { - self.mods + pub(crate) const fn get_mods(&self) -> &GameMods { + &self.mods } pub(crate) fn get_clock_rate(&self) -> f64 { - self.clock_rate - .map_or(self.mods.clock_rate(), non_zero_u32_to_f64) + let clock_rate = self + .clock_rate + .map_or(self.mods.clock_rate(), non_zero_u32_to_f32); + + f64::from(clock_rate) } pub(crate) fn get_passed_objects(&self) -> usize { @@ -334,8 +348,8 @@ impl Difficulty { } } -fn non_zero_u32_to_f64(n: NonZeroU32) -> f64 { - f64::from(f32::from_bits(n.get())) +fn non_zero_u32_to_f32(n: NonZeroU32) -> f32 { + f32::from_bits(n.get()) } impl Debug for Difficulty { @@ -354,7 +368,7 @@ impl Debug for Difficulty { f.debug_struct("Difficulty") .field("mods", mods) .field("passed_objects", passed_objects) - .field("clock_rate", &clock_rate.map(non_zero_u32_to_f64)) + .field("clock_rate", &clock_rate.map(non_zero_u32_to_f32)) .field("ar", ar) .field("cs", cs) .field("hp", hp) diff --git a/src/any/performance/mod.rs b/src/any/performance/mod.rs index 3e0647db..5ef1e869 100644 --- a/src/any/performance/mod.rs +++ b/src/any/performance/mod.rs @@ -98,7 +98,14 @@ impl<'map> Performance<'map> { } } - /// Specify mods through their bit values. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) /// /// See pub fn mods(self, mods: u32) -> Self { diff --git a/src/catch/performance/mod.rs b/src/catch/performance/mod.rs index 25a913aa..2645b951 100644 --- a/src/catch/performance/mod.rs +++ b/src/catch/performance/mod.rs @@ -2,8 +2,9 @@ use std::cmp::{self, Ordering}; use crate::{ any::{Difficulty, IntoModePerformance, IntoPerformance}, + model::mods::GameMods, osu::OsuPerformance, - util::{map_or_attrs::MapOrAttrs, mods::Mods}, + util::map_or_attrs::MapOrAttrs, Performance, }; @@ -70,10 +71,17 @@ impl<'map> CatchPerformance<'map> { } } - /// Specify mods through their bit values. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) /// /// See - pub const fn mods(mut self, mods: u32) -> Self { + pub fn mods(mut self, mods: impl Into) -> Self { self.difficulty = self.difficulty.mods(mods); self @@ -122,7 +130,7 @@ impl<'map> CatchPerformance<'map> { } /// Use the specified settings of the given [`Difficulty`]. - pub const fn difficulty(mut self, difficulty: Difficulty) -> Self { + pub fn difficulty(mut self, difficulty: Difficulty) -> Self { self.difficulty = difficulty; self @@ -135,7 +143,7 @@ impl<'map> CatchPerformance<'map> { /// `passed_objects`, you should use [`CatchGradualPerformance`]. /// /// [`CatchGradualPerformance`]: crate::catch::CatchGradualPerformance - pub const fn passed_objects(mut self, passed_objects: u32) -> Self { + pub fn passed_objects(mut self, passed_objects: u32) -> Self { self.difficulty = self.difficulty.passed_objects(passed_objects); self @@ -216,7 +224,7 @@ impl<'map> CatchPerformance<'map> { } /// Adjust patterns as if the HR mod is enabled. - pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self { + pub fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self { self.difficulty = self.difficulty.hardrock_offsets(hardrock_offsets); self @@ -486,13 +494,13 @@ impl<'map, T: IntoModePerformance<'map, Catch>> From for CatchPerformance<'ma } } -struct CatchPerformanceInner { +struct CatchPerformanceInner<'mods> { attrs: CatchDifficultyAttributes, - mods: u32, + mods: &'mods GameMods, state: CatchScoreState, } -impl CatchPerformanceInner { +impl CatchPerformanceInner<'_> { fn calculate(self) -> CatchPerformanceAttributes { let attributes = &self.attrs; let stars = attributes.stars; diff --git a/src/lib.rs b/src/lib.rs index 2388faf7..60a28124 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,7 +163,10 @@ #[doc(inline)] pub use self::{ any::{Difficulty, GradualDifficulty, GradualPerformance, Performance}, - model::beatmap::{Beatmap, Converted}, + model::{ + beatmap::{Beatmap, Converted}, + mods::GameMods, + }, }; /// Types for calculations of any mode. diff --git a/src/mania/performance/mod.rs b/src/mania/performance/mod.rs index 491ce64e..09631530 100644 --- a/src/mania/performance/mod.rs +++ b/src/mania/performance/mod.rs @@ -2,8 +2,9 @@ use std::cmp; use crate::{ any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance}, + model::mods::GameMods, osu::OsuPerformance, - util::{map_or_attrs::MapOrAttrs, mods::Mods}, + util::map_or_attrs::MapOrAttrs, Performance, }; @@ -71,17 +72,24 @@ impl<'map> ManiaPerformance<'map> { } } - /// Specify mods through their bit values. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) /// /// See - pub const fn mods(mut self, mods: u32) -> Self { + pub fn mods(mut self, mods: impl Into) -> Self { self.difficulty = self.difficulty.mods(mods); self } /// Use the specified settings of the given [`Difficulty`]. - pub const fn difficulty(mut self, difficulty: Difficulty) -> Self { + pub fn difficulty(mut self, difficulty: Difficulty) -> Self { self.difficulty = difficulty; self @@ -94,7 +102,7 @@ impl<'map> ManiaPerformance<'map> { /// `passed_objects`, you should use [`ManiaGradualPerformance`]. /// /// [`ManiaGradualPerformance`]: crate::mania::ManiaGradualPerformance - pub const fn passed_objects(mut self, passed_objects: u32) -> Self { + pub fn passed_objects(mut self, passed_objects: u32) -> Self { self.difficulty = self.difficulty.passed_objects(passed_objects); self @@ -840,13 +848,13 @@ impl<'map, T: IntoModePerformance<'map, Mania>> From for ManiaPerformance<'ma } } -struct ManiaPerformanceInner { +struct ManiaPerformanceInner<'mods> { attrs: ManiaDifficultyAttributes, - mods: u32, + mods: &'mods GameMods, state: ManiaScoreState, } -impl ManiaPerformanceInner { +impl ManiaPerformanceInner<'_> { fn calculate(self) -> ManiaPerformanceAttributes { // * Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // * The specific number has no intrinsic meaning and can be adjusted as needed. diff --git a/src/model/beatmap/attributes.rs b/src/model/beatmap/attributes.rs index 67656920..2b39abb4 100644 --- a/src/model/beatmap/attributes.rs +++ b/src/model/beatmap/attributes.rs @@ -1,6 +1,6 @@ use rosu_map::section::general::GameMode; -use crate::{any::difficulty::ModsDependent, util::mods::Mods, Difficulty}; +use crate::{any::difficulty::ModsDependent, model::mods::GameMods, Difficulty}; use super::{converted::Converted, Beatmap}; @@ -40,7 +40,7 @@ pub struct BeatmapAttributesBuilder { od: ModsDependent, cs: ModsDependent, hp: ModsDependent, - mods: u32, + mods: GameMods, clock_rate: Option, } @@ -64,20 +64,20 @@ impl BeatmapAttributesBuilder { od: ModsDependent::new(5.0), cs: ModsDependent::new(5.0), hp: ModsDependent::new(5.0), - mods: 0, + mods: GameMods::DEFAULT, clock_rate: None, } } /// Use the given [`Beatmap`]'s attributes, mode, and convert status. - pub const fn map(self, map: &Beatmap) -> Self { + pub fn map(self, map: &Beatmap) -> Self { Self { mode: map.mode, ar: ModsDependent::new(map.ar), od: ModsDependent::new(map.od), cs: ModsDependent::new(map.cs), hp: ModsDependent::new(map.hp), - mods: 0, + mods: GameMods::DEFAULT, clock_rate: None, is_convert: map.is_convert, } @@ -88,14 +88,13 @@ impl BeatmapAttributesBuilder { /// `with_mods` determines if the given value should be used before /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. - pub const fn ar(self, ar: f32, with_mods: bool) -> Self { - Self { - ar: ModsDependent { - value: ar, - with_mods, - }, - ..self - } + pub const fn ar(mut self, ar: f32, with_mods: bool) -> Self { + self.ar = ModsDependent { + value: ar, + with_mods, + }; + + self } /// Specify the overall difficulty. @@ -103,14 +102,13 @@ impl BeatmapAttributesBuilder { /// `with_mods` determines if the given value should be used before /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. - pub const fn od(self, od: f32, with_mods: bool) -> Self { - Self { - od: ModsDependent { - value: od, - with_mods, - }, - ..self - } + pub const fn od(mut self, od: f32, with_mods: bool) -> Self { + self.od = ModsDependent { + value: od, + with_mods, + }; + + self } /// Specify the circle size. @@ -118,14 +116,13 @@ impl BeatmapAttributesBuilder { /// `with_mods` determines if the given value should be used before /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. - pub const fn cs(self, cs: f32, with_mods: bool) -> Self { - Self { - cs: ModsDependent { - value: cs, - with_mods, - }, - ..self - } + pub const fn cs(mut self, cs: f32, with_mods: bool) -> Self { + self.cs = ModsDependent { + value: cs, + with_mods, + }; + + self } /// Specify the drain rate. @@ -133,36 +130,35 @@ impl BeatmapAttributesBuilder { /// `with_mods` determines if the given value should be used before /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. - pub const fn hp(self, hp: f32, with_mods: bool) -> Self { - Self { - hp: ModsDependent { - value: hp, - with_mods, - }, - ..self - } + pub const fn hp(mut self, hp: f32, with_mods: bool) -> Self { + self.hp = ModsDependent { + value: hp, + with_mods, + }; + + self } /// Specify the mods. - pub const fn mods(self, mods: u32) -> Self { - Self { mods, ..self } + pub fn mods(mut self, mods: impl Into) -> Self { + self.mods = mods.into(); + + self } /// Specify a custom clock rate. - pub const fn clock_rate(self, clock_rate: f64) -> Self { - Self { - clock_rate: Some(clock_rate), - ..self - } + pub const fn clock_rate(mut self, clock_rate: f64) -> Self { + self.clock_rate = Some(clock_rate); + + self } /// Specify a [`GameMode`] and whether it's a converted map. - pub const fn mode(self, mode: GameMode, is_convert: bool) -> Self { - Self { - mode, - is_convert, - ..self - } + pub const fn mode(mut self, mode: GameMode, is_convert: bool) -> Self { + self.mode = mode; + self.is_convert = is_convert; + + self } /// Specify all settings through [`Difficulty`]. @@ -174,16 +170,19 @@ impl BeatmapAttributesBuilder { od: difficulty.get_od().unwrap_or(self.od), cs: difficulty.get_cs().unwrap_or(self.cs), hp: difficulty.get_hp().unwrap_or(self.hp), - mods: difficulty.get_mods(), + mods: difficulty.get_mods().clone(), clock_rate: Some(difficulty.get_clock_rate()), } } /// Calculate the AR and OD hit windows. pub fn hit_windows(&self) -> HitWindows { - let mods = self.mods; + let mods = &self.mods; + + let clock_rate = self + .clock_rate + .unwrap_or_else(|| f64::from(mods.clock_rate())); - let clock_rate = self.clock_rate.unwrap_or(mods.clock_rate()); let ar_clock_rate = if self.ar.with_mods { 1.0 } else { clock_rate }; let od_clock_rate = if self.od.with_mods { 1.0 } else { clock_rate }; @@ -266,8 +265,10 @@ impl BeatmapAttributesBuilder { /// Calculate the [`BeatmapAttributes`]. pub fn build(&self) -> BeatmapAttributes { - let mods = self.mods; - let clock_rate = self.clock_rate.unwrap_or_else(|| mods.clock_rate()); + let mods = &self.mods; + let clock_rate = self + .clock_rate + .unwrap_or_else(|| f64::from(mods.clock_rate())); // HP let mut hp = self.hp.value; diff --git a/src/model/beatmap/mod.rs b/src/model/beatmap/mod.rs index abec395b..26ac7865 100644 --- a/src/model/beatmap/mod.rs +++ b/src/model/beatmap/mod.rs @@ -78,7 +78,7 @@ impl Beatmap { /// Returns a [`BeatmapAttributesBuilder`] to calculate modified beatmap /// attributes. - pub const fn attributes(&self) -> BeatmapAttributesBuilder { + pub fn attributes(&self) -> BeatmapAttributesBuilder { BeatmapAttributesBuilder::new().map(self) } diff --git a/src/model/mod.rs b/src/model/mod.rs index aa397f95..c1d727de 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -9,3 +9,5 @@ pub mod hit_object; /// Gamemode related types. pub mod mode; + +pub(crate) mod mods; diff --git a/src/model/mods.rs b/src/model/mods.rs new file mode 100644 index 00000000..bcaa5f89 --- /dev/null +++ b/src/model/mods.rs @@ -0,0 +1,156 @@ +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +use rosu_mods::{GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy}; + +/// Collection of game mods. +/// +/// This type can be created through its `From` implementations where `T` +/// can be +/// - `u32` +/// - [`rosu_mods::GameModsLegacy`] +/// - [`rosu_mods::GameMods`] +/// - [`rosu_mods::GameModsIntermode`] +/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) +/// +/// # Example +/// +/// ``` +/// use rosu_pp::GameMods; +/// use rosu_mods::{GameModsIntermode, GameModsLegacy, GameMods as GameModsLazer}; +/// +/// let int = GameMods::from(64 + 8); +/// let legacy = GameMods::from(GameModsLegacy::Hidden | GameModsLegacy::Easy); +/// let lazer = GameMods::from(GameModsLazer::new()); +/// let intermode = GameMods::from(GameModsIntermode::new()); +/// ``` +#[derive(Clone, PartialEq)] +pub struct GameMods { + inner: GameModsInner, +} + +impl Debug for GameMods { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self.inner { + GameModsInner::Lazer(ref mods) => Debug::fmt(mods, f), + GameModsInner::Intermode(ref mods) => Debug::fmt(mods, f), + GameModsInner::Legacy(ref mods) => Debug::fmt(mods, f), + } + } +} + +/// Inner type of [`GameMods`] so that remote types contained in variants don't +/// need to be re-exported. +#[derive(Clone, PartialEq)] +enum GameModsInner { + Lazer(GameModsLazer), + Intermode(GameModsIntermode), + Legacy(GameModsLegacy), +} + +impl GameMods { + pub(crate) const DEFAULT: Self = Self { + inner: GameModsInner::Legacy(GameModsLegacy::NoMod), + }; + + pub(crate) fn clock_rate(&self) -> f32 { + match self.inner { + GameModsInner::Lazer(ref mods) => mods.clock_rate().unwrap_or(1.0), + GameModsInner::Intermode(ref mods) => mods.legacy_clock_rate(), + GameModsInner::Legacy(mods) => mods.clock_rate(), + } + } + + pub(crate) fn od_ar_hp_multiplier(&self) -> f64 { + if self.hr() { + 1.4 + } else if self.ez() { + 0.5 + } else { + 1.0 + } + } +} + +macro_rules! impl_has_mod { + ( $( $fn:ident: $name:ident [ $s:literal ], )* ) => { + impl GameMods { + $( + // workaround for + #[doc = "Check whether [`GameMods`] contain `"] + #[doc = $s] + #[doc = "`."] + pub(crate) fn $fn(&self) -> bool { + match self.inner { + GameModsInner::Lazer(ref mods) => { + mods.contains_intermode(GameModIntermode::$name) + }, + GameModsInner::Intermode(ref mods) => { + mods.contains(GameModIntermode::$name) + }, + GameModsInner::Legacy(mods) => { + mods.contains(GameModsLegacy::$name) + }, + } + } + )* + } + }; +} + +impl_has_mod! { + nf: NoFail ["NoFail"], + ez: Easy ["Easy"], + td: TouchDevice ["TouchDevice"], + hd: Hidden ["Hidden"], + hr: HardRock ["HardRock"], + rx: Relax ["Relax"], + fl: Flashlight ["Flashlight"], + so: SpunOut ["SpunOut"], +} + +impl Default for GameMods { + fn default() -> Self { + Self::DEFAULT + } +} + +impl From for GameMods { + fn from(mods: GameModsLazer) -> Self { + Self { + inner: GameModsInner::Lazer(mods), + } + } +} + +impl From for GameMods { + fn from(mods: GameModsIntermode) -> Self { + Self { + inner: GameModsInner::Intermode(mods), + } + } +} + +impl From<&GameModsIntermode> for GameMods { + fn from(mods: &GameModsIntermode) -> Self { + // If only legacy mods are set, use `GameModsLegacy` and thus avoid + // allocating an owned `GameModsIntermode` instance. + match mods.checked_bits() { + Some(bits) => bits.into(), + None => mods.to_owned().into(), + } + } +} + +impl From for GameMods { + fn from(mods: GameModsLegacy) -> Self { + Self { + inner: GameModsInner::Legacy(mods), + } + } +} + +impl From for GameMods { + fn from(bits: u32) -> Self { + GameModsLegacy::from_bits(bits).into() + } +} diff --git a/src/osu/difficulty/gradual.rs b/src/osu/difficulty/gradual.rs index 507f949b..e12c4f98 100644 --- a/src/osu/difficulty/gradual.rs +++ b/src/osu/difficulty/gradual.rs @@ -7,7 +7,6 @@ use crate::{ object::{OsuObject, OsuObjectKind}, OsuBeatmap, }, - util::mods::Mods, Difficulty, }; diff --git a/src/osu/difficulty/mod.rs b/src/osu/difficulty/mod.rs index c7b784ec..94f39f73 100644 --- a/src/osu/difficulty/mod.rs +++ b/src/osu/difficulty/mod.rs @@ -2,14 +2,13 @@ use std::{cmp, pin::Pin}; use crate::{ any::difficulty::{skills::Skill, Difficulty}, - model::beatmap::BeatmapAttributes, + model::{beatmap::BeatmapAttributes, mods::GameMods}, osu::{ convert::convert_objects, difficulty::{object::OsuDifficultyObject, scaling_factor::ScalingFactor}, object::OsuObject, performance::PERFORMANCE_BASE_MULTIPLIER, }, - util::mods::Mods, }; use self::skills::OsuSkills; @@ -146,7 +145,7 @@ impl DifficultyValues { /// Process the difficulty values and store the results in `attrs`. pub fn eval( attrs: &mut OsuDifficultyAttributes, - mods: u32, + mods: &GameMods, aim_difficulty_value: f64, aim_no_sliders_difficulty_value: f64, speed_difficulty_value: f64, diff --git a/src/osu/difficulty/skills/flashlight.rs b/src/osu/difficulty/skills/flashlight.rs index afdc964d..4ed01ac6 100644 --- a/src/osu/difficulty/skills/flashlight.rs +++ b/src/osu/difficulty/skills/flashlight.rs @@ -5,8 +5,9 @@ use crate::{ object::IDifficultyObject, skills::{strain_decay, ISkill, Skill, StrainSkill}, }, + model::mods::GameMods, osu::{difficulty::object::OsuDifficultyObject, object::OsuObjectKind}, - util::{mods::Mods, strains_vec::StrainsVec}, + util::strains_vec::StrainsVec, }; use super::strain::OsuStrainSkill; @@ -22,7 +23,7 @@ pub struct Flashlight { } impl Flashlight { - pub fn new(mods: u32, radius: f64, time_preempt: f64, time_fade_in: f64) -> Self { + pub fn new(mods: &GameMods, radius: f64, time_preempt: f64, time_fade_in: f64) -> Self { let scaling_factor = 52.0 / radius; Self { diff --git a/src/osu/difficulty/skills/mod.rs b/src/osu/difficulty/skills/mod.rs index 09112741..de319f5d 100644 --- a/src/osu/difficulty/skills/mod.rs +++ b/src/osu/difficulty/skills/mod.rs @@ -1,4 +1,7 @@ -use crate::{model::beatmap::BeatmapAttributes, osu::object::OsuObject, util::mods::Mods}; +use crate::{ + model::{beatmap::BeatmapAttributes, mods::GameMods}, + osu::object::OsuObject, +}; use self::{aim::Aim, flashlight::Flashlight, speed::Speed}; @@ -18,7 +21,7 @@ pub struct OsuSkills { impl OsuSkills { pub fn new( - mods: u32, + mods: &GameMods, scaling_factor: &ScalingFactor, map_attrs: &BeatmapAttributes, time_preempt: f64, diff --git a/src/osu/performance/mod.rs b/src/osu/performance/mod.rs index d1c26721..1d205a81 100644 --- a/src/osu/performance/mod.rs +++ b/src/osu/performance/mod.rs @@ -6,8 +6,9 @@ use crate::{ any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance, Performance}, catch::CatchPerformance, mania::ManiaPerformance, + model::mods::GameMods, taiko::TaikoPerformance, - util::{float_ext::FloatExt, map_or_attrs::MapOrAttrs, mods::Mods}, + util::{float_ext::FloatExt, map_or_attrs::MapOrAttrs}, }; use super::{ @@ -118,10 +119,17 @@ impl<'map> OsuPerformance<'map> { } } - /// Specify mods through their bit values. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) /// /// See - pub const fn mods(mut self, mods: u32) -> Self { + pub fn mods(mut self, mods: impl Into) -> Self { self.difficulty = self.difficulty.mods(mods); self @@ -172,7 +180,7 @@ impl<'map> OsuPerformance<'map> { } /// Use the specified settings of the given [`Difficulty`]. - pub const fn difficulty(mut self, difficulty: Difficulty) -> Self { + pub fn difficulty(mut self, difficulty: Difficulty) -> Self { self.difficulty = difficulty; self @@ -185,7 +193,7 @@ impl<'map> OsuPerformance<'map> { /// `passed_objects`, you should use [`OsuGradualPerformance`]. /// /// [`OsuGradualPerformance`]: crate::osu::OsuGradualPerformance - pub const fn passed_objects(mut self, passed_objects: u32) -> Self { + pub fn passed_objects(mut self, passed_objects: u32) -> Self { self.difficulty = self.difficulty.passed_objects(passed_objects); self @@ -522,15 +530,15 @@ impl<'map, T: IntoModePerformance<'map, Osu>> From for OsuPerformance<'map> { pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14; -struct OsuPerformanceInner { +struct OsuPerformanceInner<'mods> { attrs: OsuDifficultyAttributes, - mods: u32, + mods: &'mods GameMods, acc: f64, state: OsuScoreState, effective_miss_count: f64, } -impl OsuPerformanceInner { +impl OsuPerformanceInner<'_> { fn calculate(mut self) -> OsuPerformanceAttributes { let total_hits = self.state.total_hits(); diff --git a/src/taiko/performance/mod.rs b/src/taiko/performance/mod.rs index 2346ea76..7b1e5b03 100644 --- a/src/taiko/performance/mod.rs +++ b/src/taiko/performance/mod.rs @@ -2,8 +2,9 @@ use std::cmp; use crate::{ any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance}, + model::mods::GameMods, osu::OsuPerformance, - util::{map_or_attrs::MapOrAttrs, mods::Mods}, + util::map_or_attrs::MapOrAttrs, Performance, }; @@ -69,10 +70,17 @@ impl<'map> TaikoPerformance<'map> { } } - /// Specify mods through their bit values. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) /// /// See - pub const fn mods(mut self, mods: u32) -> Self { + pub fn mods(mut self, mods: impl Into) -> Self { self.difficulty = self.difficulty.mods(mods); self @@ -124,7 +132,7 @@ impl<'map> TaikoPerformance<'map> { } /// Use the specified settings of the given [`Difficulty`]. - pub const fn difficulty(mut self, difficulty: Difficulty) -> Self { + pub fn difficulty(mut self, difficulty: Difficulty) -> Self { self.difficulty = difficulty; self @@ -137,7 +145,7 @@ impl<'map> TaikoPerformance<'map> { /// `passed_objects`, you should use [`TaikoGradualPerformance`]. /// /// [`TaikoGradualPerformance`]: crate::taiko::TaikoGradualPerformance - pub const fn passed_objects(mut self, passed_objects: u32) -> Self { + pub fn passed_objects(mut self, passed_objects: u32) -> Self { self.difficulty = self.difficulty.passed_objects(passed_objects); self @@ -377,13 +385,13 @@ impl<'map, T: IntoModePerformance<'map, Taiko>> From for TaikoPerformance<'ma } } -struct TaikoPerformanceInner { +struct TaikoPerformanceInner<'mods> { attrs: TaikoDifficultyAttributes, - mods: u32, + mods: &'mods GameMods, state: TaikoScoreState, } -impl TaikoPerformanceInner { +impl TaikoPerformanceInner<'_> { fn calculate(self) -> TaikoPerformanceAttributes { // * The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits // * and increasing the miss penalty for shorter object counts lower than 1000. diff --git a/src/util/mod.rs b/src/util/mod.rs index 4514eb98..4ec48e96 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -2,7 +2,6 @@ pub mod float_ext; pub mod generic_fmt; pub mod limited_queue; pub mod map_or_attrs; -pub mod mods; pub mod random; pub mod sort; pub mod strains_vec; diff --git a/src/util/mods.rs b/src/util/mods.rs deleted file mode 100644 index 42496eca..00000000 --- a/src/util/mods.rs +++ /dev/null @@ -1,53 +0,0 @@ -pub trait Mods: Copy { - fn nf(self) -> bool; - fn ez(self) -> bool; - fn td(self) -> bool; - fn hd(self) -> bool; - fn hr(self) -> bool; - fn dt(self) -> bool; - fn rx(self) -> bool; - fn ht(self) -> bool; - fn fl(self) -> bool; - fn so(self) -> bool; - - fn clock_rate(self) -> f64 { - if self.dt() { - 1.5 - } else if self.ht() { - 0.75 - } else { - 1.0 - } - } - - fn od_ar_hp_multiplier(self) -> f64 { - if self.hr() { - 1.4 - } else if self.ez() { - 0.5 - } else { - 1.0 - } - } -} - -macro_rules! impl_mods_fn { - ( $fn_name:ident, $bits:expr ) => { - fn $fn_name(self) -> bool { - self & ($bits) != 0 - } - }; -} - -impl Mods for u32 { - impl_mods_fn!(nf, 1 << 0); - impl_mods_fn!(ez, 1 << 1); - impl_mods_fn!(td, 1 << 2); - impl_mods_fn!(hd, 1 << 3); - impl_mods_fn!(hr, 1 << 4); - impl_mods_fn!(dt, 1 << 6); - impl_mods_fn!(rx, 1 << 7); - impl_mods_fn!(ht, 1 << 8); - impl_mods_fn!(fl, 1 << 10); - impl_mods_fn!(so, 1 << 12); -} From 3db992aa505078598f1a2abee60ff57a201af85a Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Fri, 5 Jul 2024 13:50:34 +0200 Subject: [PATCH 2/5] use published rosu-mods 0.1.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6bb0d113..e9aee393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ tracing = ["rosu-map/tracing"] [dependencies] rosu-map = { version = "0.1.1" } -rosu-mods = { path = "../rosu-mods", default-features = false } +rosu-mods = { version = "0.1.0" } [dev-dependencies] proptest = "1.4.0" From aa1279b8580a4f38dc1517522af9d2d049fb10d2 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Fri, 5 Jul 2024 17:17:05 +0200 Subject: [PATCH 3/5] apply values from DifficultyAdjust mod --- src/any/difficulty/mod.rs | 3 +- src/model/beatmap/attributes.rs | 190 +++++++++++++++++++++++--------- src/model/mods.rs | 62 ++++++++++- 3 files changed, 200 insertions(+), 55 deletions(-) diff --git a/src/any/difficulty/mod.rs b/src/any/difficulty/mod.rs index a9c81304..fa55fc7a 100644 --- a/src/any/difficulty/mod.rs +++ b/src/any/difficulty/mod.rs @@ -344,7 +344,8 @@ impl Difficulty { } pub(crate) fn get_hardrock_offsets(&self) -> bool { - self.hardrock_offsets.unwrap_or(self.mods.hr()) + self.hardrock_offsets + .unwrap_or_else(|| self.mods.hardrock_offsets()) } } diff --git a/src/model/beatmap/attributes.rs b/src/model/beatmap/attributes.rs index 2b39abb4..0bca09c8 100644 --- a/src/model/beatmap/attributes.rs +++ b/src/model/beatmap/attributes.rs @@ -36,10 +36,10 @@ pub struct HitWindows { pub struct BeatmapAttributesBuilder { mode: GameMode, is_convert: bool, - ar: ModsDependent, - od: ModsDependent, - cs: ModsDependent, - hp: ModsDependent, + ar: ModsDependentKind, + od: ModsDependentKind, + cs: ModsDependentKind, + hp: ModsDependentKind, mods: GameMods, clock_rate: Option, } @@ -60,10 +60,10 @@ impl BeatmapAttributesBuilder { Self { mode: GameMode::Osu, is_convert: false, - ar: ModsDependent::new(5.0), - od: ModsDependent::new(5.0), - cs: ModsDependent::new(5.0), - hp: ModsDependent::new(5.0), + ar: ModsDependentKind::DEFAULT, + od: ModsDependentKind::DEFAULT, + cs: ModsDependentKind::DEFAULT, + hp: ModsDependentKind::DEFAULT, mods: GameMods::DEFAULT, clock_rate: None, } @@ -73,10 +73,10 @@ impl BeatmapAttributesBuilder { pub fn map(self, map: &Beatmap) -> Self { Self { mode: map.mode, - ar: ModsDependent::new(map.ar), - od: ModsDependent::new(map.od), - cs: ModsDependent::new(map.cs), - hp: ModsDependent::new(map.hp), + ar: ModsDependentKind::Default(ModsDependent::new(map.ar)), + od: ModsDependentKind::Default(ModsDependent::new(map.od)), + cs: ModsDependentKind::Default(ModsDependent::new(map.cs)), + hp: ModsDependentKind::Default(ModsDependent::new(map.hp)), mods: GameMods::DEFAULT, clock_rate: None, is_convert: map.is_convert, @@ -89,10 +89,10 @@ impl BeatmapAttributesBuilder { /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. pub const fn ar(mut self, ar: f32, with_mods: bool) -> Self { - self.ar = ModsDependent { + self.ar = ModsDependentKind::Custom(ModsDependent { value: ar, with_mods, - }; + }); self } @@ -103,10 +103,10 @@ impl BeatmapAttributesBuilder { /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. pub const fn od(mut self, od: f32, with_mods: bool) -> Self { - self.od = ModsDependent { + self.od = ModsDependentKind::Custom(ModsDependent { value: od, with_mods, - }; + }); self } @@ -117,10 +117,10 @@ impl BeatmapAttributesBuilder { /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. pub const fn cs(mut self, cs: f32, with_mods: bool) -> Self { - self.cs = ModsDependent { + self.cs = ModsDependentKind::Custom(ModsDependent { value: cs, with_mods, - }; + }); self } @@ -131,10 +131,10 @@ impl BeatmapAttributesBuilder { /// or after accounting for mods, e.g. on `true` the value will be /// used as is and on `false` it will be modified based on the mods. pub const fn hp(mut self, hp: f32, with_mods: bool) -> Self { - self.hp = ModsDependent { + self.hp = ModsDependentKind::Custom(ModsDependent { value: hp, with_mods, - }; + }); self } @@ -166,10 +166,18 @@ impl BeatmapAttributesBuilder { Self { mode: self.mode, is_convert: self.is_convert, - ar: difficulty.get_ar().unwrap_or(self.ar), - od: difficulty.get_od().unwrap_or(self.od), - cs: difficulty.get_cs().unwrap_or(self.cs), - hp: difficulty.get_hp().unwrap_or(self.hp), + ar: difficulty + .get_ar() + .map_or(self.ar, ModsDependentKind::Custom), + od: difficulty + .get_od() + .map_or(self.od, ModsDependentKind::Custom), + cs: difficulty + .get_cs() + .map_or(self.cs, ModsDependentKind::Custom), + hp: difficulty + .get_hp() + .map_or(self.hp, ModsDependentKind::Custom), mods: difficulty.get_mods().clone(), clock_rate: Some(difficulty.get_clock_rate()), } @@ -183,8 +191,8 @@ impl BeatmapAttributesBuilder { .clock_rate .unwrap_or_else(|| f64::from(mods.clock_rate())); - let ar_clock_rate = if self.ar.with_mods { 1.0 } else { clock_rate }; - let od_clock_rate = if self.od.with_mods { 1.0 } else { clock_rate }; + let ar_clock_rate = if self.ar.with_mods() { 1.0 } else { clock_rate }; + let od_clock_rate = if self.od.with_mods() { 1.0 } else { clock_rate }; let mod_mult = |val: f32| { if mods.hr() { @@ -196,10 +204,10 @@ impl BeatmapAttributesBuilder { } }; - let raw_ar = if self.ar.with_mods { - self.ar.value + let raw_ar = if self.ar.with_mods() { + self.ar.value(mods, GameMods::ar) } else { - mod_mult(self.ar.value) + mod_mult(self.ar.value(mods, GameMods::ar)) }; let preempt = difficulty_range(f64::from(raw_ar), 1800.0, 1200.0, 450.0) / ar_clock_rate; @@ -207,10 +215,10 @@ impl BeatmapAttributesBuilder { // OD let hit_window = match self.mode { GameMode::Osu | GameMode::Catch => { - let raw_od = if self.od.with_mods { - self.od.value + let raw_od = if self.od.with_mods() { + self.od.value(mods, GameMods::od) } else { - mod_mult(self.od.value) + mod_mult(self.od.value(mods, GameMods::od)) }; difficulty_range( @@ -221,10 +229,10 @@ impl BeatmapAttributesBuilder { ) / od_clock_rate } GameMode::Taiko => { - let raw_od = if self.od.with_mods { - self.od.value + let raw_od = if self.od.with_mods() { + self.od.value(mods, GameMods::od) } else { - mod_mult(self.od.value) + mod_mult(self.od.value(mods, GameMods::od)) }; let diff_range = difficulty_range( @@ -238,14 +246,14 @@ impl BeatmapAttributesBuilder { } GameMode::Mania => { let mut value = if !self.is_convert { - 34.0 + 3.0 * (10.0 - self.od.value).clamp(0.0, 10.0) - } else if self.od.value.round_ties_even() > 4.0 { + 34.0 + 3.0 * (10.0 - self.od.value(mods, GameMods::od)).clamp(0.0, 10.0) + } else if self.od.value(mods, GameMods::od).round_ties_even() > 4.0 { 34.0 } else { 47.0 }; - if !self.od.with_mods { + if !self.od.with_mods() { if mods.hr() { value /= 1.4; } else if mods.ez() { @@ -271,18 +279,18 @@ impl BeatmapAttributesBuilder { .unwrap_or_else(|| f64::from(mods.clock_rate())); // HP - let mut hp = self.hp.value; + let mut hp = self.hp.value(mods, GameMods::hp); - if !self.hp.with_mods { + if !self.hp.with_mods() { hp *= mods.od_ar_hp_multiplier() as f32; } hp = hp.min(10.0); // CS - let mut cs = self.cs.value; + let mut cs = self.cs.value(mods, GameMods::cs); - if !self.cs.with_mods { + if !self.cs.with_mods() { if mods.hr() { cs = (cs * 1.3).min(10.0); } else if mods.ez() { @@ -304,7 +312,7 @@ impl BeatmapAttributesBuilder { let od = match self.mode { GameMode::Osu => (Self::OSU_MIN - od) / 6.0, GameMode::Taiko => (Self::TAIKO_MIN - od) / (Self::TAIKO_MIN - Self::TAIKO_AVG) * 5.0, - GameMode::Catch | GameMode::Mania => f64::from(self.od.value), + GameMode::Catch | GameMode::Mania => f64::from(self.od.value(mods, GameMods::od)), }; BeatmapAttributes { @@ -346,33 +354,109 @@ impl Default for BeatmapAttributesBuilder { } } +#[derive(Clone, Debug, PartialEq)] +enum ModsDependentKind { + Default(ModsDependent), + Custom(ModsDependent), +} + +impl ModsDependentKind { + const DEFAULT: Self = Self::Default(ModsDependent::new(5.0)); + + const fn with_mods(&self) -> bool { + match self { + ModsDependentKind::Default(inner) | ModsDependentKind::Custom(inner) => inner.with_mods, + } + } + + fn value(&self, mods: &GameMods, mods_fn: impl Fn(&GameMods) -> Option) -> f32 { + match self { + ModsDependentKind::Default(inner) => mods_fn(mods).unwrap_or(inner.value), + ModsDependentKind::Custom(inner) => inner.value, + } + } +} + #[cfg(test)] mod tests { - use crate::util::float_ext::FloatExt; + use rosu_mods::{generated_mods::DifficultyAdjustOsu, GameMod, GameMods}; use super::*; #[test] - fn consider_mods() { + fn default_ar() { + let gamemod = GameMod::HiddenOsu(Default::default()); + let diff = Difficulty::new().mods(GameMods::from(gamemod)); + let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build(); + + assert_eq!(attrs.ar, 5.0); + } + + #[test] + fn custom_ar_without_mods() { + let gamemod = GameMod::DoubleTimeOsu(Default::default()); + let diff = Difficulty::new().mods(GameMods::from(gamemod)); let attrs = BeatmapAttributesBuilder::new() .ar(8.5, false) - .mods(64) + .difficulty(&diff) .build(); - let expected = 10.0; - - assert!(attrs.ar.eq(expected), "{} != {expected}", attrs.ar); + assert_eq!(attrs.ar, 10.0); } #[test] - fn skip_mods() { + fn custom_ar_with_mods() { + let gamemod = GameMod::DoubleTimeOsu(Default::default()); + let diff = Difficulty::new().mods(GameMods::from(gamemod)); let attrs = BeatmapAttributesBuilder::new() .ar(8.5, true) - .mods(64) + .difficulty(&diff) .build(); - let expected = 8.5; + assert_eq!(attrs.ar, 8.5); + } + + #[test] + fn custom_mods_ar() { + let mut mods = GameMods::new(); + mods.insert(GameMod::DoubleTimeCatch(Default::default())); + mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { + approach_rate: Some(7.0), + ..Default::default() + })); + let diff = Difficulty::new().mods(mods); + let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build(); + + assert_eq!(attrs.ar, 9.0); + } - assert!(attrs.ar.eq(expected), "{} != {expected}", attrs.ar); + #[test] + fn custom_ar_custom_mods_ar_without_mods() { + let mut mods = GameMods::new(); + mods.insert(GameMod::DoubleTimeCatch(Default::default())); + mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { + approach_rate: Some(9.0), + ..Default::default() + })); + + let diff = Difficulty::new().mods(mods).ar(8.5, false); + let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build(); + + assert_eq!(attrs.ar, 10.0); + } + + #[test] + fn custom_ar_custom_mods_ar_with_mods() { + let mut mods = GameMods::new(); + mods.insert(GameMod::DoubleTimeCatch(Default::default())); + mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { + approach_rate: Some(9.0), + ..Default::default() + })); + + let diff = Difficulty::new().mods(mods).ar(8.5, true); + let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build(); + + assert_eq!(attrs.ar, 8.5); } } diff --git a/src/model/mods.rs b/src/model/mods.rs index bcaa5f89..b694c442 100644 --- a/src/model/mods.rs +++ b/src/model/mods.rs @@ -1,6 +1,11 @@ use std::fmt::{Debug, Formatter, Result as FmtResult}; -use rosu_mods::{GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy}; +use rosu_mods::{ + generated_mods::{ + DifficultyAdjustCatch, DifficultyAdjustMania, DifficultyAdjustOsu, DifficultyAdjustTaiko, + }, + GameMod, GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy, +}; /// Collection of game mods. /// @@ -52,6 +57,10 @@ impl GameMods { inner: GameModsInner::Legacy(GameModsLegacy::NoMod), }; + /// Returns the mods' clock rate. + /// + /// In case of variable clock rates like for `WindUp`, this will return + /// `1.0`. pub(crate) fn clock_rate(&self) -> f32 { match self.inner { GameModsInner::Lazer(ref mods) => mods.clock_rate().unwrap_or(1.0), @@ -69,6 +78,57 @@ impl GameMods { 1.0 } } + + /// Check whether the mods enable `hardrock_offsets`. + pub(crate) fn hardrock_offsets(&self) -> bool { + fn custom_hardrock_offsets(mods: &GameMods) -> Option { + match mods.inner { + GameModsInner::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod { + GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch { + hard_rock_offsets, + .. + }) => *hard_rock_offsets, + _ => None, + }), + GameModsInner::Intermode(_) | GameModsInner::Legacy(_) => None, + } + } + + custom_hardrock_offsets(self).unwrap_or_else(|| self.hr()) + } +} + +macro_rules! impl_map_attr { + ( $( $fn:ident: $field:ident [ $( $mode:ident ),* ] [$s:literal] ;)* ) => { + impl GameMods { + $( + #[doc = "Check whether the mods specify a custom "] + #[doc = $s] + #[doc = "value."] + pub(crate) fn $fn(&self) -> Option { + match self.inner { + GameModsInner::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod { + $( impl_map_attr!( @ $mode $field) => *$field, )* + _ => None, + }), + GameModsInner::Intermode(_) | GameModsInner::Legacy(_) => None, + } + } + )* + } + }; + + ( @ Osu $field:ident) => { GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { $field, .. }) }; + ( @ Taiko $field:ident) => { GameMod::DifficultyAdjustTaiko(DifficultyAdjustTaiko { $field, .. }) }; + ( @ Catch $field:ident) => { GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch { $field, .. }) }; + ( @ Mania $field:ident) => { GameMod::DifficultyAdjustMania(DifficultyAdjustMania { $field, .. }) }; +} + +impl_map_attr! { + ar: approach_rate [Osu, Catch] ["ar"]; + cs: circle_size [Osu, Catch] ["cs"]; + hp: drain_rate [Osu, Taiko, Catch, Mania] ["hp"]; + od: overall_difficulty [Osu, Taiko, Catch, Mania] ["od"]; } macro_rules! impl_has_mod { From 19361ccb75c546b588645204c90cc2b82f8b8926 Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Fri, 5 Jul 2024 17:27:57 +0200 Subject: [PATCH 4/5] minor doc addition --- src/model/beatmap/attributes.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/model/beatmap/attributes.rs b/src/model/beatmap/attributes.rs index 0bca09c8..ba057ad1 100644 --- a/src/model/beatmap/attributes.rs +++ b/src/model/beatmap/attributes.rs @@ -139,7 +139,16 @@ impl BeatmapAttributesBuilder { self } - /// Specify the mods. + /// Specify mods. + /// + /// Accepted types are + /// - `u32` + /// - [`rosu_mods::GameModsLegacy`] + /// - [`rosu_mods::GameMods`] + /// - [`rosu_mods::GameModsIntermode`] + /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode) + /// + /// See pub fn mods(mut self, mods: impl Into) -> Self { self.mods = mods.into(); From 7a3a5134f4ef32c94f8b83b0cd89aab02354620e Mon Sep 17 00:00:00 2001 From: MaxOhn Date: Fri, 5 Jul 2024 23:02:09 +0200 Subject: [PATCH 5/5] consider Blinds mod in perf calc --- src/model/mods.rs | 31 ++++++++++++++++++++----------- src/osu/performance/mod.rs | 21 +++++++++++++++++---- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/model/mods.rs b/src/model/mods.rs index b694c442..115522f0 100644 --- a/src/model/mods.rs +++ b/src/model/mods.rs @@ -132,7 +132,7 @@ impl_map_attr! { } macro_rules! impl_has_mod { - ( $( $fn:ident: $name:ident [ $s:literal ], )* ) => { + ( $( $fn:ident: $sign:tt $name:ident [ $s:literal ], )* ) => { impl GameMods { $( // workaround for @@ -147,25 +147,34 @@ macro_rules! impl_has_mod { GameModsInner::Intermode(ref mods) => { mods.contains(GameModIntermode::$name) }, - GameModsInner::Legacy(mods) => { - mods.contains(GameModsLegacy::$name) + GameModsInner::Legacy(_mods) => { + impl_has_mod!(LEGACY $sign $name _mods) }, } } )* } }; + + ( LEGACY + $name:ident $mods:ident ) => { + $mods.contains(GameModsLegacy::$name) + }; + + ( LEGACY - $name:ident $mods:ident ) => { + false + }; } impl_has_mod! { - nf: NoFail ["NoFail"], - ez: Easy ["Easy"], - td: TouchDevice ["TouchDevice"], - hd: Hidden ["Hidden"], - hr: HardRock ["HardRock"], - rx: Relax ["Relax"], - fl: Flashlight ["Flashlight"], - so: SpunOut ["SpunOut"], + nf: + NoFail ["NoFail"], + ez: + Easy ["Easy"], + td: + TouchDevice ["TouchDevice"], + hd: + Hidden ["Hidden"], + hr: + HardRock ["HardRock"], + rx: + Relax ["Relax"], + fl: + Flashlight ["Flashlight"], + so: + SpunOut ["SpunOut"], + bl: - Blinds ["Blinds"], } impl Default for GameMods { diff --git a/src/osu/performance/mod.rs b/src/osu/performance/mod.rs index 1d205a81..a851ab57 100644 --- a/src/osu/performance/mod.rs +++ b/src/osu/performance/mod.rs @@ -640,7 +640,13 @@ impl OsuPerformanceInner<'_> { // * Buff for longer maps with high AR. aim_value *= 1.0 + ar_factor * len_bonus; - if self.mods.hd() { + if self.mods.bl() { + aim_value *= 1.3 + + (total_hits + * (0.0016 / (1.0 + 2.0 * self.effective_miss_count)) + * self.acc.powf(16.0)) + * (1.0 - 0.003 * self.attrs.hp * self.attrs.hp); + } else if self.mods.hd() { // * We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. aim_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar); } @@ -703,7 +709,11 @@ impl OsuPerformanceInner<'_> { // * Buff for longer maps with high AR. speed_value *= 1.0 + ar_factor * len_bonus; - if self.mods.hd() { + if self.mods.bl() { + // * Increasing the speed value by object count for Blinds isn't + // * ideal, so the minimum buff is given. + speed_value *= 1.12; + } else if self.mods.hd() { // * We want to give more reward for lower AR when it comes to aim and HD. // * This nerfs high AR and buffs lower AR. speed_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar); @@ -772,8 +782,11 @@ impl OsuPerformanceInner<'_> { .powf(0.3) .min(1.15); - // * Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. - if self.mods.hd() { + // * Increasing the accuracy value by object count for Blinds isn't + // * ideal, so the minimum buff is given. + if self.mods.bl() { + acc_value *= 1.14; + } else if self.mods.hd() { acc_value *= 1.08; }