Merge pull request #24 from MaxOhn/gradual-rework

refactor!: overhauled gradual calculation
This commit is contained in:
Badewanne3
2023-11-10 14:53:25 +01:00
committed by GitHub
42 changed files with 1785 additions and 1201 deletions
+8 -2
View File
@@ -14,7 +14,10 @@ jobs:
strategy:
matrix:
feature: [default, async_tokio, async_std]
feature:
- default,gradual
- async_tokio
- async_std
steps:
- name: Checkout project
@@ -35,7 +38,10 @@ jobs:
strategy:
matrix:
feature: [default, async_tokio, async_std]
feature:
- default,gradual
- async_tokio
- async_std
steps:
- name: Checkout project
+12
View File
@@ -3,6 +3,18 @@
- __Additions:__
- Added `From<u8>` impl for `GameMode`
- 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)
+7
View File
@@ -14,6 +14,7 @@ keywords = ["osu", "pp", "stars", "async"]
default = []
async_std = ["async-std"]
async_tokio = ["tokio"]
gradual = []
[dependencies]
async-std = { version = "1.9", optional = true }
@@ -21,3 +22,9 @@ tokio = { version = "1.2", optional = true, default-features = false, features =
[dev-dependencies]
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.
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
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") {
@@ -93,12 +94,11 @@ let map = match Beatmap::from_path("/path/to/file.osu") {
let mods = 8 + 64; // HDDT
// If you're only interested in the star rating or other difficulty value,
// use `GradualDifficultyAttributes`, either through its function `new`
// 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`.
let gradual_difficulty = GradualDifficulty::new(&map, 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`, ...
for (i, difficulty) in gradual_difficulty.enumerate() {
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
// difficulty attributes but it goes the extra step and also evaluates
// 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.
let state = ScoreState {
@@ -121,7 +121,7 @@ let state = ScoreState {
};
// 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,
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
// using the mode's gradual calculator instead of the general one.
// Let's assume it's a taiko map.
// Instead of starting off with `BeatmapExt::gradual_performance` one could have
// created the struct via `TaikoGradualPerformanceAttributes::new`.
// Instead of starting off with `GradualPerformance` one could have
// used `TaikoGradualPerformance`.
let mut gradual_performance = match gradual_performance {
GradualPerformanceAttributes::Taiko(gradual) => gradual,
_ => panic!("the map was not taiko but {:?}", map.mode),
@@ -146,10 +146,10 @@ let state = TaikoScoreState {
n_misses: 1,
};
// Process the next 10 objects in one go
let curr_performance = match gradual_performance.process_next_n_objects(state, 10) {
// Process the next 10 objects in one go (`nth` takes a zero-based value).
let curr_performance = match gradual_performance.nth(state, 9) {
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());
@@ -158,10 +158,11 @@ println!("PP after the first 11 objects: {}", curr_performance.pp());
### Features
| Flag | Description |
| ------------- | ---------------------------------------------------------------------------------------- |
| ------------- |------------------------------------------------------------------------------------------|
| `default` | Beatmap parsing will be non-async |
| `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) |
| `gradual` | Enable gradual difficulty and performance calculation |
### Version
+3 -26
View File
@@ -8,9 +8,8 @@ use crate::{
osu::{OsuDifficultyAttributes, OsuObject, ScalingFactor},
taiko::{IntoTaikoObjectIter, TaikoObject},
util::FloatExt,
AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, GradualDifficultyAttributes,
GradualPerformanceAttributes, ManiaPP, ManiaStars, Mods, OsuPP, OsuStars,
PerformanceAttributes, Strains, TaikoPP, TaikoStars,
AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, ManiaPP, ManiaStars, Mods, OsuPP,
OsuStars, PerformanceAttributes, Strains, TaikoPP, TaikoStars,
};
/// Provides some additional methods on [`Beatmap`].
@@ -35,17 +34,6 @@ pub trait BeatmapExt {
/// Suitable to plot the difficulty of a map over time.
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`],
/// just like the difficulty calculation does.
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> {
let attrs = self.attributes().mods(mods).build();
let scaling_factor = ScalingFactor::new(attrs.cs);
@@ -159,7 +137,6 @@ impl BeatmapExt for Beatmap {
curve_bufs: CurveBuffers::default(),
last_pos: None,
last_time: 0.0,
map: self,
ticks: Vec::new(),
with_hr: mods.hr(),
};
@@ -167,7 +144,7 @@ impl BeatmapExt for Beatmap {
let mut hit_objects: Vec<_> = self
.hit_objects
.iter()
.filter_map(|h| FruitOrJuice::new(h, &mut params))
.filter_map(|h| FruitOrJuice::new(h, &mut params, self))
.flatten()
.collect();
+1 -1
View File
@@ -23,4 +23,4 @@ impl From<u8> for GameMode {
_ => Self::Osu,
}
}
}
}
+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 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;
#[derive(Clone, Debug)]
pub(crate) struct FruitParams<'a> {
pub(crate) struct FruitParams {
pub(crate) attributes: CatchDifficultyAttributes,
pub(crate) curve_bufs: CurveBuffers,
pub(crate) last_pos: Option<f32>,
pub(crate) last_time: f64,
pub(crate) map: &'a Beatmap,
pub(crate) ticks: Vec<(Pos2, f64)>,
pub(crate) with_hr: bool,
}
@@ -31,7 +30,7 @@ pub(crate) enum 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 {
HitObjectKind::Circle => {
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_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
.map
.difficulty_point_at(h.start_time)
.unwrap_or_default();
let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
let vel_factor =
BASE_SCORING_DISTANCE * params.map.slider_mult / timing_point.beat_len;
let tick_dist_factor =
BASE_SCORING_DISTANCE * params.map.slider_mult / params.map.tick_rate;
let vel_factor = BASE_SCORING_DISTANCE * map.slider_mult / timing_point.beat_len;
let tick_dist_factor = BASE_SCORING_DISTANCE * map.slider_mult / map.tick_rate;
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::{
catch::{difficulty_object::DifficultyObject, SECTION_LENGTH, STAR_SCALING_FACTOR},
curve::CurveBuffers,
parse::{HitObject, Pos2},
parse::Pos2,
Beatmap, Mods,
};
@@ -17,19 +17,19 @@ use super::{
/// Gradually calculate the difficulty attributes of an osu!catch map.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next fruit or droplet
/// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`], the map's next fruit or droplet
/// 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.
///
/// If you want to calculate performance attributes, use
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformanceAttributes) instead.
/// [`CatchGradualPerformance`](crate::catch::CatchGradualPerformance) instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, catch::CatchGradualDifficultyAttributes};
/// use rosu_pp::{Beatmap, catch::CatchGradualDifficulty};
///
/// # /*
/// let map: Beatmap = ...
@@ -37,7 +37,7 @@ use super::{
/// # let map = Beatmap::default();
///
/// 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 attrs2 = iter.next(); // after the second hit object
@@ -47,11 +47,119 @@ use super::{
/// // ...
/// }
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)]
pub struct CatchGradualDifficultyAttributes<'map> {
pub(crate) idx: usize,
pub struct CatchGradualDifficulty<'map> {
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,
hit_objects: CatchObjectIter<'map>,
hit_objects: CatchObjectIter,
movement: Movement,
prev: CatchObject,
half_catcher_width: f64,
@@ -61,9 +169,8 @@ pub struct CatchGradualDifficultyAttributes<'map> {
strain_peak_buf: Vec<f64>,
}
impl<'map> CatchGradualDifficultyAttributes<'map> {
/// Create a new difficulty attributes iterator for osu!catch maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
impl CatchGradualDifficultyInner {
fn new(map: &Beatmap, mods: u32) -> Self {
let map_attributes = map.attributes().mods(mods).build();
let attributes = CatchDifficultyAttributes {
@@ -71,7 +178,7 @@ impl<'map> CatchGradualDifficultyAttributes<'map> {
..Default::default()
};
let hit_objects = CatchObjectIter::new(map, mods, attributes);
let hit_objects = CatchObjectIter::new(mods, attributes);
let half_catcher_width =
(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,
);
}
}
impl Iterator for CatchGradualDifficultyAttributes<'_> {
type Item = CatchDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
let curr = self.hit_objects.next()?;
fn next(&mut self, map: &Beatmap) -> Option<CatchDifficultyAttributes> {
let curr = self.hit_objects.next(map)?;
self.idx += 1;
if self.idx == 1 {
@@ -153,59 +256,14 @@ impl Iterator for CatchGradualDifficultyAttributes<'_> {
*last = self.movement.curr_section_peak;
}
let mut attributes = self.hit_objects.attributes();
attributes.stars =
let stars =
Movement::difficulty_value(&mut self.strain_peak_buf).sqrt() * STAR_SCALING_FACTOR;
Some(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(),
let attrs = CatchDifficultyAttributes {
stars,
..self.hit_objects.attributes()
};
Self {
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
Some(attrs)
}
}
+92 -80
View File
@@ -1,44 +1,15 @@
use crate::{Beatmap, CatchPP};
use super::{CatchGradualDifficultyAttributes, CatchPerformanceAttributes};
/// 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 [`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()
}
}
use super::{
CatchGradualDifficulty, CatchOwnedGradualDifficulty, CatchPerformanceAttributes,
CatchScoreState,
};
/// Gradually calculate the performance attributes of an osu!catch map.
///
/// After each hit object you can call
/// [`process_next_object`](`CatchGradualPerformanceAttributes::process_next_object`)
/// After each hit object you can call [`next`](`CatchGradualPerformance::next`)
/// and it will return the resulting current [`CatchPerformanceAttributes`].
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`CatchGradualPerformanceAttributes::process_next_n_objects`) instead.
/// To process multiple objects at once, use [`nth`](`CatchGradualPerformance::nth`) instead.
///
/// Both methods require a [`CatchScoreState`] that contains the current
/// 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
/// to be processed. Only fruits and droplets do.
///
/// If you only want to calculate difficulty attributes use
/// [`CatchGradualDifficultyAttributes`](crate::catch::CatchGradualDifficultyAttributes) instead.
/// If you only want to calculate difficulty attributes use [`CatchGradualDifficulty`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, catch::{CatchGradualPerformanceAttributes, CatchScoreState}};
/// use rosu_pp::{Beatmap, catch::{CatchGradualPerformance, CatchScoreState}};
///
/// # /*
/// let map: Beatmap = ...
@@ -60,7 +30,7 @@ impl CatchScoreState {
/// # let map = Beatmap::default();
///
/// 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.
///
/// // The first 10 hitresults are only fruits
@@ -69,10 +39,10 @@ impl CatchScoreState {
/// 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
/// }
///
/// // Then comes a miss.
@@ -80,10 +50,10 @@ impl CatchScoreState {
/// // the next few objects because the combo is reset.
/// 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);
/// # */
/// # 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.
/// // Notice how tiny droplets from sliders do not count as hit objects
@@ -92,20 +62,21 @@ impl CatchScoreState {
/// state.n_fruits += 4;
/// state.n_droplets += 6;
/// 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);
/// # */
/// # 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.
/// state.n_fruits += 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
///
/// // Skip to the end
/// # /*
@@ -115,25 +86,26 @@ impl CatchScoreState {
/// state.n_tiny_droplets = ...
/// state.n_tiny_droplet_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);
/// # */
/// # 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,
/// // 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)]
pub struct CatchGradualPerformanceAttributes<'map> {
difficulty: CatchGradualDifficultyAttributes<'map>,
pub struct CatchGradualPerformance<'map> {
difficulty: CatchGradualDifficulty<'map>,
performance: CatchPP<'map>,
}
impl<'map> CatchGradualPerformanceAttributes<'map> {
impl<'map> CatchGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!standard maps.
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);
Self {
@@ -147,41 +119,81 @@ impl<'map> CatchGradualPerformanceAttributes<'map> {
///
/// Note that neither hits nor misses of tiny droplets require
/// to be processed. Only fruits and droplets do.
pub fn process_next_object(
&mut self,
state: CatchScoreState,
) -> Option<CatchPerformanceAttributes> {
self.process_next_n_objects(state, 1)
pub fn next(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
self.nth(state, 0)
}
/// Same as [`process_next_object`](`CatchGradualPerformanceAttributes::process_next_object`)
/// but instead of processing only one object it process `n` many.
/// 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.
///
/// 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.
pub fn process_next_n_objects(
&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?;
/// 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 = self
.performance
.clone()
.attributes(difficulty)
.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();
Some(performance)
+15 -6
View File
@@ -1,15 +1,25 @@
mod catch_object;
mod difficulty_object;
mod fruit_or_juice;
mod gradual_difficulty;
mod gradual_performance;
mod movement;
mod pp;
mod score_state;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use difficulty_object::DifficultyObject;
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};
@@ -73,7 +83,7 @@ impl<'map> CatchStars<'map> {
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`CatchStars`] multiple times with different `passed_objects`, you should use
/// [`CatchGradualDifficultyAttributes`](crate::catch::CatchGradualDifficultyAttributes).
/// [`CatchGradualDifficultyAttributes`](crate::catch::CatchGradualDifficulty).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
@@ -156,7 +166,6 @@ fn calculate_movement(params: CatchStars<'_>) -> (Movement, CatchDifficultyAttri
curve_bufs: CurveBuffers::default(),
last_pos: None,
last_time: 0.0,
map,
ticks: Vec::new(), // using the same buffer for all sliders
with_hr: mods.hr(),
};
@@ -165,7 +174,7 @@ fn calculate_movement(params: CatchStars<'_>) -> (Movement, CatchDifficultyAttri
let mut hit_objects = map
.hit_objects
.iter()
.filter_map(|h| FruitOrJuice::new(h, &mut params))
.filter_map(|h| FruitOrJuice::new(h, &mut params, map))
.flatten()
.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
/// using [`CatchPP`] multiple times with different `passed_objects`, you should use
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformanceAttributes).
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformance).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
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()
}
}
+209 -194
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::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},
Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes,
catch::{CatchGradualDifficulty, CatchGradualPerformance},
mania::{ManiaGradualDifficulty, ManiaGradualPerformance},
osu::{OsuGradualDifficulty, OsuGradualPerformance},
taiko::{TaikoGradualDifficulty, TaikoGradualPerformance},
Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes, ScoreState,
};
/// Gradually calculate the difficulty attributes on maps of any mode.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
/// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`DifficultyAttributes`] will be updated and returned.
///
/// If you want to calculate performance attributes, use
/// [`GradualPerformanceAttributes`](crate::GradualPerformanceAttributes) instead.
/// If you want to calculate performance attributes, use [`GradualPerformance`] instead.
///
/// # Example
///
/// ```no_run
/// use rosu_pp::{Beatmap, GradualDifficultyAttributes};
/// use rosu_pp::{Beatmap, GradualDifficulty};
///
/// # /*
/// let map: Beatmap = ...
@@ -26,7 +31,7 @@ use crate::{
/// # let map = Beatmap::default();
///
/// 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 attrs2 = iter.next(); // after the second hit object
@@ -36,169 +41,138 @@ use crate::{
/// // ...
/// }
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum GradualDifficultyAttributes<'map> {
pub enum GradualDifficulty<'map> {
/// Gradual osu!standard difficulty attributes.
Osu(OsuGradualDifficultyAttributes),
Osu(OsuGradualDifficulty),
/// Gradual osu!taiko difficulty attributes.
Taiko(TaikoGradualDifficultyAttributes),
Taiko(TaikoGradualDifficulty),
/// Gradual osu!catch difficulty attributes.
Catch(CatchGradualDifficultyAttributes<'map>),
Catch(CatchGradualDifficulty<'map>),
/// 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.
#[inline]
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
match map.mode {
GameMode::Osu => Self::Osu(OsuGradualDifficultyAttributes::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualDifficultyAttributes::new(map, mods)),
GameMode::Catch => Self::Catch(CatchGradualDifficultyAttributes::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaGradualDifficultyAttributes::new(map, mods)),
GameMode::Osu => Self::Osu(OsuGradualDifficulty::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualDifficulty::new(map, mods)),
GameMode::Catch => Self::Catch(CatchGradualDifficulty::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaGradualDifficulty::new(map, mods)),
}
}
}
impl Iterator for GradualDifficultyAttributes<'_> {
impl Iterator for GradualDifficulty<'_> {
type Item = DifficultyAttributes;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self {
GradualDifficultyAttributes::Osu(o) => o.next().map(DifficultyAttributes::Osu),
GradualDifficultyAttributes::Taiko(t) => t.next().map(DifficultyAttributes::Taiko),
GradualDifficultyAttributes::Catch(f) => f.next().map(DifficultyAttributes::Catch),
GradualDifficultyAttributes::Mania(m) => m.next().map(DifficultyAttributes::Mania),
Self::Osu(o) => o.next().map(DifficultyAttributes::Osu),
Self::Taiko(t) => t.next().map(DifficultyAttributes::Taiko),
Self::Catch(f) => f.next().map(DifficultyAttributes::Catch),
Self::Mania(m) => m.next().map(DifficultyAttributes::Mania),
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
GradualDifficultyAttributes::Osu(o) => o.size_hint(),
GradualDifficultyAttributes::Taiko(t) => t.size_hint(),
GradualDifficultyAttributes::Catch(f) => f.size_hint(),
GradualDifficultyAttributes::Mania(m) => m.size_hint(),
Self::Osu(o) => o.size_hint(),
Self::Taiko(t) => t.size_hint(),
Self::Catch(f) => f.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
/// the maximum combo so far, what are the current
/// hitresults and what is the current score.
/// Gradually calculate the difficulty attributes on maps of any mode.
///
/// This struct is used for [`GradualPerformanceAttributes`].
#[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,
/// Check [`GradualDifficulty`] for more information. This type does the same
/// but depending on the mode it might clone [`Beatmap`] to avoid being bound to a lifetime.
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum OwnedGradualDifficulty {
/// Gradual osu!standard difficulty attributes.
Osu(OsuGradualDifficulty),
/// Gradual osu!taiko difficulty attributes.
Taiko(TaikoGradualDifficulty),
/// Gradual osu!catch difficulty attributes.
Catch(CatchOwnedGradualDifficulty),
/// Gradual osu!mania difficulty attributes.
Mania(ManiaOwnedGradualDifficulty),
}
impl ScoreState {
/// Create a new empty score state.
impl OwnedGradualDifficulty {
// 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]
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,
pub fn new(map: &Beatmap, mods: u32) -> Self {
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)),
}
}
}
impl From<ScoreState> for TaikoScoreState {
impl Iterator for OwnedGradualDifficulty {
type Item = DifficultyAttributes;
#[inline]
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
n300: state.n300,
n100: state.n100,
n_misses: state.n_misses,
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Osu(o) => o.next().map(DifficultyAttributes::Osu),
Self::Taiko(t) => t.next().map(DifficultyAttributes::Taiko),
Self::Catch(f) => f.next().map(DifficultyAttributes::Catch),
Self::Mania(m) => m.next().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,
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
Self::Osu(o) => o.size_hint(),
Self::Taiko(t) => t.size_hint(),
Self::Catch(f) => f.size_hint(),
Self::Mania(m) => m.size_hint(),
}
}
}
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,
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),
}
}
}
/// Gradually calculate the performance attributes on maps of any mode.
///
/// After each hit object you can call
/// [`process_next_object`](`GradualPerformanceAttributes::process_next_object`)
/// After each hit object you can call [`next`](`GradualPerformance::next`)
/// and it will return the resulting current [`PerformanceAttributes`].
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`GradualPerformanceAttributes::process_next_n_objects`) instead.
/// To process multiple objects at once, use [`nth`](`GradualPerformance::nth`) instead.
///
/// 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.
@@ -206,19 +180,15 @@ impl From<ScoreState> for ManiaScoreState {
/// and should be updated properly.
///
/// Alternatively, you can match on the map's mode yourself and use the gradual
/// performance attribute struct for the corresponding mode, i.e.
/// [`OsuGradualPerformanceAttributes`],
/// [`TaikoGradualPerformanceAttributes`],
/// [`CatchGradualPerformanceAttributes`], or
/// [`ManiaGradualPerformanceAttributes`].
/// performance attribute struct for the corresponding mode, i.e. [`OsuGradualPerformance`],
/// [`TaikoGradualPerformance`], [`CatchGradualPerformance`], or [`ManiaGradualPerformance`].
///
/// If you only want to calculate difficulty attributes use
/// [`GradualDifficultyAttributes`](crate::GradualDifficultyAttributes) instead.
/// If you only want to calculate difficulty attributes use [`GradualDifficulty`] instead.
///
/// # Example
///
/// ```no_run
/// use rosu_pp::{Beatmap, GradualPerformanceAttributes, ScoreState};
/// use rosu_pp::{Beatmap, GradualPerformance, ScoreState};
///
/// # /*
/// let map: Beatmap = ...
@@ -226,123 +196,168 @@ impl From<ScoreState> for ManiaScoreState {
/// # let map = Beatmap::default();
///
/// 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.
///
/// // The first 10 hitresults are 300s and increase the score by 123 each.
/// // The first 10 hitresults are 300s
/// for _ in 0..10 {
/// state.n300 += 1;
/// state.max_combo += 1;
///
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
/// }
///
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
///
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
/// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2;
/// state.n100 += 7;
/// state.n50 += 1;
/// // Don't forget state.n_katu
/// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
///
/// // The `nth` method takes a zero-based value.
/// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1;
/// state.max_combo += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
///
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp());
///
/// // Skip to the end
/// # /*
/// state.max_combo = ...
/// 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,
/// // 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)]
#[allow(clippy::large_enum_variant)]
pub enum GradualPerformanceAttributes<'map> {
/// Gradual osu!standard performance attributes.
Osu(OsuGradualPerformanceAttributes<'map>),
/// Gradual osu!taiko performance attributes.
Taiko(TaikoGradualPerformanceAttributes<'map>),
/// Gradual osu!catch performance attributes.
Catch(CatchGradualPerformanceAttributes<'map>),
/// Gradual osu!mania performance attributes.
Mania(ManiaGradualPerformanceAttributes<'map>),
pub enum GradualPerformance<'map> {
/// Gradual osu!standard performance calculator.
Osu(OsuGradualPerformance<'map>),
/// Gradual osu!taiko performance calculator.
Taiko(TaikoGradualPerformance<'map>),
/// Gradual osu!catch performance calculator.
Catch(CatchGradualPerformance<'map>),
/// Gradual osu!mania performance calculator.
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.
#[inline]
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
match map.mode {
GameMode::Osu => Self::Osu(OsuGradualPerformanceAttributes::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualPerformanceAttributes::new(map, mods)),
GameMode::Catch => Self::Catch(CatchGradualPerformanceAttributes::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaGradualPerformanceAttributes::new(map, mods)),
GameMode::Osu => Self::Osu(OsuGradualPerformance::new(map, mods)),
GameMode::Taiko => Self::Taiko(TaikoGradualPerformance::new(map, mods)),
GameMode::Catch => Self::Catch(CatchGradualPerformance::new(map, mods)),
GameMode::Mania => Self::Mania(ManiaGradualPerformance::new(map, mods)),
}
}
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
#[inline]
pub fn process_next_object(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.process_next_n_objects(state, 1)
pub fn next(&mut self, state: ScoreState) -> Option<PerformanceAttributes> {
self.nth(state, 0)
}
/// Same as [`process_next_object`](`GradualPerformanceAttributes::process_next_object`)
/// 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.
/// Process all remaining hit objects and calculate the final performance attributes.
#[inline]
pub fn process_next_n_objects(
&mut self,
state: ScoreState,
n: usize,
) -> Option<PerformanceAttributes> {
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 {
GradualPerformanceAttributes::Osu(o) => o
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Osu),
GradualPerformanceAttributes::Taiko(t) => t
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Taiko),
GradualPerformanceAttributes::Catch(f) => f
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Catch),
GradualPerformanceAttributes::Mania(m) => m
.process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Mania),
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),
}
}
}
/// Gradually calculate the performance attributes on maps of any mode.
///
/// Check [`GradualPerformance`] for more information. This type does the same
/// 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),
}
}
}
+114 -101
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.
//!
//! Async is supported through features, see below.
@@ -74,111 +86,105 @@
//! println!("PP: {}", result.pp());
//! ```
//!
//! ## Gradual calculation
//! Sometimes you might want to calculate the difficulty of a map or performance of a score after each hit object.
//! 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.
//!
//! Instead, you should use [`GradualDifficultyAttributes`] and [`GradualPerformanceAttributes`]:
//!
//! ```no_run
//! use rosu_pp::{
//! Beatmap, BeatmapExt, GradualPerformanceAttributes, 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 = Beatmap::default();
//!
//! let mods = 8 + 64; // HDDT
//!
//! // If you're only interested in the star rating or other difficulty value,
//! // use `GradualDifficultyAttributes`, either through its function `new`
//! // or through the method `BeatmapExt::gradual_difficulty`.
//! let gradual_difficulty = map.gradual_difficulty(mods);
//!
//! // Since `GradualDifficultyAttributes` implements `Iterator`, you can use
//! // 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.
//! let mut gradual_performance = map.gradual_performance(mods);
//!
//! // The default score state is kinda chunky because it considers all modes.
//! let state = ScoreState {
//! max_combo: 1,
//! n_geki: 0, // only relevant for mania
//! n_katu: 0, // only relevant for mania and ctb
//! n300: 1,
//! 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,
//! 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
//! // using the mode's gradual calculator instead of the general one.
//! // Let's assume it's a taiko map.
//! // Instead of starting off with `BeatmapExt::gradual_performance` one could have
//! // created the struct via `TaikoGradualPerformanceAttributes::new`.
//! let mut gradual_performance = match gradual_performance {
//! GradualPerformanceAttributes::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,
//! 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,
//! None => panic!("the last `process_next_object` already processed the last object"),
//! };
//!
//! println!("PP after the first 11 objects: {}", curr_performance.pp());
//! ```
//!
#![cfg_attr(
feature = "gradual",
doc = r#"
## Gradual calculation
Sometimes you might want to calculate the difficulty of a map or performance of a score after each hit object.
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.
Instead, you should enable the `gradual` feature and use [`GradualDifficulty`] and [`GradualPerformance`]:
```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 = Beatmap::default();
let mods = 8 + 64; // HDDT
// If you're only interested in the star rating or other difficulty values,
// use `GradualDifficulty`.
let gradual_difficulty = GradualDifficulty::new(&map, mods);
// Since `GradualDifficulty` implements `Iterator`, you can use
// 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.
let mut gradual_performance = GradualPerformance::new(&map, mods);
// The default score state is kinda chunky because it considers all modes.
let state = ScoreState {
max_combo: 1,
n_geki: 0, // only relevant for mania
n_katu: 0, // only relevant for mania and ctb
n300: 1,
n100: 0,
n50: 0,
n_misses: 0,
};
// Process the score state after the first object
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
// using the mode's gradual calculator instead of the general one.
// Let's assume it's a taiko map.
// Instead of starting off with `GradualPerformance` one could have
// used `TaikoGradualPerformance`.
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,
n300: 9,
n100: 1,
n_misses: 1,
};
// Process the next 10 objects in one go (`nth` takes a zero-based value).
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());
```
"#
)]
//! ## Features
//!
//! | Flag | Description |
//! |-----|-----|
//! | `default` | Beatmap parsing will be non-async |
//! | Flag | Description |
//! |---------------|-----|
//! | `default` | Beatmap parsing will be non-async |
//! | `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.
pub mod catch;
@@ -198,8 +204,12 @@ pub mod parse;
pub mod beatmap;
pub use beatmap::{Beatmap, BeatmapExt, GameMode};
#[cfg(feature = "gradual")]
mod gradual;
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState};
#[cfg(feature = "gradual")]
pub use gradual::{
GradualDifficulty, GradualPerformance, OwnedGradualDifficulty, OwnedGradualPerformance,
};
mod pp;
pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
@@ -207,6 +217,9 @@ pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
mod stars;
pub use stars::AnyStars;
mod score_state;
pub use score_state::*;
mod curve;
mod mods;
mod util;
+190 -65
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "gradual")]
use std::borrow::Cow;
use crate::{
@@ -16,17 +18,17 @@ use super::{
/// Gradually calculate the difficulty attributes of an osu!mania map.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
/// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned.
///
/// If you want to calculate performance attributes, use
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes) instead.
/// [`ManiaGradualPerformance`](crate::mania::ManiaGradualPerformance) instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, mania::ManiaGradualDifficultyAttributes};
/// use rosu_pp::{Beatmap, mania::ManiaGradualDifficulty};
///
/// # /*
/// let map: Beatmap = ...
@@ -34,7 +36,7 @@ use super::{
/// # let map = Beatmap::default();
///
/// 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 attrs2 = iter.next(); // after the second hit object
@@ -44,21 +46,126 @@ use super::{
/// // ...
/// }
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Clone, Debug)]
pub struct ManiaGradualDifficultyAttributes<'map> {
pub(crate) idx: usize,
pub struct ManiaGradualDifficulty<'map> {
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,
strain: Strain,
diff_objects: Vec<ManiaDifficultyObject>,
diff_objects: Box<[ManiaDifficultyObject]>,
curr_combo: usize,
clock_rate: f64,
}
impl<'map> ManiaGradualDifficultyAttributes<'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);
impl ManiaGradualDifficultyInner {
fn new(map: &Beatmap, is_convert: bool, mods: u32) -> Self {
let total_columns = map.cs.round_even().max(1.0);
let clock_rate = mods.clock_rate();
let strain = Strain::new(total_columns as usize);
@@ -66,30 +173,39 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
let BeatmapHitWindows { od: hit_window, .. } = map
.attributes()
.mods(mods)
.converted(matches!(map, Cow::Owned(_)))
.converted(is_convert)
.clock_rate(clock_rate)
.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 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 => {
return Self {
idx: 0,
map,
hit_window,
strain,
diff_objects: Vec::new(),
diff_objects: Box::from([]),
curr_combo: 0,
clock_rate,
}
}
};
let curr_combo = params.max_combo;
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
let base = ManiaObject::new(h, total_columns, &mut params);
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
@@ -98,52 +214,37 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
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);
debug_assert_eq!(diff_objects.len(), diff_objects.capacity());
Self {
idx: 0,
map,
hit_window,
strain,
diff_objects,
diff_objects: diff_objects.into_boxed_slice(),
curr_combo,
clock_rate,
}
}
fn increment_combo(
h: &HitObject,
diff_obj: &ManiaDifficultyObject,
curr_combo: &mut usize,
clock_rate: f64,
) {
match &h.kind {
HitObjectKind::Circle => *curr_combo += 1,
_ => {
let start_time = diff_obj.start_time * clock_rate;
let end_time = diff_obj.end_time * clock_rate;
let duration = end_time - start_time;
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);
*curr_combo += 1 + (duration / 100.0) as usize;
}
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;
}
}
}
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,
@@ -151,34 +252,58 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
})
}
#[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);
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));
for _ in 0..skip {
let curr = self.diff_objects.get(self.idx)?;
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;
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()
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
}
}
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
#[inline]
fn len(&self) -> usize {
self.diff_objects.len() - self.idx
fn increment_combo(
h: &HitObject,
diff_obj: &ManiaDifficultyObject,
curr_combo: &mut usize,
clock_rate: f64,
) {
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,
_ => *curr_combo += 1 + ((end_time - start_time) / 100.0) as usize,
}
}
+89 -88
View File
@@ -1,74 +1,28 @@
#![cfg(feature = "gradual")]
use crate::{Beatmap, ManiaPP};
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes};
/// Aggregation for a score's current state
/// 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
}
}
use super::{
ManiaGradualDifficulty, ManiaOwnedGradualDifficulty, ManiaPerformanceAttributes,
ManiaScoreState,
};
/// Gradually calculate the performance attributes of an osu!mania map.
///
/// After each hit object you can call
/// [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
/// After each hit object you can call [`next`](`ManiaGradualPerformance::next`)
/// and it will return the resulting current [`ManiaPerformanceAttributes`].
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`ManiaGradualPerformanceAttributes::process_next_n_objects`) instead.
/// To process multiple objects at once, use [`nth`](`ManiaGradualPerformance::nth`) instead.
///
/// Both methods require a play's current score so far.
/// Be sure the given score is adjusted with respect to mods.
///
/// If you only want to calculate difficulty attributes use
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes) instead.
/// [`ManiaGradualDifficulty`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformanceAttributes, ManiaScoreState}};
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformance, ManiaScoreState}};
///
/// # /*
/// let map: Beatmap = ...
@@ -76,7 +30,7 @@ impl ManiaScoreState {
/// # let map = Beatmap::default();
///
/// 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.
///
/// // The first 10 hitresults are 320s
@@ -84,29 +38,30 @@ impl ManiaScoreState {
/// state.n320 += 1;
///
/// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap();
/// let performance = gradual_perf.next(score).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
/// }
///
/// // Then comes a miss.
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap();
/// let performance = gradual_perf.next(score).unwrap();
/// 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.
/// // Notice how all 10 objects will be processed in one go.
/// state.n320 += 3;
/// 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);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
/// # let _ = gradual_perf.nth(state.clone(), 9);
///
/// // Skip to the end
/// # /*
@@ -114,25 +69,26 @@ impl ManiaScoreState {
/// state.n300 = ...
/// state.n100 = ...
/// 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);
/// # */
/// # 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,
/// // 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)]
pub struct ManiaGradualPerformanceAttributes<'map> {
difficulty: ManiaGradualDifficultyAttributes<'map>,
pub struct ManiaGradualPerformance<'map> {
difficulty: ManiaGradualDifficulty<'map>,
performance: ManiaPP<'map>,
}
impl<'map> ManiaGradualPerformanceAttributes<'map> {
impl<'map> ManiaGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!mania maps.
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);
Self {
@@ -143,33 +99,78 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
pub fn process_next_object(
&mut self,
state: ManiaScoreState,
) -> Option<ManiaPerformanceAttributes> {
self.process_next_n_objects(state, 1)
pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
self.nth(state, 0)
}
/// Same as [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
/// but instead of processing only one object it process `n` many.
/// 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.
///
/// 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.
pub fn process_next_n_objects(
&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))?;
/// 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 = self
.performance
.clone()
.attributes(difficulty)
.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();
Some(performance)
+14 -4
View File
@@ -1,15 +1,25 @@
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod mania_object;
mod pp;
mod score_state;
mod skills;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use std::borrow::Cow;
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;
@@ -78,7 +88,7 @@ impl<'map> ManiaStars<'map> {
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`ManiaStars`] multiple times with different `passed_objects`, you should use
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes).
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficulty).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
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
/// using [`ManiaPP`] multiple times with different `passed_objects`, you should use
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes).
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformance).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
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
}
}
+36 -46
View File
@@ -2,6 +2,7 @@ use crate::{
osu::osu_object::{NestedObjectKind, OsuObjectKind},
parse::Pos2,
};
use std::pin::Pin;
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) start_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) dists: Distances,
pub(crate) idx: usize,
@@ -19,7 +20,7 @@ impl<'h> OsuDifficultyObject<'h> {
pub(crate) const MIN_DELTA_TIME: u32 = 25;
pub(crate) fn new(
base: &'h OsuObject,
base: Pin<&'h OsuObject>,
last: &'h OsuObject,
clock_rate: f64,
idx: usize,
@@ -89,36 +90,40 @@ impl Distances {
const MAXIMUM_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 2.4;
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(
base: &mut OsuObject,
base: &mut Pin<&mut OsuObject>,
last: &OsuObject,
last_last: Option<&OsuObject>,
clock_rate: f64,
strain_time: f64,
scaling_factor_: &ScalingFactor,
) -> Self {
let mut this =
if let Some(slider_values) = Self::compute_slider_cursor_pos(base, scaling_factor_) {
let SliderValues {
lazy_travel_dist,
slider,
} = slider_values;
let pos = base.pos();
let stack_offset = base.stack_offset;
let repeat_count = slider.repeat_count();
let mut this = if let OsuObjectKind::Slider(ref mut slider) = base.kind {
let lazy_travel_dist =
Self::compute_slider_travel_dist(pos, stack_offset, slider, scaling_factor_);
Self {
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
travel_dist: (lazy_travel_dist
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
as f64,
travel_time: (base.lazy_travel_time() / clock_rate)
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
lazy_travel_dist,
..Default::default()
}
} else {
Self::default()
};
let repeat_count = slider.repeat_count();
Self {
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
travel_dist: (lazy_travel_dist
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
as f64,
travel_time: (base.lazy_travel_time() / clock_rate)
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
lazy_travel_dist,
..Default::default()
}
} else {
Self::default()
};
// * We don't need to calculate either angle or distance when
// * one of the last->curr objects is a spinner
@@ -196,26 +201,19 @@ impl Distances {
this
}
pub(crate) fn compute_slider_cursor_pos<'h>(
hit_object: &'h mut OsuObject,
pub(crate) fn compute_slider_travel_dist(
pos: Pos2,
stack_offset: Pos2,
slider: &mut OsuSlider,
scaling_factor_: &ScalingFactor,
) -> Option<SliderValues<'h>> {
let pos = hit_object.pos();
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;
) -> f32 {
let mut curr_cursor_pos = pos + stack_offset;
let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64;
let mut lazy_travel_dist: f32 = 0.0;
for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) {
let mut curr_movement =
(curr_movement_obj.pos + hit_object.stack_offset) - curr_cursor_pos;
let mut curr_movement = (curr_movement_obj.pos + stack_offset) - curr_cursor_pos;
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.
@@ -253,18 +251,10 @@ impl Distances {
slider.lazy_end_pos = curr_cursor_pos;
Some(SliderValues {
lazy_travel_dist,
slider,
})
lazy_travel_dist
}
fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 {
hit_object.lazy_end_pos()
}
}
pub(crate) struct SliderValues<'s> {
lazy_travel_dist: f32,
slider: &'s OsuSlider,
}
+133 -87
View File
@@ -1,33 +1,37 @@
#![cfg(feature = "gradual")]
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
mem,
pin::Pin,
};
use crate::{curve::CurveBuffers, Beatmap, Mods};
use crate::{Beatmap, Mods};
use self::osu_objects::OsuObjects;
use super::{
difficulty_object::{Distances, OsuDifficultyObject},
old_stacking,
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
osu_object::{OsuObject, OsuObjectKind},
scaling_factor::ScalingFactor,
skills::{Skill, Skills},
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN,
};
/// Gradually calculate the difficulty attributes of an osu!standard map.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
/// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`OsuDifficultyAttributes`] will be updated and returned.
///
/// If you want to calculate performance attributes, use
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes) instead.
/// [`OsuGradualPerformance`](crate::osu::OsuGradualPerformance) instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, osu::OsuGradualDifficultyAttributes};
/// use rosu_pp::{Beatmap, osu::OsuGradualDifficulty};
///
/// # /*
/// let map: Beatmap = ...
@@ -35,7 +39,7 @@ use super::{
/// # let map = Beatmap::default();
///
/// 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 attrs2 = iter.next(); // after the second hit object
@@ -45,21 +49,27 @@ use super::{
/// // ...
/// }
/// ```
#[derive(Clone)]
pub struct OsuGradualDifficultyAttributes {
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
pub struct OsuGradualDifficulty {
pub(crate) idx: usize,
mods: u32,
attrs: OsuDifficultyAttributes,
// Unused but `diff_objects`' lifetimes secretly depend on it
#[allow(unused)]
hit_objects: Vec<OsuObject>,
diff_objects: Vec<OsuDifficultyObject<'static>>,
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 {
f.debug_struct("OsuGradualDifficultyAttributes")
f.debug_struct("OsuGradualDifficulty")
.field("idx", &self.idx)
.field("attrs", &self.attrs)
.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.
pub fn new(map: &Beatmap, mods: u32) -> Self {
let clock_rate = mods.clock_rate();
@@ -99,38 +109,22 @@ impl OsuGradualDifficultyAttributes {
..Default::default()
};
let mut params = ObjectParameters {
let hit_objects = crate::osu::create_osu_objects(
map,
attrs: &mut attrs,
ticks: Vec::new(),
curve_bufs: CurveBuffers::default(),
};
&mut attrs,
&scaling_factor,
map.hit_objects.len(),
hr,
time_preempt,
);
let mut hit_objects: Vec<_> = map
.hit_objects
.iter()
.map(|h| OsuObject::new(h, &mut params))
.collect();
let mut osu_objects = OsuObjects::new(hit_objects);
attrs.n_circles = 0;
attrs.n_sliders = 0;
attrs.n_spinners = 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(
mods,
scaling_factor.radius,
@@ -139,59 +133,72 @@ impl OsuGradualDifficultyAttributes {
hit_window,
);
let last = match hit_objects_iter.next() {
Some(prev) => prev,
None => {
return Self {
idx: 0,
mods,
attrs,
hit_objects: Vec::new(),
diff_objects: Vec::new(),
skills,
}
}
let mut osu_objects_iter = osu_objects.iter_mut();
let Some(mut last) = osu_objects_iter.next() else {
return Self {
idx: 0,
mods,
attrs,
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;
// 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));
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;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new(
curr,
last,
last_last,
&mut curr,
last.get_ref(),
last_last.map(Pin::get_ref),
clock_rate,
strain_time,
&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);
last_last = Some(last);
last = &*curr;
last = curr;
}
Self {
idx: 0,
mods,
attrs,
diff_objects: extend_lifetime(diff_objects),
hit_objects,
skills,
diff_objects: extend_lifetime(diff_objects),
osu_objects,
_not_clonable: NotClonable,
}
}
@@ -212,22 +219,28 @@ impl OsuGradualDifficultyAttributes {
fn extend_lifetime(
diff_objects: Vec<OsuDifficultyObject<'_>>,
) -> Vec<OsuDifficultyObject<'static>> {
// SAFETY: Owned values of the references will be contained
// in the same struct and hence live just as long as this vec.
// SAFETY: Owned values of the references will be contained in the same struct (same lifetime).
// Also, the only mutable access wraps them in `Pin` to ensure that they won't move.
unsafe { mem::transmute(diff_objects) }
}
impl Iterator for OsuGradualDifficultyAttributes {
impl Iterator for OsuGradualDifficulty {
type Item = OsuDifficultyAttributes;
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
// 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::increment_combo(curr.base.get_ref(), &mut self.attrs);
} else if self.osu_objects.is_empty() {
return None;
}
self.idx += 1;
self.skills.process(curr, &self.diff_objects);
Self::increment_combo(curr.base, &mut self.attrs);
let Skills {
mut aim,
mut aim_no_sliders,
@@ -284,13 +297,15 @@ impl Iterator for OsuGradualDifficultyAttributes {
0.0
};
let mut attrs = self.attrs.clone();
attrs.aim = aim_rating;
attrs.speed = speed_rating;
attrs.flashlight = flashlight_rating;
attrs.slider_factor = slider_factor;
attrs.stars = star_rating;
attrs.speed_note_count = speed_notes;
let attrs = OsuDifficultyAttributes {
aim: aim_rating,
speed: speed_rating,
flashlight: flashlight_rating,
slider_factor,
stars: star_rating,
speed_note_count: speed_notes,
..self.attrs.clone()
};
Some(attrs)
}
@@ -303,24 +318,55 @@ impl Iterator for OsuGradualDifficultyAttributes {
}
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 curr = self.diff_objects.get(self.idx)?;
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 in skip_iter.take(take) {
self.skills.process(curr, &self.diff_objects);
Self::increment_combo(curr.base, &mut self.attrs);
Self::increment_combo(curr.base.get_ref(), &mut self.attrs);
self.idx += 1;
}
self.next()
}
}
impl ExactSizeIterator for OsuGradualDifficultyAttributes {
impl ExactSizeIterator for OsuGradualDifficulty {
#[inline]
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 super::{OsuGradualDifficultyAttributes, OsuPerformanceAttributes};
/// 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
}
}
use super::{OsuGradualDifficulty, OsuPerformanceAttributes, OsuScoreState};
/// Gradually calculate the performance attributes of an osu!standard map.
///
/// After each hit object you can call
/// [`process_next_object`](`OsuGradualPerformanceAttributes::process_next_object`)
/// After each hit object you can call [`next`](`OsuGradualPerformance::next`)
/// and it will return the resulting current [`OsuPerformanceAttributes`].
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`OsuGradualPerformanceAttributes::process_next_n_objects`) instead.
/// To process multiple objects at once, use [`nth`](`OsuGradualPerformance::nth`) instead.
///
/// Both methods require an [`OsuScoreState`] that contains the current
/// hitresults as well as the maximum combo so far.
///
/// If you only want to calculate difficulty attributes use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes) instead.
/// [`OsuGradualDifficulty`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, osu::{OsuGradualPerformanceAttributes, OsuScoreState}};
/// use rosu_pp::{Beatmap, osu::{OsuGradualPerformance, OsuScoreState}};
///
/// # /*
/// let map: Beatmap = ...
@@ -75,7 +27,7 @@ impl OsuScoreState {
/// # let map = Beatmap::default();
///
/// 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.
///
/// // The first 10 hitresults are 300s and there are no sliders for additional combo
@@ -84,10 +36,10 @@ impl OsuScoreState {
/// 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
/// }
///
/// // Then comes a miss.
@@ -95,30 +47,31 @@ impl OsuScoreState {
/// // the next few objects because the combo is reset.
/// 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);
/// # */
/// # 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.
/// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2;
/// state.n100 += 7;
/// 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);
/// # */
/// # 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.
/// state.n300 += 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
///
/// // Skip to the end
/// # /*
@@ -127,25 +80,26 @@ impl OsuScoreState {
/// state.n100 = ...
/// state.n50 = ...
/// 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);
/// # */
/// # 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,
/// // 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)]
pub struct OsuGradualPerformanceAttributes<'map> {
difficulty: OsuGradualDifficultyAttributes,
pub struct OsuGradualPerformance<'map> {
difficulty: OsuGradualDifficulty,
performance: OsuPP<'map>,
}
impl<'map> OsuGradualPerformanceAttributes<'map> {
impl<'map> OsuGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!standard maps.
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);
Self {
@@ -156,33 +110,83 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the
/// performance attributes for the resulting score state.
pub fn process_next_object(
&mut self,
state: OsuScoreState,
) -> Option<OsuPerformanceAttributes> {
self.process_next_n_objects(state, 1)
pub fn next(&mut self, state: OsuScoreState) -> Option<OsuPerformanceAttributes> {
self.nth(state, 0)
}
/// Same as [`process_next_object`](`OsuGradualPerformanceAttributes::process_next_object`)
/// but instead of processing only one object it process `n` many.
/// 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.
///
/// 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.
pub fn process_next_n_objects(
&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))?;
/// 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 = self
.performance
.clone()
.attributes(difficulty)
.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();
Some(performance)
+35 -19
View File
@@ -1,19 +1,30 @@
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod osu_object;
mod pp;
mod scaling_factor;
mod score_state;
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 std::pin::Pin;
use self::{
difficulty_object::{Distances, OsuDifficultyObject},
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;
@@ -91,7 +102,7 @@ impl<'map> OsuStars<'map> {
///
/// If you want to calculate the difficulty after every few objects, instead of
/// using [`OsuStars`] multiple times with different `passed_objects`, you should use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes).
/// [`OsuGradualDifficulty`].
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
@@ -271,7 +282,7 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
let mut hit_objects =
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(
mods,
@@ -281,39 +292,45 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
hit_window,
);
let last = match hit_objects_iter.next() {
Some(prev) => prev,
None => return (skills, attrs),
let Some(mut last) = hit_objects_iter.next() else {
return (skills, attrs);
};
let mut last_last = None;
// 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());
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;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new(
curr,
last,
last_last,
&mut curr,
last.get_ref(),
last_last.map(Pin::get_ref),
clock_rate,
strain_time,
&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);
last_last = Some(last);
last = &*curr;
last = curr;
}
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) {
let mut extended_start_idx = 0;
let extended_end_idx = match hit_objects.len().checked_sub(1) {
Some(idx) => idx,
None => return,
let Some(extended_end_idx) = hit_objects.len().checked_sub(1) else {
return;
};
// 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
/// using [`OsuPP`] multiple times with different `passed_objects`, you should use
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformanceAttributes).
/// [`OsuGradualPerformanceAttributes`](crate::osu::OsuGradualPerformance).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
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.
///
/// 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]`).
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
parse_body!(input)
@@ -967,7 +967,7 @@ impl Beatmap {
/// 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),
/// passing `&file` to [`parse`](Beatmap::parse) should be preferred.
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
/// using [`AnyPP`] multiple times with different `passed_objects`, you should use
/// [`GradualPerformanceAttributes`](crate::GradualPerformanceAttributes).
/// [`GradualPerformanceAttributes`](crate::GradualPerformance).
#[inline]
pub fn passed_objects(self, passed_objects: usize) -> 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
/// using [`AnyStars`] multiple times with different `passed_objects`, you should use
/// [`GradualDifficultyAttributes`](crate::GradualDifficultyAttributes).
/// [`GradualDifficultyAttributes`](crate::GradualDifficulty).
#[inline]
pub fn passed_objects(self, passed_objects: usize) -> Self {
match self {
+1 -1
View File
@@ -154,7 +154,7 @@ impl ColourDifficultyPreprocessor {
mut data: VecDeque<Rc<RefCell<AlternatingMonoPattern>>>,
) -> Vec<Rc<RefCell<RepeatingHitPatterns>>> {
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() {
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) {
let mut other = match self.prev.as_ref().and_then(Weak::upgrade) {
Some(prev) => prev,
None => return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1,
let Some(mut other) = self.prev.as_ref().and_then(Weak::upgrade) else {
return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1;
};
let mut interval = 1;
while interval < Self::MAX_REPETITION_INTERVAL {
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) {
Some(prev) => prev,
None => break,
let Some(next) = other.borrow().prev.as_ref().and_then(Weak::upgrade) else {
break;
};
// gotta love NLL...
+1
View File
@@ -111,6 +111,7 @@ fn closest_rhythm(
.unwrap()
}
// TODO: Remove Default impl and replace with `with_capacity` method for efficiency
#[derive(Clone, Debug, Default)]
pub(crate) struct ObjectLists {
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject>>>,
+71 -59
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "gradual")]
use std::{borrow::Cow, cell::RefCell, rc::Rc, vec::IntoIter};
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.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
/// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`], the map's next hit object will
/// be processed and the [`TaikoDifficultyAttributes`] will be updated and returned.
///
/// If you want to calculate performance attributes, use
/// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformanceAttributes) instead.
/// [`TaikoGradualPerformance`](crate::taiko::TaikoGradualPerformance) instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, taiko::TaikoGradualDifficultyAttributes};
/// use rosu_pp::{Beatmap, taiko::TaikoGradualDifficulty};
///
/// # /*
/// let map: Beatmap = ...
@@ -30,7 +32,7 @@ use super::{
/// # let map = Beatmap::default();
///
/// 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 attrs2 = iter.next(); // after the second hit object
@@ -40,18 +42,19 @@ use super::{
/// // ...
/// }
/// ```
#[derive(Clone, Debug)]
pub struct TaikoGradualDifficultyAttributes {
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
pub struct TaikoGradualDifficulty {
pub(crate) idx: usize,
attrs: TaikoDifficultyAttributes,
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
diff_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
lists: ObjectLists,
peaks: Peaks,
total_hits: usize,
is_convert: bool,
pub(crate) started: bool,
}
impl TaikoGradualDifficultyAttributes {
impl TaikoGradualDifficulty {
/// Create a new difficulty attributes iterator for osu!taiko maps.
pub fn new(map: &Beatmap, mods: u32) -> Self {
let map = map.convert_mode(GameMode::Taiko);
@@ -77,89 +80,92 @@ impl TaikoGradualDifficultyAttributes {
if map.hit_objects.len() < 2 {
return Self {
hit_objects: Vec::new().into_iter(),
idx: 0,
diff_objects: Vec::new().into_iter(),
lists: ObjectLists::default(),
peaks,
attrs,
total_hits: 0,
is_convert,
started: false,
};
}
attrs.max_combo += map.hit_objects[0].is_circle() as usize;
attrs.max_combo += map.hit_objects[1].is_circle() as usize;
let mut total_hits = attrs.max_combo;
let mut diff_objects = ObjectLists::default();
let mut diff_objects = map
.taiko_objects()
map.taiko_objects()
.skip(2)
.zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter())
.enumerate()
.fold(
ObjectLists::default(),
|mut lists, (idx, (((base, base_start_time), last), last_last))| {
total_hits += base.is_hit as usize;
.for_each(|(idx, (((base, base_start_time), last), last_last))| {
total_hits += base.is_hit as usize;
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&lists,
idx,
);
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&diff_objects,
idx,
);
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => diff_objects.centres.push(idx),
MonoIndex::Rim(_) => diff_objects.rims.push(idx),
MonoIndex::None => {}
}
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
if diff_obj.note_idx.is_some() {
diff_objects.notes.push(idx);
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
lists
},
);
diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
});
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
Self {
hit_objects: diff_objects.all.clone().into_iter(),
idx: 0,
diff_objects: diff_objects.all.clone().into_iter(),
lists: diff_objects,
peaks,
attrs,
total_hits,
is_convert,
started: false,
}
}
}
impl Iterator for TaikoGradualDifficultyAttributes {
impl Iterator for TaikoGradualDifficulty {
type Item = TaikoDifficultyAttributes;
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 {
let curr = self.diff_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
loop {
let curr = self.hit_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
if borrowed.base.is_hit {
self.attrs.max_combo += 1;
if borrowed.base.is_hit {
self.attrs.max_combo += 1;
break;
break;
}
}
} else if self.lists.all.is_empty() {
return None;
}
self.idx += 1;
let PeaksDifficultyValues {
mut colour_rating,
mut rhythm_rating,
@@ -203,18 +209,24 @@ impl Iterator for TaikoGradualDifficultyAttributes {
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n
.min(self.total_hits - self.attrs.max_combo)
.saturating_sub(1);
let mut take = n.min(self.len().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 {
let curr = self.hit_objects.next()?;
let curr = self.diff_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
if borrowed.base.is_hit {
self.attrs.max_combo += 1;
self.idx += 1;
break;
}
@@ -225,9 +237,9 @@ impl Iterator for TaikoGradualDifficultyAttributes {
}
}
impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
impl ExactSizeIterator for TaikoGradualDifficulty {
#[inline]
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
/// 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
}
}
use super::{TaikoGradualDifficulty, TaikoPerformanceAttributes};
/// Gradually calculate the performance attributes of an osu!taiko map.
///
/// After each hit object you can call
/// [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
/// After each hit object you can call [`next`](`TaikoGradualPerformance::next`)
/// and it will return the resulting current [`TaikoPerformanceAttributes`].
/// To process multiple objects at once, use
/// [`process_next_n_objects`](`TaikoGradualPerformanceAttributes::process_next_n_objects`) instead.
/// To process multiple objects at once, use [`nth`](`TaikoGradualPerformance::nth`) instead.
///
/// Both methods require a [`TaikoScoreState`] that contains the current
/// hitresults as well as the maximum combo so far.
///
/// If you only want to calculate difficulty attributes use
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficultyAttributes) instead.
/// [`TaikoGradualDifficulty`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, taiko::{TaikoGradualPerformanceAttributes, TaikoScoreState}};
/// use rosu_pp::{Beatmap, taiko::{TaikoGradualPerformance, TaikoScoreState}};
///
/// # /*
/// let map: Beatmap = ...
@@ -73,7 +28,7 @@ impl TaikoScoreState {
/// # let map = Beatmap::default();
///
/// 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.
///
/// // The first 10 hitresults are 300s
@@ -82,10 +37,10 @@ impl TaikoScoreState {
/// 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
/// }
///
/// // Then comes a miss.
@@ -93,29 +48,30 @@ impl TaikoScoreState {
/// // the next few objects because the combo is reset.
/// 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);
/// # */
/// # 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.
/// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 3;
/// 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);
/// # */
/// # 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.
/// state.n300 += 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
///
/// // Skip to the end
/// # /*
@@ -123,25 +79,26 @@ impl TaikoScoreState {
/// state.n300 = ...
/// state.n100 = ...
/// 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);
/// # */
/// # 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,
/// // 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)]
pub struct TaikoGradualPerformanceAttributes<'map> {
difficulty: TaikoGradualDifficultyAttributes,
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
#[derive(Debug)]
pub struct TaikoGradualPerformance<'map> {
difficulty: TaikoGradualDifficulty,
performance: TaikoPP<'map>,
}
impl<'map> TaikoGradualPerformanceAttributes<'map> {
impl<'map> TaikoGradualPerformance<'map> {
/// Create a new gradual performance calculator for osu!taiko maps.
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);
Self {
@@ -152,34 +109,88 @@ impl<'map> TaikoGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the
/// performance attributes for the resulting score.
pub fn process_next_object(
&mut self,
state: TaikoScoreState,
) -> Option<TaikoPerformanceAttributes> {
self.process_next_n_objects(state, 1)
pub fn next(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
self.nth(state, 0)
}
/// Same as [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
/// but instead of processing only one object it process `n` many.
/// 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.
///
/// 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.
pub fn process_next_n_objects(
&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;
/// 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 = self
.performance
.clone()
.attributes(difficulty)
.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();
Some(performance)
+35 -32
View File
@@ -1,17 +1,24 @@
mod colours;
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod pp;
mod rim;
mod score_state;
mod skills;
mod taiko_object;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
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::{
gradual_difficulty::*, gradual_performance::*, pp::*,
taiko_object::TaikoObjectPub as TaikoObject,
gradual_difficulty::TaikoGradualDifficulty,
gradual_performance::{TaikoGradualPerformance, TaikoOwnedGradualPerformance},
};
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
/// using [`TaikoStars`] multiple times with different `passed_objects`, you should use
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficultyAttributes).
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficulty).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
@@ -226,8 +233,9 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
let mut peaks = Peaks::new();
let mut max_combo = 0;
let mut diff_objects = map
.taiko_objects()
let mut diff_objects = ObjectLists::default();
map.taiko_objects()
.take_while(|(h, _)| {
if h.is_hit {
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())
.enumerate()
.fold(
ObjectLists::default(),
|mut lists, (idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&lists,
idx,
);
.for_each(|(idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&diff_objects,
idx,
);
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => diff_objects.centres.push(idx),
MonoIndex::Rim(_) => diff_objects.rims.push(idx),
MonoIndex::None => {}
}
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
if diff_obj.note_idx.is_some() {
diff_objects.notes.push(idx);
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
lists
},
);
diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
});
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
/// using [`TaikoPP`] multiple times with different `passed_objects`, you should use
/// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformanceAttributes).
/// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformance).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
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::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
catch::{CatchGradualDifficulty, CatchGradualPerformance, CatchScoreState},
Beatmap, CatchPP, CatchStars,
};
@@ -12,7 +15,7 @@ mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0);
let mut attributes = CatchGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none());
}
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchStars::new(&map).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, 0)
let iter_end = CatchGradualDifficulty::new(&map, 0)
.last()
.expect("empty iter");
@@ -32,13 +35,13 @@ fn iter_end_eq_regular() {
#[test]
fn correct_empty() {
let map = test_map!(Catch);
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual = CatchGradualPerformance::new(&map, 0);
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!(gradual.process_next_object(state).is_none());
assert!(gradual.next(state).is_none());
}
#[test]
@@ -46,18 +49,18 @@ fn next_and_next_n() {
let map = test_map!(Catch);
let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual1 = CatchGradualPerformance::new(&map, 0);
let mut gradual2 = CatchGradualPerformance::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
let _ = gradual2.next(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
}
let state = CatchScoreState {
@@ -69,8 +72,8 @@ fn next_and_next_n() {
n_misses: 0,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
let next = gradual1.next(state.clone());
let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next);
}
@@ -80,7 +83,7 @@ fn gradual_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchPP::new(&map).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual = CatchGradualPerformance::new(&map, 0);
let state = CatchScoreState {
max_combo: 730,
@@ -91,7 +94,7 @@ fn gradual_end_eq_regular() {
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);
}
@@ -102,7 +105,7 @@ fn gradual_eq_regular_passed() {
let n = 100;
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 {
max_combo: 101,
@@ -113,7 +116,7 @@ fn gradual_eq_regular_passed() {
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);
}
+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::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
mania::{ManiaGradualDifficulty, ManiaGradualPerformance, ManiaScoreState},
Beatmap, ManiaPP, ManiaStars,
};
@@ -12,7 +15,7 @@ mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
let mut attributes = ManiaGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none());
}
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaStars::new(&map).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, 0)
let iter_end = ManiaGradualDifficulty::new(&map, 0)
.last()
.expect("empty iter");
@@ -32,54 +35,40 @@ fn iter_end_eq_regular() {
#[test]
fn correct_empty() {
let map = test_map!(Mania);
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual = ManiaGradualPerformance::new(&map, 0);
let state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let state = ManiaScoreState::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!(gradual.process_next_object(state).is_none());
assert!(gradual.next(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Mania);
let mut state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut state = ManiaScoreState::default();
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual1 = ManiaGradualPerformance::new(&map, 0);
let mut gradual2 = ManiaGradualPerformance::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
let _ = gradual2.next(state.clone());
state.n320 += 1;
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
state.n320 += 1;
}
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
let next = gradual1.next(state.clone());
let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next);
}
@@ -89,18 +78,14 @@ fn gradual_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaPP::new(&map).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual = ManiaGradualPerformance::new(&map, 0);
let state = ManiaScoreState {
n320: 3238,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
n320: map.hit_objects.len(),
..Default::default()
};
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);
}
@@ -112,11 +97,7 @@ fn gradual_eq_regular_passed() {
let state = ManiaScoreState {
n320: 100,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
..Default::default()
};
let regular = ManiaPP::new(&map)
@@ -124,8 +105,9 @@ fn gradual_eq_regular_passed() {
.state(state.clone())
.calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let gradual = gradual.process_next_n_objects(state, n).unwrap();
let gradual = ManiaGradualPerformance::new(&map, 0)
.nth(state, n - 1)
.unwrap();
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::{
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
osu::{OsuGradualDifficulty, OsuGradualPerformance, OsuScoreState},
Beatmap, OsuPP, OsuStars,
};
@@ -12,7 +15,7 @@ mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
let mut attributes = OsuGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none());
}
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuStars::new(&map).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, 0)
let iter_end = OsuGradualDifficulty::new(&map, 0)
.last()
.expect("empty iter");
@@ -32,13 +35,13 @@ fn iter_end_eq_regular() {
#[test]
fn correct_empty() {
let map = test_map!(Osu);
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual = OsuGradualPerformance::new(&map, 0);
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!(gradual.process_next_object(state).is_none());
assert!(gradual.next(state).is_none());
}
#[test]
@@ -46,18 +49,18 @@ fn next_and_next_n() {
let map = test_map!(Osu);
let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual1 = OsuGradualPerformance::new(&map, 0);
let mut gradual2 = OsuGradualPerformance::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
let _ = gradual2.next(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
}
let state = OsuScoreState {
@@ -68,8 +71,8 @@ fn next_and_next_n() {
n_misses: 2,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
let next = gradual1.next(state.clone());
let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next);
}
@@ -78,7 +81,7 @@ fn next_and_next_n() {
fn gradual_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuPP::new(&map).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual = OsuGradualPerformance::new(&map, 0);
let state = OsuScoreState {
max_combo: 909,
@@ -88,7 +91,7 @@ fn gradual_end_eq_regular() {
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);
}
@@ -99,7 +102,7 @@ fn gradual_eq_regular_passed() {
let n = 100;
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 {
max_combo: 122,
@@ -109,7 +112,7 @@ fn gradual_eq_regular_passed() {
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);
}
+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::{
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},
taiko::{TaikoGradualDifficulty, TaikoGradualPerformance, TaikoScoreState},
Beatmap, TaikoPP, TaikoStars,
};
@@ -12,7 +15,7 @@ mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attrs = TaikoGradualDifficultyAttributes::new(&map, 0);
let mut attrs = TaikoGradualDifficulty::new(&map, 0);
assert!(attrs.next().is_none());
}
@@ -22,7 +25,7 @@ fn iter_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoStars::new(&map).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, 0)
let iter_end = TaikoGradualDifficulty::new(&map, 0)
.last()
.expect("empty iter");
@@ -32,13 +35,13 @@ fn iter_end_eq_regular() {
#[test]
fn correct_empty() {
let map = test_map!(Taiko);
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let mut gradual = TaikoGradualPerformance::new(&map, 0);
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!(gradual.process_next_object(state).is_none());
assert!(gradual.next(state).is_none());
}
#[test]
@@ -46,18 +49,18 @@ fn next_and_next_n() {
let map = test_map!(Taiko);
let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = TaikoGradualPerformanceAttributes::new(&map, 0);
let mut gradual1 = TaikoGradualPerformance::new(&map, 0);
let mut gradual2 = TaikoGradualPerformance::new(&map, 0);
for _ in 0..50 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
let _ = gradual2.next(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
}
let state = TaikoScoreState {
@@ -67,8 +70,8 @@ fn next_and_next_n() {
n_misses: 6,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
let next = gradual1.next(state.clone());
let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next);
}
@@ -77,7 +80,7 @@ fn next_and_next_n() {
fn gradual_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoPP::new(&map).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let mut gradual = TaikoGradualPerformance::new(&map, 0);
let state = TaikoScoreState {
max_combo: 289,
@@ -86,7 +89,7 @@ fn gradual_end_eq_regular() {
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);
}
@@ -97,7 +100,7 @@ fn gradual_eq_regular_passed() {
let n = 250;
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 {
max_combo: 250,
@@ -106,7 +109,7 @@ fn gradual_eq_regular_passed() {
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);
}