fixed attributes on very short maps + fixed gradual attributes offset

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