Merge pull request #24 from MaxOhn/gradual-rework

refactor!: overhauled gradual calculation
This commit is contained in:
Badewanne3 authored and GitHub committed 2023-11-10 14:53:25 +01:00
commit 5393d36782
41 files changed
+1748 -1164

No files matched your search

+8 -2
View File
@@ -14,7 +14,10 @@ jobs:
strategy: strategy:
matrix: matrix:
feature: [default, async_tokio, async_std] feature:
- default,gradual
- async_tokio
- async_std
steps: steps:
- name: Checkout project - name: Checkout project
@@ -35,7 +38,10 @@ jobs:
strategy: strategy:
matrix: matrix:
feature: [default, async_tokio, async_std] feature:
- default,gradual
- async_tokio
- async_std
steps: steps:
- name: Checkout project - name: Checkout project
+12
View File
@@ -4,6 +4,18 @@
- Added `From<u8>` impl for `GameMode` - Added `From<u8>` impl for `GameMode`
- Added the method `AnyPP::hitresult_priority` - Added the method `AnyPP::hitresult_priority`
- __Breaking adjustments:__
- Overhauled gradual calculation. All relevant types are now gated behind the `gradual` feature which must be enabled.
- `*GradualDifficultyAttributes` has been renamed to `*GradualDifficulty` and `*GradualPerformanceAttributes`
has been renamed to `*GradualPerformance`.
- Types for gradual calculation that depend on a lifetime now have a counterpart without a lifetime that might clone
underlying data along the way. E.g. now there is `CatchOwnedGradualDifficulty` and `[Mode]OwnedGradualPerformance`.
- `OsuGradualDifficulty` and thus `GradualDifficulty` no longer implement `Clone`.
- Gradual performance calculators' method `process_next_object` has been renamed to `next` and `process_next_n_objects`
has been renamed to `nth`. They now also have the new method `last`.
- Similar to `Iterator::nth`, gradual performance calculators' method `nth` is now zero-indexed i.e. passing `n=0`
will process 1 object, `n=1` will process 2, and so on.
# v0.9.5 (2023-09-06) # v0.9.5 (2023-09-06)
- __Additions:__ - __Additions:__
+7
View File
@@ -14,6 +14,7 @@ keywords = ["osu", "pp", "stars", "async"]
default = [] default = []
async_std = ["async-std"] async_std = ["async-std"]
async_tokio = ["tokio"] async_tokio = ["tokio"]
gradual = []
[dependencies] [dependencies]
async-std = { version = "1.9", optional = true } async-std = { version = "1.9", optional = true }
@@ -21,3 +22,9 @@ tokio = { version = "1.2", optional = true, default-features = false, features =
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.2", default-features = false, features = ["fs", "rt"] } tokio = { version = "1.2", default-features = false, features = ["fs", "rt"] }
[package.metadata.docs.rs]
# document these features
features = ["gradual"]
# defines the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]
+16 -15
View File
@@ -79,11 +79,12 @@ Sometimes you might want to calculate the difficulty of a map or performance of
This could be done by using `passed_objects` as the amount of objects that were passed so far. This could be done by using `passed_objects` as the amount of objects that were passed so far.
However, this requires to recalculate the beginning again and again, we can be more efficient than that. However, this requires to recalculate the beginning again and again, we can be more efficient than that.
Instead, you should use `GradualDifficultyAttributes` and `GradualPerformanceAttributes`: Instead, you should enable the `gradual` feature and use `GradualDifficulty` and `GradualPerformance`:
```rust ```rust
use rosu_pp::{ use rosu_pp::{
Beatmap, BeatmapExt, GradualPerformanceAttributes, ScoreState, taiko::TaikoScoreState, Beatmap, BeatmapExt, GradualDifficulty, GradualPerformance, ScoreState,
taiko::TaikoScoreState,
}; };
let map = match Beatmap::from_path("/path/to/file.osu") { let map = match Beatmap::from_path("/path/to/file.osu") {
@@ -93,12 +94,11 @@ let map = match Beatmap::from_path("/path/to/file.osu") {
let mods = 8 + 64; // HDDT let mods = 8 + 64; // HDDT
// If you're only interested in the star rating or other difficulty value, // If you're only interested in the star rating or other difficulty values,
// use `GradualDifficultyAttributes`, either through its function `new` // use `GradualDifficulty`.
// or through the method `BeatmapExt::gradual_difficulty`. let gradual_difficulty = GradualDifficulty::new(&map, mods);
let gradual_difficulty = map.gradual_difficulty(mods);
// Since `GradualDifficultyAttributes` implements `Iterator`, you can use // Since `GradualDifficulty` implements `Iterator`, you can use
// any iterate function on it, use it in loops, collect them into a `Vec`, ... // any iterate function on it, use it in loops, collect them into a `Vec`, ...
for (i, difficulty) in gradual_difficulty.enumerate() { for (i, difficulty) in gradual_difficulty.enumerate() {
println!("Stars after object {}: {}", i, difficulty.stars()); println!("Stars after object {}: {}", i, difficulty.stars());
@@ -107,7 +107,7 @@ for (i, difficulty) in gradual_difficulty.enumerate() {
// Gradually calculating performance values does the same as calculating // Gradually calculating performance values does the same as calculating
// difficulty attributes but it goes the extra step and also evaluates // difficulty attributes but it goes the extra step and also evaluates
// the state of a score for these difficulty attributes. // the state of a score for these difficulty attributes.
let mut gradual_performance = map.gradual_performance(mods); let mut gradual_performance = GradualPerformance::new(&map, mods);
// The default score state is kinda chunky because it considers all modes. // The default score state is kinda chunky because it considers all modes.
let state = ScoreState { let state = ScoreState {
@@ -121,7 +121,7 @@ let state = ScoreState {
}; };
// Process the score state after the first object // Process the score state after the first object
let curr_performance = match gradual_performance.process_next_object(state) { let curr_performance = match gradual_performance.next(state) {
Some(perf) => perf, Some(perf) => perf,
None => panic!("the map has no hit objects"), None => panic!("the map has no hit objects"),
}; };
@@ -131,8 +131,8 @@ println!("PP after the first object: {}", curr_performance.pp());
// If you're only interested in maps of a specific mode, consider // If you're only interested in maps of a specific mode, consider
// using the mode's gradual calculator instead of the general one. // using the mode's gradual calculator instead of the general one.
// Let's assume it's a taiko map. // Let's assume it's a taiko map.
// Instead of starting off with `BeatmapExt::gradual_performance` one could have // Instead of starting off with `GradualPerformance` one could have
// created the struct via `TaikoGradualPerformanceAttributes::new`. // used `TaikoGradualPerformance`.
let mut gradual_performance = match gradual_performance { let mut gradual_performance = match gradual_performance {
GradualPerformanceAttributes::Taiko(gradual) => gradual, GradualPerformanceAttributes::Taiko(gradual) => gradual,
_ => panic!("the map was not taiko but {:?}", map.mode), _ => panic!("the map was not taiko but {:?}", map.mode),
@@ -146,10 +146,10 @@ let state = TaikoScoreState {
n_misses: 1, n_misses: 1,
}; };
// Process the next 10 objects in one go // Process the next 10 objects in one go (`nth` takes a zero-based value).
let curr_performance = match gradual_performance.process_next_n_objects(state, 10) { let curr_performance = match gradual_performance.nth(state, 9) {
Some(perf) => perf, Some(perf) => perf,
None => panic!("the last `process_next_object` already processed the last object"), None => panic!("the previous `next` already processed the last object"),
}; };
println!("PP after the first 11 objects: {}", curr_performance.pp()); println!("PP after the first 11 objects: {}", curr_performance.pp());
@@ -158,10 +158,11 @@ println!("PP after the first 11 objects: {}", curr_performance.pp());
### Features ### Features
| Flag | Description | | Flag | Description |
| ------------- | ---------------------------------------------------------------------------------------- | | ------------- |------------------------------------------------------------------------------------------|
| `default` | Beatmap parsing will be non-async | | `default` | Beatmap parsing will be non-async |
| `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) |
| `gradual` | Enable gradual difficulty and performance calculation |
### Version ### Version
+3 -26
View File
@@ -8,9 +8,8 @@ use crate::{
osu::{OsuDifficultyAttributes, OsuObject, ScalingFactor}, osu::{OsuDifficultyAttributes, OsuObject, ScalingFactor},
taiko::{IntoTaikoObjectIter, TaikoObject}, taiko::{IntoTaikoObjectIter, TaikoObject},
util::FloatExt, util::FloatExt,
AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, GradualDifficultyAttributes, AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, ManiaPP, ManiaStars, Mods, OsuPP,
GradualPerformanceAttributes, ManiaPP, ManiaStars, Mods, OsuPP, OsuStars, OsuStars, PerformanceAttributes, Strains, TaikoPP, TaikoStars,
PerformanceAttributes, Strains, TaikoPP, TaikoStars,
}; };
/// Provides some additional methods on [`Beatmap`]. /// Provides some additional methods on [`Beatmap`].
@@ -35,17 +34,6 @@ pub trait BeatmapExt {
/// Suitable to plot the difficulty of a map over time. /// Suitable to plot the difficulty of a map over time.
fn strains(&self, mods: u32) -> Strains; fn strains(&self, mods: u32) -> Strains;
/// Return an iterator that gives you the [`DifficultyAttributes`](crate::DifficultyAttributes) after each hit object.
///
/// Suitable to efficiently get the map's star rating after multiple different locations.
fn gradual_difficulty(&self, mods: u32) -> GradualDifficultyAttributes<'_>;
/// Return a struct that gives you the [`PerformanceAttributes`] after every (few) hit object(s).
///
/// Suitable to efficiently get a score's performance after multiple different locations,
/// i.e. live update a score's pp.
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_>;
/// Process each [`HitObject`](crate::parse::HitObject) into a an osu!-specific [`OsuObject`], /// Process each [`HitObject`](crate::parse::HitObject) into a an osu!-specific [`OsuObject`],
/// just like the difficulty calculation does. /// just like the difficulty calculation does.
fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject>; fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject>;
@@ -112,16 +100,6 @@ impl BeatmapExt for Beatmap {
} }
} }
#[inline]
fn gradual_difficulty(&self, mods: u32) -> GradualDifficultyAttributes<'_> {
GradualDifficultyAttributes::new(self, mods)
}
#[inline]
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_> {
GradualPerformanceAttributes::new(self, mods)
}
fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject> { fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject> {
let attrs = self.attributes().mods(mods).build(); let attrs = self.attributes().mods(mods).build();
let scaling_factor = ScalingFactor::new(attrs.cs); let scaling_factor = ScalingFactor::new(attrs.cs);
@@ -159,7 +137,6 @@ impl BeatmapExt for Beatmap {
curve_bufs: CurveBuffers::default(), curve_bufs: CurveBuffers::default(),
last_pos: None, last_pos: None,
last_time: 0.0, last_time: 0.0,
map: self,
ticks: Vec::new(), ticks: Vec::new(),
with_hr: mods.hr(), with_hr: mods.hr(),
}; };
@@ -167,7 +144,7 @@ impl BeatmapExt for Beatmap {
let mut hit_objects: Vec<_> = self let mut hit_objects: Vec<_> = self
.hit_objects .hit_objects
.iter() .iter()
.filter_map(|h| FruitOrJuice::new(h, &mut params)) .filter_map(|h| FruitOrJuice::new(h, &mut params, self))
.flatten() .flatten()
.collect(); .collect();
+1 -1
View File
@@ -29,7 +29,7 @@ impl CatchObject {
} }
} }
pub(crate) fn with_hr(mut self, params: &mut FruitParams<'_>) -> Self { pub(crate) fn with_hr(mut self, params: &mut FruitParams) -> Self {
let mut offset_pos = self.pos; let mut offset_pos = self.pos;
let time_diff = self.time - params.last_time; let time_diff = self.time - params.last_time;
+6 -12
View File
@@ -12,12 +12,11 @@ const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
const BASE_SCORING_DISTANCE: f64 = 100.0; const BASE_SCORING_DISTANCE: f64 = 100.0;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct FruitParams<'a> { pub(crate) struct FruitParams {
pub(crate) attributes: CatchDifficultyAttributes, pub(crate) attributes: CatchDifficultyAttributes,
pub(crate) curve_bufs: CurveBuffers, pub(crate) curve_bufs: CurveBuffers,
pub(crate) last_pos: Option<f32>, pub(crate) last_pos: Option<f32>,
pub(crate) last_time: f64, pub(crate) last_time: f64,
pub(crate) map: &'a Beatmap,
pub(crate) ticks: Vec<(Pos2, f64)>, pub(crate) ticks: Vec<(Pos2, f64)>,
pub(crate) with_hr: bool, pub(crate) with_hr: bool,
} }
@@ -31,7 +30,7 @@ pub(crate) enum FruitOrJuice {
} }
impl FruitOrJuice { impl FruitOrJuice {
pub(crate) fn new(h: &HitObject, params: &mut FruitParams<'_>) -> Option<Self> { pub(crate) fn new(h: &HitObject, params: &mut FruitParams, map: &Beatmap) -> Option<Self> {
match &h.kind { match &h.kind {
HitObjectKind::Circle => { HitObjectKind::Circle => {
let mut h = CatchObject::new((h.pos, h.start_time)); let mut h = CatchObject::new((h.pos, h.start_time));
@@ -54,17 +53,12 @@ impl FruitOrJuice {
params.last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x); params.last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x);
params.last_time = h.start_time; params.last_time = h.start_time;
let timing_point = params.map.timing_point_at(h.start_time); let timing_point = map.timing_point_at(h.start_time);
let difficulty_point = params let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
.map
.difficulty_point_at(h.start_time)
.unwrap_or_default();
let vel_factor = let vel_factor = BASE_SCORING_DISTANCE * map.slider_mult / timing_point.beat_len;
BASE_SCORING_DISTANCE * params.map.slider_mult / timing_point.beat_len; let tick_dist_factor = BASE_SCORING_DISTANCE * map.slider_mult / map.tick_rate;
let tick_dist_factor =
BASE_SCORING_DISTANCE * params.map.slider_mult / params.map.tick_rate;
let vel = vel_factor * difficulty_point.slider_vel; let vel = vel_factor * difficulty_point.slider_vel;
+128 -70
View File
@@ -1,9 +1,9 @@
use std::{iter, slice::Iter}; use std::iter;
use crate::{ use crate::{
catch::{difficulty_object::DifficultyObject, SECTION_LENGTH, STAR_SCALING_FACTOR}, catch::{difficulty_object::DifficultyObject, SECTION_LENGTH, STAR_SCALING_FACTOR},
curve::CurveBuffers, curve::CurveBuffers,
parse::{HitObject, Pos2}, parse::Pos2,
Beatmap, Mods, Beatmap, Mods,
}; };
@@ -17,19 +17,19 @@ use super::{
/// Gradually calculate the difficulty attributes of an osu!catch 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`].
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next fruit or droplet /// On every call of [`Iterator::next`], the map's next fruit or droplet
/// will be processed and the [`CatchDifficultyAttributes`] will be updated and returned. /// will be processed and the [`CatchDifficultyAttributes`] will be updated and returned.
/// ///
/// Note that it does not return attributes after a tiny droplet. Only for fruits and droplets. /// Note that it does not return attributes after a tiny droplet. Only for fruits and droplets.
/// ///
/// If you want to calculate performance attributes, use /// If you want to calculate performance attributes, use
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformanceAttributes) instead. /// [`CatchGradualPerformance`](crate::catch::CatchGradualPerformance) instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, catch::CatchGradualDifficultyAttributes}; /// use rosu_pp::{Beatmap, catch::CatchGradualDifficulty};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -37,7 +37,7 @@ use super::{
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut iter = CatchGradualDifficultyAttributes::new(&map, mods); /// let mut iter = CatchGradualDifficulty::new(&map, mods);
/// ///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object /// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object /// let attrs2 = iter.next(); // after the second hit object
@@ -47,11 +47,119 @@ use super::{
/// // ... /// // ...
/// } /// }
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CatchGradualDifficultyAttributes<'map> { pub struct CatchGradualDifficulty<'map> {
pub(crate) idx: usize, map: &'map Beatmap,
inner: CatchGradualDifficultyInner,
}
impl<'map> CatchGradualDifficulty<'map> {
/// Create a new difficulty attributes iterator for osu!catch maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let inner = CatchGradualDifficultyInner::new(map, mods);
Self { map, inner }
}
pub(crate) fn idx(&self) -> usize {
self.inner.idx
}
}
impl Iterator for CatchGradualDifficulty<'_> {
type Item = CatchDifficultyAttributes;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next(self.map)
}
}
/// Gradually calculate the difficulty attributes of an osu!catch map.
///
/// Check [`CatchGradualDifficulty`] for more information. This struct does the same
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)]
pub struct CatchOwnedGradualDifficulty {
pub(crate) map: Beatmap,
inner: CatchGradualDifficultyInner,
}
impl CatchOwnedGradualDifficulty {
/// Create a new difficulty attributes iterator for osu!catch maps.
pub fn new(map: Beatmap, mods: u32) -> Self {
let inner = CatchGradualDifficultyInner::new(&map, mods);
Self { map, inner }
}
#[allow(unused)]
pub(crate) fn idx(&self) -> usize {
self.inner.idx
}
}
impl Iterator for CatchOwnedGradualDifficulty {
type Item = CatchDifficultyAttributes;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next(&self.map)
}
}
#[derive(Clone, Debug)]
struct CatchObjectIter {
hit_object_idx: usize,
last_object: Option<FruitOrJuice>,
params: FruitParams,
}
impl CatchObjectIter {
fn new(mods: impl Mods, attributes: CatchDifficultyAttributes) -> Self {
let params = FruitParams {
attributes,
curve_bufs: CurveBuffers::default(),
last_pos: None,
last_time: 0.0,
ticks: Vec::new(),
with_hr: mods.hr(),
};
Self {
hit_object_idx: 0,
last_object: None,
params,
}
}
fn attributes(&self) -> CatchDifficultyAttributes {
self.params.attributes.clone()
}
fn next(&mut self, map: &Beatmap) -> Option<CatchObject> {
if let opt @ Some(_) = self.last_object.as_mut().and_then(Iterator::next) {
return opt;
}
map.hit_objects[self.hit_object_idx..]
.iter()
.find_map(|h| {
self.hit_object_idx += 1;
FruitOrJuice::new(h, &mut self.params, map)
})
.and_then(|h| self.last_object.insert(h).next())
}
}
#[derive(Clone, Debug)]
struct CatchGradualDifficultyInner {
idx: usize,
clock_rate: f64, clock_rate: f64,
hit_objects: CatchObjectIter<'map>, hit_objects: CatchObjectIter,
movement: Movement, movement: Movement,
prev: CatchObject, prev: CatchObject,
half_catcher_width: f64, half_catcher_width: f64,
@@ -61,9 +169,8 @@ pub struct CatchGradualDifficultyAttributes<'map> {
strain_peak_buf: Vec<f64>, strain_peak_buf: Vec<f64>,
} }
impl<'map> CatchGradualDifficultyAttributes<'map> { impl CatchGradualDifficultyInner {
/// Create a new difficulty attributes iterator for osu!catch maps. fn new(map: &Beatmap, mods: u32) -> Self {
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let map_attributes = map.attributes().mods(mods).build(); let map_attributes = map.attributes().mods(mods).build();
let attributes = CatchDifficultyAttributes { let attributes = CatchDifficultyAttributes {
@@ -71,7 +178,7 @@ impl<'map> CatchGradualDifficultyAttributes<'map> {
..Default::default() ..Default::default()
}; };
let hit_objects = CatchObjectIter::new(map, mods, attributes); let hit_objects = CatchObjectIter::new(mods, attributes);
let half_catcher_width = let half_catcher_width =
(calculate_catch_width(map_attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64; (calculate_catch_width(map_attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
@@ -103,13 +210,9 @@ impl<'map> CatchGradualDifficultyAttributes<'map> {
&mut self.last_excess, &mut self.last_excess,
); );
} }
}
impl Iterator for CatchGradualDifficultyAttributes<'_> { fn next(&mut self, map: &Beatmap) -> Option<CatchDifficultyAttributes> {
type Item = CatchDifficultyAttributes; let curr = self.hit_objects.next(map)?;
fn next(&mut self) -> Option<Self::Item> {
let curr = self.hit_objects.next()?;
self.idx += 1; self.idx += 1;
if self.idx == 1 { if self.idx == 1 {
@@ -153,59 +256,14 @@ impl Iterator for CatchGradualDifficultyAttributes<'_> {
*last = self.movement.curr_section_peak; *last = self.movement.curr_section_peak;
} }
let mut attributes = self.hit_objects.attributes(); let stars =
attributes.stars =
Movement::difficulty_value(&mut self.strain_peak_buf).sqrt() * STAR_SCALING_FACTOR; Movement::difficulty_value(&mut self.strain_peak_buf).sqrt() * STAR_SCALING_FACTOR;
Some(attributes) let attrs = CatchDifficultyAttributes {
} stars,
} ..self.hit_objects.attributes()
#[derive(Clone, Debug)]
struct CatchObjectIter<'map> {
last_object: Option<FruitOrJuice>,
hit_objects: Iter<'map, HitObject>,
params: FruitParams<'map>,
}
impl<'map> CatchObjectIter<'map> {
fn new(map: &'map Beatmap, mods: impl Mods, attributes: CatchDifficultyAttributes) -> Self {
let params = FruitParams {
attributes,
curve_bufs: CurveBuffers::default(),
last_pos: None,
last_time: 0.0,
map,
ticks: Vec::new(),
with_hr: mods.hr(),
}; };
Self { Some(attrs)
last_object: None,
hit_objects: map.hit_objects.iter(),
params,
}
}
fn attributes(&self) -> CatchDifficultyAttributes {
self.params.attributes.clone()
}
}
impl Iterator for CatchObjectIter<'_> {
type Item = CatchObject;
fn next(&mut self) -> Option<Self::Item> {
if let Some(h) = self.last_object.as_mut().and_then(Iterator::next) {
return Some(h);
}
for h in &mut self.hit_objects {
if let Some(h) = FruitOrJuice::new(h, &mut self.params) {
return self.last_object.insert(h).next();
}
}
None
} }
} }
+92 -80
View File
@@ -1,44 +1,15 @@
use crate::{Beatmap, CatchPP}; use crate::{Beatmap, CatchPP};
use super::{CatchGradualDifficultyAttributes, CatchPerformanceAttributes}; use super::{
CatchGradualDifficulty, CatchOwnedGradualDifficulty, CatchPerformanceAttributes,
/// Aggregation for a score's current state i.e. what was the CatchScoreState,
/// maximum combo so far and what are the current hitresults. };
///
/// This struct is used for [`CatchGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CatchScoreState {
/// 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!catch combo.
pub max_combo: usize,
/// Amount of current fruits (300s).
pub n_fruits: usize,
/// Amount of current droplets (100s).
pub n_droplets: usize,
/// Amount of current tiny droplets (50s).
pub n_tiny_droplets: usize,
/// Amount of current tiny droplet misses (katus).
pub n_tiny_droplet_misses: usize,
/// Amount of current misses (fruits and droplets).
pub n_misses: usize,
}
impl CatchScoreState {
/// Create a new empty score state.
pub fn new() -> Self {
Self::default()
}
}
/// Gradually calculate the performance attributes of an osu!catch 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 [`next`](`CatchGradualPerformance::next`)
/// [`process_next_object`](`CatchGradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`CatchPerformanceAttributes`]. /// and it will return the resulting current [`CatchPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use [`nth`](`CatchGradualPerformance::nth`) instead.
/// [`process_next_n_objects`](`CatchGradualPerformanceAttributes::process_next_n_objects`) instead.
/// ///
/// Both methods require a [`CatchScoreState`] that contains the current /// Both methods require a [`CatchScoreState`] that contains the current
/// hitresults as well as the maximum combo so far. /// hitresults as well as the maximum combo so far.
@@ -46,13 +17,12 @@ impl CatchScoreState {
/// Note that neither hits nor misses of tiny droplets require /// Note that neither hits nor misses of tiny droplets require
/// to be processed. Only fruits and droplets do. /// to be processed. Only fruits and droplets do.
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use [`CatchGradualDifficulty`] instead.
/// [`CatchGradualDifficultyAttributes`](crate::catch::CatchGradualDifficultyAttributes) instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, catch::{CatchGradualPerformanceAttributes, CatchScoreState}}; /// use rosu_pp::{Beatmap, catch::{CatchGradualPerformance, CatchScoreState}};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -60,7 +30,7 @@ impl CatchScoreState {
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut gradual_perf = CatchGradualPerformanceAttributes::new(&map, mods); /// let mut gradual_perf = CatchGradualPerformance::new(&map, mods);
/// let mut state = CatchScoreState::new(); // empty state, everything is on 0. /// let mut state = CatchScoreState::new(); // empty state, everything is on 0.
/// ///
/// // The first 10 hitresults are only fruits /// // The first 10 hitresults are only fruits
@@ -69,10 +39,10 @@ impl CatchScoreState {
/// state.max_combo += 1; /// state.max_combo += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
@@ -80,10 +50,10 @@ impl CatchScoreState {
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // The next 10 objects will be a mixture of fruits and droplets. /// // The next 10 objects will be a mixture of fruits and droplets.
/// // Notice how tiny droplets from sliders do not count as hit objects /// // Notice how tiny droplets from sliders do not count as hit objects
@@ -92,20 +62,21 @@ impl CatchScoreState {
/// state.n_fruits += 4; /// state.n_fruits += 4;
/// state.n_droplets += 6; /// state.n_droplets += 6;
/// state.n_tiny_droplets += 12; /// state.n_tiny_droplets += 12;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10); /// # let _ = gradual_perf.nth(state.clone(), 9);
/// ///
/// // Now comes another fruit. Note that the max combo gets incremented again. /// // Now comes another fruit. Note that the max combo gets incremented again.
/// state.n_fruits += 1; /// state.n_fruits += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -115,25 +86,26 @@ impl CatchScoreState {
/// state.n_tiny_droplets = ... /// state.n_tiny_droplets = ...
/// state.n_tiny_droplet_misses = ... /// state.n_tiny_droplet_misses = ...
/// state.n_misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CatchGradualPerformanceAttributes<'map> { pub struct CatchGradualPerformance<'map> {
difficulty: CatchGradualDifficultyAttributes<'map>, difficulty: CatchGradualDifficulty<'map>,
performance: CatchPP<'map>, performance: CatchPP<'map>,
} }
impl<'map> CatchGradualPerformanceAttributes<'map> { impl<'map> CatchGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!standard maps. /// Create a new gradual performance calculator for osu!standard maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let difficulty = CatchGradualDifficultyAttributes::new(map, mods); let difficulty = CatchGradualDifficulty::new(map, mods);
let performance = CatchPP::new(map).mods(mods).passed_objects(0); let performance = CatchPP::new(map).mods(mods).passed_objects(0);
Self { Self {
@@ -147,41 +119,81 @@ impl<'map> CatchGradualPerformanceAttributes<'map> {
/// ///
/// Note that neither hits nor misses of tiny droplets require /// Note that neither hits nor misses of tiny droplets require
/// to be processed. Only fruits and droplets do. /// to be processed. Only fruits and droplets do.
pub fn process_next_object( pub fn next(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: CatchScoreState,
) -> Option<CatchPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`CatchGradualPerformanceAttributes::process_next_object`) /// Process all remaining hit objects and calculate the final performance attributes.
/// but instead of processing only one object it process `n` many. pub fn last(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
/// ///
/// If `n` is 0 it will be considered as 1. /// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// If there are still objects to be processed but `n` is larger than the amount /// and so on.
/// of remaining objects, `n` will be considered as the amount of remaining objects. pub fn nth(&mut self, state: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.by_ref().take(n.saturating_add(1)).last()?;
&mut self,
state: CatchScoreState,
n: usize,
) -> Option<CatchPerformanceAttributes> {
let mut difficulty = None;
for _ in 0..n.max(1) {
match self.difficulty.next() {
Some(attrs) => difficulty = Some(attrs),
None => break,
}
}
let difficulty = difficulty?;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(self.difficulty.idx) .passed_objects(self.difficulty.idx())
.calculate();
Some(performance)
}
}
/// Gradually calculate the performance attributes of an osu!catch map.
///
/// Check [`CatchGradualPerformance`] for more information. This struct does the same
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)]
pub struct CatchOwnedGradualPerformance {
difficulty: CatchOwnedGradualDifficulty,
mods: u32,
}
impl CatchOwnedGradualPerformance {
/// Create a new gradual performance calculator for osu!standard maps.
pub fn new(map: Beatmap, mods: u32) -> Self {
let difficulty = CatchOwnedGradualDifficulty::new(map, mods);
Self { difficulty, mods }
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score state.
///
/// Note that neither hits nor misses of tiny droplets require
/// to be processed. Only fruits and droplets do.
pub fn next(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance attributes.
pub fn last(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
pub fn nth(&mut self, state: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
let difficulty = self.difficulty.by_ref().take(n.saturating_add(1)).last()?;
let performance = CatchPP::new(&self.difficulty.map)
.mods(self.mods)
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx())
.calculate(); .calculate();
Some(performance) Some(performance)
+15 -6
View File
@@ -1,15 +1,25 @@
mod catch_object; mod catch_object;
mod difficulty_object; mod difficulty_object;
mod fruit_or_juice; mod fruit_or_juice;
mod gradual_difficulty;
mod gradual_performance;
mod movement; mod movement;
mod pp; mod pp;
mod score_state;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use difficulty_object::DifficultyObject; use difficulty_object::DifficultyObject;
use movement::Movement; use movement::Movement;
pub use self::{catch_object::CatchObject, gradual_difficulty::*, gradual_performance::*, pp::*}; pub use self::{catch_object::CatchObject, pp::*, score_state::CatchScoreState};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::{CatchGradualDifficulty, CatchOwnedGradualDifficulty},
gradual_performance::{CatchGradualPerformance, CatchOwnedGradualPerformance},
};
pub(crate) use self::fruit_or_juice::{FruitOrJuice, FruitParams}; pub(crate) use self::fruit_or_juice::{FruitOrJuice, FruitParams};
@@ -73,7 +83,7 @@ impl<'map> CatchStars<'map> {
/// ///
/// If you want to calculate the difficulty after every few objects, instead of /// If you want to calculate the difficulty after every few objects, instead of
/// using [`CatchStars`] multiple times with different `passed_objects`, you should use /// using [`CatchStars`] multiple times with different `passed_objects`, you should use
/// [`CatchGradualDifficultyAttributes`](crate::catch::CatchGradualDifficultyAttributes). /// [`CatchGradualDifficultyAttributes`](crate::catch::CatchGradualDifficulty).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
@@ -156,7 +166,6 @@ fn calculate_movement(params: CatchStars<'_>) -> (Movement, CatchDifficultyAttri
curve_bufs: CurveBuffers::default(), curve_bufs: CurveBuffers::default(),
last_pos: None, last_pos: None,
last_time: 0.0, last_time: 0.0,
map,
ticks: Vec::new(), // using the same buffer for all sliders ticks: Vec::new(), // using the same buffer for all sliders
with_hr: mods.hr(), with_hr: mods.hr(),
}; };
@@ -165,7 +174,7 @@ fn calculate_movement(params: CatchStars<'_>) -> (Movement, CatchDifficultyAttri
let mut hit_objects = map let mut hit_objects = map
.hit_objects .hit_objects
.iter() .iter()
.filter_map(|h| FruitOrJuice::new(h, &mut params)) .filter_map(|h| FruitOrJuice::new(h, &mut params, map))
.flatten() .flatten()
.take(take); .take(take);
+1 -1
View File
@@ -141,7 +141,7 @@ impl<'map> CatchPP<'map> {
/// ///
/// If you want to calculate the performance after every few objects, instead of /// If you want to calculate the performance after every few objects, instead of
/// using [`CatchPP`] multiple times with different `passed_objects`, you should use /// using [`CatchPP`] multiple times with different `passed_objects`, you should use
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformanceAttributes). /// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformance).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects.replace(passed_objects); self.passed_objects.replace(passed_objects);
+27
View File
@@ -0,0 +1,27 @@
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CatchScoreState {
/// 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!catch combo.
pub max_combo: usize,
/// Amount of current fruits (300s).
pub n_fruits: usize,
/// Amount of current droplets (100s).
pub n_droplets: usize,
/// Amount of current tiny droplets (50s).
pub n_tiny_droplets: usize,
/// Amount of current tiny droplet misses (katus).
pub n_tiny_droplet_misses: usize,
/// Amount of current misses (fruits and droplets).
pub n_misses: usize,
}
impl CatchScoreState {
/// Create a new empty score state.
pub fn new() -> Self {
Self::default()
}
}
+211 -196
View File
@@ -1,24 +1,29 @@
#![cfg(feature = "gradual")]
use crate::catch::{CatchOwnedGradualDifficulty, CatchOwnedGradualPerformance};
use crate::mania::{ManiaOwnedGradualDifficulty, ManiaOwnedGradualPerformance};
use crate::osu::OsuOwnedGradualPerformance;
use crate::taiko::TaikoOwnedGradualPerformance;
use crate::{ use crate::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState}, catch::{CatchGradualDifficulty, CatchGradualPerformance},
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState}, mania::{ManiaGradualDifficulty, ManiaGradualPerformance},
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState}, osu::{OsuGradualDifficulty, OsuGradualPerformance},
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState}, taiko::{TaikoGradualDifficulty, TaikoGradualPerformance},
Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes, Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes, ScoreState,
}; };
/// Gradually calculate the difficulty attributes on maps of any mode. /// Gradually calculate the difficulty attributes on maps of any mode.
/// ///
/// Note that this struct implements [`Iterator`](std::iter::Iterator). /// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will /// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`DifficultyAttributes`] will be updated and returned. /// be processed and the [`DifficultyAttributes`] will be updated and returned.
/// ///
/// If you want to calculate performance attributes, use /// If you want to calculate performance attributes, use [`GradualPerformance`] instead.
/// [`GradualPerformanceAttributes`](crate::GradualPerformanceAttributes) instead.
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```no_run
/// use rosu_pp::{Beatmap, GradualDifficultyAttributes}; /// use rosu_pp::{Beatmap, GradualDifficulty};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -26,7 +31,7 @@ use crate::{
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut iter = GradualDifficultyAttributes::new(&map, mods); /// let mut iter = GradualDifficulty::new(&map, mods);
/// ///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object /// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object /// let attrs2 = iter.next(); // after the second hit object
@@ -36,169 +41,138 @@ use crate::{
/// // ... /// // ...
/// } /// }
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)] pub enum GradualDifficulty<'map> {
pub enum GradualDifficultyAttributes<'map> {
/// Gradual osu!standard difficulty attributes. /// Gradual osu!standard difficulty attributes.
Osu(OsuGradualDifficultyAttributes), Osu(OsuGradualDifficulty),
/// Gradual osu!taiko difficulty attributes. /// Gradual osu!taiko difficulty attributes.
Taiko(TaikoGradualDifficultyAttributes), Taiko(TaikoGradualDifficulty),
/// Gradual osu!catch difficulty attributes. /// Gradual osu!catch difficulty attributes.
Catch(CatchGradualDifficultyAttributes<'map>), Catch(CatchGradualDifficulty<'map>),
/// Gradual osu!mania difficulty attributes. /// Gradual osu!mania difficulty attributes.
Mania(ManiaGradualDifficultyAttributes<'map>), Mania(ManiaGradualDifficulty<'map>),
} }
impl<'map> GradualDifficultyAttributes<'map> { impl<'map> GradualDifficulty<'map> {
// FIXME: converted catch maps will always count as osu!std since their mode is not modified
/// Create a new gradual difficulty calculator for maps of any mode. /// Create a new gradual difficulty calculator for maps of any mode.
#[inline] #[inline]
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
match map.mode { match map.mode {
GameMode::Osu => Self::Osu(OsuGradualDifficultyAttributes::new(map, mods)), GameMode::Osu => Self::Osu(OsuGradualDifficulty::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualDifficultyAttributes::new(map, mods)), GameMode::Taiko => Self::Taiko(TaikoGradualDifficulty::new(map, mods)),
GameMode::Catch => Self::Catch(CatchGradualDifficultyAttributes::new(map, mods)), GameMode::Catch => Self::Catch(CatchGradualDifficulty::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaGradualDifficultyAttributes::new(map, mods)), GameMode::Mania => Self::Mania(ManiaGradualDifficulty::new(map, mods)),
} }
} }
} }
impl Iterator for GradualDifficultyAttributes<'_> { impl Iterator for GradualDifficulty<'_> {
type Item = DifficultyAttributes; type Item = DifficultyAttributes;
#[inline] #[inline]
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self {
GradualDifficultyAttributes::Osu(o) => o.next().map(DifficultyAttributes::Osu), Self::Osu(o) => o.next().map(DifficultyAttributes::Osu),
GradualDifficultyAttributes::Taiko(t) => t.next().map(DifficultyAttributes::Taiko), Self::Taiko(t) => t.next().map(DifficultyAttributes::Taiko),
GradualDifficultyAttributes::Catch(f) => f.next().map(DifficultyAttributes::Catch), Self::Catch(f) => f.next().map(DifficultyAttributes::Catch),
GradualDifficultyAttributes::Mania(m) => m.next().map(DifficultyAttributes::Mania), Self::Mania(m) => m.next().map(DifficultyAttributes::Mania),
} }
} }
#[inline] #[inline]
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
match self { match self {
GradualDifficultyAttributes::Osu(o) => o.size_hint(), Self::Osu(o) => o.size_hint(),
GradualDifficultyAttributes::Taiko(t) => t.size_hint(), Self::Taiko(t) => t.size_hint(),
GradualDifficultyAttributes::Catch(f) => f.size_hint(), Self::Catch(f) => f.size_hint(),
GradualDifficultyAttributes::Mania(m) => m.size_hint(), Self::Mania(m) => m.size_hint(),
}
}
#[inline]
fn nth(&mut self, n: usize) -> Option<Self::Item> {
match self {
Self::Osu(o) => o.nth(n).map(DifficultyAttributes::Osu),
Self::Taiko(t) => t.nth(n).map(DifficultyAttributes::Taiko),
Self::Catch(c) => c.nth(n).map(DifficultyAttributes::Catch),
Self::Mania(m) => m.nth(n).map(DifficultyAttributes::Mania),
} }
} }
} }
/// Aggregation for a score's current state i.e. what is /// Gradually calculate the difficulty attributes on maps of any mode.
/// the maximum combo so far, what are the current
/// hitresults and what is the current score.
/// ///
/// This struct is used for [`GradualPerformanceAttributes`]. /// Check [`GradualDifficulty`] for more information. This type does the same
#[derive(Clone, Debug, Default, Eq, PartialEq)] /// but depending on the mode it might clone [`Beatmap`] to avoid being bound to a lifetime.
pub struct ScoreState { #[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
/// Maximum combo that the score has had so far. #[derive(Debug)]
/// **Not** the maximum possible combo of the map so far. #[allow(clippy::large_enum_variant)]
/// pub enum OwnedGradualDifficulty {
/// Note that for osu!catch only fruits and droplets are considered for combo. /// Gradual osu!standard difficulty attributes.
/// Osu(OsuGradualDifficulty),
/// Irrelevant for osu!mania. /// Gradual osu!taiko difficulty attributes.
pub max_combo: usize, Taiko(TaikoGradualDifficulty),
/// Amount of current gekis (n320 for osu!mania). /// Gradual osu!catch difficulty attributes.
pub n_geki: usize, Catch(CatchOwnedGradualDifficulty),
/// Amount of current katus (tiny droplet misses for osu!catch / n200 for osu!mania). /// Gradual osu!mania difficulty attributes.
pub n_katu: usize, Mania(ManiaOwnedGradualDifficulty),
/// Amount of current 300s (fruits for osu!catch).
pub n300: usize,
/// Amount of current 100s (droplets for osu!catch).
pub n100: usize,
/// Amount of current 50s (tiny droplets for osu!catch).
pub n50: usize,
/// Amount of current misses (fruits + droplets for osu!catch).
pub n_misses: usize,
} }
impl ScoreState { impl OwnedGradualDifficulty {
/// Create a new empty score state. // FIXME: converted catch maps will always count as osu!std since their mode is not modified
/// Create a new gradual difficulty calculator for maps of any mode.
#[inline] #[inline]
pub fn new() -> Self { pub fn new(map: &Beatmap, mods: u32) -> Self {
Self::default() match map.mode {
GameMode::Osu => Self::Osu(OsuGradualDifficulty::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualDifficulty::new(map, mods)),
GameMode::Catch => Self::Catch(CatchOwnedGradualDifficulty::new(map.to_owned(), mods)),
GameMode::Mania => Self::Mania(ManiaOwnedGradualDifficulty::new(map.to_owned(), mods)),
}
}
} }
/// Return the total amount of hits by adding everything up based on the mode. impl Iterator for OwnedGradualDifficulty {
type Item = DifficultyAttributes;
#[inline] #[inline]
pub fn total_hits(&self, mode: GameMode) -> usize { fn next(&mut self) -> Option<Self::Item> {
let mut amount = self.n300 + self.n100 + self.n_misses; match self {
Self::Osu(o) => o.next().map(DifficultyAttributes::Osu),
if mode != GameMode::Taiko { Self::Taiko(t) => t.next().map(DifficultyAttributes::Taiko),
amount += self.n50; Self::Catch(f) => f.next().map(DifficultyAttributes::Catch),
Self::Mania(m) => m.next().map(DifficultyAttributes::Mania),
if mode != GameMode::Osu {
amount += self.n_katu;
amount += (mode != GameMode::Catch) as usize * self.n_geki;
} }
} }
amount
}
}
impl From<ScoreState> for OsuScoreState {
#[inline] #[inline]
fn from(state: ScoreState) -> Self { fn size_hint(&self) -> (usize, Option<usize>) {
Self { match self {
max_combo: state.max_combo, Self::Osu(o) => o.size_hint(),
n300: state.n300, Self::Taiko(t) => t.size_hint(),
n100: state.n100, Self::Catch(f) => f.size_hint(),
n50: state.n50, Self::Mania(m) => m.size_hint(),
n_misses: state.n_misses,
}
} }
} }
impl From<ScoreState> for TaikoScoreState {
#[inline] #[inline]
fn from(state: ScoreState) -> Self { fn nth(&mut self, n: usize) -> Option<Self::Item> {
Self { match self {
max_combo: state.max_combo, Self::Osu(o) => o.nth(n).map(DifficultyAttributes::Osu),
n300: state.n300, Self::Taiko(t) => t.nth(n).map(DifficultyAttributes::Taiko),
n100: state.n100, Self::Catch(c) => c.nth(n).map(DifficultyAttributes::Catch),
n_misses: state.n_misses, Self::Mania(m) => m.nth(n).map(DifficultyAttributes::Mania),
}
}
}
impl From<ScoreState> for CatchScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n_fruits: state.n300,
n_droplets: state.n100,
n_tiny_droplets: state.n50,
n_tiny_droplet_misses: state.n_katu,
n_misses: state.n_misses,
}
}
}
impl From<ScoreState> for ManiaScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
n320: state.n_geki,
n300: state.n300,
n200: state.n_katu,
n100: state.n100,
n50: state.n50,
n_misses: state.n_misses,
} }
} }
} }
/// Gradually calculate the performance attributes on maps of any mode. /// Gradually calculate the performance attributes on maps of any mode.
/// ///
/// After each hit object you can call /// After each hit object you can call [`next`](`GradualPerformance::next`)
/// [`process_next_object`](`GradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`PerformanceAttributes`]. /// and it will return the resulting current [`PerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use [`nth`](`GradualPerformance::nth`) instead.
/// [`process_next_n_objects`](`GradualPerformanceAttributes::process_next_n_objects`) instead.
/// ///
/// Both methods require a [`ScoreState`] that contains the current hitresults /// Both methods require a [`ScoreState`] that contains the current hitresults
/// as well as the maximum combo so far or just the current score for osu!mania. /// as well as the maximum combo so far or just the current score for osu!mania.
@@ -206,19 +180,15 @@ impl From<ScoreState> for ManiaScoreState {
/// and should be updated properly. /// and should be updated properly.
/// ///
/// Alternatively, you can match on the map's mode yourself and use the gradual /// Alternatively, you can match on the map's mode yourself and use the gradual
/// performance attribute struct for the corresponding mode, i.e. /// performance attribute struct for the corresponding mode, i.e. [`OsuGradualPerformance`],
/// [`OsuGradualPerformanceAttributes`], /// [`TaikoGradualPerformance`], [`CatchGradualPerformance`], or [`ManiaGradualPerformance`].
/// [`TaikoGradualPerformanceAttributes`],
/// [`CatchGradualPerformanceAttributes`], or
/// [`ManiaGradualPerformanceAttributes`].
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use [`GradualDifficulty`] instead.
/// [`GradualDifficultyAttributes`](crate::GradualDifficultyAttributes) instead.
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```no_run
/// use rosu_pp::{Beatmap, GradualPerformanceAttributes, ScoreState}; /// use rosu_pp::{Beatmap, GradualPerformance, ScoreState};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -226,123 +196,168 @@ impl From<ScoreState> for ManiaScoreState {
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut gradual_perf = GradualPerformanceAttributes::new(&map, mods); /// let mut gradual_perf = GradualPerformance::new(&map, mods);
/// let mut state = ScoreState::new(); // empty state, everything is on 0. /// let mut state = ScoreState::new(); // empty state, everything is on 0.
/// ///
/// // The first 10 hitresults are 300s and increase the score by 123 each. /// // The first 10 hitresults are 300s
/// for _ in 0..10 { /// for _ in 0..10 {
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// ///
/// # /* /// let performance = gradual_perf.next(state.clone()).unwrap();
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// println!("PP: {}", performance.pp());
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
/// // Note that state's max combo won't be incremented for /// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* ///
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp());
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// ///
/// // The next 10 objects will be a mixture of 300s, 100s, and 50s. /// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
/// // Notice how all 10 objects will be processed in one go. /// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2; /// state.n300 += 2;
/// state.n100 += 7; /// state.n100 += 7;
/// state.n50 += 1; /// state.n50 += 1;
/// // Don't forget state.n_katu ///
/// # /* /// // The `nth` method takes a zero-based value.
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp());
/// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
/// ///
/// // Now comes another 300. Note that the max combo gets incremented again. /// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// # /* ///
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp());
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
/// state.max_combo = ... /// state.max_combo = ...
/// state.n300 = ... /// state.n300 = ...
/// ... /// ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// let final_performance = gradual_perf.last(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum GradualPerformanceAttributes<'map> { pub enum GradualPerformance<'map> {
/// Gradual osu!standard performance attributes. /// Gradual osu!standard performance calculator.
Osu(OsuGradualPerformanceAttributes<'map>), Osu(OsuGradualPerformance<'map>),
/// Gradual osu!taiko performance attributes. /// Gradual osu!taiko performance calculator.
Taiko(TaikoGradualPerformanceAttributes<'map>), Taiko(TaikoGradualPerformance<'map>),
/// Gradual osu!catch performance attributes. /// Gradual osu!catch performance calculator.
Catch(CatchGradualPerformanceAttributes<'map>), Catch(CatchGradualPerformance<'map>),
/// Gradual osu!mania performance attributes. /// Gradual osu!mania performance calculator.
Mania(ManiaGradualPerformanceAttributes<'map>), Mania(ManiaGradualPerformance<'map>),
} }
impl<'map> GradualPerformanceAttributes<'map> { impl<'map> GradualPerformance<'map> {
// FIXME: converted catch maps will always count as osu!std since their mode is not modified
/// Create a new gradual performance calculator for maps of any mode. /// Create a new gradual performance calculator for maps of any mode.
#[inline] #[inline]
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
match map.mode { match map.mode {
GameMode::Osu => Self::Osu(OsuGradualPerformanceAttributes::new(map, mods)), GameMode::Osu => Self::Osu(OsuGradualPerformance::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualPerformanceAttributes::new(map, mods)), GameMode::Taiko => Self::Taiko(TaikoGradualPerformance::new(map, mods)),
GameMode::Catch => Self::Catch(CatchGradualPerformanceAttributes::new(map, mods)), GameMode::Catch => Self::Catch(CatchGradualPerformance::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaGradualPerformanceAttributes::new(map, mods)), GameMode::Mania => Self::Mania(ManiaGradualPerformance::new(map, mods)),
} }
} }
/// Process the next hit object and calculate the /// Process the next hit object and calculate the
/// performance attributes for the resulting score. /// performance attributes for the resulting score.
#[inline] #[inline]
pub fn process_next_object(&mut self, state: ScoreState) -> Option<PerformanceAttributes> { pub fn next(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.process_next_n_objects(state, 1) self.nth(state, 0)
} }
/// Same as [`process_next_object`](`GradualPerformanceAttributes::process_next_object`) /// Process all remaining hit objects and calculate the final performance attributes.
/// but instead of processing only one object it process `n` many.
///
/// If `n` is 0 it will be considered as 1.
/// If there are still objects to be processed but `n` is larger than the amount
/// of remaining objects, `n` will be considered as the amount of remaining objects.
#[inline] #[inline]
pub fn process_next_n_objects( pub fn last(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
&mut self, self.nth(state, usize::MAX)
state: ScoreState, }
n: usize,
) -> Option<PerformanceAttributes> { /// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
#[inline]
pub fn nth(&mut self, state: ScoreState, n: usize) -> Option<PerformanceAttributes> {
match self { match self {
GradualPerformanceAttributes::Osu(o) => o Self::Osu(o) => o.nth(state.into(), n).map(PerformanceAttributes::Osu),
.process_next_n_objects(state.into(), n) Self::Taiko(t) => t.nth(state.into(), n).map(PerformanceAttributes::Taiko),
.map(PerformanceAttributes::Osu), Self::Catch(f) => f.nth(state.into(), n).map(PerformanceAttributes::Catch),
GradualPerformanceAttributes::Taiko(t) => t Self::Mania(m) => m.nth(state.into(), n).map(PerformanceAttributes::Mania),
.process_next_n_objects(state.into(), n) }
.map(PerformanceAttributes::Taiko), }
GradualPerformanceAttributes::Catch(f) => f }
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Catch), /// Gradually calculate the performance attributes on maps of any mode.
GradualPerformanceAttributes::Mania(m) => m ///
.process_next_n_objects(state.into(), n) /// Check [`GradualPerformance`] for more information. This type does the same
.map(PerformanceAttributes::Mania), /// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum OwnedGradualPerformance {
/// Gradual osu!standard performance calculator.
Osu(OsuOwnedGradualPerformance),
/// Gradual osu!taiko performance calculator.
Taiko(TaikoOwnedGradualPerformance),
/// Gradual osu!catch performance calculator.
Catch(CatchOwnedGradualPerformance),
/// Gradual osu!mania performance calculator.
Mania(ManiaOwnedGradualPerformance),
}
impl OwnedGradualPerformance {
// FIXME: converted catch maps will always count as osu!std since their mode is not modified
/// Create a new gradual performance calculator for maps of any mode.
#[inline]
pub fn new(map: Beatmap, mods: u32) -> Self {
match map.mode {
GameMode::Osu => Self::Osu(OsuOwnedGradualPerformance::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoOwnedGradualPerformance::new(map, mods)),
GameMode::Catch => Self::Catch(CatchOwnedGradualPerformance::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaOwnedGradualPerformance::new(map, mods)),
}
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
#[inline]
pub fn next(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance attributes.
#[inline]
pub fn last(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
#[inline]
pub fn nth(&mut self, state: ScoreState, n: usize) -> Option<PerformanceAttributes> {
match self {
Self::Osu(o) => o.nth(state.into(), n).map(PerformanceAttributes::Osu),
Self::Taiko(t) => t.nth(state.into(), n).map(PerformanceAttributes::Taiko),
Self::Catch(f) => f.nth(state.into(), n).map(PerformanceAttributes::Catch),
Self::Mania(m) => m.nth(state.into(), n).map(PerformanceAttributes::Mania),
} }
} }
} }
+111 -98
View File
@@ -1,3 +1,15 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(
clippy::all,
nonstandard_style,
rust_2018_idioms,
unused,
warnings,
missing_debug_implementations,
missing_docs,
rustdoc::broken_intra_doc_links
)]
//! A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes. //! A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
//! //!
//! Async is supported through features, see below. //! Async is supported through features, see below.
@@ -74,111 +86,105 @@
//! println!("PP: {}", result.pp()); //! println!("PP: {}", result.pp());
//! ``` //! ```
//! //!
//! ## Gradual calculation #![cfg_attr(
//! Sometimes you might want to calculate the difficulty of a map or performance of a score after each hit object. feature = "gradual",
//! This could be done by using `passed_objects` as the amount of objects that were passed so far. doc = r#"
//! However, this requires to recalculate the beginning again and again, we can be more efficient than that. ## Gradual calculation
//! Sometimes you might want to calculate the difficulty of a map or performance of a score after each hit object.
//! Instead, you should use [`GradualDifficultyAttributes`] and [`GradualPerformanceAttributes`]: This could be done by using `passed_objects` as the amount of objects that were passed so far.
//! However, this requires to recalculate the beginning again and again, we can be more efficient than that.
//! ```no_run
//! use rosu_pp::{ Instead, you should enable the `gradual` feature and use [`GradualDifficulty`] and [`GradualPerformance`]:
//! Beatmap, BeatmapExt, GradualPerformanceAttributes, ScoreState,
//! taiko::TaikoScoreState, ```no_run
//! }; use rosu_pp::{
//! Beatmap, BeatmapExt, GradualDifficulty, GradualPerformance, ScoreState,
//! # /* taiko::TaikoScoreState,
//! let map = match Beatmap::from_path("/path/to/file.osu") { };
//! Ok(map) => map,
//! Err(why) => panic!("Error while parsing map: {}", why), # /*
//! }; let map = match Beatmap::from_path("/path/to/file.osu") {
//! # */ Ok(map) => map,
//! # let map = Beatmap::default(); Err(why) => panic!("Error while parsing map: {}", why),
//! };
//! let mods = 8 + 64; // HDDT # */
//! # let map = Beatmap::default();
//! // If you're only interested in the star rating or other difficulty value,
//! // use `GradualDifficultyAttributes`, either through its function `new` let mods = 8 + 64; // HDDT
//! // or through the method `BeatmapExt::gradual_difficulty`.
//! let gradual_difficulty = map.gradual_difficulty(mods); // If you're only interested in the star rating or other difficulty values,
//! // use `GradualDifficulty`.
//! // Since `GradualDifficultyAttributes` implements `Iterator`, you can use let gradual_difficulty = GradualDifficulty::new(&map, mods);
//! // any iterate function on it, use it in loops, collect them into a `Vec`, ...
//! for (i, difficulty) in gradual_difficulty.enumerate() { // Since `GradualDifficulty` implements `Iterator`, you can use
//! println!("Stars after object {}: {}", i, difficulty.stars()); // any iterate function on it, use it in loops, collect them into a `Vec`, ...
//! } for (i, difficulty) in gradual_difficulty.enumerate() {
//! println!("Stars after object {}: {}", i, difficulty.stars());
//! // Gradually calculating performance values does the same as calculating }
//! // difficulty attributes but it goes the extra step and also evaluates
//! // the state of a score for these difficulty attributes. // Gradually calculating performance values does the same as calculating
//! let mut gradual_performance = map.gradual_performance(mods); // difficulty attributes but it goes the extra step and also evaluates
//! // the state of a score for these difficulty attributes.
//! // The default score state is kinda chunky because it considers all modes. let mut gradual_performance = GradualPerformance::new(&map, mods);
//! let state = ScoreState {
//! max_combo: 1, // The default score state is kinda chunky because it considers all modes.
//! n_geki: 0, // only relevant for mania let state = ScoreState {
//! n_katu: 0, // only relevant for mania and ctb max_combo: 1,
//! n300: 1, n_geki: 0, // only relevant for mania
//! n100: 0, n_katu: 0, // only relevant for mania and ctb
//! n50: 0, n300: 1,
//! n_misses: 0, n100: 0,
//! }; n50: 0,
//! n_misses: 0,
//! // Process the score state after the first object };
//! let curr_performance = match gradual_performance.process_next_object(state) {
//! Some(perf) => perf, // Process the score state after the first object
//! None => panic!("the map has no hit objects"), let curr_performance = match gradual_performance.next(state) {
//! }; Some(perf) => perf,
//! None => panic!("the map has no hit objects"),
//! println!("PP after the first object: {}", curr_performance.pp()); };
//!
//! // If you're only interested in maps of a specific mode, consider println!("PP after the first object: {}", curr_performance.pp());
//! // using the mode's gradual calculator instead of the general one.
//! // Let's assume it's a taiko map. // If you're only interested in maps of a specific mode, consider
//! // Instead of starting off with `BeatmapExt::gradual_performance` one could have // using the mode's gradual calculator instead of the general one.
//! // created the struct via `TaikoGradualPerformanceAttributes::new`. // Let's assume it's a taiko map.
//! let mut gradual_performance = match gradual_performance { // Instead of starting off with `GradualPerformance` one could have
//! GradualPerformanceAttributes::Taiko(gradual) => gradual, // used `TaikoGradualPerformance`.
//! _ => panic!("the map was not taiko but {:?}", map.mode), let mut gradual_performance = match gradual_performance {
//! }; GradualPerformance::Taiko(gradual) => gradual,
//! _ => panic!("the map was not taiko but {:?}", map.mode),
//! // A little simpler than the general score state. };
//! let state = TaikoScoreState {
//! max_combo: 11, // A little simpler than the general score state.
//! n300: 9, let state = TaikoScoreState {
//! n100: 1, max_combo: 11,
//! n_misses: 1, n300: 9,
//! }; n100: 1,
//! n_misses: 1,
//! // Process the next 10 objects in one go };
//! let curr_performance = match gradual_performance.process_next_n_objects(state, 10) {
//! Some(perf) => perf, // Process the next 10 objects in one go (`nth` takes a zero-based value).
//! None => panic!("the last `process_next_object` already processed the last object"), let curr_performance = match gradual_performance.nth(state, 9) {
//! }; Some(perf) => perf,
//! None => panic!("the last `next` already processed the last object"),
//! println!("PP after the first 11 objects: {}", curr_performance.pp()); };
//! ```
//! println!("PP after the first 11 objects: {}", curr_performance.pp());
```
"#
)]
//! ## Features //! ## Features
//! //!
//! | Flag | Description | //! | Flag | Description |
//! |-----|-----| //! |---------------|-----|
//! | `default` | Beatmap parsing will be non-async | //! | `default` | Beatmap parsing will be non-async |
//! | `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) |
//! | `gradual` | Enable gradual difficulty and performance calculation |
//! //!
#![cfg_attr(docsrs, feature(doc_cfg), deny(broken_intra_doc_links))]
#![deny(
clippy::all,
nonstandard_style,
rust_2018_idioms,
unused,
warnings,
missing_debug_implementations,
missing_docs
)]
/// Everything about osu!catch. /// Everything about osu!catch.
pub mod catch; pub mod catch;
@@ -198,8 +204,12 @@ pub mod parse;
pub mod beatmap; pub mod beatmap;
pub use beatmap::{Beatmap, BeatmapExt, GameMode}; pub use beatmap::{Beatmap, BeatmapExt, GameMode};
#[cfg(feature = "gradual")]
mod gradual; mod gradual;
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState}; #[cfg(feature = "gradual")]
pub use gradual::{
GradualDifficulty, GradualPerformance, OwnedGradualDifficulty, OwnedGradualPerformance,
};
mod pp; mod pp;
pub use pp::{AnyPP, AttributeProvider, HitResultPriority}; pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
@@ -207,6 +217,9 @@ pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
mod stars; mod stars;
pub use stars::AnyStars; pub use stars::AnyStars;
mod score_state;
pub use score_state::*;
mod curve; mod curve;
mod mods; mod mods;
mod util; mod util;
+208 -83
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "gradual")]
use std::borrow::Cow; use std::borrow::Cow;
use crate::{ use crate::{
@@ -16,17 +18,17 @@ use super::{
/// Gradually calculate the difficulty attributes of an osu!mania map. /// Gradually calculate the difficulty attributes of an osu!mania map.
/// ///
/// Note that this struct implements [`Iterator`](std::iter::Iterator). /// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will /// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned. /// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned.
/// ///
/// If you want to calculate performance attributes, use /// If you want to calculate performance attributes, use
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes) instead. /// [`ManiaGradualPerformance`](crate::mania::ManiaGradualPerformance) instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, mania::ManiaGradualDifficultyAttributes}; /// use rosu_pp::{Beatmap, mania::ManiaGradualDifficulty};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -34,7 +36,7 @@ use super::{
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut iter = ManiaGradualDifficultyAttributes::new(&map, mods); /// let mut iter = ManiaGradualDifficulty::new(&map, mods);
/// ///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object /// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object /// let attrs2 = iter.next(); // after the second hit object
@@ -44,21 +46,126 @@ use super::{
/// // ... /// // ...
/// } /// }
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ManiaGradualDifficultyAttributes<'map> { pub struct ManiaGradualDifficulty<'map> {
pub(crate) idx: usize,
map: Cow<'map, Beatmap>, map: Cow<'map, Beatmap>,
inner: ManiaGradualDifficultyInner,
}
impl<'map> ManiaGradualDifficulty<'map> {
/// Create a new difficulty attributes iterator for osu!mania maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let map = map.convert_mode(GameMode::Mania);
let is_convert = matches!(map, Cow::Owned(_));
let inner = ManiaGradualDifficultyInner::new(map.as_ref(), is_convert, mods);
Self { map, inner }
}
pub(crate) fn idx(&self) -> usize {
self.inner.idx
}
}
impl Iterator for ManiaGradualDifficulty<'_> {
type Item = ManiaDifficultyAttributes;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next(&self.map.hit_objects)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
#[inline]
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.inner.nth(n, &self.map.hit_objects)
}
}
impl ExactSizeIterator for ManiaGradualDifficulty<'_> {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
/// Gradually calculate the difficulty attributes of an osu!mania map.
///
/// Check [`ManiaGradualDifficulty`] for more information. This struct does the same
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)]
pub struct ManiaOwnedGradualDifficulty {
// Technically only `Beatmap::hit_objects` are required here but storing
// the full map lets us get away with not storing the map in `ManiaOwnedGradualPerformance`.
pub(crate) map: Beatmap,
inner: ManiaGradualDifficultyInner,
}
impl ManiaOwnedGradualDifficulty {
/// Create a new owned difficulty attributes iterator for osu!mania maps.
pub fn new(map: Beatmap, mods: u32) -> Self {
let converted_map = map.convert_mode(GameMode::Mania);
let is_convert = matches!(converted_map, Cow::Owned(_));
let inner = ManiaGradualDifficultyInner::new(&converted_map, is_convert, mods);
let map = match converted_map {
Cow::Owned(map) => map,
Cow::Borrowed(_) => map,
};
Self { map, inner }
}
#[allow(unused)]
pub(crate) fn idx(&self) -> usize {
self.inner.idx
}
}
impl Iterator for ManiaOwnedGradualDifficulty {
type Item = ManiaDifficultyAttributes;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next(&self.map.hit_objects)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
#[inline]
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.inner.nth(n, &self.map.hit_objects)
}
}
impl ExactSizeIterator for ManiaOwnedGradualDifficulty {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
#[derive(Clone, Debug)]
struct ManiaGradualDifficultyInner {
pub(crate) idx: usize,
hit_window: f64, hit_window: f64,
strain: Strain, strain: Strain,
diff_objects: Vec<ManiaDifficultyObject>, diff_objects: Box<[ManiaDifficultyObject]>,
curr_combo: usize, curr_combo: usize,
clock_rate: f64, clock_rate: f64,
} }
impl<'map> ManiaGradualDifficultyAttributes<'map> { impl ManiaGradualDifficultyInner {
/// Create a new difficulty attributes iterator for osu!mania maps. fn new(map: &Beatmap, is_convert: bool, mods: u32) -> Self {
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let map = map.convert_mode(GameMode::Mania);
let total_columns = map.cs.round_even().max(1.0); let total_columns = map.cs.round_even().max(1.0);
let clock_rate = mods.clock_rate(); let clock_rate = mods.clock_rate();
let strain = Strain::new(total_columns as usize); let strain = Strain::new(total_columns as usize);
@@ -66,30 +173,39 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
let BeatmapHitWindows { od: hit_window, .. } = map let BeatmapHitWindows { od: hit_window, .. } = map
.attributes() .attributes()
.mods(mods) .mods(mods)
.converted(matches!(map, Cow::Owned(_))) .converted(is_convert)
.clock_rate(clock_rate) .clock_rate(clock_rate)
.hit_windows(); .hit_windows();
let mut params = ObjectParameters::new(map.as_ref()); let mut params = ObjectParameters::new(map);
let mut curr_combo = 0;
let mut hit_objects = map.hit_objects.iter(); let mut hit_objects = map.hit_objects.iter();
let first = match hit_objects.next() { let first = match hit_objects.next() {
Some(h) => ManiaObject::new(h, total_columns, &mut params), Some(h) => {
let hit_object = ManiaObject::new(h, total_columns, &mut params);
increment_combo_raw(
h,
hit_object.start_time,
hit_object.end_time,
&mut curr_combo,
);
hit_object
}
None => { None => {
return Self { return Self {
idx: 0, idx: 0,
map,
hit_window, hit_window,
strain, strain,
diff_objects: Vec::new(), diff_objects: Box::from([]),
curr_combo: 0, curr_combo: 0,
clock_rate, clock_rate,
} }
} }
}; };
let curr_combo = params.max_combo;
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| { let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
let base = ManiaObject::new(h, total_columns, &mut params); let base = ManiaObject::new(h, total_columns, &mut params);
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i); let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
@@ -98,87 +214,96 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
Some(diff_object) Some(diff_object)
}); });
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1)); let mut diff_objects = Vec::with_capacity(map.hit_objects.len() - 1);
diff_objects.extend(diff_objects_iter); diff_objects.extend(diff_objects_iter);
debug_assert_eq!(diff_objects.len(), diff_objects.capacity());
Self { Self {
idx: 0, idx: 0,
map,
hit_window, hit_window,
strain, strain,
diff_objects, diff_objects: diff_objects.into_boxed_slice(),
curr_combo, curr_combo,
clock_rate, clock_rate,
} }
} }
fn next(&mut self, hit_objects: &[HitObject]) -> Option<ManiaDifficultyAttributes> {
// The first difficulty object belongs to the second note since each difficulty
// object requires the current and the last note. Hence, if we're still on the first
// object, we don't have a difficulty object yet and just skip processing.
if self.idx > 0 {
let curr = self.diff_objects.get(self.idx - 1)?;
self.strain.process(curr, &self.diff_objects);
let h = &hit_objects[self.idx];
increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
} else if hit_objects.is_empty() {
return None;
}
self.idx += 1;
Some(ManiaDifficultyAttributes {
stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR,
hit_window: self.hit_window,
max_combo: self.curr_combo,
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
fn nth(&mut self, n: usize, hit_objects: &[HitObject]) -> Option<ManiaDifficultyAttributes> {
let skip_iter = self
.diff_objects
.iter()
.zip(hit_objects.iter().skip(1))
.skip(self.idx.saturating_sub(1));
let mut take = n.min(self.len().saturating_sub(1));
// The first note has no difficulty object
if self.idx == 0 && take > 0 {
take -= 1;
self.idx += 1;
}
for (curr, h) in skip_iter.take(take) {
increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
self.strain.process(curr, &self.diff_objects);
self.idx += 1;
}
self.next(hit_objects)
}
fn len(&self) -> usize {
self.diff_objects.len() + 1 - self.idx
}
}
fn increment_combo( fn increment_combo(
h: &HitObject, h: &HitObject,
diff_obj: &ManiaDifficultyObject, diff_obj: &ManiaDifficultyObject,
curr_combo: &mut usize, curr_combo: &mut usize,
clock_rate: f64, clock_rate: f64,
) { ) {
match &h.kind { increment_combo_raw(
h,
diff_obj.start_time * clock_rate,
diff_obj.end_time * clock_rate,
curr_combo,
);
}
fn increment_combo_raw(h: &HitObject, start_time: f64, end_time: f64, curr_combo: &mut usize) {
match h.kind {
HitObjectKind::Circle => *curr_combo += 1, HitObjectKind::Circle => *curr_combo += 1,
_ => { _ => *curr_combo += 1 + ((end_time - start_time) / 100.0) as usize,
let start_time = diff_obj.start_time * clock_rate;
let end_time = diff_obj.end_time * clock_rate;
let duration = end_time - start_time;
*curr_combo += 1 + (duration / 100.0) as usize;
}
}
}
}
impl Iterator for ManiaGradualDifficultyAttributes<'_> {
type Item = ManiaDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
let curr = self.diff_objects.get(self.idx)?;
self.idx += 1;
if let Some(h) = self.map.hit_objects.get(self.idx) {
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
}
self.strain.process(curr, &self.diff_objects);
Some(ManiaDifficultyAttributes {
stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR,
hit_window: self.hit_window,
max_combo: self.curr_combo,
})
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.diff_objects.get(self.idx)?;
self.idx += 1;
if let Some(h) = self.map.hit_objects.get(self.idx) {
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
}
self.strain.process(curr, &self.diff_objects);
}
self.next()
}
}
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
#[inline]
fn len(&self) -> usize {
self.diff_objects.len() - self.idx
} }
} }
+89 -88
View File
@@ -1,74 +1,28 @@
#![cfg(feature = "gradual")]
use crate::{Beatmap, ManiaPP}; use crate::{Beatmap, ManiaPP};
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes}; use super::{
ManiaGradualDifficulty, ManiaOwnedGradualDifficulty, ManiaPerformanceAttributes,
/// Aggregation for a score's current state ManiaScoreState,
/// i.e. what are the current hitresults. };
///
/// This struct is used for [`ManiaGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ManiaScoreState {
/// Amount of current 320s.
pub n320: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 200s.
pub n200: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl ManiaScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * (self.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
/// Gradually calculate the performance attributes of an osu!mania map. /// Gradually calculate the performance attributes of an osu!mania map.
/// ///
/// After each hit object you can call /// After each hit object you can call [`next`](`ManiaGradualPerformance::next`)
/// [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`ManiaPerformanceAttributes`]. /// and it will return the resulting current [`ManiaPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use [`nth`](`ManiaGradualPerformance::nth`) instead.
/// [`process_next_n_objects`](`ManiaGradualPerformanceAttributes::process_next_n_objects`) instead.
/// ///
/// Both methods require a play's current score so far. /// Both methods require a play's current score so far.
/// Be sure the given score is adjusted with respect to mods. /// Be sure the given score is adjusted with respect to mods.
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes) instead. /// [`ManiaGradualDifficulty`] instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformanceAttributes, ManiaScoreState}}; /// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformance, ManiaScoreState}};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -76,7 +30,7 @@ impl ManiaScoreState {
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut gradual_perf = ManiaGradualPerformanceAttributes::new(&map, mods); /// let mut gradual_perf = ManiaGradualPerformance::new(&map, mods);
/// let mut state = ManiaScoreState::new(); // empty state, everything is on 0. /// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
/// ///
/// // The first 10 hitresults are 320s /// // The first 10 hitresults are 320s
@@ -84,29 +38,30 @@ impl ManiaScoreState {
/// state.n320 += 1; /// state.n320 += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap(); /// let performance = gradual_perf.next(score).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap(); /// let performance = gradual_perf.next(score).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // The next 10 objects will be a mixture of 320s and 100s. /// // The next 10 objects will be a mixture of 320s and 100s.
/// // Notice how all 10 objects will be processed in one go. /// // Notice how all 10 objects will be processed in one go.
/// state.n320 += 3; /// state.n320 += 3;
/// state.n100 += 7; /// state.n100 += 7;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap(); /// let performance = gradual_perf.nth(score, 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10); /// # let _ = gradual_perf.nth(state.clone(), 9);
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -114,25 +69,26 @@ impl ManiaScoreState {
/// state.n300 = ... /// state.n300 = ...
/// state.n100 = ... /// state.n100 = ...
/// state.n_misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ManiaGradualPerformanceAttributes<'map> { pub struct ManiaGradualPerformance<'map> {
difficulty: ManiaGradualDifficultyAttributes<'map>, difficulty: ManiaGradualDifficulty<'map>,
performance: ManiaPP<'map>, performance: ManiaPP<'map>,
} }
impl<'map> ManiaGradualPerformanceAttributes<'map> { impl<'map> ManiaGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!mania maps. /// Create a new gradual performance calculator for osu!mania maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let difficulty = ManiaGradualDifficultyAttributes::new(map, mods); let difficulty = ManiaGradualDifficulty::new(map, mods);
let performance = ManiaPP::new(map).mods(mods).passed_objects(0); let performance = ManiaPP::new(map).mods(mods).passed_objects(0);
Self { Self {
@@ -143,33 +99,78 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the /// Process the next hit object and calculate the
/// performance attributes for the resulting score. /// performance attributes for the resulting score.
pub fn process_next_object( pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: ManiaScoreState,
) -> Option<ManiaPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`) /// Process all remaining hit objects and calculate the final performance attributes.
/// but instead of processing only one object it process `n` many. pub fn last(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
/// ///
/// If `n` is 0 it will be considered as 1. /// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// If there are still objects to be processed but `n` is larger than the amount /// and so on.
/// of remaining objects, `n` will be considered as the amount of remaining objects. pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.nth(n)?;
&mut self,
state: ManiaScoreState,
n: usize,
) -> Option<ManiaPerformanceAttributes> {
let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(self.difficulty.idx) .passed_objects(self.difficulty.idx())
.calculate();
Some(performance)
}
}
/// Gradually calculate the performance attributes of an osu!mania map.
///
/// Check [`ManiaGradualPerformance`] for more information. This struct does the same
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)]
pub struct ManiaOwnedGradualPerformance {
difficulty: ManiaOwnedGradualDifficulty,
mods: u32,
}
impl ManiaOwnedGradualPerformance {
/// Create a new gradual performance calculator for osu!mania maps.
pub fn new(map: Beatmap, mods: u32) -> Self {
let difficulty = ManiaOwnedGradualDifficulty::new(map, mods);
Self { difficulty, mods }
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance attributes.
pub fn last(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
let difficulty = self.difficulty.nth(n)?;
let performance = ManiaPP::new(&self.difficulty.map)
.mods(self.mods)
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx())
.calculate(); .calculate();
Some(performance) Some(performance)
+14 -4
View File
@@ -1,15 +1,25 @@
mod difficulty_object; mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod mania_object; mod mania_object;
mod pp; mod pp;
mod score_state;
mod skills; mod skills;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use std::borrow::Cow; use std::borrow::Cow;
use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars}; use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars};
pub use self::{gradual_difficulty::*, gradual_performance::*, mania_object::ManiaObject, pp::*}; pub use self::{mania_object::ManiaObject, pp::*, score_state::ManiaScoreState};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::{ManiaGradualDifficulty, ManiaOwnedGradualDifficulty},
gradual_performance::{ManiaGradualPerformance, ManiaOwnedGradualPerformance},
};
pub(crate) use self::mania_object::ObjectParameters; pub(crate) use self::mania_object::ObjectParameters;
@@ -78,7 +88,7 @@ impl<'map> ManiaStars<'map> {
/// ///
/// If you want to calculate the difficulty after every few objects, instead of /// If you want to calculate the difficulty after every few objects, instead of
/// using [`ManiaStars`] multiple times with different `passed_objects`, you should use /// using [`ManiaStars`] multiple times with different `passed_objects`, you should use
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes). /// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficulty).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
+1 -1
View File
@@ -109,7 +109,7 @@ impl<'map> ManiaPP<'map> {
/// ///
/// If you want to calculate the performance after every few objects, instead of /// If you want to calculate the performance after every few objects, instead of
/// using [`ManiaPP`] multiple times with different `passed_objects`, you should use /// using [`ManiaPP`] multiple times with different `passed_objects`, you should use
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes). /// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformance).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
+45
View File
@@ -0,0 +1,45 @@
/// Aggregation for a score's current state i.e. what are the current hitresults.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ManiaScoreState {
/// Amount of current 320s.
pub n320: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 200s.
pub n200: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl ManiaScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * (self.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
+22 -32
View File
@@ -2,6 +2,7 @@ use crate::{
osu::osu_object::{NestedObjectKind, OsuObjectKind}, osu::osu_object::{NestedObjectKind, OsuObjectKind},
parse::Pos2, parse::Pos2,
}; };
use std::pin::Pin;
use super::{osu_object::OsuSlider, OsuObject, ScalingFactor}; use super::{osu_object::OsuSlider, OsuObject, ScalingFactor};
@@ -9,7 +10,7 @@ use super::{osu_object::OsuSlider, OsuObject, ScalingFactor};
pub(crate) struct OsuDifficultyObject<'h> { pub(crate) struct OsuDifficultyObject<'h> {
pub(crate) start_time: f64, pub(crate) start_time: f64,
pub(crate) delta_time: f64, pub(crate) delta_time: f64,
pub(crate) base: &'h OsuObject, pub(crate) base: Pin<&'h OsuObject>,
pub(crate) strain_time: f64, pub(crate) strain_time: f64,
pub(crate) dists: Distances, pub(crate) dists: Distances,
pub(crate) idx: usize, pub(crate) idx: usize,
@@ -19,7 +20,7 @@ impl<'h> OsuDifficultyObject<'h> {
pub(crate) const MIN_DELTA_TIME: u32 = 25; pub(crate) const MIN_DELTA_TIME: u32 = 25;
pub(crate) fn new( pub(crate) fn new(
base: &'h OsuObject, base: Pin<&'h OsuObject>,
last: &'h OsuObject, last: &'h OsuObject,
clock_rate: f64, clock_rate: f64,
idx: usize, idx: usize,
@@ -89,20 +90,24 @@ impl Distances {
const MAXIMUM_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 2.4; const MAXIMUM_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 2.4;
const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 1.8; const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 1.8;
/// Create a new instance of [`Distances`].
///
/// By taking in [`Pin<&mut OsuObject>`](Pin), we imply that the argument will be
/// modified but it won't be moved.
pub(crate) fn new( pub(crate) fn new(
base: &mut OsuObject, base: &mut Pin<&mut OsuObject>,
last: &OsuObject, last: &OsuObject,
last_last: Option<&OsuObject>, last_last: Option<&OsuObject>,
clock_rate: f64, clock_rate: f64,
strain_time: f64, strain_time: f64,
scaling_factor_: &ScalingFactor, scaling_factor_: &ScalingFactor,
) -> Self { ) -> Self {
let mut this = let pos = base.pos();
if let Some(slider_values) = Self::compute_slider_cursor_pos(base, scaling_factor_) { let stack_offset = base.stack_offset;
let SliderValues {
lazy_travel_dist, let mut this = if let OsuObjectKind::Slider(ref mut slider) = base.kind {
slider, let lazy_travel_dist =
} = slider_values; Self::compute_slider_travel_dist(pos, stack_offset, slider, scaling_factor_);
let repeat_count = slider.repeat_count(); let repeat_count = slider.repeat_count();
@@ -196,26 +201,19 @@ impl Distances {
this this
} }
pub(crate) fn compute_slider_cursor_pos<'h>( pub(crate) fn compute_slider_travel_dist(
hit_object: &'h mut OsuObject, pos: Pos2,
stack_offset: Pos2,
slider: &mut OsuSlider,
scaling_factor_: &ScalingFactor, scaling_factor_: &ScalingFactor,
) -> Option<SliderValues<'h>> { ) -> f32 {
let pos = hit_object.pos(); let mut curr_cursor_pos = pos + stack_offset;
let slider = if let OsuObjectKind::Slider(slider) = &mut hit_object.kind {
slider
} else {
return None;
};
let mut curr_cursor_pos = pos + hit_object.stack_offset;
let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64; let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64;
let mut lazy_travel_dist: f32 = 0.0; let mut lazy_travel_dist: f32 = 0.0;
for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) { for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) {
let mut curr_movement = let mut curr_movement = (curr_movement_obj.pos + stack_offset) - curr_cursor_pos;
(curr_movement_obj.pos + hit_object.stack_offset) - curr_cursor_pos;
let mut curr_movement_len = scaling_factor * curr_movement.length() as f64; let mut curr_movement_len = scaling_factor * curr_movement.length() as f64;
// * Amount of movement required so that the cursor position needs to be updated. // * Amount of movement required so that the cursor position needs to be updated.
@@ -253,18 +251,10 @@ impl Distances {
slider.lazy_end_pos = curr_cursor_pos; slider.lazy_end_pos = curr_cursor_pos;
Some(SliderValues { lazy_travel_dist
lazy_travel_dist,
slider,
})
} }
fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 { fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 {
hit_object.lazy_end_pos() hit_object.lazy_end_pos()
} }
} }
pub(crate) struct SliderValues<'s> {
lazy_travel_dist: f32,
slider: &'s OsuSlider,
}
+127 -81
View File
@@ -1,33 +1,37 @@
#![cfg(feature = "gradual")]
use std::{ use std::{
fmt::{Debug, Formatter, Result as FmtResult}, fmt::{Debug, Formatter, Result as FmtResult},
mem, mem,
pin::Pin,
}; };
use crate::{curve::CurveBuffers, Beatmap, Mods}; use crate::{Beatmap, Mods};
use self::osu_objects::OsuObjects;
use super::{ use super::{
difficulty_object::{Distances, OsuDifficultyObject}, difficulty_object::{Distances, OsuDifficultyObject},
old_stacking, osu_object::{OsuObject, OsuObjectKind},
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
scaling_factor::ScalingFactor, scaling_factor::ScalingFactor,
skills::{Skill, Skills}, skills::{Skill, Skills},
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN, PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN,
}; };
/// Gradually calculate the difficulty attributes of an osu!standard map. /// Gradually calculate the difficulty attributes of an osu!standard map.
/// ///
/// Note that this struct implements [`Iterator`](std::iter::Iterator). /// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will /// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`OsuDifficultyAttributes`] will be updated and returned. /// be processed and the [`OsuDifficultyAttributes`] will be updated and returned.
/// ///
/// If you want to calculate performance attributes, use /// If you want to calculate performance attributes, use
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes) instead. /// [`OsuGradualPerformance`](crate::osu::OsuGradualPerformance) instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, osu::OsuGradualDifficultyAttributes}; /// use rosu_pp::{Beatmap, osu::OsuGradualDifficulty};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -35,7 +39,7 @@ use super::{
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut iter = OsuGradualDifficultyAttributes::new(&map, mods); /// let mut iter = OsuGradualDifficulty::new(&map, mods);
/// ///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object /// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object /// let attrs2 = iter.next(); // after the second hit object
@@ -45,21 +49,27 @@ use super::{
/// // ... /// // ...
/// } /// }
/// ``` /// ```
#[derive(Clone)] #[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
pub struct OsuGradualDifficultyAttributes { pub struct OsuGradualDifficulty {
pub(crate) idx: usize, pub(crate) idx: usize,
mods: u32, mods: u32,
attrs: OsuDifficultyAttributes, attrs: OsuDifficultyAttributes,
// Unused but `diff_objects`' lifetimes secretly depend on it
#[allow(unused)]
hit_objects: Vec<OsuObject>,
diff_objects: Vec<OsuDifficultyObject<'static>>,
skills: Skills, skills: Skills,
// Lifetimes actually depend on `osu_objects` so this type is self-referential.
// This field must be treated with great caution, moving `osu_objects` will immediately
// invalidate `diff_objects`.
diff_objects: Vec<OsuDifficultyObject<'static>>,
osu_objects: OsuObjects,
// Additional safety measure that this type can't be cloned which would invalidate
// `diff_objects`.
_not_clonable: NotClonable,
} }
impl Debug for OsuGradualDifficultyAttributes { struct NotClonable;
impl Debug for OsuGradualDifficulty {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("OsuGradualDifficultyAttributes") f.debug_struct("OsuGradualDifficulty")
.field("idx", &self.idx) .field("idx", &self.idx)
.field("attrs", &self.attrs) .field("attrs", &self.attrs)
.field("diff_objects", &self.diff_objects) .field("diff_objects", &self.diff_objects)
@@ -68,7 +78,7 @@ impl Debug for OsuGradualDifficultyAttributes {
} }
} }
impl OsuGradualDifficultyAttributes { impl OsuGradualDifficulty {
/// Create a new difficulty attributes iterator for osu!standard maps. /// Create a new difficulty attributes iterator for osu!standard maps.
pub fn new(map: &Beatmap, mods: u32) -> Self { pub fn new(map: &Beatmap, mods: u32) -> Self {
let clock_rate = mods.clock_rate(); let clock_rate = mods.clock_rate();
@@ -99,38 +109,22 @@ impl OsuGradualDifficultyAttributes {
..Default::default() ..Default::default()
}; };
let mut params = ObjectParameters { let hit_objects = crate::osu::create_osu_objects(
map, map,
attrs: &mut attrs, &mut attrs,
ticks: Vec::new(), &scaling_factor,
curve_bufs: CurveBuffers::default(), map.hit_objects.len(),
}; hr,
time_preempt,
);
let mut hit_objects: Vec<_> = map let mut osu_objects = OsuObjects::new(hit_objects);
.hit_objects
.iter()
.map(|h| OsuObject::new(h, &mut params))
.collect();
attrs.n_circles = 0; attrs.n_circles = 0;
attrs.n_sliders = 0; attrs.n_sliders = 0;
attrs.n_spinners = 0; attrs.n_spinners = 0;
attrs.max_combo = 0; attrs.max_combo = 0;
let stack_threshold = time_preempt * map.stack_leniency as f64;
if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold);
} else {
old_stacking(&mut hit_objects, stack_threshold);
}
let mut hit_objects_iter = hit_objects.iter_mut().map(|h| {
h.post_process(hr, &scaling_factor);
h
});
let skills = Skills::new( let skills = Skills::new(
mods, mods,
scaling_factor.radius, scaling_factor.radius,
@@ -139,59 +133,72 @@ impl OsuGradualDifficultyAttributes {
hit_window, hit_window,
); );
let last = match hit_objects_iter.next() { let mut osu_objects_iter = osu_objects.iter_mut();
Some(prev) => prev,
None => { let Some(mut last) = osu_objects_iter.next() else {
return Self { return Self {
idx: 0, idx: 0,
mods, mods,
attrs, attrs,
hit_objects: Vec::new(),
diff_objects: Vec::new(),
skills, skills,
} diff_objects: Vec::new(),
} osu_objects: OsuObjects::new(Vec::new()),
_not_clonable: NotClonable,
};
}; };
Self::increment_combo(last, &mut attrs); Self::increment_combo(last.as_ref().get_ref(), &mut attrs);
let mut last_last = None; let mut last_last = None;
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually // Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
Distances::compute_slider_cursor_pos(last, &scaling_factor); let last_pos = last.pos();
let last_stack_offset = last.stack_offset;
let mut last = &*last; if let OsuObjectKind::Slider(ref mut slider) = last.kind {
Distances::compute_slider_travel_dist(
last_pos,
last_stack_offset,
slider,
&scaling_factor,
);
}
let mut last = last.into_ref();
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2)); let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2));
for (i, curr) in hit_objects_iter.enumerate() { for (i, mut curr) in osu_objects_iter.enumerate() {
let delta_time = (curr.start_time - last.start_time) / clock_rate; let delta_time = (curr.start_time - last.start_time) / clock_rate;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. // * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64); let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new( let dists = Distances::new(
curr, &mut curr,
last, last.get_ref(),
last_last, last_last.map(Pin::get_ref),
clock_rate, clock_rate,
strain_time, strain_time,
&scaling_factor, &scaling_factor,
); );
let diff_obj = OsuDifficultyObject::new(curr, last, clock_rate, i, dists); let curr = curr.into_ref();
let diff_obj = OsuDifficultyObject::new(curr, last.get_ref(), clock_rate, i, dists);
diff_objects.push(diff_obj); diff_objects.push(diff_obj);
last_last = Some(last); last_last = Some(last);
last = &*curr; last = curr;
} }
Self { Self {
idx: 0, idx: 0,
mods, mods,
attrs, attrs,
diff_objects: extend_lifetime(diff_objects),
hit_objects,
skills, skills,
diff_objects: extend_lifetime(diff_objects),
osu_objects,
_not_clonable: NotClonable,
} }
} }
@@ -212,21 +219,27 @@ impl OsuGradualDifficultyAttributes {
fn extend_lifetime( fn extend_lifetime(
diff_objects: Vec<OsuDifficultyObject<'_>>, diff_objects: Vec<OsuDifficultyObject<'_>>,
) -> Vec<OsuDifficultyObject<'static>> { ) -> Vec<OsuDifficultyObject<'static>> {
// SAFETY: Owned values of the references will be contained // SAFETY: Owned values of the references will be contained in the same struct (same lifetime).
// in the same struct and hence live just as long as this vec. // Also, the only mutable access wraps them in `Pin` to ensure that they won't move.
unsafe { mem::transmute(diff_objects) } unsafe { mem::transmute(diff_objects) }
} }
impl Iterator for OsuGradualDifficultyAttributes { impl Iterator for OsuGradualDifficulty {
type Item = OsuDifficultyAttributes; type Item = OsuDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let curr = self.diff_objects.get(self.idx)?; // The first difficulty object belongs to the second note since each difficulty
self.idx += 1; // object requires the current and the last note. Hence, if we're still on the first
// object, we don't have a difficulty object yet and just skip processing.
if self.idx > 0 {
let curr = self.diff_objects.get(self.idx - 1)?;
self.skills.process(curr, &self.diff_objects); self.skills.process(curr, &self.diff_objects);
Self::increment_combo(curr.base.get_ref(), &mut self.attrs);
} else if self.osu_objects.is_empty() {
return None;
}
Self::increment_combo(curr.base, &mut self.attrs); self.idx += 1;
let Skills { let Skills {
mut aim, mut aim,
@@ -284,13 +297,15 @@ impl Iterator for OsuGradualDifficultyAttributes {
0.0 0.0
}; };
let mut attrs = self.attrs.clone(); let attrs = OsuDifficultyAttributes {
attrs.aim = aim_rating; aim: aim_rating,
attrs.speed = speed_rating; speed: speed_rating,
attrs.flashlight = flashlight_rating; flashlight: flashlight_rating,
attrs.slider_factor = slider_factor; slider_factor,
attrs.stars = star_rating; stars: star_rating,
attrs.speed_note_count = speed_notes; speed_note_count: speed_notes,
..self.attrs.clone()
};
Some(attrs) Some(attrs)
} }
@@ -303,24 +318,55 @@ impl Iterator for OsuGradualDifficultyAttributes {
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1); let skip_iter = self.diff_objects.iter().skip(self.idx.saturating_sub(1));
for _ in 0..skip { let mut take = n.min(self.len().saturating_sub(1));
let curr = self.diff_objects.get(self.idx)?;
// The first note has no difficulty object
if self.idx == 0 && take > 0 {
take -= 1;
self.idx += 1; self.idx += 1;
}
for curr in skip_iter.take(take) {
self.skills.process(curr, &self.diff_objects); self.skills.process(curr, &self.diff_objects);
Self::increment_combo(curr.base.get_ref(), &mut self.attrs);
Self::increment_combo(curr.base, &mut self.attrs); self.idx += 1;
} }
self.next() self.next()
} }
} }
impl ExactSizeIterator for OsuGradualDifficultyAttributes { impl ExactSizeIterator for OsuGradualDifficulty {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
self.diff_objects.len() - self.idx self.diff_objects.len() + 1 - self.idx
}
}
mod osu_objects {
use crate::osu::OsuObject;
use std::pin::Pin;
// Wrapper to ensure that the data will not be moved
pub(super) struct OsuObjects {
objects: Box<[OsuObject]>,
}
impl OsuObjects {
pub(super) fn new(objects: Vec<OsuObject>) -> Self {
Self {
objects: objects.into_boxed_slice(),
}
}
pub(super) fn is_empty(&self) -> bool {
self.objects.is_empty()
}
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = Pin<&mut OsuObject>> {
self.objects.iter_mut().map(Pin::new)
}
} }
} }
+93 -89
View File
@@ -1,73 +1,25 @@
#![cfg(feature = "gradual")]
use crate::{Beatmap, OsuPP}; use crate::{Beatmap, OsuPP};
use super::{OsuGradualDifficultyAttributes, OsuPerformanceAttributes}; use super::{OsuGradualDifficulty, OsuPerformanceAttributes, OsuScoreState};
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`OsuGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct OsuScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
pub max_combo: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl OsuScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * self.n300 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
/// Gradually calculate the performance attributes of an osu!standard map. /// Gradually calculate the performance attributes of an osu!standard map.
/// ///
/// After each hit object you can call /// After each hit object you can call [`next`](`OsuGradualPerformance::next`)
/// [`process_next_object`](`OsuGradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`OsuPerformanceAttributes`]. /// and it will return the resulting current [`OsuPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use [`nth`](`OsuGradualPerformance::nth`) instead.
/// [`process_next_n_objects`](`OsuGradualPerformanceAttributes::process_next_n_objects`) instead.
/// ///
/// Both methods require an [`OsuScoreState`] that contains the current /// Both methods require an [`OsuScoreState`] that contains the current
/// hitresults as well as the maximum combo so far. /// hitresults as well as the maximum combo so far.
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes) instead. /// [`OsuGradualDifficulty`] instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, osu::{OsuGradualPerformanceAttributes, OsuScoreState}}; /// use rosu_pp::{Beatmap, osu::{OsuGradualPerformance, OsuScoreState}};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -75,7 +27,7 @@ impl OsuScoreState {
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut gradual_perf = OsuGradualPerformanceAttributes::new(&map, mods); /// let mut gradual_perf = OsuGradualPerformance::new(&map, mods);
/// let mut state = OsuScoreState::new(); // empty state, everything is on 0. /// let mut state = OsuScoreState::new(); // empty state, everything is on 0.
/// ///
/// // The first 10 hitresults are 300s and there are no sliders for additional combo /// // The first 10 hitresults are 300s and there are no sliders for additional combo
@@ -84,10 +36,10 @@ impl OsuScoreState {
/// state.max_combo += 1; /// state.max_combo += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
@@ -95,30 +47,31 @@ impl OsuScoreState {
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // The next 10 objects will be a mixture of 300s, 100s, and 50s. /// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
/// // Notice how all 10 objects will be processed in one go. /// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2; /// state.n300 += 2;
/// state.n100 += 7; /// state.n100 += 7;
/// state.n50 += 1; /// state.n50 += 1;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10); /// # let _ = gradual_perf.nth(state.clone(), 9);
/// ///
/// // Now comes another 300. Note that the max combo gets incremented again. /// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -127,25 +80,26 @@ impl OsuScoreState {
/// state.n100 = ... /// state.n100 = ...
/// state.n50 = ... /// state.n50 = ...
/// state.n_misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)] #[derive(Debug)]
pub struct OsuGradualPerformanceAttributes<'map> { pub struct OsuGradualPerformance<'map> {
difficulty: OsuGradualDifficultyAttributes, difficulty: OsuGradualDifficulty,
performance: OsuPP<'map>, performance: OsuPP<'map>,
} }
impl<'map> OsuGradualPerformanceAttributes<'map> { impl<'map> OsuGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!standard maps. /// Create a new gradual performance calculator for osu!standard maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let difficulty = OsuGradualDifficultyAttributes::new(map, mods); let difficulty = OsuGradualDifficulty::new(map, mods);
let performance = OsuPP::new(map).mods(mods).passed_objects(0); let performance = OsuPP::new(map).mods(mods).passed_objects(0);
Self { Self {
@@ -156,33 +110,83 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the /// Process the next hit object and calculate the
/// performance attributes for the resulting score state. /// performance attributes for the resulting score state.
pub fn process_next_object( pub fn next(&mut self, state: OsuScoreState) -> Option<OsuPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: OsuScoreState,
) -> Option<OsuPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`OsuGradualPerformanceAttributes::process_next_object`) /// Process all remaining hit objects and calculate the final performance attributes.
/// but instead of processing only one object it process `n` many. pub fn last(&mut self, state: OsuScoreState) -> Option<OsuPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
/// ///
/// If `n` is 0 it will be considered as 1. /// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// If there are still objects to be processed but `n` is larger than the amount /// and so on.
/// of remaining objects, `n` will be considered as the amount of remaining objects. pub fn nth(&mut self, state: OsuScoreState, n: usize) -> Option<OsuPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.nth(n)?;
&mut self,
state: OsuScoreState,
n: usize,
) -> Option<OsuPerformanceAttributes> {
let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(self.difficulty.idx + 1) .passed_objects(self.difficulty.idx)
.calculate();
Some(performance)
}
}
/// Gradually calculate the performance attributes of an osu!standard map.
///
/// Check [`OsuGradualPerformance`] for more information. This struct does the same
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
pub struct OsuOwnedGradualPerformance {
difficulty: OsuGradualDifficulty,
map: Beatmap,
mods: u32,
}
impl OsuOwnedGradualPerformance {
/// Create a new gradual performance calculator for osu!standard maps.
pub fn new(map: Beatmap, mods: u32) -> Self {
let difficulty = OsuGradualDifficulty::new(&map, mods);
Self {
difficulty,
map,
mods,
}
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score state.
pub fn next(&mut self, state: OsuScoreState) -> Option<OsuPerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance attributes.
pub fn last(&mut self, state: OsuScoreState) -> Option<OsuPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
pub fn nth(&mut self, state: OsuScoreState, n: usize) -> Option<OsuPerformanceAttributes> {
let difficulty = self.difficulty.nth(n)?;
let performance = OsuPP::new(&self.map)
.mods(self.mods)
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx)
.calculate(); .calculate();
Some(performance) Some(performance)
+35 -19
View File
@@ -1,19 +1,30 @@
mod difficulty_object; mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod osu_object; mod osu_object;
mod pp; mod pp;
mod scaling_factor; mod scaling_factor;
mod score_state;
mod skills; mod skills;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods}; use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods};
use std::pin::Pin;
use self::{ use self::{
difficulty_object::{Distances, OsuDifficultyObject}, difficulty_object::{Distances, OsuDifficultyObject},
skills::{Skill, Skills}, skills::{Skill, Skills},
}; };
pub use self::{gradual_difficulty::*, gradual_performance::*, osu_object::*, pp::*}; pub use self::{osu_object::*, pp::*, score_state::OsuScoreState};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::OsuGradualDifficulty,
gradual_performance::{OsuGradualPerformance, OsuOwnedGradualPerformance},
};
pub(crate) use self::scaling_factor::ScalingFactor; pub(crate) use self::scaling_factor::ScalingFactor;
@@ -91,7 +102,7 @@ impl<'map> OsuStars<'map> {
/// ///
/// If you want to calculate the difficulty after every few objects, instead of /// If you want to calculate the difficulty after every few objects, instead of
/// using [`OsuStars`] multiple times with different `passed_objects`, you should use /// using [`OsuStars`] multiple times with different `passed_objects`, you should use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes). /// [`OsuGradualDifficulty`].
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
@@ -271,7 +282,7 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
let mut hit_objects = let mut hit_objects =
create_osu_objects(map, &mut attrs, &scaling_factor, take, hr, time_preempt); create_osu_objects(map, &mut attrs, &scaling_factor, take, hr, time_preempt);
let mut hit_objects_iter = hit_objects.iter_mut(); let mut hit_objects_iter = hit_objects.iter_mut().map(Pin::new);
let mut skills = Skills::new( let mut skills = Skills::new(
mods, mods,
@@ -281,39 +292,45 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
hit_window, hit_window,
); );
let last = match hit_objects_iter.next() { let Some(mut last) = hit_objects_iter.next() else {
Some(prev) => prev, return (skills, attrs);
None => return (skills, attrs),
}; };
let mut last_last = None; let mut last_last = None;
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually // Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
Distances::compute_slider_cursor_pos(last, &scaling_factor); let last_pos = last.pos();
let last_stack_offset = last.stack_offset;
let mut last = &*last; if let OsuObjectKind::Slider(ref mut slider) = last.kind {
Distances::compute_slider_travel_dist(last_pos, last_stack_offset, slider, &scaling_factor);
}
let mut last = last.into_ref();
let mut diff_objects = Vec::with_capacity(hit_objects_iter.len()); let mut diff_objects = Vec::with_capacity(hit_objects_iter.len());
for (i, curr) in hit_objects_iter.enumerate() { for (i, mut curr) in hit_objects_iter.enumerate() {
let delta_time = (curr.start_time - last.start_time) / clock_rate; let delta_time = (curr.start_time - last.start_time) / clock_rate;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. // * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64); let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new( let dists = Distances::new(
curr, &mut curr,
last, last.get_ref(),
last_last, last_last.map(Pin::get_ref),
clock_rate, clock_rate,
strain_time, strain_time,
&scaling_factor, &scaling_factor,
); );
let diff_obj = OsuDifficultyObject::new(curr, last, clock_rate, i, dists); let curr = curr.into_ref();
let diff_obj = OsuDifficultyObject::new(curr, last.get_ref(), clock_rate, i, dists);
diff_objects.push(diff_obj); diff_objects.push(diff_obj);
last_last = Some(last); last_last = Some(last);
last = &*curr; last = curr;
} }
for curr in diff_objects.iter() { for curr in diff_objects.iter() {
@@ -363,9 +380,8 @@ pub(crate) fn create_osu_objects(
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) { fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
let mut extended_start_idx = 0; let mut extended_start_idx = 0;
let extended_end_idx = match hit_objects.len().checked_sub(1) { let Some(extended_end_idx) = hit_objects.len().checked_sub(1) else {
Some(idx) => idx, return;
None => return,
}; };
// First big `if` in osu!lazer's function can be skipped // First big `if` in osu!lazer's function can be skipped
+1 -1
View File
@@ -161,7 +161,7 @@ impl<'map> OsuPP<'map> {
/// ///
/// If you want to calculate the performance after every few objects, instead of /// If you want to calculate the performance after every few objects, instead of
/// using [`OsuPP`] multiple times with different `passed_objects`, you should use /// using [`OsuPP`] multiple times with different `passed_objects`, you should use
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes). /// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformance).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
+45
View File
@@ -0,0 +1,45 @@
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct OsuScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
pub max_combo: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl OsuScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * self.n300 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
+2 -2
View File
@@ -919,7 +919,7 @@ impl Beatmap {
/// Parse a beatmap from a `.osu` file. /// Parse a beatmap from a `.osu` file.
/// ///
/// As argument you can give anything that implements [`std::io::Read`]. /// As argument you can give anything that implements [`std::io::Read`].
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File) /// You'll likely want to pass (a reference of) a [`File`]
/// or the file's content as a slice of bytes (`&[u8]`). /// or the file's content as a slice of bytes (`&[u8]`).
pub fn parse<R: Read>(input: R) -> ParseResult<Self> { pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
parse_body!(input) parse_body!(input)
@@ -967,7 +967,7 @@ impl Beatmap {
/// Pass the path to a `.osu` file. /// Pass the path to a `.osu` file.
/// ///
/// Useful when you don't want to create the [`File`](std::fs::File) manually. /// Useful when you don't want to create the [`File`] manually.
/// If you have the file lying around already though (and plan on re-using it), /// If you have the file lying around already though (and plan on re-using it),
/// passing `&file` to [`parse`](Beatmap::parse) should be preferred. /// passing `&file` to [`parse`](Beatmap::parse) should be preferred.
pub fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> { pub fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
+1 -1
View File
@@ -116,7 +116,7 @@ impl<'map> AnyPP<'map> {
/// ///
/// If you want to calculate the performance after every few objects, instead of /// If you want to calculate the performance after every few objects, instead of
/// using [`AnyPP`] multiple times with different `passed_objects`, you should use /// using [`AnyPP`] multiple times with different `passed_objects`, you should use
/// [`GradualPerformanceAttributes`](crate::GradualPerformanceAttributes). /// [`GradualPerformanceAttributes`](crate::GradualPerformance).
#[inline] #[inline]
pub fn passed_objects(self, passed_objects: usize) -> Self { pub fn passed_objects(self, passed_objects: usize) -> Self {
match self { match self {
+110
View File
@@ -0,0 +1,110 @@
use crate::{
catch::CatchScoreState, mania::ManiaScoreState, osu::OsuScoreState, taiko::TaikoScoreState,
GameMode,
};
/// Aggregation for a score's current state i.e. what is
/// the maximum combo so far, what are the current
/// hitresults and what is the current score.
///
/// This struct is used for [`GradualPerformance`](crate::GradualPerformance).
#[derive(Clone, Debug, Default, Eq, PartialEq)]
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!catch only fruits and droplets are considered for combo.
///
/// Irrelevant for osu!mania.
pub max_combo: usize,
/// Amount of current gekis (n320 for osu!mania).
pub n_geki: usize,
/// Amount of current katus (tiny droplet misses for osu!catch / n200 for osu!mania).
pub n_katu: usize,
/// Amount of current 300s (fruits for osu!catch).
pub n300: usize,
/// Amount of current 100s (droplets for osu!catch).
pub n100: usize,
/// Amount of current 50s (tiny droplets for osu!catch).
pub n50: usize,
/// Amount of current misses (fruits + droplets for osu!catch).
pub n_misses: usize,
}
impl ScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up based on the mode.
#[inline]
pub fn total_hits(&self, mode: GameMode) -> usize {
let mut amount = self.n300 + self.n100 + self.n_misses;
if mode != GameMode::Taiko {
amount += self.n50;
if mode != GameMode::Osu {
amount += self.n_katu;
amount += (mode != GameMode::Catch) as usize * self.n_geki;
}
}
amount
}
}
impl From<ScoreState> for OsuScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n300: state.n300,
n100: state.n100,
n50: state.n50,
n_misses: state.n_misses,
}
}
}
impl From<ScoreState> for TaikoScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n300: state.n300,
n100: state.n100,
n_misses: state.n_misses,
}
}
}
impl From<ScoreState> for CatchScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n_fruits: state.n300,
n_droplets: state.n100,
n_tiny_droplets: state.n50,
n_tiny_droplet_misses: state.n_katu,
n_misses: state.n_misses,
}
}
}
impl From<ScoreState> for ManiaScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
n320: state.n_geki,
n300: state.n300,
n200: state.n_katu,
n100: state.n100,
n50: state.n50,
n_misses: state.n_misses,
}
}
}
+1 -1
View File
@@ -75,7 +75,7 @@ impl<'map> AnyStars<'map> {
/// ///
/// If you want to calculate the performance after every few objects, instead of /// If you want to calculate the performance after every few objects, instead of
/// using [`AnyStars`] multiple times with different `passed_objects`, you should use /// using [`AnyStars`] multiple times with different `passed_objects`, you should use
/// [`GradualDifficultyAttributes`](crate::GradualDifficultyAttributes). /// [`GradualDifficultyAttributes`](crate::GradualDifficulty).
#[inline] #[inline]
pub fn passed_objects(self, passed_objects: usize) -> Self { pub fn passed_objects(self, passed_objects: usize) -> Self {
match self { match self {
+1 -1
View File
@@ -154,7 +154,7 @@ impl ColourDifficultyPreprocessor {
mut data: VecDeque<Rc<RefCell<AlternatingMonoPattern>>>, mut data: VecDeque<Rc<RefCell<AlternatingMonoPattern>>>,
) -> Vec<Rc<RefCell<RepeatingHitPatterns>>> { ) -> Vec<Rc<RefCell<RepeatingHitPatterns>>> {
let mut hit_patterns = Vec::new(); let mut hit_patterns = Vec::new();
let mut curr_hit_pattern: Option<Rc<std::cell::RefCell<_>>> = None; let mut curr_hit_pattern: Option<Rc<RefCell<_>>> = None;
while !data.is_empty() { while !data.is_empty() {
let old = curr_hit_pattern.as_ref().map(Rc::downgrade); let old = curr_hit_pattern.as_ref().map(Rc::downgrade);
+7 -7
View File
@@ -62,21 +62,21 @@ impl RepeatingHitPatterns {
} }
pub(crate) fn find_repetition_interval(&mut self) { pub(crate) fn find_repetition_interval(&mut self) {
let mut other = match self.prev.as_ref().and_then(Weak::upgrade) { let Some(mut other) = self.prev.as_ref().and_then(Weak::upgrade) else {
Some(prev) => prev, return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1;
None => return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1,
}; };
let mut interval = 1; let mut interval = 1;
while interval < Self::MAX_REPETITION_INTERVAL { while interval < Self::MAX_REPETITION_INTERVAL {
if self.is_repetition_of(&other.borrow()) { if self.is_repetition_of(&other.borrow()) {
return self.repetition_interval = interval.min(Self::MAX_REPETITION_INTERVAL); self.repetition_interval = interval.min(Self::MAX_REPETITION_INTERVAL);
return;
} }
let next = match other.borrow().prev.as_ref().and_then(Weak::upgrade) { let Some(next) = other.borrow().prev.as_ref().and_then(Weak::upgrade) else {
Some(prev) => prev, break;
None => break,
}; };
// gotta love NLL... // gotta love NLL...
+1
View File
@@ -111,6 +111,7 @@ fn closest_rhythm(
.unwrap() .unwrap()
} }
// TODO: Remove Default impl and replace with `with_capacity` method for efficiency
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub(crate) struct ObjectLists { pub(crate) struct ObjectLists {
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject>>>, pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject>>>,
+51 -39
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "gradual")]
use std::{borrow::Cow, cell::RefCell, rc::Rc, vec::IntoIter}; use std::{borrow::Cow, cell::RefCell, rc::Rc, vec::IntoIter};
use crate::{beatmap::BeatmapHitWindows, taiko::rescale, Beatmap, GameMode, Mods}; use crate::{beatmap::BeatmapHitWindows, taiko::rescale, Beatmap, GameMode, Mods};
@@ -12,17 +14,17 @@ use super::{
/// Gradually calculate the difficulty attributes of an osu!taiko map. /// Gradually calculate the difficulty attributes of an osu!taiko map.
/// ///
/// Note that this struct implements [`Iterator`](std::iter::Iterator). /// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will /// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`TaikoDifficultyAttributes`] will be updated and returned. /// be processed and the [`TaikoDifficultyAttributes`] will be updated and returned.
/// ///
/// If you want to calculate performance attributes, use /// If you want to calculate performance attributes, use
/// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformanceAttributes) instead. /// [`TaikoGradualPerformance`](crate::taiko::TaikoGradualPerformance) instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, taiko::TaikoGradualDifficultyAttributes}; /// use rosu_pp::{Beatmap, taiko::TaikoGradualDifficulty};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -30,7 +32,7 @@ use super::{
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut iter = TaikoGradualDifficultyAttributes::new(&map, mods); /// let mut iter = TaikoGradualDifficulty::new(&map, mods);
/// ///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object /// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object /// let attrs2 = iter.next(); // after the second hit object
@@ -40,18 +42,19 @@ use super::{
/// // ... /// // ...
/// } /// }
/// ``` /// ```
#[derive(Clone, Debug)] #[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
pub struct TaikoGradualDifficultyAttributes { #[derive(Debug)]
pub struct TaikoGradualDifficulty {
pub(crate) idx: usize,
attrs: TaikoDifficultyAttributes, attrs: TaikoDifficultyAttributes,
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>, diff_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
lists: ObjectLists, lists: ObjectLists,
peaks: Peaks, peaks: Peaks,
total_hits: usize, total_hits: usize,
is_convert: bool, is_convert: bool,
pub(crate) started: bool,
} }
impl TaikoGradualDifficultyAttributes { impl TaikoGradualDifficulty {
/// Create a new difficulty attributes iterator for osu!taiko maps. /// Create a new difficulty attributes iterator for osu!taiko maps.
pub fn new(map: &Beatmap, mods: u32) -> Self { pub fn new(map: &Beatmap, mods: u32) -> Self {
let map = map.convert_mode(GameMode::Taiko); let map = map.convert_mode(GameMode::Taiko);
@@ -77,29 +80,27 @@ impl TaikoGradualDifficultyAttributes {
if map.hit_objects.len() < 2 { if map.hit_objects.len() < 2 {
return Self { return Self {
hit_objects: Vec::new().into_iter(), idx: 0,
diff_objects: Vec::new().into_iter(),
lists: ObjectLists::default(), lists: ObjectLists::default(),
peaks, peaks,
attrs, attrs,
total_hits: 0, total_hits: 0,
is_convert, is_convert,
started: false,
}; };
} }
attrs.max_combo += map.hit_objects[0].is_circle() as usize; attrs.max_combo += map.hit_objects[0].is_circle() as usize;
attrs.max_combo += map.hit_objects[1].is_circle() as usize; attrs.max_combo += map.hit_objects[1].is_circle() as usize;
let mut total_hits = attrs.max_combo; let mut total_hits = attrs.max_combo;
let mut diff_objects = ObjectLists::default();
let mut diff_objects = map map.taiko_objects()
.taiko_objects()
.skip(2) .skip(2)
.zip(map.hit_objects.iter().skip(1)) .zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter()) .zip(map.hit_objects.iter())
.enumerate() .enumerate()
.fold( .for_each(|(idx, (((base, base_start_time), last), last_last))| {
ObjectLists::default(),
|mut lists, (idx, (((base, base_start_time), last), last_last))| {
total_hits += base.is_hit as usize; total_hits += base.is_hit as usize;
let diff_obj = TaikoDifficultyObject::new( let diff_obj = TaikoDifficultyObject::new(
@@ -108,48 +109,48 @@ impl TaikoGradualDifficultyAttributes {
last.start_time, last.start_time,
last_last.start_time, last_last.start_time,
clock_rate, clock_rate,
&lists, &diff_objects,
idx, idx,
); );
match &diff_obj.mono_idx { match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx), MonoIndex::Centre(_) => diff_objects.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx), MonoIndex::Rim(_) => diff_objects.rims.push(idx),
MonoIndex::None => {} MonoIndex::None => {}
} }
if diff_obj.note_idx.is_some() { if diff_obj.note_idx.is_some() {
lists.notes.push(idx); diff_objects.notes.push(idx);
} }
lists.all.push(Rc::new(RefCell::new(diff_obj))); diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
});
lists
},
);
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects); ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
Self { Self {
hit_objects: diff_objects.all.clone().into_iter(), idx: 0,
diff_objects: diff_objects.all.clone().into_iter(),
lists: diff_objects, lists: diff_objects,
peaks, peaks,
attrs, attrs,
total_hits, total_hits,
is_convert, is_convert,
started: false,
} }
} }
} }
impl Iterator for TaikoGradualDifficultyAttributes { impl Iterator for TaikoGradualDifficulty {
type Item = TaikoDifficultyAttributes; type Item = TaikoDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.started = true; // The first difficulty object belongs to the third note since each difficulty
// object requires the current the last, and the second to last note. Hence, if we're still
// on the first or second object, we don't have a difficulty object yet and just skip
// processing.
if self.idx >= 2 {
loop { loop {
let curr = self.hit_objects.next()?; let curr = self.diff_objects.next()?;
let borrowed = curr.borrow(); let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists); self.peaks.process(&borrowed, &self.lists);
@@ -159,6 +160,11 @@ impl Iterator for TaikoGradualDifficultyAttributes {
break; break;
} }
} }
} else if self.lists.all.is_empty() {
return None;
}
self.idx += 1;
let PeaksDifficultyValues { let PeaksDifficultyValues {
mut colour_rating, mut colour_rating,
@@ -203,18 +209,24 @@ impl Iterator for TaikoGradualDifficultyAttributes {
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n let mut take = n.min(self.len().saturating_sub(1));
.min(self.total_hits - self.attrs.max_combo)
.saturating_sub(1);
for _ in 0..skip { // The first two notes have no difficulty object
if self.idx < 2 && take > 0 {
let skipped = take.min(2);
take -= skipped;
self.idx += skipped;
}
for _ in 0..take {
loop { loop {
let curr = self.hit_objects.next()?; let curr = self.diff_objects.next()?;
let borrowed = curr.borrow(); let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists); self.peaks.process(&borrowed, &self.lists);
if borrowed.base.is_hit { if borrowed.base.is_hit {
self.attrs.max_combo += 1; self.attrs.max_combo += 1;
self.idx += 1;
break; break;
} }
@@ -225,9 +237,9 @@ impl Iterator for TaikoGradualDifficultyAttributes {
} }
} }
impl ExactSizeIterator for TaikoGradualDifficultyAttributes { impl ExactSizeIterator for TaikoGradualDifficulty {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
self.hit_objects.len() self.total_hits - self.idx
} }
} }
+100 -89
View File
@@ -1,71 +1,26 @@
use crate::{Beatmap, TaikoPP}; #![cfg(feature = "gradual")]
use super::{TaikoGradualDifficultyAttributes, TaikoPerformanceAttributes}; use std::borrow::Cow;
use crate::{taiko::TaikoScoreState, Beatmap, TaikoPP, GameMode};
/// Aggregation for a score's current state i.e. what was the use super::{TaikoGradualDifficulty, TaikoPerformanceAttributes};
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`TaikoGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TaikoScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
pub max_combo: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl TaikoScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 2 * self.n300 + self.n100;
let denominator = 2 * total_hits;
numerator as f64 / denominator as f64
}
}
/// Gradually calculate the performance attributes of an osu!taiko map. /// Gradually calculate the performance attributes of an osu!taiko map.
/// ///
/// After each hit object you can call /// After each hit object you can call [`next`](`TaikoGradualPerformance::next`)
/// [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`TaikoPerformanceAttributes`]. /// and it will return the resulting current [`TaikoPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use [`nth`](`TaikoGradualPerformance::nth`) instead.
/// [`process_next_n_objects`](`TaikoGradualPerformanceAttributes::process_next_n_objects`) instead.
/// ///
/// Both methods require a [`TaikoScoreState`] that contains the current /// Both methods require a [`TaikoScoreState`] that contains the current
/// hitresults as well as the maximum combo so far. /// hitresults as well as the maximum combo so far.
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficultyAttributes) instead. /// [`TaikoGradualDifficulty`] instead.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, taiko::{TaikoGradualPerformanceAttributes, TaikoScoreState}}; /// use rosu_pp::{Beatmap, taiko::{TaikoGradualPerformance, TaikoScoreState}};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -73,7 +28,7 @@ impl TaikoScoreState {
/// # let map = Beatmap::default(); /// # let map = Beatmap::default();
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut gradual_perf = TaikoGradualPerformanceAttributes::new(&map, mods); /// let mut gradual_perf = TaikoGradualPerformance::new(&map, mods);
/// let mut state = TaikoScoreState::new(); // empty state, everything is on 0. /// let mut state = TaikoScoreState::new(); // empty state, everything is on 0.
/// ///
/// // The first 10 hitresults are 300s /// // The first 10 hitresults are 300s
@@ -82,10 +37,10 @@ impl TaikoScoreState {
/// state.max_combo += 1; /// state.max_combo += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
@@ -93,29 +48,30 @@ impl TaikoScoreState {
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // The next 10 objects will be a mixture of 300s and 100s. /// // The next 10 objects will be a mixture of 300s and 100s.
/// // Notice how all 10 objects will be processed in one go. /// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 3; /// state.n300 += 3;
/// state.n100 += 7; /// state.n100 += 7;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10); /// # let _ = gradual_perf.nth(state.clone(), 9);
/// ///
/// // Now comes another 300. Note that the max combo gets incremented again. /// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -123,25 +79,26 @@ impl TaikoScoreState {
/// state.n300 = ... /// state.n300 = ...
/// state.n100 = ... /// state.n100 = ...
/// state.n_misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[derive(Clone, Debug)] #[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
pub struct TaikoGradualPerformanceAttributes<'map> { #[derive(Debug)]
difficulty: TaikoGradualDifficultyAttributes, pub struct TaikoGradualPerformance<'map> {
difficulty: TaikoGradualDifficulty,
performance: TaikoPP<'map>, performance: TaikoPP<'map>,
} }
impl<'map> TaikoGradualPerformanceAttributes<'map> { impl<'map> TaikoGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!taiko maps. /// Create a new gradual performance calculator for osu!taiko maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let difficulty = TaikoGradualDifficultyAttributes::new(map, mods); let difficulty = TaikoGradualDifficulty::new(map, mods);
let performance = TaikoPP::new(map).mods(mods).passed_objects(0); let performance = TaikoPP::new(map).mods(mods).passed_objects(0);
Self { Self {
@@ -152,34 +109,88 @@ impl<'map> TaikoGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the /// Process the next hit object and calculate the
/// performance attributes for the resulting score. /// performance attributes for the resulting score.
pub fn process_next_object( pub fn next(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: TaikoScoreState,
) -> Option<TaikoPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`) /// Process all remaining hit objects and calculate the final performance attributes.
/// but instead of processing only one object it process `n` many. pub fn last(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
/// ///
/// If `n` is 0 it will be considered as 1. /// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// If there are still objects to be processed but `n` is larger than the amount /// and so on.
/// of remaining objects, `n` will be considered as the amount of remaining objects. pub fn nth(&mut self, state: TaikoScoreState, n: usize) -> Option<TaikoPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.nth(n)?;
&mut self,
state: TaikoScoreState,
n: usize,
) -> Option<TaikoPerformanceAttributes> {
let sub = 2 * !self.difficulty.started as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let passed_objects = difficulty.max_combo;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(passed_objects) .passed_objects(self.difficulty.idx)
.calculate();
Some(performance)
}
}
/// Gradually calculate the performance attributes of an osu!taiko map.
///
/// Check [`TaikoGradualPerformance`] for more information. This struct does the same
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
pub struct TaikoOwnedGradualPerformance {
difficulty: TaikoGradualDifficulty,
map: Beatmap,
mods: u32,
}
impl TaikoOwnedGradualPerformance {
/// Create a new gradual performance calculator for osu!taiko maps.
pub fn new(map: Beatmap, mods: u32) -> Self {
let map = match map.convert_mode(GameMode::Taiko) {
Cow::Owned(map) => map,
Cow::Borrowed(_) => map,
};
let difficulty = TaikoGradualDifficulty::new(&map, mods);
Self {
difficulty,
map,
mods,
}
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
pub fn next(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance attributes.
pub fn last(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
pub fn nth(&mut self, state: TaikoScoreState, n: usize) -> Option<TaikoPerformanceAttributes> {
let difficulty = self.difficulty.nth(n)?;
let performance = TaikoPP::new(&self.map)
.mods(self.mods)
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx)
.calculate(); .calculate();
Some(performance) Some(performance)
+22 -19
View File
@@ -1,17 +1,24 @@
mod colours; mod colours;
mod difficulty_object; mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod pp; mod pp;
mod rim; mod rim;
mod score_state;
mod skills; mod skills;
mod taiko_object; mod taiko_object;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use std::{borrow::Cow, cell::RefCell, rc::Rc}; use std::{borrow::Cow, cell::RefCell, rc::Rc};
pub use self::{pp::*, score_state::TaikoScoreState, taiko_object::TaikoObjectPub as TaikoObject};
#[cfg(feature = "gradual")]
pub use self::{ pub use self::{
gradual_difficulty::*, gradual_performance::*, pp::*, gradual_difficulty::TaikoGradualDifficulty,
taiko_object::TaikoObjectPub as TaikoObject, gradual_performance::{TaikoGradualPerformance, TaikoOwnedGradualPerformance},
}; };
pub(crate) use self::taiko_object::IntoTaikoObjectIter; pub(crate) use self::taiko_object::IntoTaikoObjectIter;
@@ -85,7 +92,7 @@ impl<'map> TaikoStars<'map> {
/// ///
/// If you want to calculate the difficulty after every few objects, instead of /// If you want to calculate the difficulty after every few objects, instead of
/// using [`TaikoStars`] multiple times with different `passed_objects`, you should use /// using [`TaikoStars`] multiple times with different `passed_objects`, you should use
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficultyAttributes). /// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficulty).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
@@ -226,8 +233,9 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
let mut peaks = Peaks::new(); let mut peaks = Peaks::new();
let mut max_combo = 0; let mut max_combo = 0;
let mut diff_objects = map let mut diff_objects = ObjectLists::default();
.taiko_objects()
map.taiko_objects()
.take_while(|(h, _)| { .take_while(|(h, _)| {
if h.is_hit { if h.is_hit {
if take == 0 { if take == 0 {
@@ -244,34 +252,29 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
.zip(map.hit_objects.iter().skip(1)) .zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter()) .zip(map.hit_objects.iter())
.enumerate() .enumerate()
.fold( .for_each(|(idx, (((base, base_start_time), last), last_last))| {
ObjectLists::default(),
|mut lists, (idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new( let diff_obj = TaikoDifficultyObject::new(
base, base,
base_start_time, base_start_time,
last.start_time, last.start_time,
last_last.start_time, last_last.start_time,
clock_rate, clock_rate,
&lists, &diff_objects,
idx, idx,
); );
match &diff_obj.mono_idx { match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx), MonoIndex::Centre(_) => diff_objects.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx), MonoIndex::Rim(_) => diff_objects.rims.push(idx),
MonoIndex::None => {} MonoIndex::None => {}
} }
if diff_obj.note_idx.is_some() { if diff_obj.note_idx.is_some() {
lists.notes.push(idx); diff_objects.notes.push(idx);
} }
lists.all.push(Rc::new(RefCell::new(diff_obj))); diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
});
lists
},
);
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects); ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
+1 -1
View File
@@ -151,7 +151,7 @@ impl<'map> TaikoPP<'map> {
/// ///
/// If you want to calculate the performance after every few objects, instead of /// If you want to calculate the performance after every few objects, instead of
/// using [`TaikoPP`] multiple times with different `passed_objects`, you should use /// using [`TaikoPP`] multiple times with different `passed_objects`, you should use
/// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformanceAttributes). /// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformance).
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
+43
View File
@@ -0,0 +1,43 @@
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TaikoScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
pub max_combo: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl TaikoScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 2 * self.n300 + self.n100;
let denominator = 2 * total_hits;
numerator as f64 / denominator as f64
}
}
+21 -18
View File
@@ -1,7 +1,10 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{ use rosu_pp::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState}, catch::{CatchGradualDifficulty, CatchGradualPerformance, CatchScoreState},
Beatmap, CatchPP, CatchStars, Beatmap, CatchPP, CatchStars,
}; };
@@ -12,7 +15,7 @@ mod common;
#[test] #[test]
fn empty_map() { fn empty_map() {
let map = Beatmap::default(); let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0); let mut attributes = CatchGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none()); assert!(attributes.next().is_none());
} }
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Catch); let map = test_map!(Catch);
let regular = CatchStars::new(&map).calculate(); let regular = CatchStars::new(&map).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, 0) let iter_end = CatchGradualDifficulty::new(&map, 0)
.last() .last()
.expect("empty iter"); .expect("empty iter");
@@ -32,13 +35,13 @@ fn iter_end_eq_regular() {
#[test] #[test]
fn correct_empty() { fn correct_empty() {
let map = test_map!(Catch); let map = test_map!(Catch);
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual = CatchGradualPerformance::new(&map, 0);
let state = CatchScoreState::default(); let state = CatchScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX); let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some()); assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none()); assert!(gradual.next(state).is_none());
} }
#[test] #[test]
@@ -46,18 +49,18 @@ fn next_and_next_n() {
let map = test_map!(Catch); let map = test_map!(Catch);
let state = CatchScoreState::default(); let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual1 = CatchGradualPerformance::new(&map, 0);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = CatchGradualPerformance::new(&map, 0);
for _ in 0..20 { for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
let _ = gradual2.process_next_object(state.clone()); let _ = gradual2.next(state.clone());
} }
let n = 80; let n = 80;
for _ in 1..n { for _ in 1..n {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
} }
let state = CatchScoreState { let state = CatchScoreState {
@@ -69,8 +72,8 @@ fn next_and_next_n() {
n_misses: 0, n_misses: 0,
}; };
let next = gradual1.process_next_object(state.clone()); let next = gradual1.next(state.clone());
let next_n = gradual2.process_next_n_objects(state, n); let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next); assert_eq!(next_n, next);
} }
@@ -80,7 +83,7 @@ fn gradual_end_eq_regular() {
let map = test_map!(Catch); let map = test_map!(Catch);
let regular = CatchPP::new(&map).calculate(); let regular = CatchPP::new(&map).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual = CatchGradualPerformance::new(&map, 0);
let state = CatchScoreState { let state = CatchScoreState {
max_combo: 730, max_combo: 730,
@@ -91,7 +94,7 @@ fn gradual_end_eq_regular() {
n_misses: 0, n_misses: 0,
}; };
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap(); let gradual_end = gradual.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
@@ -102,7 +105,7 @@ fn gradual_eq_regular_passed() {
let n = 100; let n = 100;
let regular = CatchPP::new(&map).passed_objects(n).calculate(); let regular = CatchPP::new(&map).passed_objects(n).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual = CatchGradualPerformance::new(&map, 0);
let state = CatchScoreState { let state = CatchScoreState {
max_combo: 101, max_combo: 101,
@@ -113,7 +116,7 @@ fn gradual_eq_regular_passed() {
n_misses: 0, n_misses: 0,
}; };
let gradual = gradual.process_next_n_objects(state, n).unwrap(); let gradual = gradual.nth(state, n - 1).unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }
+27 -45
View File
@@ -1,7 +1,10 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{ use rosu_pp::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState}, mania::{ManiaGradualDifficulty, ManiaGradualPerformance, ManiaScoreState},
Beatmap, ManiaPP, ManiaStars, Beatmap, ManiaPP, ManiaStars,
}; };
@@ -12,7 +15,7 @@ mod common;
#[test] #[test]
fn empty_map() { fn empty_map() {
let map = Beatmap::default(); let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0); let mut attributes = ManiaGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none()); assert!(attributes.next().is_none());
} }
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Mania); let map = test_map!(Mania);
let regular = ManiaStars::new(&map).calculate(); let regular = ManiaStars::new(&map).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, 0) let iter_end = ManiaGradualDifficulty::new(&map, 0)
.last() .last()
.expect("empty iter"); .expect("empty iter");
@@ -32,54 +35,40 @@ fn iter_end_eq_regular() {
#[test] #[test]
fn correct_empty() { fn correct_empty() {
let map = test_map!(Mania); let map = test_map!(Mania);
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual = ManiaGradualPerformance::new(&map, 0);
let state = ManiaScoreState { let state = ManiaScoreState::default();
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX); let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some()); assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none()); assert!(gradual.next(state).is_none());
} }
#[test] #[test]
fn next_and_next_n() { fn next_and_next_n() {
let map = test_map!(Mania); let map = test_map!(Mania);
let mut state = ManiaScoreState { let mut state = ManiaScoreState::default();
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual1 = ManiaGradualPerformance::new(&map, 0);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = ManiaGradualPerformance::new(&map, 0);
for _ in 0..20 { for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
let _ = gradual2.process_next_object(state.clone()); let _ = gradual2.next(state.clone());
state.n320 += 1; state.n320 += 1;
} }
let n = 80; let n = 80;
for _ in 1..n { for _ in 1..n {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
state.n320 += 1; state.n320 += 1;
} }
let next = gradual1.process_next_object(state.clone()); let next = gradual1.next(state.clone());
let next_n = gradual2.process_next_n_objects(state, n); let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next); assert_eq!(next_n, next);
} }
@@ -89,18 +78,14 @@ fn gradual_end_eq_regular() {
let map = test_map!(Mania); let map = test_map!(Mania);
let regular = ManiaPP::new(&map).calculate(); let regular = ManiaPP::new(&map).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual = ManiaGradualPerformance::new(&map, 0);
let state = ManiaScoreState { let state = ManiaScoreState {
n320: 3238, n320: map.hit_objects.len(),
n300: 0, ..Default::default()
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
}; };
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap(); let gradual_end = gradual.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
@@ -112,11 +97,7 @@ fn gradual_eq_regular_passed() {
let state = ManiaScoreState { let state = ManiaScoreState {
n320: 100, n320: 100,
n300: 0, ..Default::default()
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
}; };
let regular = ManiaPP::new(&map) let regular = ManiaPP::new(&map)
@@ -124,8 +105,9 @@ fn gradual_eq_regular_passed() {
.state(state.clone()) .state(state.clone())
.calculate(); .calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0); let gradual = ManiaGradualPerformance::new(&map, 0)
let gradual = gradual.process_next_n_objects(state, n).unwrap(); .nth(state, n - 1)
.unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }
+21 -18
View File
@@ -1,7 +1,10 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{ use rosu_pp::{
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState}, osu::{OsuGradualDifficulty, OsuGradualPerformance, OsuScoreState},
Beatmap, OsuPP, OsuStars, Beatmap, OsuPP, OsuStars,
}; };
@@ -12,7 +15,7 @@ mod common;
#[test] #[test]
fn empty_map() { fn empty_map() {
let map = Beatmap::default(); let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0); let mut attributes = OsuGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none()); assert!(attributes.next().is_none());
} }
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Osu); let map = test_map!(Osu);
let regular = OsuStars::new(&map).calculate(); let regular = OsuStars::new(&map).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, 0) let iter_end = OsuGradualDifficulty::new(&map, 0)
.last() .last()
.expect("empty iter"); .expect("empty iter");
@@ -32,13 +35,13 @@ fn iter_end_eq_regular() {
#[test] #[test]
fn correct_empty() { fn correct_empty() {
let map = test_map!(Osu); let map = test_map!(Osu);
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual = OsuGradualPerformance::new(&map, 0);
let state = OsuScoreState::default(); let state = OsuScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX); let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some()); assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none()); assert!(gradual.next(state).is_none());
} }
#[test] #[test]
@@ -46,18 +49,18 @@ fn next_and_next_n() {
let map = test_map!(Osu); let map = test_map!(Osu);
let state = OsuScoreState::default(); let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual1 = OsuGradualPerformance::new(&map, 0);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = OsuGradualPerformance::new(&map, 0);
for _ in 0..20 { for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
let _ = gradual2.process_next_object(state.clone()); let _ = gradual2.next(state.clone());
} }
let n = 80; let n = 80;
for _ in 1..n { for _ in 1..n {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
} }
let state = OsuScoreState { let state = OsuScoreState {
@@ -68,8 +71,8 @@ fn next_and_next_n() {
n_misses: 2, n_misses: 2,
}; };
let next = gradual1.process_next_object(state.clone()); let next = gradual1.next(state.clone());
let next_n = gradual2.process_next_n_objects(state, n); let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next); assert_eq!(next_n, next);
} }
@@ -78,7 +81,7 @@ fn next_and_next_n() {
fn gradual_end_eq_regular() { fn gradual_end_eq_regular() {
let map = test_map!(Osu); let map = test_map!(Osu);
let regular = OsuPP::new(&map).calculate(); let regular = OsuPP::new(&map).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual = OsuGradualPerformance::new(&map, 0);
let state = OsuScoreState { let state = OsuScoreState {
max_combo: 909, max_combo: 909,
@@ -88,7 +91,7 @@ fn gradual_end_eq_regular() {
n_misses: 0, n_misses: 0,
}; };
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap(); let gradual_end = gradual.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
@@ -99,7 +102,7 @@ fn gradual_eq_regular_passed() {
let n = 100; let n = 100;
let regular = OsuPP::new(&map).passed_objects(n).calculate(); let regular = OsuPP::new(&map).passed_objects(n).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual = OsuGradualPerformance::new(&map, 0);
let state = OsuScoreState { let state = OsuScoreState {
max_combo: 122, max_combo: 122,
@@ -109,7 +112,7 @@ fn gradual_eq_regular_passed() {
n_misses: 0, n_misses: 0,
}; };
let gradual = gradual.process_next_n_objects(state, n).unwrap(); let gradual = gradual.nth(state, n - 1).unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }
+21 -18
View File
@@ -1,7 +1,10 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{ use rosu_pp::{
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState}, taiko::{TaikoGradualDifficulty, TaikoGradualPerformance, TaikoScoreState},
Beatmap, TaikoPP, TaikoStars, Beatmap, TaikoPP, TaikoStars,
}; };
@@ -12,7 +15,7 @@ mod common;
#[test] #[test]
fn empty_map() { fn empty_map() {
let map = Beatmap::default(); let map = Beatmap::default();
let mut attrs = TaikoGradualDifficultyAttributes::new(&map, 0); let mut attrs = TaikoGradualDifficulty::new(&map, 0);
assert!(attrs.next().is_none()); assert!(attrs.next().is_none());
} }
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Taiko); let map = test_map!(Taiko);
let regular = TaikoStars::new(&map).calculate(); let regular = TaikoStars::new(&map).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, 0) let iter_end = TaikoGradualDifficulty::new(&map, 0)
.last() .last()
.expect("empty iter"); .expect("empty iter");
@@ -32,13 +35,13 @@ fn iter_end_eq_regular() {
#[test] #[test]
fn correct_empty() { fn correct_empty() {
let map = test_map!(Taiko); let map = test_map!(Taiko);
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0); let mut gradual = TaikoGradualPerformance::new(&map, 0);
let state = TaikoScoreState::default(); let state = TaikoScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX); let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some()); assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none()); assert!(gradual.next(state).is_none());
} }
#[test] #[test]
@@ -46,18 +49,18 @@ fn next_and_next_n() {
let map = test_map!(Taiko); let map = test_map!(Taiko);
let state = TaikoScoreState::default(); let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformanceAttributes::new(&map, 0); let mut gradual1 = TaikoGradualPerformance::new(&map, 0);
let mut gradual2 = TaikoGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = TaikoGradualPerformance::new(&map, 0);
for _ in 0..50 { for _ in 0..50 {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
let _ = gradual2.process_next_object(state.clone()); let _ = gradual2.next(state.clone());
} }
let n = 200; let n = 200;
for _ in 1..n { for _ in 1..n {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
} }
let state = TaikoScoreState { let state = TaikoScoreState {
@@ -67,8 +70,8 @@ fn next_and_next_n() {
n_misses: 6, n_misses: 6,
}; };
let next = gradual1.process_next_object(state.clone()); let next = gradual1.next(state.clone());
let next_n = gradual2.process_next_n_objects(state, n); let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next); assert_eq!(next_n, next);
} }
@@ -77,7 +80,7 @@ fn next_and_next_n() {
fn gradual_end_eq_regular() { fn gradual_end_eq_regular() {
let map = test_map!(Taiko); let map = test_map!(Taiko);
let regular = TaikoPP::new(&map).calculate(); let regular = TaikoPP::new(&map).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0); let mut gradual = TaikoGradualPerformance::new(&map, 0);
let state = TaikoScoreState { let state = TaikoScoreState {
max_combo: 289, max_combo: 289,
@@ -86,7 +89,7 @@ fn gradual_end_eq_regular() {
n_misses: 0, n_misses: 0,
}; };
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap(); let gradual_end = gradual.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
@@ -97,7 +100,7 @@ fn gradual_eq_regular_passed() {
let n = 250; let n = 250;
let regular = TaikoPP::new(&map).passed_objects(n).calculate(); let regular = TaikoPP::new(&map).passed_objects(n).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0); let mut gradual = TaikoGradualPerformance::new(&map, 0);
let state = TaikoScoreState { let state = TaikoScoreState {
max_combo: 250, max_combo: 250,
@@ -106,7 +109,7 @@ fn gradual_eq_regular_passed() {
n_misses: 0, n_misses: 0,
}; };
let gradual = gradual.process_next_n_objects(state, n).unwrap(); let gradual = gradual.nth(state, n - 1).unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }