refactor!: overhauled gradual calc for osu
This commit is contained in:
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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
@@ -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,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},
|
||||||
|
|||||||
Reference in New Issue
Block a user