fixed attributes on very short maps + fixed gradual attributes offset
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- [BREAKING] Renamed the `attributes` field to `difficulty` for all `{Mode}PerformanceAttributes` structs
|
||||
- Added `OsuGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
||||
- Added `OsuGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `OsuPP` over and over.
|
||||
- osu: Fixed incorrect attributes on maps with only 1 or 2 hit objects
|
||||
|
||||
# v0.3.0
|
||||
|
||||
|
||||
@@ -75,9 +75,16 @@ impl OsuGradualDifficultyAttributes {
|
||||
let time_preempt = difficulty_range_ar(raw_ar);
|
||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||
|
||||
let mut attributes = OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
max_combo: 0,
|
||||
attributes: &mut attributes,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
@@ -91,6 +98,11 @@ impl OsuGradualDifficultyAttributes {
|
||||
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||
hit_objects.extend(hit_objects_iter);
|
||||
|
||||
attributes.n_circles = 0;
|
||||
attributes.n_sliders = 0;
|
||||
attributes.n_spinners = 0;
|
||||
attributes.max_combo = 0;
|
||||
|
||||
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||
|
||||
if map.version >= 6 {
|
||||
@@ -99,13 +111,6 @@ impl OsuGradualDifficultyAttributes {
|
||||
old_stacking(&mut hit_objects, stack_threshold);
|
||||
}
|
||||
|
||||
let attributes = OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let skills = Skills::new(hit_window, mods.rx(), scaling_factor.radius(), mods.fl());
|
||||
|
||||
let hit_objects = OsuObjectIter {
|
||||
@@ -122,16 +127,13 @@ impl OsuGradualDifficultyAttributes {
|
||||
kind: OsuObjectKind::Circle,
|
||||
};
|
||||
|
||||
let curr_section_end =
|
||||
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
Self {
|
||||
idx: 0,
|
||||
attributes,
|
||||
clock_rate: map_attributes.clock_rate,
|
||||
hit_objects,
|
||||
skills,
|
||||
curr_section_end,
|
||||
curr_section_end: 0.0,
|
||||
prev_prev,
|
||||
prev,
|
||||
strain_peak_buf: Vec::new(),
|
||||
@@ -159,6 +161,8 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
|
||||
if self.idx == 1 {
|
||||
self.prev = curr;
|
||||
self.curr_section_end =
|
||||
(self.prev.time / self.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
return Some(self.attributes.clone());
|
||||
}
|
||||
@@ -189,24 +193,29 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
self.skills.process(&h);
|
||||
self.prev_prev = Some(mem::replace(&mut self.prev, curr));
|
||||
|
||||
if self.hit_objects.len() == 0 {
|
||||
self.skills.save_current_peak();
|
||||
}
|
||||
|
||||
let missing = self.skills.aim().strain_peaks.len() - self.strain_peak_buf.len();
|
||||
let missing = self.skills.aim().strain_peaks.len() + 1 - self.strain_peak_buf.len();
|
||||
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
|
||||
|
||||
let aim_rating = {
|
||||
let aim = self.skills.aim();
|
||||
self.strain_peak_buf.copy_from_slice(&aim.strain_peaks);
|
||||
self.strain_peak_buf[..aim.strain_peaks.len()].copy_from_slice(&aim.strain_peaks);
|
||||
|
||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||
*last = aim.curr_section_peak;
|
||||
}
|
||||
|
||||
Skill::difficulty_value(&mut self.strain_peak_buf, aim).sqrt() * DIFFICULTY_MULTIPLIER
|
||||
};
|
||||
|
||||
let slider_factor = if aim_rating > 0.0 {
|
||||
let aim_no_sliders = self.skills.aim_no_sliders();
|
||||
self.strain_peak_buf
|
||||
self.strain_peak_buf[..aim_no_sliders.strain_peaks.len()]
|
||||
.copy_from_slice(&aim_no_sliders.strain_peaks);
|
||||
|
||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||
*last = aim_no_sliders.curr_section_peak;
|
||||
}
|
||||
|
||||
let aim_rating_no_sliders =
|
||||
Skill::difficulty_value(&mut self.strain_peak_buf, aim_no_sliders).sqrt()
|
||||
* DIFFICULTY_MULTIPLIER;
|
||||
@@ -219,7 +228,11 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
let (speed, flashlight) = self.skills.speed_flashlight();
|
||||
|
||||
let speed_rating = if let Some(speed) = speed {
|
||||
self.strain_peak_buf.copy_from_slice(&speed.strain_peaks);
|
||||
self.strain_peak_buf[..speed.strain_peaks.len()].copy_from_slice(&speed.strain_peaks);
|
||||
|
||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||
*last = speed.curr_section_peak;
|
||||
}
|
||||
|
||||
Skill::difficulty_value(&mut self.strain_peak_buf, speed).sqrt() * DIFFICULTY_MULTIPLIER
|
||||
} else {
|
||||
@@ -227,9 +240,13 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
};
|
||||
|
||||
let flashlight_rating = if let Some(flashlight) = flashlight {
|
||||
self.strain_peak_buf
|
||||
self.strain_peak_buf[..flashlight.strain_peaks.len()]
|
||||
.copy_from_slice(&flashlight.strain_peaks);
|
||||
|
||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||
*last = flashlight.curr_section_peak;
|
||||
}
|
||||
|
||||
Skill::difficulty_value(&mut self.strain_peak_buf, flashlight).sqrt()
|
||||
* DIFFICULTY_MULTIPLIER
|
||||
} else {
|
||||
|
||||
@@ -247,7 +247,6 @@ mod tests {
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
#[ignore = "currently broken due to incorrect object counts when the map is not fully processed"]
|
||||
fn gradual_eq_regular_passed() {
|
||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
@@ -257,8 +256,8 @@ mod tests {
|
||||
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
let state = OsuScoreState {
|
||||
max_combo: 110,
|
||||
n300: 102,
|
||||
max_combo: 122,
|
||||
n300: 100,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
misses: 0,
|
||||
|
||||
+18
-39
@@ -43,21 +43,7 @@ pub fn stars(
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> OsuDifficultyAttributes {
|
||||
let (mut skills, mut attributes) = match calculate_skills(map, mods, passed_objects) {
|
||||
Some(tuple) => tuple,
|
||||
None => {
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
return OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
let (mut skills, mut attributes) = calculate_skills(map, mods, passed_objects);
|
||||
|
||||
let aim_rating = {
|
||||
let aim = skills.aim();
|
||||
@@ -142,10 +128,7 @@ fn calculate_star_rating(aim_rating: f64, speed_rating: f64, flashlight_rating:
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let mut skills = match calculate_skills(map, mods, None) {
|
||||
Some((skills, _)) => skills,
|
||||
None => return Strains::default(),
|
||||
};
|
||||
let (mut skills, _) = calculate_skills(map, mods, None);
|
||||
|
||||
let mut aim = mem::take(&mut skills.aim().strain_peaks);
|
||||
let tuple = skills.speed_flashlight();
|
||||
@@ -182,17 +165,13 @@ fn calculate_skills(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> Option<(Skills, OsuDifficultyAttributes)> {
|
||||
) -> (Skills, OsuDifficultyAttributes) {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
if take < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut raw_ar = map.ar as f64;
|
||||
let hr = mods.hr();
|
||||
|
||||
@@ -205,9 +184,16 @@ fn calculate_skills(
|
||||
let time_preempt = difficulty_range_ar(raw_ar);
|
||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||
|
||||
let mut attributes = OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
max_combo: 0,
|
||||
attributes: &mut attributes,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
@@ -239,15 +225,19 @@ fn calculate_skills(
|
||||
|
||||
let mut skills = Skills::new(hit_window, mods.rx(), scaling_factor.radius(), mods.fl());
|
||||
|
||||
let (mut prev, curr) = match (hit_objects.next(), hit_objects.next()) {
|
||||
(Some(prev), Some(curr)) => (prev, curr),
|
||||
(Some(_), None) | (None, None) => return (skills, attributes),
|
||||
(None, Some(_)) => unreachable!(),
|
||||
};
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut curr_section_end =
|
||||
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let curr = hit_objects.next().unwrap();
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&mut prev,
|
||||
@@ -289,18 +279,7 @@ fn calculate_skills(
|
||||
|
||||
skills.save_current_peak();
|
||||
|
||||
let attributes = OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
n_circles: map.n_circles as usize,
|
||||
n_sliders: map.n_sliders as usize,
|
||||
n_spinners: map.n_spinners as usize,
|
||||
max_combo: params.max_combo,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Some((skills, attributes))
|
||||
(skills, attributes)
|
||||
}
|
||||
|
||||
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
|
||||
+29
-19
@@ -1,6 +1,6 @@
|
||||
use std::{cmp::Ordering, convert::identity};
|
||||
|
||||
use super::slider_state::SliderState;
|
||||
use super::{slider_state::SliderState, OsuDifficultyAttributes};
|
||||
|
||||
use crate::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
@@ -49,7 +49,7 @@ pub(crate) enum NestedObjectKind {
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) max_combo: usize,
|
||||
pub(crate) attributes: &'a mut OsuDifficultyAttributes,
|
||||
pub(crate) ticks: Vec<(Pos2, f64)>,
|
||||
pub(crate) slider_state: SliderState<'a>,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
@@ -60,13 +60,13 @@ impl OsuObject {
|
||||
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
max_combo,
|
||||
attributes,
|
||||
ticks,
|
||||
slider_state,
|
||||
curve_bufs,
|
||||
} = params;
|
||||
|
||||
*max_combo += 1; // hitcircle, slider head, or spinner
|
||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
let mut pos = h.pos;
|
||||
|
||||
if hr {
|
||||
@@ -74,17 +74,23 @@ impl OsuObject {
|
||||
}
|
||||
|
||||
let obj = match &h.kind {
|
||||
HitObjectKind::Circle => Self {
|
||||
time: h.start_time,
|
||||
pos,
|
||||
stack_height: 0.0,
|
||||
kind: OsuObjectKind::Circle,
|
||||
},
|
||||
HitObjectKind::Circle => {
|
||||
attributes.n_circles += 1;
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos,
|
||||
stack_height: 0.0,
|
||||
kind: OsuObjectKind::Circle,
|
||||
}
|
||||
}
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
} => {
|
||||
attributes.n_sliders += 1;
|
||||
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
@@ -248,7 +254,7 @@ impl OsuObject {
|
||||
_ => nested_objects.push(legacy_last_tick),
|
||||
};
|
||||
|
||||
*max_combo += nested_objects.len();
|
||||
attributes.max_combo += nested_objects.len();
|
||||
|
||||
let lazy_travel_time = final_span_end_time - h.start_time;
|
||||
let mut end_time_min = lazy_travel_time / span_duration;
|
||||
@@ -278,14 +284,18 @@ impl OsuObject {
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { end_time } => Self {
|
||||
time: h.start_time,
|
||||
pos,
|
||||
stack_height: 0.0,
|
||||
kind: OsuObjectKind::Spinner {
|
||||
end_time: *end_time,
|
||||
},
|
||||
},
|
||||
HitObjectKind::Spinner { end_time } => {
|
||||
attributes.n_spinners += 1;
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos,
|
||||
stack_height: 0.0,
|
||||
kind: OsuObjectKind::Spinner {
|
||||
end_time: *end_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Hold { .. } => return None,
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ impl Skills {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Skill {
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
pub(crate) curr_section_peak: f64,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
|
||||
Reference in New Issue
Block a user