feat: use rosu-mods (#36)

* use rosu-mods

* apply values from DifficultyAdjust mod

* minor doc addition

* consider Blinds mod in perf calc
This commit is contained in:
Badewanne3
2024-07-08 11:35:07 +02:00
committed by GitHub
parent 5c951b8bd4
commit 26547d7dea
19 changed files with 576 additions and 238 deletions
+1
View File
@@ -18,6 +18,7 @@ tracing = ["rosu-map/tracing"]
[dependencies]
rosu-map = { version = "0.1.1" }
rosu-mods = { version = "0.1.0" }
[dev-dependencies]
proptest = "1.4.0"
+3 -5
View File
@@ -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 <https://github.com/ppy/osu-api/wiki#mods>
pub mods: u32,
/// Specify mods.
pub mods: GameMods,
/// Amount of passed objects for partial plays, e.g. a fail.
pub passed_objects: Option<u32>,
/// Adjust the clock rate used in the calculation.
+41 -26
View File
@@ -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<u32>,
/// 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,19 +134,28 @@ 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 <https://github.com/ppy/osu-api/wiki#mods>
pub const fn mods(self, mods: u32) -> Self {
Self { mods, ..self }
pub fn mods(self, mods: impl Into<GameMods>) -> Self {
Self {
mods: mods.into(),
..self
}
}
/// Amount of passed objects for partial plays, e.g. a fail.
pub const fn passed_objects(self, passed_objects: u32) -> Self {
Self {
passed_objects: Some(passed_objects),
..self
}
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.
@@ -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 {
@@ -330,12 +344,13 @@ 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())
}
}
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 +369,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)
+8 -1
View File
@@ -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 <https://github.com/ppy/osu-api/wiki#mods>
pub fn mods(self, mods: u32) -> Self {
+17 -9
View File
@@ -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 <https://github.com/ppy/osu-api/wiki#mods>
pub const fn mods(mut self, mods: u32) -> Self {
pub fn mods(mut self, mods: impl Into<GameMods>) -> 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<T> 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;
+4 -1
View File
@@ -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.
+16 -8
View File
@@ -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 <https://github.com/ppy/osu-api/wiki#mods>
pub const fn mods(mut self, mods: u32) -> Self {
pub fn mods(mut self, mods: impl Into<GameMods>) -> 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<T> 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.
+199 -105
View File
@@ -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};
@@ -36,11 +36,11 @@ pub struct HitWindows {
pub struct BeatmapAttributesBuilder {
mode: GameMode,
is_convert: bool,
ar: ModsDependent,
od: ModsDependent,
cs: ModsDependent,
hp: ModsDependent,
mods: u32,
ar: ModsDependentKind,
od: ModsDependentKind,
cs: ModsDependentKind,
hp: ModsDependentKind,
mods: GameMods,
clock_rate: Option<f64>,
}
@@ -60,24 +60,24 @@ 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),
mods: 0,
ar: ModsDependentKind::DEFAULT,
od: ModsDependentKind::DEFAULT,
cs: ModsDependentKind::DEFAULT,
hp: ModsDependentKind::DEFAULT,
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,
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,
}
@@ -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 = ModsDependentKind::Custom(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 = ModsDependentKind::Custom(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 = ModsDependentKind::Custom(ModsDependent {
value: cs,
with_mods,
});
self
}
/// Specify the drain rate.
@@ -133,36 +130,44 @@ 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 = ModsDependentKind::Custom(ModsDependent {
value: hp,
with_mods,
});
self
}
/// Specify the mods.
pub const fn mods(self, mods: u32) -> Self {
Self { mods, ..self }
/// Specify mods.
///
/// Accepted types are
/// - `u32`
/// - [`rosu_mods::GameModsLegacy`]
/// - [`rosu_mods::GameMods`]
/// - [`rosu_mods::GameModsIntermode`]
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
///
/// See <https://github.com/ppy/osu-api/wiki#mods>
pub fn mods(mut self, mods: impl Into<GameMods>) -> 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`].
@@ -170,22 +175,33 @@ 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),
mods: difficulty.get_mods(),
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()),
}
}
/// 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(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 clock_rate = self
.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 mod_mult = |val: f32| {
if mods.hr() {
@@ -197,10 +213,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;
@@ -208,10 +224,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(
@@ -222,10 +238,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(
@@ -239,14 +255,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() {
@@ -266,22 +282,24 @@ 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;
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() {
@@ -303,7 +321,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 {
@@ -345,33 +363,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>) -> 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() {
let attrs = BeatmapAttributesBuilder::new()
.ar(8.5, false)
.mods(64)
.build();
fn default_ar() {
let gamemod = GameMod::HiddenOsu(Default::default());
let diff = Difficulty::new().mods(GameMods::from(gamemod));
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
let expected = 10.0;
assert!(attrs.ar.eq(expected), "{} != {expected}", attrs.ar);
assert_eq!(attrs.ar, 5.0);
}
#[test]
fn skip_mods() {
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, true)
.mods(64)
.ar(8.5, false)
.difficulty(&diff)
.build();
let expected = 8.5;
assert_eq!(attrs.ar, 10.0);
}
assert!(attrs.ar.eq(expected), "{} != {expected}", attrs.ar);
#[test]
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)
.difficulty(&diff)
.build();
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);
}
#[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);
}
}
+1 -1
View File
@@ -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)
}
+2
View File
@@ -9,3 +9,5 @@ pub mod hit_object;
/// Gamemode related types.
pub mod mode;
pub(crate) mod mods;
+225
View File
@@ -0,0 +1,225 @@
use std::fmt::{Debug, Formatter, Result as FmtResult};
use rosu_mods::{
generated_mods::{
DifficultyAdjustCatch, DifficultyAdjustMania, DifficultyAdjustOsu, DifficultyAdjustTaiko,
},
GameMod, GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy,
};
/// Collection of game mods.
///
/// This type can be created through its `From<T>` 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),
};
/// 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),
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
}
}
/// Check whether the mods enable `hardrock_offsets`.
pub(crate) fn hardrock_offsets(&self) -> bool {
fn custom_hardrock_offsets(mods: &GameMods) -> Option<bool> {
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<f32> {
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 {
( $( $fn:ident: $sign:tt $name:ident [ $s:literal ], )* ) => {
impl GameMods {
$(
// workaround for <https://github.com/rust-lang/rust-analyzer/issues/8092>
#[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) => {
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"],
bl: - Blinds ["Blinds"],
}
impl Default for GameMods {
fn default() -> Self {
Self::DEFAULT
}
}
impl From<GameModsLazer> for GameMods {
fn from(mods: GameModsLazer) -> Self {
Self {
inner: GameModsInner::Lazer(mods),
}
}
}
impl From<GameModsIntermode> 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<GameModsLegacy> for GameMods {
fn from(mods: GameModsLegacy) -> Self {
Self {
inner: GameModsInner::Legacy(mods),
}
}
}
impl From<u32> for GameMods {
fn from(bits: u32) -> Self {
GameModsLegacy::from_bits(bits).into()
}
}
-1
View File
@@ -7,7 +7,6 @@ use crate::{
object::{OsuObject, OsuObjectKind},
OsuBeatmap,
},
util::mods::Mods,
Difficulty,
};
+2 -3
View File
@@ -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,
+3 -2
View File
@@ -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 {
+5 -2
View File
@@ -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,
+33 -12
View File
@@ -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 <https://github.com/ppy/osu-api/wiki#mods>
pub const fn mods(mut self, mods: u32) -> Self {
pub fn mods(mut self, mods: impl Into<GameMods>) -> 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<T> 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();
@@ -632,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);
}
@@ -695,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);
@@ -764,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;
}
+16 -8
View File
@@ -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 <https://github.com/ppy/osu-api/wiki#mods>
pub const fn mods(mut self, mods: u32) -> Self {
pub fn mods(mut self, mods: impl Into<GameMods>) -> 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<T> 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.
-1
View File
@@ -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;
-53
View File
@@ -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);
}