Merge pull request #24 from MaxOhn/gradual-rework
refactor!: overhauled gradual calculation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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
@@ -23,4 +23,4 @@ impl From<u8> for GameMode {
|
||||
_ => Self::Osu,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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>>>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user