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 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.
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -670,7 +670,7 @@ pub enum GameMode {
|
||||
STD = 0,
|
||||
/// osu!taiko
|
||||
TKO = 1,
|
||||
/// osu!ctb
|
||||
/// osu!catch
|
||||
CTB = 2,
|
||||
/// osu!mania
|
||||
MNA = 3,
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user