use builder pattern to calculate difficulty attrs

This commit is contained in:
MaxOhn
2022-03-20 14:51:24 +01:00
parent 7e05216740
commit 6c1250fff3
20 changed files with 621 additions and 264 deletions
+2
View File
@@ -6,6 +6,8 @@
- Fixed handling .osu files with missing difficulty attributes
- Fixed huge memory allocations caused by incorrectly parsed .osu files by clamping beat length
- Added `AttributeProvider` impl for `{Mode}PerformanceAttributes`
- [BREAKING] The `stars` and `strains` functions for all modes were removed. Instead use the `{Mode}Stars` builder pattern which is similar to `{Mode}PP`.
- [BREAKING] `BeatmapExt::stars`'s definition was adjusted to use the `AnyStars` builder struct
- [BREAKING] Store `HitObject::sound` in `Beatmap::sounds` instead to reduce the struct size
- [BREAKING] Removed the mode features `osu`, `fruits`, `taiko`, and `mania`. Now all modes are always supported.
+6 -2
View File
@@ -44,7 +44,11 @@ let next_result = map.pp()
println!("Next PP: {}", next_result.pp());
let stars = map.stars(16, None).stars(); // HR
let stars = map.stars()
.mods(16) // HR
.calculate()
.stars();
let max_pp = map.max_pp(16).pp();
println!("Stars: {} | Max PP: {}", stars, max_pp);
@@ -161,7 +165,7 @@ println!("PP after the first 11 objects: {}", curr_performance.pp());
| `default` | Enable all modes. |
| `osu` | Enable osu!standard. |
| `taiko` | Enable osu!taiko. |
| `fruits` | Enable osu!ctb. |
| `fruits` | Enable osu!catch. |
| `mania` | Enable osu!mania. |
| `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
| `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
+3 -3
View File
@@ -18,7 +18,7 @@ use super::{
FruitsDifficultyAttributes, ALLOWED_CATCH_RANGE,
};
/// Gradually calculate the difficulty attributes of an osu!ctb map.
/// Gradually calculate the difficulty attributes of an osu!catch map.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next fruit or droplet
@@ -65,7 +65,7 @@ pub struct FruitsGradualDifficultyAttributes<'map> {
}
impl<'map> FruitsGradualDifficultyAttributes<'map> {
/// Create a new difficulty attributes iterator for osu!ctb maps.
/// Create a new difficulty attributes iterator for osu!catch maps.
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
let map_attributes = map.attributes().mods(mods);
@@ -230,7 +230,7 @@ mod tests {
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::fruits::stars(&map, mods, None);
let regular = crate::FruitsStars::new(&map).mods(mods).calculate();
let iter_end = FruitsGradualDifficultyAttributes::new(&map, mods)
.last()
+2 -2
View File
@@ -11,7 +11,7 @@ pub struct FruitsScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
///
/// Note that only fruits and droplets are considered for osu!ctb combo.
/// Note that only fruits and droplets are considered for osu!catch combo.
pub max_combo: usize,
/// Amount of current fruits (300s).
pub n_fruits: usize,
@@ -32,7 +32,7 @@ impl FruitsScoreState {
}
}
/// Gradually calculate the performance attributes of an osu!ctb map.
/// Gradually calculate the performance attributes of an osu!catch map.
///
/// After each hit object you can call
/// [`process_next_object`](`FruitsGradualPerformanceAttributes::process_next_object`)
+84 -27
View File
@@ -24,39 +24,96 @@ const STAR_SCALING_FACTOR: f64 = 0.153;
const ALLOWED_CATCH_RANGE: f32 = 0.8;
const CATCHER_SIZE: f32 = 106.75;
/// Difficulty calculation for osu!ctb maps.
/// Difficulty calculator on osu!catch maps.
///
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
pub fn stars(
map: &Beatmap,
mods: impl Mods,
/// # Example
///
/// ```
/// use rosu_pp::{FruitsStars, Beatmap};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let difficulty_attrs = FruitsStars::new(&map)
/// .mods(8 + 64) // HDDT
/// .calculate();
///
/// println!("Stars: {}", difficulty_attrs.stars);
/// ```
#[derive(Clone, Debug)]
pub struct FruitsStars<'map> {
map: &'map Beatmap,
mods: u32,
passed_objects: Option<usize>,
) -> FruitsDifficultyAttributes {
let (mut movement, mut attributes) = calculate_movement(map, mods, passed_objects);
attributes.stars =
Movement::difficulty_value(&mut movement.strain_peaks).sqrt() * STAR_SCALING_FACTOR;
attributes
}
/// Essentially the same as the [`stars`] function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let (movement, _) = calculate_movement(map, mods, None);
impl<'map> FruitsStars<'map> {
/// Create a new difficulty calculator for osu!catch maps.
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
Self {
map,
mods: 0,
passed_objects: None,
}
}
Strains {
section_length: SECTION_LENGTH * mods.speed(),
strains: movement.strain_peaks,
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
/// Amount of passed objects for partial plays, e.g. a fail.
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`FruitsStars`] multiple times with different `passed_objects`, you should use
/// [`FruitsGradualDifficultyAttributes`](crate::fruits::FruitsGradualDifficultyAttributes).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
self
}
/// Calculate all difficulty related values, including stars.
#[inline]
pub fn calculate(self) -> FruitsDifficultyAttributes {
let (mut movement, mut attributes) = calculate_movement(self);
attributes.stars =
Movement::difficulty_value(&mut movement.strain_peaks).sqrt() * STAR_SCALING_FACTOR;
attributes
}
/// Calculate the skill strains.
///
/// Suitable to plot the difficulty of a map over time.
#[inline]
pub fn strains(self) -> Strains {
let mods = self.mods;
let (movement, _) = calculate_movement(self);
Strains {
section_length: SECTION_LENGTH * mods.speed(),
strains: movement.strain_peaks,
}
}
}
fn calculate_movement(
map: &Beatmap,
mods: impl Mods,
passed_objects: Option<usize>,
) -> (Movement, FruitsDifficultyAttributes) {
fn calculate_movement(params: FruitsStars<'_>) -> (Movement, FruitsDifficultyAttributes) {
let FruitsStars {
map,
mods,
passed_objects,
} = params;
let take = passed_objects.unwrap_or(usize::MAX);
let map_attributes = map.attributes().mods(mods);
@@ -161,7 +218,7 @@ pub(crate) fn calculate_catch_width(cs: f32) -> f32 {
CATCHER_SIZE * scale.abs() * ALLOWED_CATCH_RANGE
}
/// The result of a difficulty calculation on an osu!ctb map.
/// The result of a difficulty calculation on an osu!catch map.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FruitsDifficultyAttributes {
/// The final star rating
@@ -184,7 +241,7 @@ impl FruitsDifficultyAttributes {
}
}
/// The result of a performance calculation on an osu!ctb map.
/// The result of a performance calculation on an osu!catch map.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FruitsPerformanceAttributes {
/// The difficulty attributes that were used for the performance calculation
+17 -8
View File
@@ -1,7 +1,9 @@
use super::{stars, FruitsDifficultyAttributes, FruitsPerformanceAttributes, FruitsScoreState};
use super::{
FruitsDifficultyAttributes, FruitsPerformanceAttributes, FruitsScoreState, FruitsStars,
};
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
/// Performance calculator on osu!ctb maps.
/// Performance calculator on osu!catch maps.
///
/// # Example
///
@@ -47,7 +49,7 @@ pub struct FruitsPP<'map> {
}
impl<'map> FruitsPP<'map> {
/// Create a new performance calculator for osu!ctb maps.
/// Create a new performance calculator for osu!catch maps.
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
Self {
@@ -174,7 +176,12 @@ impl<'map> FruitsPP<'map> {
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
pub fn accuracy(mut self, mut acc: f64) -> Self {
if self.attributes.is_none() {
self.attributes = Some(stars(self.map, self.mods, self.passed_objects));
let attrs = FruitsStars::new(self.map)
.mods(self.mods)
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
.calculate();
self.attributes = Some(attrs);
}
let attributes = self.attributes.as_ref().unwrap();
@@ -283,10 +290,12 @@ impl<'map> FruitsPP<'map> {
/// Calculate all performance related values, including pp and stars.
pub fn calculate(mut self) -> FruitsPerformanceAttributes {
let attributes = self
.attributes
.take()
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
let attributes = self.attributes.take().unwrap_or_else(|| {
FruitsStars::new(self.map)
.mods(self.mods)
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
.calculate()
});
self.assert_hitresults(attributes).calculate()
}
+8 -8
View File
@@ -96,27 +96,27 @@ pub struct ScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
///
/// Note that for osu!ctb only fruits and droplets are considered for combo.
/// Note that for osu!catch only fruits and droplets are considered for combo.
///
/// Irrelevant for osu!mania.
pub max_combo: usize,
/// Amount of current katus (tiny droplet misses for osu!ctb).
/// Amount of current katus (tiny droplet misses for osu!catch).
///
/// Only relevant for osu!ctb.
/// Only relevant for osu!catch.
pub n_katu: usize,
/// Amount of current 300s (fruits for osu!ctb).
/// Amount of current 300s (fruits for osu!catch).
///
/// Irrelevant for osu!mania.
pub n300: usize,
/// Amount of current 100s (droplets for osu!ctb).
/// Amount of current 100s (droplets for osu!catch).
///
/// Irrelevant for osu!mania.
pub n100: usize,
/// Amount of current 50s (tiny droplets for osu!ctb).
/// Amount of current 50s (tiny droplets for osu!catch).
///
/// Irrelevant for osu!taiko and osu!mania.
pub n50: usize,
/// Amount of current misses (fruits + droplets for osu!ctb).
/// Amount of current misses (fruits + droplets for osu!catch).
///
/// Irrelevant for osu!mania.
pub misses: usize,
@@ -271,7 +271,7 @@ impl From<ScoreState> for TaikoScoreState {
/// ```
#[derive(Clone, Debug)]
pub enum GradualPerformanceAttributes<'map> {
/// Gradual osu!ctb performance attributes.
/// Gradual osu!catch performance attributes.
Fruits(FruitsGradualPerformanceAttributes<'map>),
/// Gradual osu!mania performance attributes.
Mania(ManiaGradualPerformanceAttributes<'map>),
+28 -23
View File
@@ -42,7 +42,11 @@
//!
//! println!("Next PP: {}", next_result.pp());
//!
//! let stars = map.stars(16, None).stars(); // HR
//! let stars = map.stars()
//! .mods(16) // HR
//! .calculate()
//! .stars();
//!
//! let max_pp = map.max_pp(16).pp();
//!
//! println!("Stars: {} | Max PP: {}", stars, max_pp);
@@ -162,7 +166,7 @@
//! | `default` | Enable all modes. |
//! | `osu` | Enable osu!standard. |
//! | `taiko` | Enable osu!taiko. |
//! | `fruits` | Enable osu!ctb. |
//! | `fruits` | Enable osu!catch. |
//! | `mania` | Enable osu!mania. |
//! | `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
//! | `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
@@ -179,7 +183,7 @@
missing_docs
)]
/// Everything about osu!ctb.
/// Everything about osu!catch.
pub mod fruits;
/// Everything about osu!mania.
@@ -200,6 +204,9 @@ pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, Sco
mod pp;
pub use pp::{AnyPP, AttributeProvider};
mod stars;
pub use stars::AnyStars;
mod curve;
mod mods;
@@ -207,10 +214,10 @@ pub(crate) mod control_point_iter;
pub(crate) use control_point_iter::{ControlPoint, ControlPointIter};
pub use fruits::FruitsPP;
pub use mania::ManiaPP;
pub use osu::OsuPP;
pub use taiko::TaikoPP;
pub use fruits::{FruitsPP, FruitsStars};
pub use mania::{ManiaPP, ManiaStars};
pub use osu::{OsuPP, OsuStars};
pub use taiko::{TaikoPP, TaikoStars};
pub use mods::Mods;
pub use parse::{Beatmap, BeatmapAttributes, GameMode, ParseError, ParseResult};
@@ -218,7 +225,7 @@ pub use parse::{Beatmap, BeatmapAttributes, GameMode, ParseError, ParseResult};
/// Provides some additional methods on [`Beatmap`](crate::Beatmap).
pub trait BeatmapExt {
/// Calculate the stars and other attributes of a beatmap which are required for pp calculation.
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> DifficultyAttributes;
fn stars(&self) -> AnyStars<'_>;
/// Calculate the max pp of a beatmap.
///
@@ -235,7 +242,7 @@ pub trait BeatmapExt {
/// instead of evaluating the final strains, they are just returned as is.
///
/// Suitable to plot the difficulty of a map over time.
fn strains(&self, mods: impl Mods) -> Strains;
fn strains(&self, mods: u32) -> Strains;
/// Return an iterator that gives you the `DifficultyAttributes` after each hit object.
///
@@ -251,14 +258,12 @@ pub trait BeatmapExt {
impl BeatmapExt for Beatmap {
#[inline]
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> DifficultyAttributes {
fn stars(&self) -> AnyStars<'_> {
match self.mode {
GameMode::STD => DifficultyAttributes::Osu(osu::stars(self, mods, passed_objects)),
GameMode::MNA => DifficultyAttributes::Mania(mania::stars(self, mods, passed_objects)),
GameMode::TKO => DifficultyAttributes::Taiko(taiko::stars(self, mods, passed_objects)),
GameMode::CTB => {
DifficultyAttributes::Fruits(fruits::stars(self, mods, passed_objects))
}
GameMode::STD => AnyStars::Osu(OsuStars::new(self)),
GameMode::MNA => AnyStars::Mania(ManiaStars::new(self)),
GameMode::TKO => AnyStars::Taiko(TaikoStars::new(self)),
GameMode::CTB => AnyStars::Fruits(FruitsStars::new(self)),
}
}
@@ -284,12 +289,12 @@ impl BeatmapExt for Beatmap {
}
#[inline]
fn strains(&self, mods: impl Mods) -> Strains {
fn strains(&self, mods: u32) -> Strains {
match self.mode {
GameMode::STD => osu::strains(self, mods),
GameMode::MNA => mania::strains(self, mods),
GameMode::TKO => taiko::strains(self, mods),
GameMode::CTB => fruits::strains(self, mods),
GameMode::STD => OsuStars::new(self).mods(mods).strains(),
GameMode::MNA => ManiaStars::new(self).mods(mods).strains(),
GameMode::TKO => TaikoStars::new(self).mods(mods).strains(),
GameMode::CTB => FruitsStars::new(self).mods(mods).strains(),
}
}
@@ -317,7 +322,7 @@ pub struct Strains {
/// The result of a difficulty calculation based on the mode.
#[derive(Clone, Debug)]
pub enum DifficultyAttributes {
/// osu!ctb difficulty calculation reseult.
/// osu!catch difficulty calculation reseult.
Fruits(fruits::FruitsDifficultyAttributes),
/// osu!mania difficulty calculation reseult.
Mania(mania::ManiaDifficultyAttributes),
@@ -384,7 +389,7 @@ impl From<taiko::TaikoDifficultyAttributes> for DifficultyAttributes {
/// The result of a performance calculation based on the mode.
#[derive(Clone, Debug)]
pub enum PerformanceAttributes {
/// osu!ctb performance calculation result.
/// osu!catch performance calculation result.
Fruits(fruits::FruitsPerformanceAttributes),
/// osu!mania performance calculation result.
Mania(mania::ManiaPerformanceAttributes),
+1 -1
View File
@@ -207,7 +207,7 @@ mod tests {
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::mania::stars(&map, mods, None);
let regular = crate::ManiaStars::new(&map).mods(mods).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, mods)
.last()
+83 -22
View File
@@ -13,35 +13,96 @@ use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
const SECTION_LEN: f64 = 400.0;
const STAR_SCALING_FACTOR: f64 = 0.018;
/// Difficulty calculation for osu!mania maps.
/// Difficulty calculator on osu!mania maps.
///
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
pub fn stars(
map: &Beatmap,
mods: impl Mods,
/// # Example
///
/// ```
/// use rosu_pp::{ManiaStars, Beatmap};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let difficulty_attrs = ManiaStars::new(&map)
/// .mods(8 + 64) // HDDT
/// .calculate();
///
/// println!("Stars: {}", difficulty_attrs.stars);
/// ```
#[derive(Clone, Debug)]
pub struct ManiaStars<'map> {
map: &'map Beatmap,
mods: u32,
passed_objects: Option<usize>,
) -> ManiaDifficultyAttributes {
let mut strain = calculate_strain(map, mods, passed_objects);
}
ManiaDifficultyAttributes {
stars: Strain::difficulty_value(&mut strain.strain_peaks) * STAR_SCALING_FACTOR,
impl<'map> ManiaStars<'map> {
/// Create a new difficulty calculator for osu!mania maps.
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
Self {
map,
mods: 0,
passed_objects: None,
}
}
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
/// Amount of passed objects for partial plays, e.g. a fail.
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`ManiaStars`] multiple times with different `passed_objects`, you should use
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
self
}
/// Calculate all difficulty related values, including stars.
#[inline]
pub fn calculate(self) -> ManiaDifficultyAttributes {
let mut strain = calculate_strain(self);
ManiaDifficultyAttributes {
stars: Strain::difficulty_value(&mut strain.strain_peaks) * STAR_SCALING_FACTOR,
}
}
/// Calculate the skill strains.
///
/// Suitable to plot the difficulty of a map over time.
#[inline]
pub fn strains(self) -> Strains {
let mods = self.mods;
let strain = calculate_strain(self);
Strains {
section_length: SECTION_LEN * mods.speed(),
strains: strain.strain_peaks,
}
}
}
/// Essentially the same as the [`stars`] function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let strain = calculate_strain(map, mods, None);
fn calculate_strain(params: ManiaStars<'_>) -> Strain {
let ManiaStars {
map,
mods,
passed_objects,
} = params;
Strains {
section_length: SECTION_LEN * mods.speed(),
strains: strain.strain_peaks,
}
}
fn calculate_strain(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> Strain {
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
let rounded_cs = map.cs.round();
+8 -4
View File
@@ -1,4 +1,4 @@
use super::{stars, ManiaDifficultyAttributes, ManiaPerformanceAttributes};
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaStars};
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
/// Performance calculator on osu!mania maps.
@@ -99,9 +99,13 @@ impl<'map> ManiaPP<'map> {
/// Calculate all performance related values, including pp and stars.
pub fn calculate(self) -> ManiaPerformanceAttributes {
let stars = self
.stars
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars);
let stars = self.stars.unwrap_or_else(|| {
ManiaStars::new(self.map)
.mods(self.mods)
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
.calculate()
.stars
});
let ez = self.mods.ez();
let nf = self.mods.nf();
+1 -1
View File
@@ -324,7 +324,7 @@ mod tests {
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::osu::stars(&map, mods, None);
let regular = crate::OsuStars::new(&map).mods(mods).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, mods)
.last()
+146 -92
View File
@@ -29,71 +29,161 @@ const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
const STACK_DISTANCE: f32 = 3.0;
/// Difficulty calculation for osu!standard maps.
/// Difficulty calculator on osu!standard maps.
///
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
/// # Example
///
/// If you want to calculate the difficulty after every few objects, instead of
/// calling this function multiple times with different `passed_objects`, you should use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes).
pub fn stars(
map: &Beatmap,
mods: impl Mods,
/// ```
/// use rosu_pp::{OsuStars, Beatmap};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let difficulty_attrs = OsuStars::new(&map)
/// .mods(8 + 64) // HDDT
/// .calculate();
///
/// println!("Stars: {}", difficulty_attrs.stars);
/// ```
#[derive(Clone, Debug)]
pub struct OsuStars<'map> {
map: &'map Beatmap,
mods: u32,
passed_objects: Option<usize>,
) -> OsuDifficultyAttributes {
let (mut skills, mut attributes) = calculate_skills(map, mods, passed_objects);
}
let aim_rating = {
let aim = skills.aim();
let mut aim_strains = mem::take(&mut aim.strain_peaks);
impl<'map> OsuStars<'map> {
/// Create a new difficulty calculator for osu!standard maps.
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
Self {
map,
mods: 0,
passed_objects: None,
}
}
Skill::difficulty_value(&mut aim_strains, aim).sqrt() * DIFFICULTY_MULTIPLIER
};
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
let slider_factor = if aim_rating > 0.0 {
let aim_no_sliders = skills.aim_no_sliders();
self
}
let mut aim_strains_no_sliders = mem::take(&mut aim_no_sliders.strain_peaks);
let aim_rating_no_sliders =
Skill::difficulty_value(&mut aim_strains_no_sliders, aim_no_sliders).sqrt()
* DIFFICULTY_MULTIPLIER;
/// Amount of passed objects for partial plays, e.g. a fail.
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`OsuStars`] multiple times with different `passed_objects`, you should use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
aim_rating_no_sliders / aim_rating
} else {
1.0
};
self
}
let (speed, flashlight) = skills.speed_flashlight();
/// Calculate all difficulty related values, including stars.
#[inline]
pub fn calculate(self) -> OsuDifficultyAttributes {
let (mut skills, mut attributes) = calculate_skills(self);
let speed_rating = if let Some(speed) = speed {
let mut speed_strains = mem::take(&mut speed.strain_peaks);
let aim_rating = {
let aim = skills.aim();
let mut aim_strains = mem::take(&mut aim.strain_peaks);
Skill::difficulty_value(&mut speed_strains, speed).sqrt() * DIFFICULTY_MULTIPLIER
} else {
0.0
};
Skill::difficulty_value(&mut aim_strains, aim).sqrt() * DIFFICULTY_MULTIPLIER
};
let flashlight_rating = if let Some(flashlight) = flashlight {
let mut flashlight_strains = mem::take(&mut flashlight.strain_peaks);
let slider_factor = if aim_rating > 0.0 {
let aim_no_sliders = skills.aim_no_sliders();
Skill::difficulty_value(&mut flashlight_strains, flashlight).sqrt() * DIFFICULTY_MULTIPLIER
} else {
0.0
};
let mut aim_strains_no_sliders = mem::take(&mut aim_no_sliders.strain_peaks);
let aim_rating_no_sliders =
Skill::difficulty_value(&mut aim_strains_no_sliders, aim_no_sliders).sqrt()
* DIFFICULTY_MULTIPLIER;
let star_rating = if attributes.max_combo == 0 {
0.0
} else {
calculate_star_rating(aim_rating, speed_rating, flashlight_rating)
};
aim_rating_no_sliders / aim_rating
} else {
1.0
};
attributes.aim_strain = aim_rating;
attributes.speed_strain = speed_rating;
attributes.flashlight_rating = flashlight_rating;
attributes.slider_factor = slider_factor;
attributes.stars = star_rating;
let (speed, flashlight) = skills.speed_flashlight();
attributes
let speed_rating = if let Some(speed) = speed {
let mut speed_strains = mem::take(&mut speed.strain_peaks);
Skill::difficulty_value(&mut speed_strains, speed).sqrt() * DIFFICULTY_MULTIPLIER
} else {
0.0
};
let flashlight_rating = if let Some(flashlight) = flashlight {
let mut flashlight_strains = mem::take(&mut flashlight.strain_peaks);
Skill::difficulty_value(&mut flashlight_strains, flashlight).sqrt()
* DIFFICULTY_MULTIPLIER
} else {
0.0
};
let star_rating = if attributes.max_combo == 0 {
0.0
} else {
calculate_star_rating(aim_rating, speed_rating, flashlight_rating)
};
attributes.aim_strain = aim_rating;
attributes.speed_strain = speed_rating;
attributes.flashlight_rating = flashlight_rating;
attributes.slider_factor = slider_factor;
attributes.stars = star_rating;
attributes
}
/// Calculate the skill strains.
///
/// Suitable to plot the difficulty of a map over time.
#[inline]
pub fn strains(self) -> Strains {
let mods = self.mods;
let (mut skills, _) = calculate_skills(self);
let mut aim = mem::take(&mut skills.aim().strain_peaks);
let tuple = skills.speed_flashlight();
let strains = match tuple {
(Some(speed), Some(flashlight)) => {
for ((aim, speed), flashlight) in aim
.iter_mut()
.zip(&speed.strain_peaks)
.zip(&flashlight.strain_peaks)
{
*aim += speed + flashlight;
}
aim
}
(Some(strains), None) | (None, Some(strains)) => {
for (aim, strain) in aim.iter_mut().zip(&strains.strain_peaks) {
*aim += strain;
}
aim
}
(None, None) => aim,
};
Strains {
section_length: SECTION_LEN * mods.speed(),
strains,
}
}
}
fn calculate_star_rating(aim_rating: f64, speed_rating: f64, flashlight_rating: f64) -> f64 {
@@ -125,49 +215,13 @@ fn calculate_star_rating(aim_rating: f64, speed_rating: f64, flashlight_rating:
}
}
/// Essentially the same as the [`stars`] function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let (mut skills, _) = calculate_skills(map, mods, None);
fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
let OsuStars {
map,
mods,
passed_objects,
} = params;
let mut aim = mem::take(&mut skills.aim().strain_peaks);
let tuple = skills.speed_flashlight();
let strains = match tuple {
(Some(speed), Some(flashlight)) => {
for ((aim, speed), flashlight) in aim
.iter_mut()
.zip(&speed.strain_peaks)
.zip(&flashlight.strain_peaks)
{
*aim += speed + flashlight;
}
aim
}
(Some(strains), None) | (None, Some(strains)) => {
for (aim, strain) in aim.iter_mut().zip(&strains.strain_peaks) {
*aim += strain;
}
aim
}
(None, None) => aim,
};
Strains {
section_length: SECTION_LEN * mods.speed(),
strains,
}
}
fn calculate_skills(
map: &Beatmap,
mods: impl Mods,
passed_objects: Option<usize>,
) -> (Skills, OsuDifficultyAttributes) {
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
let map_attributes = map.attributes().mods(mods);
@@ -207,7 +261,7 @@ fn calculate_skills(
.take(take)
.filter_map(|h| OsuObject::new(h, hr, &mut params));
let mut hit_objects = Vec::with_capacity(take);
let mut hit_objects = Vec::with_capacity(take.min(map.hit_objects.len()));
hit_objects.extend(hit_objects_iter);
let stack_threshold = time_preempt * map.stack_leniency as f64;
+7 -5
View File
@@ -1,5 +1,5 @@
use super::{OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState};
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
use crate::{Beatmap, DifficultyAttributes, Mods, OsuStars, PerformanceAttributes};
/// Performance calculator on osu!standard maps.
///
@@ -312,10 +312,12 @@ impl<'map> OsuPP<'map> {
/// Calculate all performance related values, including pp and stars.
pub fn calculate(mut self) -> OsuPerformanceAttributes {
let attributes = self
.attributes
.take()
.unwrap_or_else(|| super::stars(self.map, self.mods, self.passed_objects));
let attributes = self.attributes.take().unwrap_or_else(|| {
OsuStars::new(self.map)
.mods(self.mods)
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
.calculate()
});
self.assert_hitresults(attributes).calculate()
}
+1 -1
View File
@@ -670,7 +670,7 @@ pub enum GameMode {
STD = 0,
/// osu!taiko
TKO = 1,
/// osu!ctb
/// osu!catch
CTB = 2,
/// osu!mania
MNA = 3,
+2 -2
View File
@@ -38,7 +38,7 @@ use crate::{
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)]
pub enum AnyPP<'map> {
/// osu!ctb performance calculator
/// osu!catch performance calculator
Fruits(FruitsPP<'map>),
/// osu!mania performance calculator
Mania(ManiaPP<'map>),
@@ -213,7 +213,7 @@ impl<'map> AnyPP<'map> {
/// Specify the amount of katus of a play.
///
/// This value is only relevant for osu!ctb for which it represents
/// This value is only relevant for osu!catch for which it represents
/// the amount of tiny droplet misses.
#[allow(unused_variables)]
#[inline]
+100
View File
@@ -0,0 +1,100 @@
use crate::{
Beatmap, DifficultyAttributes, FruitsStars, GameMode, ManiaStars, OsuStars, Strains, TaikoStars,
};
/// Difficulty calculator on maps of any mode.
///
/// # Example
///
/// ```
/// use rosu_pp::{AnyStars, Beatmap};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let difficulty_attrs = AnyStars::new(&map)
/// .mods(8 + 64) // HDDT
/// .calculate();
///
/// println!("Stars: {}", difficulty_attrs.stars());
/// ```
#[derive(Clone, Debug)]
pub enum AnyStars<'map> {
/// osu!catch difficulty calculator
Fruits(FruitsStars<'map>),
/// osu!mania difficulty calculator
Mania(ManiaStars<'map>),
/// osu!standard difficulty calculator
Osu(OsuStars<'map>),
/// osu!taiko difficulty calculator
Taiko(TaikoStars<'map>),
}
impl<'map> AnyStars<'map> {
/// Create a new difficulty calculator for maps of any mode.
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
match map.mode {
GameMode::CTB => Self::Fruits(FruitsStars::new(map)),
GameMode::MNA => Self::Mania(ManiaStars::new(map)),
GameMode::STD => Self::Osu(OsuStars::new(map)),
GameMode::TKO => Self::Taiko(TaikoStars::new(map)),
}
}
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
#[inline]
pub fn mods(self, mods: u32) -> Self {
match self {
Self::Fruits(f) => Self::Fruits(f.mods(mods)),
Self::Mania(m) => Self::Mania(m.mods(mods)),
Self::Osu(o) => Self::Osu(o.mods(mods)),
Self::Taiko(t) => Self::Taiko(t.mods(mods)),
}
}
/// Amount of passed objects for partial plays, e.g. a fail.
///
/// If you want to calculate the performance after every few objects, instead of
/// using [`AnyStars`] multiple times with different `passed_objects`, you should use
/// [`GradualDifficultyAttributes`](crate::GradualDifficultyAttributes).
#[inline]
pub fn passed_objects(self, passed_objects: usize) -> Self {
match self {
Self::Fruits(f) => Self::Fruits(f.passed_objects(passed_objects)),
Self::Mania(m) => Self::Mania(m.passed_objects(passed_objects)),
Self::Osu(o) => Self::Osu(o.passed_objects(passed_objects)),
Self::Taiko(t) => Self::Taiko(t.passed_objects(passed_objects)),
}
}
/// Consume the difficulty calculator and calculate
/// difficulty attributes for the given parameters.
#[inline]
pub fn calculate(self) -> DifficultyAttributes {
match self {
Self::Fruits(f) => DifficultyAttributes::Fruits(f.calculate()),
Self::Mania(m) => DifficultyAttributes::Mania(m.calculate()),
Self::Osu(o) => DifficultyAttributes::Osu(o.calculate()),
Self::Taiko(t) => DifficultyAttributes::Taiko(t.calculate()),
}
}
/// Consume the difficulty calculator and calculate
/// skill strains for the given parameters.
///
/// Suitable to plot the difficulty of a map over time.
#[inline]
pub fn strains(self) -> Strains {
match self {
Self::Fruits(f) => f.strains(),
Self::Mania(m) => m.strains(),
Self::Osu(o) => o.strains(),
Self::Taiko(t) => t.strains(),
}
}
}
+1 -1
View File
@@ -390,7 +390,7 @@ mod tests {
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::taiko::stars(&map, mods, None);
let regular = crate::TaikoStars::new(&map).mods(mods).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, mods)
.last()
+114 -57
View File
@@ -33,72 +33,129 @@ const COLOR_SKILL_MULTIPLIER: f64 = 0.01;
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
/// Difficulty calculation for osu!taiko maps.
/// Difficulty calculator on osu!taiko maps.
///
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
pub fn stars(
map: &Beatmap,
mods: impl Mods,
/// # Example
///
/// ```
/// use rosu_pp::{TaikoStars, Beatmap};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let difficulty_attrs = TaikoStars::new(&map)
/// .mods(8 + 64) // HDDT
/// .calculate();
///
/// println!("Stars: {}", difficulty_attrs.stars);
/// ```
#[derive(Clone, Debug)]
pub struct TaikoStars<'map> {
map: &'map Beatmap,
mods: u32,
passed_objects: Option<usize>,
) -> TaikoDifficultyAttributes {
let (skills, max_combo) = calculate_skills(map, mods, passed_objects);
let mut buf = vec![0.0; skills.strain_peaks_len()];
skills.color.copy_strain_peaks(&mut buf);
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
skills.rhythm.copy_strain_peaks(&mut buf);
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
skills.stamina_right.copy_strain_peaks(&mut buf);
let stamina_right = skills.stamina_right.difficulty_value(&mut buf);
skills.stamina_left.copy_strain_peaks(&mut buf);
let stamina_left = skills.stamina_left.difficulty_value(&mut buf);
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
stamina_rating *= stamina_penalty;
let combined_rating = locally_combined_difficulty(&mut buf, &skills, stamina_penalty);
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
TaikoDifficultyAttributes { stars, max_combo }
}
/// Essentially the same as the [`stars`] function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let (skills, _) = calculate_skills(map, mods, None);
impl<'map> TaikoStars<'map> {
/// Create a new difficulty calculator for osu!taiko maps.
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
Self {
map,
mods: 0,
passed_objects: None,
}
}
let strains = skills
.color
.strain_peaks
.iter()
.zip(skills.rhythm.strain_peaks.iter())
.zip(skills.stamina_right.strain_peaks.iter())
.zip(skills.stamina_left.strain_peaks.iter())
.map(|(((color, rhythm), stamina_right), stamina_left)| {
color + rhythm + stamina_right + stamina_left
})
.collect();
/// Specify mods through their bit values.
///
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
Strains {
section_length: SECTION_LEN * mods.speed(),
strains,
self
}
/// Amount of passed objects for partial plays, e.g. a fail.
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`TaikoStars`] multiple times with different `passed_objects`, you should use
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficultyAttributes).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
self
}
/// Calculate all difficulty related values, including stars.
#[inline]
pub fn calculate(self) -> TaikoDifficultyAttributes {
let (skills, max_combo) = calculate_skills(self);
let mut buf = vec![0.0; skills.strain_peaks_len()];
skills.color.copy_strain_peaks(&mut buf);
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
skills.rhythm.copy_strain_peaks(&mut buf);
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
skills.stamina_right.copy_strain_peaks(&mut buf);
let stamina_right = skills.stamina_right.difficulty_value(&mut buf);
skills.stamina_left.copy_strain_peaks(&mut buf);
let stamina_left = skills.stamina_left.difficulty_value(&mut buf);
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
stamina_rating *= stamina_penalty;
let combined_rating = locally_combined_difficulty(&mut buf, &skills, stamina_penalty);
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
TaikoDifficultyAttributes { stars, max_combo }
}
/// Calculate the skill strains.
///
/// Suitable to plot the difficulty of a map over time.
#[inline]
pub fn strains(self) -> Strains {
let mods = self.mods;
let (skills, _) = calculate_skills(self);
let strains = skills
.color
.strain_peaks
.iter()
.zip(skills.rhythm.strain_peaks.iter())
.zip(skills.stamina_right.strain_peaks.iter())
.zip(skills.stamina_left.strain_peaks.iter())
.map(|(((color, rhythm), stamina_right), stamina_left)| {
color + rhythm + stamina_right + stamina_left
})
.collect();
Strains {
section_length: SECTION_LEN * mods.speed(),
strains,
}
}
}
fn calculate_skills(
map: &Beatmap,
mods: impl Mods,
passed_objects: Option<usize>,
) -> (Skills, usize) {
fn calculate_skills(params: TaikoStars<'_>) -> (Skills, usize) {
let TaikoStars {
map,
mods,
passed_objects,
} = params;
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
// True if the object at that index is stamina cheese
+7 -5
View File
@@ -1,4 +1,4 @@
use super::{stars, TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState};
use super::{TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState, TaikoStars};
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
/// Performance calculator on osu!taiko maps.
@@ -158,10 +158,12 @@ impl<'map> TaikoPP<'map> {
/// Calculate all performance related values, including pp and stars.
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
let attributes = self
.attributes
.take()
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
let attributes = self.attributes.take().unwrap_or_else(|| {
TaikoStars::new(self.map)
.mods(self.mods)
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
.calculate()
});
if self.n300.or(self.n100).is_some() {
let total = self.map.n_circles as usize;