use builder pattern to calculate difficulty attrs
This commit is contained in:
@@ -6,6 +6,8 @@
|
|||||||
- Fixed handling .osu files with missing difficulty attributes
|
- Fixed handling .osu files with missing difficulty attributes
|
||||||
- Fixed huge memory allocations caused by incorrectly parsed .osu files by clamping beat length
|
- Fixed huge memory allocations caused by incorrectly parsed .osu files by clamping beat length
|
||||||
- Added `AttributeProvider` impl for `{Mode}PerformanceAttributes`
|
- 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] 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.
|
- [BREAKING] Removed the mode features `osu`, `fruits`, `taiko`, and `mania`. Now all modes are always supported.
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ let next_result = map.pp()
|
|||||||
|
|
||||||
println!("Next PP: {}", next_result.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();
|
let max_pp = map.max_pp(16).pp();
|
||||||
|
|
||||||
println!("Stars: {} | Max PP: {}", stars, max_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. |
|
| `default` | Enable all modes. |
|
||||||
| `osu` | Enable osu!standard. |
|
| `osu` | Enable osu!standard. |
|
||||||
| `taiko` | Enable osu!taiko. |
|
| `taiko` | Enable osu!taiko. |
|
||||||
| `fruits` | Enable osu!ctb. |
|
| `fruits` | Enable osu!catch. |
|
||||||
| `mania` | Enable osu!mania. |
|
| `mania` | Enable osu!mania. |
|
||||||
| `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
|
| `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) |
|
| `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use super::{
|
|||||||
FruitsDifficultyAttributes, ALLOWED_CATCH_RANGE,
|
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).
|
/// 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
|
/// 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> {
|
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 {
|
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ mod tests {
|
|||||||
fn iter_end_eq_regular() {
|
fn iter_end_eq_regular() {
|
||||||
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
|
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
|
||||||
let mods = 64;
|
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)
|
let iter_end = FruitsGradualDifficultyAttributes::new(&map, mods)
|
||||||
.last()
|
.last()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct FruitsScoreState {
|
|||||||
/// Maximum combo that the score has had so far.
|
/// Maximum combo that the score has had so far.
|
||||||
/// **Not** the maximum possible combo of the map 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,
|
pub max_combo: usize,
|
||||||
/// Amount of current fruits (300s).
|
/// Amount of current fruits (300s).
|
||||||
pub n_fruits: usize,
|
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
|
/// After each hit object you can call
|
||||||
/// [`process_next_object`](`FruitsGradualPerformanceAttributes::process_next_object`)
|
/// [`process_next_object`](`FruitsGradualPerformanceAttributes::process_next_object`)
|
||||||
|
|||||||
+78
-21
@@ -24,39 +24,96 @@ const STAR_SCALING_FACTOR: f64 = 0.153;
|
|||||||
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
||||||
const CATCHER_SIZE: f32 = 106.75;
|
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.
|
/// # Example
|
||||||
pub fn stars(
|
///
|
||||||
map: &Beatmap,
|
/// ```
|
||||||
mods: impl Mods,
|
/// 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>,
|
passed_objects: Option<usize>,
|
||||||
) -> FruitsDifficultyAttributes {
|
}
|
||||||
let (mut movement, mut attributes) = calculate_movement(map, mods, passed_objects);
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 =
|
attributes.stars =
|
||||||
Movement::difficulty_value(&mut movement.strain_peaks).sqrt() * STAR_SCALING_FACTOR;
|
Movement::difficulty_value(&mut movement.strain_peaks).sqrt() * STAR_SCALING_FACTOR;
|
||||||
|
|
||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the [`stars`] function but instead of
|
/// Calculate the skill strains.
|
||||||
/// evaluating the final strains, it just returns them as is.
|
///
|
||||||
///
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
#[inline]
|
||||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
pub fn strains(self) -> Strains {
|
||||||
let (movement, _) = calculate_movement(map, mods, None);
|
let mods = self.mods;
|
||||||
|
let (movement, _) = calculate_movement(self);
|
||||||
|
|
||||||
Strains {
|
Strains {
|
||||||
section_length: SECTION_LENGTH * mods.speed(),
|
section_length: SECTION_LENGTH * mods.speed(),
|
||||||
strains: movement.strain_peaks,
|
strains: movement.strain_peaks,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_movement(
|
fn calculate_movement(params: FruitsStars<'_>) -> (Movement, FruitsDifficultyAttributes) {
|
||||||
map: &Beatmap,
|
let FruitsStars {
|
||||||
mods: impl Mods,
|
map,
|
||||||
passed_objects: Option<usize>,
|
mods,
|
||||||
) -> (Movement, FruitsDifficultyAttributes) {
|
passed_objects,
|
||||||
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(usize::MAX);
|
let take = passed_objects.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
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
|
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)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct FruitsDifficultyAttributes {
|
pub struct FruitsDifficultyAttributes {
|
||||||
/// The final star rating
|
/// 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)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct FruitsPerformanceAttributes {
|
pub struct FruitsPerformanceAttributes {
|
||||||
/// The difficulty attributes that were used for the performance calculation
|
/// The difficulty attributes that were used for the performance calculation
|
||||||
|
|||||||
+17
-8
@@ -1,7 +1,9 @@
|
|||||||
use super::{stars, FruitsDifficultyAttributes, FruitsPerformanceAttributes, FruitsScoreState};
|
use super::{
|
||||||
|
FruitsDifficultyAttributes, FruitsPerformanceAttributes, FruitsScoreState, FruitsStars,
|
||||||
|
};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||||
|
|
||||||
/// Performance calculator on osu!ctb maps.
|
/// Performance calculator on osu!catch maps.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
@@ -47,7 +49,7 @@ pub struct FruitsPP<'map> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> 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]
|
#[inline]
|
||||||
pub fn new(map: &'map Beatmap) -> Self {
|
pub fn new(map: &'map Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -174,7 +176,12 @@ impl<'map> FruitsPP<'map> {
|
|||||||
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
||||||
pub fn accuracy(mut self, mut acc: f64) -> Self {
|
pub fn accuracy(mut self, mut acc: f64) -> Self {
|
||||||
if self.attributes.is_none() {
|
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();
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
@@ -283,10 +290,12 @@ impl<'map> FruitsPP<'map> {
|
|||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> FruitsPerformanceAttributes {
|
pub fn calculate(mut self) -> FruitsPerformanceAttributes {
|
||||||
let attributes = self
|
let attributes = self.attributes.take().unwrap_or_else(|| {
|
||||||
.attributes
|
FruitsStars::new(self.map)
|
||||||
.take()
|
.mods(self.mods)
|
||||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
|
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
|
||||||
|
.calculate()
|
||||||
|
});
|
||||||
|
|
||||||
self.assert_hitresults(attributes).calculate()
|
self.assert_hitresults(attributes).calculate()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -96,27 +96,27 @@ pub struct ScoreState {
|
|||||||
/// Maximum combo that the score has had so far.
|
/// Maximum combo that the score has had so far.
|
||||||
/// **Not** the maximum possible combo of the map 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.
|
/// Irrelevant for osu!mania.
|
||||||
pub max_combo: usize,
|
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,
|
pub n_katu: usize,
|
||||||
/// Amount of current 300s (fruits for osu!ctb).
|
/// Amount of current 300s (fruits for osu!catch).
|
||||||
///
|
///
|
||||||
/// Irrelevant for osu!mania.
|
/// Irrelevant for osu!mania.
|
||||||
pub n300: usize,
|
pub n300: usize,
|
||||||
/// Amount of current 100s (droplets for osu!ctb).
|
/// Amount of current 100s (droplets for osu!catch).
|
||||||
///
|
///
|
||||||
/// Irrelevant for osu!mania.
|
/// Irrelevant for osu!mania.
|
||||||
pub n100: usize,
|
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.
|
/// Irrelevant for osu!taiko and osu!mania.
|
||||||
pub n50: usize,
|
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.
|
/// Irrelevant for osu!mania.
|
||||||
pub misses: usize,
|
pub misses: usize,
|
||||||
@@ -271,7 +271,7 @@ impl From<ScoreState> for TaikoScoreState {
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum GradualPerformanceAttributes<'map> {
|
pub enum GradualPerformanceAttributes<'map> {
|
||||||
/// Gradual osu!ctb performance attributes.
|
/// Gradual osu!catch performance attributes.
|
||||||
Fruits(FruitsGradualPerformanceAttributes<'map>),
|
Fruits(FruitsGradualPerformanceAttributes<'map>),
|
||||||
/// Gradual osu!mania performance attributes.
|
/// Gradual osu!mania performance attributes.
|
||||||
Mania(ManiaGradualPerformanceAttributes<'map>),
|
Mania(ManiaGradualPerformanceAttributes<'map>),
|
||||||
|
|||||||
+28
-23
@@ -42,7 +42,11 @@
|
|||||||
//!
|
//!
|
||||||
//! println!("Next PP: {}", next_result.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();
|
//! let max_pp = map.max_pp(16).pp();
|
||||||
//!
|
//!
|
||||||
//! println!("Stars: {} | Max PP: {}", stars, max_pp);
|
//! println!("Stars: {} | Max PP: {}", stars, max_pp);
|
||||||
@@ -162,7 +166,7 @@
|
|||||||
//! | `default` | Enable all modes. |
|
//! | `default` | Enable all modes. |
|
||||||
//! | `osu` | Enable osu!standard. |
|
//! | `osu` | Enable osu!standard. |
|
||||||
//! | `taiko` | Enable osu!taiko. |
|
//! | `taiko` | Enable osu!taiko. |
|
||||||
//! | `fruits` | Enable osu!ctb. |
|
//! | `fruits` | Enable osu!catch. |
|
||||||
//! | `mania` | Enable osu!mania. |
|
//! | `mania` | Enable osu!mania. |
|
||||||
//! | `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
|
//! | `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) |
|
//! | `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
|
||||||
@@ -179,7 +183,7 @@
|
|||||||
missing_docs
|
missing_docs
|
||||||
)]
|
)]
|
||||||
|
|
||||||
/// Everything about osu!ctb.
|
/// Everything about osu!catch.
|
||||||
pub mod fruits;
|
pub mod fruits;
|
||||||
|
|
||||||
/// Everything about osu!mania.
|
/// Everything about osu!mania.
|
||||||
@@ -200,6 +204,9 @@ pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, Sco
|
|||||||
mod pp;
|
mod pp;
|
||||||
pub use pp::{AnyPP, AttributeProvider};
|
pub use pp::{AnyPP, AttributeProvider};
|
||||||
|
|
||||||
|
mod stars;
|
||||||
|
pub use stars::AnyStars;
|
||||||
|
|
||||||
mod curve;
|
mod curve;
|
||||||
mod mods;
|
mod mods;
|
||||||
|
|
||||||
@@ -207,10 +214,10 @@ pub(crate) mod control_point_iter;
|
|||||||
|
|
||||||
pub(crate) use control_point_iter::{ControlPoint, ControlPointIter};
|
pub(crate) use control_point_iter::{ControlPoint, ControlPointIter};
|
||||||
|
|
||||||
pub use fruits::FruitsPP;
|
pub use fruits::{FruitsPP, FruitsStars};
|
||||||
pub use mania::ManiaPP;
|
pub use mania::{ManiaPP, ManiaStars};
|
||||||
pub use osu::OsuPP;
|
pub use osu::{OsuPP, OsuStars};
|
||||||
pub use taiko::TaikoPP;
|
pub use taiko::{TaikoPP, TaikoStars};
|
||||||
|
|
||||||
pub use mods::Mods;
|
pub use mods::Mods;
|
||||||
pub use parse::{Beatmap, BeatmapAttributes, GameMode, ParseError, ParseResult};
|
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).
|
/// Provides some additional methods on [`Beatmap`](crate::Beatmap).
|
||||||
pub trait BeatmapExt {
|
pub trait BeatmapExt {
|
||||||
/// Calculate the stars and other attributes of a beatmap which are required for pp calculation.
|
/// 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.
|
/// 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.
|
/// instead of evaluating the final strains, they are just returned as is.
|
||||||
///
|
///
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
/// 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.
|
/// Return an iterator that gives you the `DifficultyAttributes` after each hit object.
|
||||||
///
|
///
|
||||||
@@ -251,14 +258,12 @@ pub trait BeatmapExt {
|
|||||||
|
|
||||||
impl BeatmapExt for Beatmap {
|
impl BeatmapExt for Beatmap {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> DifficultyAttributes {
|
fn stars(&self) -> AnyStars<'_> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
GameMode::STD => DifficultyAttributes::Osu(osu::stars(self, mods, passed_objects)),
|
GameMode::STD => AnyStars::Osu(OsuStars::new(self)),
|
||||||
GameMode::MNA => DifficultyAttributes::Mania(mania::stars(self, mods, passed_objects)),
|
GameMode::MNA => AnyStars::Mania(ManiaStars::new(self)),
|
||||||
GameMode::TKO => DifficultyAttributes::Taiko(taiko::stars(self, mods, passed_objects)),
|
GameMode::TKO => AnyStars::Taiko(TaikoStars::new(self)),
|
||||||
GameMode::CTB => {
|
GameMode::CTB => AnyStars::Fruits(FruitsStars::new(self)),
|
||||||
DifficultyAttributes::Fruits(fruits::stars(self, mods, passed_objects))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,12 +289,12 @@ impl BeatmapExt for Beatmap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strains(&self, mods: impl Mods) -> Strains {
|
fn strains(&self, mods: u32) -> Strains {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
GameMode::STD => osu::strains(self, mods),
|
GameMode::STD => OsuStars::new(self).mods(mods).strains(),
|
||||||
GameMode::MNA => mania::strains(self, mods),
|
GameMode::MNA => ManiaStars::new(self).mods(mods).strains(),
|
||||||
GameMode::TKO => taiko::strains(self, mods),
|
GameMode::TKO => TaikoStars::new(self).mods(mods).strains(),
|
||||||
GameMode::CTB => fruits::strains(self, mods),
|
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.
|
/// The result of a difficulty calculation based on the mode.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DifficultyAttributes {
|
pub enum DifficultyAttributes {
|
||||||
/// osu!ctb difficulty calculation reseult.
|
/// osu!catch difficulty calculation reseult.
|
||||||
Fruits(fruits::FruitsDifficultyAttributes),
|
Fruits(fruits::FruitsDifficultyAttributes),
|
||||||
/// osu!mania difficulty calculation reseult.
|
/// osu!mania difficulty calculation reseult.
|
||||||
Mania(mania::ManiaDifficultyAttributes),
|
Mania(mania::ManiaDifficultyAttributes),
|
||||||
@@ -384,7 +389,7 @@ impl From<taiko::TaikoDifficultyAttributes> for DifficultyAttributes {
|
|||||||
/// The result of a performance calculation based on the mode.
|
/// The result of a performance calculation based on the mode.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PerformanceAttributes {
|
pub enum PerformanceAttributes {
|
||||||
/// osu!ctb performance calculation result.
|
/// osu!catch performance calculation result.
|
||||||
Fruits(fruits::FruitsPerformanceAttributes),
|
Fruits(fruits::FruitsPerformanceAttributes),
|
||||||
/// osu!mania performance calculation result.
|
/// osu!mania performance calculation result.
|
||||||
Mania(mania::ManiaPerformanceAttributes),
|
Mania(mania::ManiaPerformanceAttributes),
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ mod tests {
|
|||||||
fn iter_end_eq_regular() {
|
fn iter_end_eq_regular() {
|
||||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
let mods = 64;
|
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)
|
let iter_end = ManiaGradualDifficultyAttributes::new(&map, mods)
|
||||||
.last()
|
.last()
|
||||||
|
|||||||
+76
-15
@@ -13,35 +13,96 @@ use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
|
|||||||
const SECTION_LEN: f64 = 400.0;
|
const SECTION_LEN: f64 = 400.0;
|
||||||
const STAR_SCALING_FACTOR: f64 = 0.018;
|
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.
|
/// # Example
|
||||||
pub fn stars(
|
///
|
||||||
map: &Beatmap,
|
/// ```
|
||||||
mods: impl Mods,
|
/// 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>,
|
passed_objects: Option<usize>,
|
||||||
) -> ManiaDifficultyAttributes {
|
}
|
||||||
let mut strain = calculate_strain(map, mods, passed_objects);
|
|
||||||
|
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 {
|
ManiaDifficultyAttributes {
|
||||||
stars: Strain::difficulty_value(&mut strain.strain_peaks) * STAR_SCALING_FACTOR,
|
stars: Strain::difficulty_value(&mut strain.strain_peaks) * STAR_SCALING_FACTOR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the [`stars`] function but instead of
|
/// Calculate the skill strains.
|
||||||
/// evaluating the final strains, it just returns them as is.
|
///
|
||||||
///
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
#[inline]
|
||||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
pub fn strains(self) -> Strains {
|
||||||
let strain = calculate_strain(map, mods, None);
|
let mods = self.mods;
|
||||||
|
let strain = calculate_strain(self);
|
||||||
|
|
||||||
Strains {
|
Strains {
|
||||||
section_length: SECTION_LEN * mods.speed(),
|
section_length: SECTION_LEN * mods.speed(),
|
||||||
strains: strain.strain_peaks,
|
strains: strain.strain_peaks,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_strain(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> Strain {
|
fn calculate_strain(params: ManiaStars<'_>) -> Strain {
|
||||||
|
let ManiaStars {
|
||||||
|
map,
|
||||||
|
mods,
|
||||||
|
passed_objects,
|
||||||
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
let rounded_cs = map.cs.round();
|
let rounded_cs = map.cs.round();
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -1,4 +1,4 @@
|
|||||||
use super::{stars, ManiaDifficultyAttributes, ManiaPerformanceAttributes};
|
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaStars};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||||
|
|
||||||
/// Performance calculator on osu!mania maps.
|
/// Performance calculator on osu!mania maps.
|
||||||
@@ -99,9 +99,13 @@ impl<'map> ManiaPP<'map> {
|
|||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(self) -> ManiaPerformanceAttributes {
|
pub fn calculate(self) -> ManiaPerformanceAttributes {
|
||||||
let stars = self
|
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
|
.stars
|
||||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars);
|
});
|
||||||
|
|
||||||
let ez = self.mods.ez();
|
let ez = self.mods.ez();
|
||||||
let nf = self.mods.nf();
|
let nf = self.mods.nf();
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ mod tests {
|
|||||||
fn iter_end_eq_regular() {
|
fn iter_end_eq_regular() {
|
||||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||||
let mods = 64;
|
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)
|
let iter_end = OsuGradualDifficultyAttributes::new(&map, mods)
|
||||||
.last()
|
.last()
|
||||||
|
|||||||
+108
-54
@@ -29,19 +29,68 @@ const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
|||||||
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
|
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
|
||||||
const STACK_DISTANCE: f32 = 3.0;
|
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
|
/// use rosu_pp::{OsuStars, Beatmap};
|
||||||
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes).
|
///
|
||||||
pub fn stars(
|
/// # /*
|
||||||
map: &Beatmap,
|
/// let map: Beatmap = ...
|
||||||
mods: impl Mods,
|
/// # */
|
||||||
|
/// # 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>,
|
passed_objects: Option<usize>,
|
||||||
) -> OsuDifficultyAttributes {
|
}
|
||||||
let (mut skills, mut attributes) = calculate_skills(map, mods, passed_objects);
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 [`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);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate all difficulty related values, including stars.
|
||||||
|
#[inline]
|
||||||
|
pub fn calculate(self) -> OsuDifficultyAttributes {
|
||||||
|
let (mut skills, mut attributes) = calculate_skills(self);
|
||||||
|
|
||||||
let aim_rating = {
|
let aim_rating = {
|
||||||
let aim = skills.aim();
|
let aim = skills.aim();
|
||||||
@@ -76,7 +125,8 @@ pub fn stars(
|
|||||||
let flashlight_rating = if let Some(flashlight) = flashlight {
|
let flashlight_rating = if let Some(flashlight) = flashlight {
|
||||||
let mut flashlight_strains = mem::take(&mut flashlight.strain_peaks);
|
let mut flashlight_strains = mem::take(&mut flashlight.strain_peaks);
|
||||||
|
|
||||||
Skill::difficulty_value(&mut flashlight_strains, flashlight).sqrt() * DIFFICULTY_MULTIPLIER
|
Skill::difficulty_value(&mut flashlight_strains, flashlight).sqrt()
|
||||||
|
* DIFFICULTY_MULTIPLIER
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
@@ -94,6 +144,46 @@ pub fn stars(
|
|||||||
attributes.stars = star_rating;
|
attributes.stars = star_rating;
|
||||||
|
|
||||||
attributes
|
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 {
|
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
|
fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
|
||||||
/// evaluating the final strains, it just returns them as is.
|
let OsuStars {
|
||||||
///
|
map,
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
mods,
|
||||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
passed_objects,
|
||||||
let (mut skills, _) = calculate_skills(map, mods, None);
|
} = 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 take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
@@ -207,7 +261,7 @@ fn calculate_skills(
|
|||||||
.take(take)
|
.take(take)
|
||||||
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
.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);
|
hit_objects.extend(hit_objects_iter);
|
||||||
|
|
||||||
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||||
|
|||||||
+7
-5
@@ -1,5 +1,5 @@
|
|||||||
use super::{OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState};
|
use super::{OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, Mods, OsuStars, PerformanceAttributes};
|
||||||
|
|
||||||
/// Performance calculator on osu!standard maps.
|
/// Performance calculator on osu!standard maps.
|
||||||
///
|
///
|
||||||
@@ -312,10 +312,12 @@ impl<'map> OsuPP<'map> {
|
|||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> OsuPerformanceAttributes {
|
pub fn calculate(mut self) -> OsuPerformanceAttributes {
|
||||||
let attributes = self
|
let attributes = self.attributes.take().unwrap_or_else(|| {
|
||||||
.attributes
|
OsuStars::new(self.map)
|
||||||
.take()
|
.mods(self.mods)
|
||||||
.unwrap_or_else(|| super::stars(self.map, self.mods, self.passed_objects));
|
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
|
||||||
|
.calculate()
|
||||||
|
});
|
||||||
|
|
||||||
self.assert_hitresults(attributes).calculate()
|
self.assert_hitresults(attributes).calculate()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -670,7 +670,7 @@ pub enum GameMode {
|
|||||||
STD = 0,
|
STD = 0,
|
||||||
/// osu!taiko
|
/// osu!taiko
|
||||||
TKO = 1,
|
TKO = 1,
|
||||||
/// osu!ctb
|
/// osu!catch
|
||||||
CTB = 2,
|
CTB = 2,
|
||||||
/// osu!mania
|
/// osu!mania
|
||||||
MNA = 3,
|
MNA = 3,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use crate::{
|
|||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum AnyPP<'map> {
|
pub enum AnyPP<'map> {
|
||||||
/// osu!ctb performance calculator
|
/// osu!catch performance calculator
|
||||||
Fruits(FruitsPP<'map>),
|
Fruits(FruitsPP<'map>),
|
||||||
/// osu!mania performance calculator
|
/// osu!mania performance calculator
|
||||||
Mania(ManiaPP<'map>),
|
Mania(ManiaPP<'map>),
|
||||||
@@ -213,7 +213,7 @@ impl<'map> AnyPP<'map> {
|
|||||||
|
|
||||||
/// Specify the amount of katus of a play.
|
/// 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.
|
/// the amount of tiny droplet misses.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
+100
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -390,7 +390,7 @@ mod tests {
|
|||||||
fn iter_end_eq_regular() {
|
fn iter_end_eq_regular() {
|
||||||
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
|
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
|
||||||
let mods = 64;
|
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)
|
let iter_end = TaikoGradualDifficultyAttributes::new(&map, mods)
|
||||||
.last()
|
.last()
|
||||||
|
|||||||
+76
-19
@@ -33,15 +33,68 @@ const COLOR_SKILL_MULTIPLIER: f64 = 0.01;
|
|||||||
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
|
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
|
||||||
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
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.
|
/// # Example
|
||||||
pub fn stars(
|
///
|
||||||
map: &Beatmap,
|
/// ```
|
||||||
mods: impl Mods,
|
/// 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>,
|
passed_objects: Option<usize>,
|
||||||
) -> TaikoDifficultyAttributes {
|
}
|
||||||
let (skills, max_combo) = calculate_skills(map, mods, passed_objects);
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 [`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()];
|
let mut buf = vec![0.0; skills.strain_peaks_len()];
|
||||||
|
|
||||||
skills.color.copy_strain_peaks(&mut buf);
|
skills.color.copy_strain_peaks(&mut buf);
|
||||||
@@ -67,14 +120,15 @@ pub fn stars(
|
|||||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
||||||
|
|
||||||
TaikoDifficultyAttributes { stars, max_combo }
|
TaikoDifficultyAttributes { stars, max_combo }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the [`stars`] function but instead of
|
/// Calculate the skill strains.
|
||||||
/// evaluating the final strains, it just returns them as is.
|
///
|
||||||
///
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
#[inline]
|
||||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
pub fn strains(self) -> Strains {
|
||||||
let (skills, _) = calculate_skills(map, mods, None);
|
let mods = self.mods;
|
||||||
|
let (skills, _) = calculate_skills(self);
|
||||||
|
|
||||||
let strains = skills
|
let strains = skills
|
||||||
.color
|
.color
|
||||||
@@ -92,13 +146,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
section_length: SECTION_LEN * mods.speed(),
|
section_length: SECTION_LEN * mods.speed(),
|
||||||
strains,
|
strains,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_skills(
|
fn calculate_skills(params: TaikoStars<'_>) -> (Skills, usize) {
|
||||||
map: &Beatmap,
|
let TaikoStars {
|
||||||
mods: impl Mods,
|
map,
|
||||||
passed_objects: Option<usize>,
|
mods,
|
||||||
) -> (Skills, usize) {
|
passed_objects,
|
||||||
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
// True if the object at that index is stamina cheese
|
// True if the object at that index is stamina cheese
|
||||||
|
|||||||
+7
-5
@@ -1,4 +1,4 @@
|
|||||||
use super::{stars, TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState};
|
use super::{TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState, TaikoStars};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||||
|
|
||||||
/// Performance calculator on osu!taiko maps.
|
/// Performance calculator on osu!taiko maps.
|
||||||
@@ -158,10 +158,12 @@ impl<'map> TaikoPP<'map> {
|
|||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||||
let attributes = self
|
let attributes = self.attributes.take().unwrap_or_else(|| {
|
||||||
.attributes
|
TaikoStars::new(self.map)
|
||||||
.take()
|
.mods(self.mods)
|
||||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
|
.passed_objects(self.passed_objects.unwrap_or(usize::MAX))
|
||||||
|
.calculate()
|
||||||
|
});
|
||||||
|
|
||||||
if self.n300.or(self.n100).is_some() {
|
if self.n300.or(self.n100).is_some() {
|
||||||
let total = self.map.n_circles as usize;
|
let total = self.map.n_circles as usize;
|
||||||
|
|||||||
Reference in New Issue
Block a user