refactor!: overhauled gradual calc for osu

This commit is contained in:
MaxOhn
2023-11-07 16:26:50 +01:00
parent 5fd29c2ab4
commit 79e19d5a9b
15 changed files with 402 additions and 375 deletions
+2 -1
View File
@@ -11,9 +11,10 @@ description = "osu! difficulty and pp calculation for all modes"
keywords = ["osu", "pp", "stars", "async"] keywords = ["osu", "pp", "stars", "async"]
[features] [features]
default = [] default = ["gradual"]
async_std = ["async-std"] async_std = ["async-std"]
async_tokio = ["tokio"] async_tokio = ["tokio"]
gradual = []
[dependencies] [dependencies]
async-std = { version = "1.9", optional = true } async-std = { version = "1.9", optional = true }
+2 -24
View File
@@ -8,9 +8,8 @@ use crate::{
osu::{OsuDifficultyAttributes, OsuObject, ScalingFactor}, osu::{OsuDifficultyAttributes, OsuObject, ScalingFactor},
taiko::{IntoTaikoObjectIter, TaikoObject}, taiko::{IntoTaikoObjectIter, TaikoObject},
util::FloatExt, util::FloatExt,
AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, GradualDifficultyAttributes, AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, ManiaPP, ManiaStars, Mods, OsuPP,
GradualPerformanceAttributes, ManiaPP, ManiaStars, Mods, OsuPP, OsuStars, OsuStars, PerformanceAttributes, Strains, TaikoPP, TaikoStars,
PerformanceAttributes, Strains, TaikoPP, TaikoStars,
}; };
/// Provides some additional methods on [`Beatmap`]. /// Provides some additional methods on [`Beatmap`].
@@ -35,17 +34,6 @@ pub trait BeatmapExt {
/// Suitable to plot the difficulty of a map over time. /// Suitable to plot the difficulty of a map over time.
fn strains(&self, mods: u32) -> Strains; fn strains(&self, mods: u32) -> Strains;
/// Return an iterator that gives you the [`DifficultyAttributes`](crate::DifficultyAttributes) after each hit object.
///
/// Suitable to efficiently get the map's star rating after multiple different locations.
fn gradual_difficulty(&self, mods: u32) -> GradualDifficultyAttributes<'_>;
/// Return a struct that gives you the [`PerformanceAttributes`] after every (few) hit object(s).
///
/// Suitable to efficiently get a score's performance after multiple different locations,
/// i.e. live update a score's pp.
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_>;
/// Process each [`HitObject`](crate::parse::HitObject) into a an osu!-specific [`OsuObject`], /// Process each [`HitObject`](crate::parse::HitObject) into a an osu!-specific [`OsuObject`],
/// just like the difficulty calculation does. /// just like the difficulty calculation does.
fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject>; fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject>;
@@ -112,16 +100,6 @@ impl BeatmapExt for Beatmap {
} }
} }
#[inline]
fn gradual_difficulty(&self, mods: u32) -> GradualDifficultyAttributes<'_> {
GradualDifficultyAttributes::new(self, mods)
}
#[inline]
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_> {
GradualPerformanceAttributes::new(self, mods)
}
fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject> { fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject> {
let attrs = self.attributes().mods(mods).build(); let attrs = self.attributes().mods(mods).build();
let scaling_factor = ScalingFactor::new(attrs.cs); let scaling_factor = ScalingFactor::new(attrs.cs);
+8 -115
View File
@@ -1,9 +1,9 @@
use crate::{ use crate::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState}, catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes},
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState}, mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes},
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState}, osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes},
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState}, taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes},
Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes, Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes, ScoreState,
}; };
/// Gradually calculate the difficulty attributes on maps of any mode. /// Gradually calculate the difficulty attributes on maps of any mode.
@@ -37,7 +37,6 @@ use crate::{
/// } /// }
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum GradualDifficultyAttributes<'map> { pub enum GradualDifficultyAttributes<'map> {
/// Gradual osu!standard difficulty attributes. /// Gradual osu!standard difficulty attributes.
Osu(OsuGradualDifficultyAttributes), Osu(OsuGradualDifficultyAttributes),
@@ -86,112 +85,6 @@ impl Iterator for GradualDifficultyAttributes<'_> {
} }
} }
/// 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 [`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,
}
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,
}
}
}
/// Gradually calculate the performance attributes on maps of any mode. /// Gradually calculate the performance attributes on maps of any mode.
/// ///
/// After each hit object you can call /// After each hit object you can call
@@ -331,9 +224,9 @@ impl<'map> GradualPerformanceAttributes<'map> {
n: usize, n: usize,
) -> Option<PerformanceAttributes> { ) -> Option<PerformanceAttributes> {
match self { match self {
GradualPerformanceAttributes::Osu(o) => o GradualPerformanceAttributes::Osu(o) => {
.process_next_n_objects(state.into(), n) o.nth(state.into(), n).map(PerformanceAttributes::Osu)
.map(PerformanceAttributes::Osu), }
GradualPerformanceAttributes::Taiko(t) => t GradualPerformanceAttributes::Taiko(t) => t
.process_next_n_objects(state.into(), n) .process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Taiko), .map(PerformanceAttributes::Taiko),
+6 -1
View File
@@ -198,8 +198,10 @@ pub mod parse;
pub mod beatmap; pub mod beatmap;
pub use beatmap::{Beatmap, BeatmapExt, GameMode}; pub use beatmap::{Beatmap, BeatmapExt, GameMode};
#[cfg(feature = "gradual")]
mod gradual; mod gradual;
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes, ScoreState}; #[cfg(feature = "gradual")]
pub use gradual::{GradualDifficultyAttributes, GradualPerformanceAttributes};
mod pp; mod pp;
pub use pp::{AnyPP, AttributeProvider, HitResultPriority}; pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
@@ -207,6 +209,9 @@ pub use pp::{AnyPP, AttributeProvider, HitResultPriority};
mod stars; mod stars;
pub use stars::AnyStars; pub use stars::AnyStars;
mod score_state;
pub use score_state::*;
mod curve; mod curve;
mod mods; mod mods;
mod util; mod util;
+36 -46
View File
@@ -2,6 +2,7 @@ use crate::{
osu::osu_object::{NestedObjectKind, OsuObjectKind}, osu::osu_object::{NestedObjectKind, OsuObjectKind},
parse::Pos2, parse::Pos2,
}; };
use std::pin::Pin;
use super::{osu_object::OsuSlider, OsuObject, ScalingFactor}; use super::{osu_object::OsuSlider, OsuObject, ScalingFactor};
@@ -9,7 +10,7 @@ use super::{osu_object::OsuSlider, OsuObject, ScalingFactor};
pub(crate) struct OsuDifficultyObject<'h> { pub(crate) struct OsuDifficultyObject<'h> {
pub(crate) start_time: f64, pub(crate) start_time: f64,
pub(crate) delta_time: f64, pub(crate) delta_time: f64,
pub(crate) base: &'h OsuObject, pub(crate) base: Pin<&'h OsuObject>,
pub(crate) strain_time: f64, pub(crate) strain_time: f64,
pub(crate) dists: Distances, pub(crate) dists: Distances,
pub(crate) idx: usize, pub(crate) idx: usize,
@@ -19,7 +20,7 @@ impl<'h> OsuDifficultyObject<'h> {
pub(crate) const MIN_DELTA_TIME: u32 = 25; pub(crate) const MIN_DELTA_TIME: u32 = 25;
pub(crate) fn new( pub(crate) fn new(
base: &'h OsuObject, base: Pin<&'h OsuObject>,
last: &'h OsuObject, last: &'h OsuObject,
clock_rate: f64, clock_rate: f64,
idx: usize, idx: usize,
@@ -89,36 +90,40 @@ impl Distances {
const MAXIMUM_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 2.4; const MAXIMUM_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 2.4;
const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 1.8; const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALISED_RADIUS * 1.8;
/// Create a new instance of [`Distances`].
///
/// By taking in [`Pin<&mut OsuObject>`](Pin), we imply that the argument will be
/// modified but it won't be moved.
pub(crate) fn new( pub(crate) fn new(
base: &mut OsuObject, base: &mut Pin<&mut OsuObject>,
last: &OsuObject, last: &OsuObject,
last_last: Option<&OsuObject>, last_last: Option<&OsuObject>,
clock_rate: f64, clock_rate: f64,
strain_time: f64, strain_time: f64,
scaling_factor_: &ScalingFactor, scaling_factor_: &ScalingFactor,
) -> Self { ) -> Self {
let mut this = let pos = base.pos();
if let Some(slider_values) = Self::compute_slider_cursor_pos(base, scaling_factor_) { let stack_offset = base.stack_offset;
let SliderValues {
lazy_travel_dist,
slider,
} = slider_values;
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 { let repeat_count = slider.repeat_count();
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
travel_dist: (lazy_travel_dist Self {
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32) // * Bonus for repeat sliders until a better per nested object strain system can be achieved.
as f64, travel_dist: (lazy_travel_dist
travel_time: (base.lazy_travel_time() / clock_rate) * (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64), as f64,
lazy_travel_dist, travel_time: (base.lazy_travel_time() / clock_rate)
..Default::default() .max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
} lazy_travel_dist,
} else { ..Default::default()
Self::default() }
}; } else {
Self::default()
};
// * We don't need to calculate either angle or distance when // * We don't need to calculate either angle or distance when
// * one of the last->curr objects is a spinner // * one of the last->curr objects is a spinner
@@ -196,26 +201,19 @@ impl Distances {
this this
} }
pub(crate) fn compute_slider_cursor_pos<'h>( pub(crate) fn compute_slider_travel_dist(
hit_object: &'h mut OsuObject, pos: Pos2,
stack_offset: Pos2,
slider: &mut OsuSlider,
scaling_factor_: &ScalingFactor, scaling_factor_: &ScalingFactor,
) -> Option<SliderValues<'h>> { ) -> f32 {
let pos = hit_object.pos(); let mut curr_cursor_pos = pos + stack_offset;
let slider = if let OsuObjectKind::Slider(slider) = &mut hit_object.kind {
slider
} else {
return None;
};
let mut curr_cursor_pos = pos + hit_object.stack_offset;
let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64; let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64;
let mut lazy_travel_dist: f32 = 0.0; let mut lazy_travel_dist: f32 = 0.0;
for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) { for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) {
let mut curr_movement = let mut curr_movement = (curr_movement_obj.pos + stack_offset) - curr_cursor_pos;
(curr_movement_obj.pos + hit_object.stack_offset) - curr_cursor_pos;
let mut curr_movement_len = scaling_factor * curr_movement.length() as f64; let mut curr_movement_len = scaling_factor * curr_movement.length() as f64;
// * Amount of movement required so that the cursor position needs to be updated. // * Amount of movement required so that the cursor position needs to be updated.
@@ -253,18 +251,10 @@ impl Distances {
slider.lazy_end_pos = curr_cursor_pos; slider.lazy_end_pos = curr_cursor_pos;
Some(SliderValues { lazy_travel_dist
lazy_travel_dist,
slider,
})
} }
fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 { fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 {
hit_object.lazy_end_pos() hit_object.lazy_end_pos()
} }
} }
pub(crate) struct SliderValues<'s> {
lazy_travel_dist: f32,
slider: &'s OsuSlider,
}
+112 -74
View File
@@ -1,17 +1,21 @@
#![cfg(feature = "gradual")]
use std::{ use std::{
fmt::{Debug, Formatter, Result as FmtResult}, fmt::{Debug, Formatter, Result as FmtResult},
mem, mem,
pin::Pin,
}; };
use crate::{curve::CurveBuffers, Beatmap, Mods}; use crate::{Beatmap, Mods};
use self::osu_objects::OsuObjects;
use super::{ use super::{
difficulty_object::{Distances, OsuDifficultyObject}, difficulty_object::{Distances, OsuDifficultyObject},
old_stacking, osu_object::{OsuObject, OsuObjectKind},
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
scaling_factor::ScalingFactor, scaling_factor::ScalingFactor,
skills::{Skill, Skills}, skills::{Skill, Skills},
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN, PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN,
}; };
@@ -45,15 +49,15 @@ use super::{
/// // ... /// // ...
/// } /// }
/// ``` /// ```
#[derive(Clone)]
pub struct OsuGradualDifficultyAttributes { pub struct OsuGradualDifficultyAttributes {
pub(crate) idx: usize, pub(crate) idx: usize,
mods: u32, mods: u32,
attrs: OsuDifficultyAttributes, attrs: OsuDifficultyAttributes,
// Unused but `diff_objects`' lifetimes secretly depend on it // Lifetimes actually depend on `_osu_objects` so this type is self-referential.
#[allow(unused)] // This field must be treated with great caution, moving `_osu_objects` will immediately
hit_objects: Vec<OsuObject>, // invalidate `diff_objects`.
diff_objects: Vec<OsuDifficultyObject<'static>>, diff_objects: Vec<OsuDifficultyObject<'static>>,
osu_objects: OsuObjects,
skills: Skills, skills: Skills,
} }
@@ -99,38 +103,22 @@ impl OsuGradualDifficultyAttributes {
..Default::default() ..Default::default()
}; };
let mut params = ObjectParameters { let hit_objects = crate::osu::create_osu_objects(
map, map,
attrs: &mut attrs, &mut attrs,
ticks: Vec::new(), &scaling_factor,
curve_bufs: CurveBuffers::default(), map.hit_objects.len(),
}; hr,
time_preempt,
);
let mut hit_objects: Vec<_> = map let mut osu_objects = OsuObjects::new(hit_objects);
.hit_objects
.iter()
.map(|h| OsuObject::new(h, &mut params))
.collect();
attrs.n_circles = 0; attrs.n_circles = 0;
attrs.n_sliders = 0; attrs.n_sliders = 0;
attrs.n_spinners = 0; attrs.n_spinners = 0;
attrs.max_combo = 0; attrs.max_combo = 0;
let stack_threshold = time_preempt * map.stack_leniency as f64;
if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold);
} else {
old_stacking(&mut hit_objects, stack_threshold);
}
let mut hit_objects_iter = hit_objects.iter_mut().map(|h| {
h.post_process(hr, &scaling_factor);
h
});
let skills = Skills::new( let skills = Skills::new(
mods, mods,
scaling_factor.radius, scaling_factor.radius,
@@ -139,50 +127,61 @@ impl OsuGradualDifficultyAttributes {
hit_window, hit_window,
); );
let last = match hit_objects_iter.next() { let mut osu_objects_iter = osu_objects.iter_mut();
Some(prev) => prev,
None => { let Some(mut last) = osu_objects_iter.next() else {
return Self { return Self {
idx: 0, idx: 0,
mods, mods,
attrs, attrs,
hit_objects: Vec::new(), diff_objects: Vec::new(),
diff_objects: Vec::new(), osu_objects: OsuObjects::new(Vec::new()),
skills, skills,
} };
}
}; };
Self::increment_combo(last, &mut attrs); Self::increment_combo(last.as_ref().get_ref(), &mut attrs);
let mut last_last = None; let mut last_last = None;
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually // Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
Distances::compute_slider_cursor_pos(last, &scaling_factor); let last_pos = last.pos();
let last_stack_offset = last.stack_offset;
let mut last = &*last; if let OsuObjectKind::Slider(ref mut slider) = last.kind {
Distances::compute_slider_travel_dist(
last_pos,
last_stack_offset,
slider,
&scaling_factor,
);
}
let mut last = last.into_ref();
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2)); let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2));
for (i, curr) in hit_objects_iter.enumerate() { for (i, mut curr) in osu_objects_iter.enumerate() {
let delta_time = (curr.start_time - last.start_time) / clock_rate; let delta_time = (curr.start_time - last.start_time) / clock_rate;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. // * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64); let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new( let dists = Distances::new(
curr, &mut curr,
last, last.get_ref(),
last_last, last_last.map(Pin::get_ref),
clock_rate, clock_rate,
strain_time, strain_time,
&scaling_factor, &scaling_factor,
); );
let diff_obj = OsuDifficultyObject::new(curr, last, clock_rate, i, dists); let curr = curr.into_ref();
let diff_obj = OsuDifficultyObject::new(curr, last.get_ref(), clock_rate, i, dists);
diff_objects.push(diff_obj); diff_objects.push(diff_obj);
last_last = Some(last); last_last = Some(last);
last = &*curr; last = curr;
} }
Self { Self {
@@ -190,7 +189,7 @@ impl OsuGradualDifficultyAttributes {
mods, mods,
attrs, attrs,
diff_objects: extend_lifetime(diff_objects), diff_objects: extend_lifetime(diff_objects),
hit_objects, osu_objects,
skills, skills,
} }
} }
@@ -212,8 +211,8 @@ impl OsuGradualDifficultyAttributes {
fn extend_lifetime( fn extend_lifetime(
diff_objects: Vec<OsuDifficultyObject<'_>>, diff_objects: Vec<OsuDifficultyObject<'_>>,
) -> Vec<OsuDifficultyObject<'static>> { ) -> Vec<OsuDifficultyObject<'static>> {
// SAFETY: Owned values of the references will be contained // SAFETY: Owned values of the references will be contained in the same struct (same lifetime).
// in the same struct and hence live just as long as this vec. // Also, the only mutable access wraps them in `Pin` to ensure that they won't move.
unsafe { mem::transmute(diff_objects) } unsafe { mem::transmute(diff_objects) }
} }
@@ -221,13 +220,19 @@ impl Iterator for OsuGradualDifficultyAttributes {
type Item = OsuDifficultyAttributes; type Item = OsuDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let curr = self.diff_objects.get(self.idx)?; // The first difficulty object belongs to the second note since each difficulty
// 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.idx += 1;
self.skills.process(curr, &self.diff_objects);
Self::increment_combo(curr.base, &mut self.attrs);
let Skills { let Skills {
mut aim, mut aim,
mut aim_no_sliders, mut aim_no_sliders,
@@ -284,13 +289,15 @@ impl Iterator for OsuGradualDifficultyAttributes {
0.0 0.0
}; };
let mut attrs = self.attrs.clone(); let attrs = OsuDifficultyAttributes {
attrs.aim = aim_rating; aim: aim_rating,
attrs.speed = speed_rating; speed: speed_rating,
attrs.flashlight = flashlight_rating; flashlight: flashlight_rating,
attrs.slider_factor = slider_factor; slider_factor,
attrs.stars = star_rating; stars: star_rating,
attrs.speed_note_count = speed_notes; speed_note_count: speed_notes,
..self.attrs.clone()
};
Some(attrs) Some(attrs)
} }
@@ -303,15 +310,20 @@ impl Iterator for OsuGradualDifficultyAttributes {
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1); let skip_iter = self.diff_objects.iter().skip(self.idx.saturating_sub(1));
for _ in 0..skip { let mut take = n.min(self.len().saturating_sub(1));
let curr = self.diff_objects.get(self.idx)?;
// The first note has no difficulty object
if self.idx == 0 && take > 0 {
take -= 1;
self.idx += 1; self.idx += 1;
}
for curr in skip_iter.take(take) {
self.skills.process(curr, &self.diff_objects); self.skills.process(curr, &self.diff_objects);
Self::increment_combo(curr.base.get_ref(), &mut self.attrs);
Self::increment_combo(curr.base, &mut self.attrs); self.idx += 1;
} }
self.next() self.next()
@@ -321,6 +333,32 @@ impl Iterator for OsuGradualDifficultyAttributes {
impl ExactSizeIterator for OsuGradualDifficultyAttributes { impl ExactSizeIterator for OsuGradualDifficultyAttributes {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
self.diff_objects.len() - self.idx self.diff_objects.len() + 1 - self.idx
}
}
mod osu_objects {
use crate::osu::OsuObject;
use std::pin::Pin;
// Wrapper to ensure that the data will not be moved
pub(super) struct OsuObjects {
objects: Box<[OsuObject]>,
}
impl OsuObjects {
pub(super) fn new(objects: Vec<OsuObject>) -> Self {
Self {
objects: objects.into_boxed_slice(),
}
}
pub(super) fn is_empty(&self) -> bool {
self.objects.is_empty()
}
pub(super) fn iter_mut(&mut self) -> impl Iterator<Item = Pin<&mut OsuObject>> {
self.objects.iter_mut().map(Pin::new)
}
} }
} }
+27 -82
View File
@@ -1,68 +1,21 @@
#![cfg(feature = "gradual")]
use crate::{Beatmap, OsuPP}; use crate::{Beatmap, OsuPP};
use super::{OsuGradualDifficultyAttributes, OsuPerformanceAttributes}; use super::{OsuGradualDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState};
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`OsuGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct OsuScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
pub max_combo: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl OsuScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * self.n300 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
/// Gradually calculate the performance attributes of an osu!standard map. /// Gradually calculate the performance attributes of an osu!standard map.
/// ///
/// After each hit object you can call /// After each hit object you can call [`next`](`OsuGradualPerformanceAttributes::next`)
/// [`process_next_object`](`OsuGradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`OsuPerformanceAttributes`]. /// and it will return the resulting current [`OsuPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use
/// [`process_next_n_objects`](`OsuGradualPerformanceAttributes::process_next_n_objects`) instead. /// [`nth`](`OsuGradualPerformanceAttributes::nth`) instead.
/// ///
/// Both methods require an [`OsuScoreState`] that contains the current /// Both methods require an [`OsuScoreState`] that contains the current
/// hitresults as well as the maximum combo so far. /// hitresults as well as the maximum combo so far.
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes) instead. /// [`OsuGradualDifficultyAttributes`] instead.
/// ///
/// # Example /// # Example
/// ///
@@ -84,10 +37,10 @@ impl OsuScoreState {
/// state.max_combo += 1; /// state.max_combo += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
@@ -95,30 +48,31 @@ impl OsuScoreState {
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // The next 10 objects will be a mixture of 300s, 100s, and 50s. /// // The next 10 objects will be a mixture of 300s, 100s, and 50s.
/// // Notice how all 10 objects will be processed in one go. /// // Notice how all 10 objects will be processed in one go.
/// state.n300 += 2; /// state.n300 += 2;
/// state.n100 += 7; /// state.n100 += 7;
/// state.n50 += 1; /// state.n50 += 1;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10); /// # let _ = gradual_perf.nth(state.clone(), 9);
/// ///
/// // Now comes another 300. Note that the max combo gets incremented again. /// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -127,14 +81,14 @@ impl OsuScoreState {
/// state.n100 = ... /// state.n100 = ...
/// state.n50 = ... /// state.n50 = ...
/// state.n_misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct OsuGradualPerformanceAttributes<'map> { pub struct OsuGradualPerformanceAttributes<'map> {
@@ -156,33 +110,24 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the /// Process the next hit object and calculate the
/// performance attributes for the resulting score state. /// performance attributes for the resulting score state.
pub fn process_next_object( pub fn next(&mut self, state: OsuScoreState) -> Option<OsuPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: OsuScoreState,
) -> Option<OsuPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`OsuGradualPerformanceAttributes::process_next_object`) /// Process everything up the the next `n`th hit object and calculate the performance
/// but instead of processing only one object it process `n` many. /// attributes for the resulting score state.
/// ///
/// If `n` is 0 it will be considered as 1. /// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// If there are still objects to be processed but `n` is larger than the amount /// and so on.
/// of remaining objects, `n` will be considered as the amount of remaining objects. pub fn nth(&mut self, state: OsuScoreState, n: usize) -> Option<OsuPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.nth(n)?;
&mut self,
state: OsuScoreState,
n: usize,
) -> Option<OsuPerformanceAttributes> {
let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(self.difficulty.idx + 1) .passed_objects(self.difficulty.idx)
.calculate(); .calculate();
Some(performance) Some(performance)
+36 -19
View File
@@ -1,19 +1,30 @@
mod difficulty_object; mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod osu_object; mod osu_object;
mod pp; mod pp;
mod scaling_factor; mod scaling_factor;
mod score_state;
mod skills; mod skills;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods}; use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods};
use std::pin::Pin;
use self::{ use self::{
difficulty_object::{Distances, OsuDifficultyObject}, difficulty_object::{Distances, OsuDifficultyObject},
skills::{Skill, Skills}, skills::{Skill, Skills},
}; };
pub use self::{gradual_difficulty::*, gradual_performance::*, osu_object::*, pp::*}; pub use self::{osu_object::*, pp::*, score_state::OsuScoreState};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::OsuGradualDifficultyAttributes,
gradual_performance::OsuGradualPerformanceAttributes,
};
pub(crate) use self::scaling_factor::ScalingFactor; pub(crate) use self::scaling_factor::ScalingFactor;
@@ -91,7 +102,7 @@ impl<'map> OsuStars<'map> {
/// ///
/// If you want to calculate the difficulty after every few objects, instead of /// If you want to calculate the difficulty after every few objects, instead of
/// using [`OsuStars`] multiple times with different `passed_objects`, you should use /// using [`OsuStars`] multiple times with different `passed_objects`, you should use
/// [`OsuGradualDifficultyAttributes`](crate::osu::OsuGradualDifficultyAttributes). /// [`OsuGradualDifficultyAttributes`].
#[inline] #[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self { pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects); self.passed_objects = Some(passed_objects);
@@ -271,7 +282,7 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
let mut hit_objects = let mut hit_objects =
create_osu_objects(map, &mut attrs, &scaling_factor, take, hr, time_preempt); create_osu_objects(map, &mut attrs, &scaling_factor, take, hr, time_preempt);
let mut hit_objects_iter = hit_objects.iter_mut(); let mut hit_objects_iter = hit_objects.iter_mut().map(Pin::new);
let mut skills = Skills::new( let mut skills = Skills::new(
mods, mods,
@@ -281,39 +292,46 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
hit_window, hit_window,
); );
let last = match hit_objects_iter.next() { let Some(mut last) = hit_objects_iter.next() else {
Some(prev) => prev, return (skills, attrs);
None => return (skills, attrs),
}; };
let mut last_last = None; let mut last_last = None;
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually // Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
Distances::compute_slider_cursor_pos(last, &scaling_factor); let last_pos = last.pos();
let last_stack_offset = last.stack_offset;
let mut last = &*last; if let OsuObjectKind::Slider(ref mut slider) = last.kind {
Distances::compute_slider_travel_dist(last_pos, last_stack_offset, slider, &scaling_factor);
}
let mut last = last.into_ref();
let mut diff_objects = Vec::with_capacity(hit_objects_iter.len()); let mut diff_objects = Vec::with_capacity(hit_objects_iter.len());
for (i, curr) in hit_objects_iter.enumerate() { for (i, mut curr) in hit_objects_iter.enumerate() {
let delta_time = (curr.start_time - last.start_time) / clock_rate; let delta_time = (curr.start_time - last.start_time) / clock_rate;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. // * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64); let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new( let dists = Distances::new(
curr, &mut curr,
last, last.get_ref(),
last_last, last_last.map(Pin::get_ref),
clock_rate, clock_rate,
strain_time, strain_time,
&scaling_factor, &scaling_factor,
); );
let diff_obj = OsuDifficultyObject::new(curr, last, clock_rate, i, dists); let curr = curr.into_ref();
let diff_obj =
OsuDifficultyObject::new(curr, last.get_ref(), clock_rate, i, dists);
diff_objects.push(diff_obj); diff_objects.push(diff_obj);
last_last = Some(last); last_last = Some(last);
last = &*curr; last = curr;
} }
for curr in diff_objects.iter() { for curr in diff_objects.iter() {
@@ -363,9 +381,8 @@ pub(crate) fn create_osu_objects(
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) { fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
let mut extended_start_idx = 0; let mut extended_start_idx = 0;
let extended_end_idx = match hit_objects.len().checked_sub(1) { let Some(extended_end_idx) = hit_objects.len().checked_sub(1) else {
Some(idx) => idx, return;
None => return,
}; };
// First big `if` in osu!lazer's function can be skipped // First big `if` in osu!lazer's function can be skipped
+47
View File
@@ -0,0 +1,47 @@
/// 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`](crate::osu::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
}
}
+110
View File
@@ -0,0 +1,110 @@
use crate::{
catch::CatchScoreState, mania::ManiaScoreState, osu::OsuScoreState, taiko::TaikoScoreState,
GameMode,
};
/// Aggregation for a score's current state i.e. what is
/// the maximum combo so far, what are the current
/// hitresults and what is the current score.
///
/// This struct is used for [`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,
}
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,
}
}
}
View File
+1 -1
View File
@@ -1,4 +1,4 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(not(any(feature = "async_tokio", feature = "async_std")), feature = "gradual"))]
use rosu_pp::{ use rosu_pp::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState}, catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
+1 -1
View File
@@ -1,4 +1,4 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(not(any(feature = "async_tokio", feature = "async_std")), feature = "gradual"))]
use rosu_pp::{ use rosu_pp::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState}, mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
+13 -10
View File
@@ -1,4 +1,7 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{ use rosu_pp::{
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState}, osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
@@ -35,10 +38,10 @@ fn correct_empty() {
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState::default(); let state = OsuScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX); let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some()); assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none()); assert!(gradual.next(state).is_none());
} }
#[test] #[test]
@@ -50,14 +53,14 @@ fn next_and_next_n() {
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 { for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
let _ = gradual2.process_next_object(state.clone()); let _ = gradual2.next(state.clone());
} }
let n = 80; let n = 80;
for _ in 1..n { for _ in 1..n {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
} }
let state = OsuScoreState { let state = OsuScoreState {
@@ -68,8 +71,8 @@ fn next_and_next_n() {
n_misses: 2, n_misses: 2,
}; };
let next = gradual1.process_next_object(state.clone()); let next = gradual1.next(state.clone());
let next_n = gradual2.process_next_n_objects(state, n); let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next); assert_eq!(next_n, next);
} }
@@ -88,7 +91,7 @@ fn gradual_end_eq_regular() {
n_misses: 0, n_misses: 0,
}; };
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap(); let gradual_end = gradual.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
@@ -109,7 +112,7 @@ fn gradual_eq_regular_passed() {
n_misses: 0, n_misses: 0,
}; };
let gradual = gradual.process_next_n_objects(state, n).unwrap(); let gradual = gradual.nth(state, n - 1).unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }
+1 -1
View File
@@ -1,4 +1,4 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))] #![cfg(all(not(any(feature = "async_tokio", feature = "async_std")), feature = "gradual"))]
use rosu_pp::{ use rosu_pp::{
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState}, taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},