added functions to calculate only the strains

This commit is contained in:
MaxOhn
2021-01-18 17:40:18 +01:00
parent 44538a7022
commit f585b38efc
11 changed files with 639 additions and 10 deletions
+231 -1
View File
@@ -11,7 +11,7 @@ use movement::Movement;
pub use pp::*;
use slider_state::SliderState;
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2, StarResult};
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2, StarResult, Strains};
use std::convert::identity;
@@ -278,6 +278,236 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
StarResult::Fruits { attributes }
}
/// Essentially the same as the `stars` function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
if map.hit_objects.len() < 2 {
return Strains::default();
}
let attributes = map.attributes().mods(mods);
let with_hr = mods.hr();
let mut ticks = Vec::new(); // using the same buffer for all sliders
let mut slider_state = SliderState::new(map);
// BUG: Incorrect object order on 2B maps that have fruits within sliders
let mut hit_objects = map
.hit_objects
.iter()
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
HitObjectKind::Circle => {
let mut h = CatchObject::new((h.pos, h.start_time));
if with_hr {
h = h.with_hr(last_pos, last_time);
}
Some(Some(FruitOrJuice::Fruit(Some(h))))
}
HitObjectKind::Slider {
pixel_len,
repeats,
curve_points,
path_type,
} => {
// HR business
last_pos
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
*last_time = h.start_time;
// Responsible for timing point values
slider_state.update(h.start_time);
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
if map.version >= 8 {
tick_distance /=
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
}
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
/ (map.sv * slider_state.speed_mult)
/ 100.0;
// Ensure path type validity
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
PathType::Bezier
} else if curve_points.len() == 2 {
PathType::Linear
} else {
*path_type
};
// Build the curve w.r.t. the curve points
let curve = match path_type {
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
PathType::Bezier => Curve::bezier(curve_points),
PathType::Catmull => Curve::catmull(curve_points),
PathType::PerfectCurve => Curve::perfect(curve_points),
};
let mut current_distance = tick_distance;
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
let target = *pixel_len - tick_distance / 8.0;
ticks.reserve((target / tick_distance) as usize);
// Tick of the first span
if current_distance < target {
for tick_idx in 1.. {
let pos = curve.point_at_distance(current_distance);
let time = h.start_time + time_add * tick_idx as f32;
ticks.push((pos, time));
current_distance += tick_distance;
if current_distance >= target {
break;
}
}
}
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
slider_objects.push((h.pos, h.start_time));
// Other spans
if *repeats <= 1 {
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
} else {
slider_objects.append(&mut ticks.clone());
for repeat_id in 1..*repeats {
let dist = (repeat_id % 2) as f32 * *pixel_len;
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
let pos = curve.point_at_distance(dist);
// Reverse tick
slider_objects.push((pos, h.start_time + time_offset));
// Actual ticks
if repeat_id & 1 == 1 {
slider_objects.extend(ticks.iter().copied().rev());
} else {
slider_objects.extend(ticks.iter().copied());
}
}
ticks.clear();
}
// Slider tail
let dist_end = (*repeats % 2) as f32 * *pixel_len;
let pos = curve.point_at_distance(dist_end);
slider_objects.push((pos, h.start_time + duration));
let iter = slider_objects.into_iter().map(CatchObject::new);
Some(Some(FruitOrJuice::Juice(iter)))
}
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => Some(None),
})
.filter_map(identity)
.flatten();
// Hyper dash business
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
let mut last_direction = 0;
let mut last_excess = half_catcher_width;
// Strain business
let mut movement = Movement::new(attributes.cs);
let section_len = SECTION_LENGTH * attributes.clock_rate;
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev = hit_objects.next().unwrap();
let mut curr = hit_objects.next().unwrap();
prev.init_hyper_dash(
half_catcher_width,
&curr,
&mut last_direction,
&mut last_excess,
);
// Handle second object separately to remove later if-branching
let next = hit_objects.next().unwrap();
curr.init_hyper_dash(
half_catcher_width,
&next,
&mut last_direction,
&mut last_excess,
);
let h = DifficultyObject::new(
&curr,
&prev,
movement.half_catcher_width,
attributes.clock_rate,
);
while h.base.time > current_section_end {
current_section_end += section_len;
}
movement.process(&h);
prev = curr;
curr = next;
// Handle all other objects
for next in hit_objects {
curr.init_hyper_dash(
half_catcher_width,
&next,
&mut last_direction,
&mut last_excess,
);
let h = DifficultyObject::new(
&curr,
&prev,
movement.half_catcher_width,
attributes.clock_rate,
);
while h.base.time > current_section_end {
movement.save_current_peak();
movement.start_new_section_from(current_section_end);
current_section_end += section_len;
}
movement.process(&h);
prev = curr;
curr = next;
}
// Same as in loop but without init_hyper_dash because `curr` is the last element
let h = DifficultyObject::new(
&curr,
&prev,
movement.half_catcher_width,
attributes.clock_rate,
);
while h.base.time > current_section_end {
movement.save_current_peak();
movement.start_new_section_from(current_section_end);
current_section_end += section_len;
}
movement.process(&h);
movement.save_current_peak();
Strains {
section_length: section_len,
strains: movement.strain_peaks,
}
}
fn tiny_droplet_count(
start_time: f32,
time_between_ticks: f32,
+1 -1
View File
@@ -20,7 +20,7 @@ pub(crate) struct Movement {
current_strain: f32,
current_section_peak: f32,
strain_peaks: Vec<f32>,
pub(crate) strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
+32 -1
View File
@@ -114,7 +114,7 @@ pub use parse::{
};
pub trait BeatmapExt {
/// Calculate the stars of a beatmap.
/// Calculate the stars and other attributes of a beatmap which are required for pp calculation.
///
/// For osu!standard maps, the `no_leniency` version will be used.
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult;
@@ -126,6 +126,15 @@ pub trait BeatmapExt {
/// If you seek more fine-tuning and options you need to match on the map's
/// mode and use the mode's corresponding calculator, e.g. [`TaikoPP`](crate::TaikoPP) for taiko.
fn max_pp(&self, mods: u32) -> PpResult;
/// Calculate the strains of a map.
/// This essentially performs the same calculation as a `stars` function but
/// instead of evaluating the final strains, they are just returned as is.
///
/// Suitable to plot the difficulty of a map over time.
///
/// For osu!standard maps, the `no_leniency` version will be used.
fn strains(&self, mods: impl Mods) -> Strains;
}
impl BeatmapExt for Beatmap {
@@ -147,9 +156,30 @@ impl BeatmapExt for Beatmap {
GameMode::CTB => FruitsPP::new(self).mods(mods).calculate(),
}
}
fn strains(&self, mods: impl Mods) -> Strains {
match self.mode {
GameMode::STD => osu::no_leniency::strains(self, mods),
GameMode::MNA => mania::strains(self, mods),
GameMode::TKO => taiko::strains(self, mods),
GameMode::CTB => fruits::strains(self, mods),
}
}
}
/// The result of calculating the strains on a map.
/// Suitable to plot the difficulty of a map over time.
///
/// `strains` will be the summed strains for each skill of the map's mode.
///
/// `section_length` is the time in ms inbetween two strains.
#[derive(Clone, Debug, Default)]
pub struct Strains {
pub section_length: f32,
pub strains: Vec<f32>,
}
/// Basic enum containing the result of a star calculation based on the mode.
#[derive(Clone, Debug)]
pub enum StarResult {
Fruits {
attributes: fruits::DifficultyAttributes,
@@ -179,6 +209,7 @@ impl StarResult {
}
/// Basic struct containing the result of a PP calculation.
#[derive(Clone, Debug)]
pub struct PpResult {
pub pp: f32,
pub attributes: StarResult,
+54 -1
View File
@@ -4,7 +4,7 @@ mod strain;
pub use pp::*;
use strain::Strain;
use crate::{Beatmap, HitObject, Mods, StarResult};
use crate::{Beatmap, HitObject, Mods, StarResult, Strains};
const SECTION_LEN: f32 = 400.0;
const STAR_SCALING_FACTOR: f32 = 0.018;
@@ -63,6 +63,59 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
StarResult::Mania { stars }
}
/// Essentially the same as the `stars` function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
if map.hit_objects.len() < 2 {
return Strains::default();
}
let clock_rate = mods.speed();
let section_len = SECTION_LEN * clock_rate;
let mut strain = Strain::new(map.cs as u8);
let mut hit_objects = map
.hit_objects
.iter()
.skip(1)
.zip(map.hit_objects.iter())
.map(|(base, prev)| DifficultyHitObject::new(base, prev, map.cs, clock_rate));
// No strain for first object
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
// Handle second object separately to remove later if-branching
let h = hit_objects.next().unwrap();
while h.base.start_time > current_section_end {
current_section_end += section_len;
}
strain.process(&h);
// Handle all other objects
for h in hit_objects {
while h.base.start_time > current_section_end {
strain.save_current_peak();
strain.start_new_section_from(current_section_end);
current_section_end += section_len;
}
strain.process(&h);
}
strain.save_current_peak();
Strains {
section_length: section_len,
strains: strain.strain_peaks,
}
}
#[derive(Debug)]
pub(crate) struct DifficultyHitObject<'o> {
base: &'o HitObject,
+1 -1
View File
@@ -11,7 +11,7 @@ pub(crate) struct Strain {
hold_end_times: Vec<f32>,
individual_strains: Vec<f32>,
pub strain_peaks: Vec<f32>,
pub(crate) strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
+2
View File
@@ -130,6 +130,8 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
StarResult::Osu { attributes }
}
// TODO: strains function
#[cfg(test)]
mod tests {
use super::super::super::OsuPP;
+120 -1
View File
@@ -17,7 +17,7 @@ use skill::Skill;
use skill_kind::SkillKind;
use slider_state::SliderState;
use crate::{Beatmap, Mods, StarResult};
use crate::{Beatmap, Mods, StarResult, Strains};
const OBJECT_RADIUS: f32 = 64.0;
const SECTION_LEN: f32 = 400.0;
@@ -153,6 +153,125 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
}
}
/// Essentially the same as the `stars` function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let map_attributes = map.attributes().mods(mods);
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
let od = (80.0 - hitwindow) / 6.0;
let mut diff_attributes = DifficultyAttributes {
ar: map_attributes.ar,
od,
..Default::default()
};
if map.hit_objects.len() < 2 {
return Strains::default();
}
let section_len = SECTION_LEN * map_attributes.clock_rate;
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
let mut scaling_factor = NORMALIZED_RADIUS / radius;
if radius < 30.0 {
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
scaling_factor *= 1.0 + small_circle_bonus;
}
let mut slider_state = SliderState::new(map);
let mut ticks_buf = Vec::new();
let mut hit_objects = map.hit_objects.iter().filter_map(|h| {
OsuObject::new(
h,
map,
radius,
&mut ticks_buf,
&mut diff_attributes,
&mut slider_state,
)
});
let mut aim = Skill::new(SkillKind::Aim);
let mut speed = Skill::new(SkillKind::Speed);
// First object has no predecessor and thus no strain, handle distinctly
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_vals = None;
// Handle second object separately to remove later if-branching
let curr = hit_objects.next().unwrap();
let h = DifficultyObject::new(
&curr,
&prev,
prev_vals,
prev_prev,
map_attributes.clock_rate,
scaling_factor,
);
while h.base.time > current_section_end {
current_section_end += section_len;
}
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
// Handle all other objects
for curr in hit_objects {
let h = DifficultyObject::new(
&curr,
&prev,
prev_vals,
prev_prev,
map_attributes.clock_rate,
scaling_factor,
);
while h.base.time > current_section_end {
aim.save_current_peak();
aim.start_new_section_from(current_section_end);
speed.save_current_peak();
speed.start_new_section_from(current_section_end);
current_section_end += section_len;
}
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
}
aim.save_current_peak();
speed.save_current_peak();
let strains = aim
.strain_peaks
.into_iter()
.zip(speed.strain_peaks.into_iter())
.map(|(aim, speed)| aim + speed)
.collect();
Strains {
section_length: section_len,
strains,
}
}
#[cfg(test)]
mod tests {
use super::super::super::OsuPP;
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
current_section_peak: f32,
kind: SkillKind,
strain_peaks: Vec<f32>,
pub(crate) strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
+110 -1
View File
@@ -15,7 +15,7 @@ use skill::Skill;
use skill_kind::SkillKind;
use slider_state::SliderState;
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult};
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult, Strains};
use std::borrow::Cow;
@@ -177,6 +177,115 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
}
}
/// Essentially the same as the `stars` function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let attributes = map.attributes().mods(mods);
if map.hit_objects.len() < 2 {
return Strains::default();
}
let section_len = SECTION_LEN * attributes.clock_rate;
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
let mut scaling_factor = NORMALIZED_RADIUS / radius;
if radius < 30.0 {
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
scaling_factor *= 1.0 + small_circle_bonus;
}
let mut hit_objects = map.hit_objects.iter().filter_map(|h| match &h.kind {
HitObjectKind::Circle => Some(Cow::Borrowed(h)),
HitObjectKind::Slider { .. } => Some(Cow::Owned(HitObject {
pos: h.pos,
start_time: h.start_time,
kind: HitObjectKind::Circle,
sound: h.sound,
})),
HitObjectKind::Spinner { .. } => Some(Cow::Borrowed(h)),
HitObjectKind::Hold { .. } => None,
});
let mut aim = Skill::new(SkillKind::Aim);
let mut speed = Skill::new(SkillKind::Speed);
// First object has no predecessor and thus no strain, handle distinctly
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_vals = None;
// Handle second object separately to remove later if-branching
let curr = hit_objects.next().unwrap();
let h = DifficultyObject::new(
curr.as_ref(),
prev.as_ref(),
prev_vals,
prev_prev,
attributes.clock_rate,
scaling_factor,
);
while h.base.start_time > current_section_end {
current_section_end += section_len;
}
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
// Handle all other objects
for curr in hit_objects {
let h = DifficultyObject::new(
curr.as_ref(),
prev.as_ref(),
prev_vals,
prev_prev,
attributes.clock_rate,
scaling_factor,
);
while h.base.start_time > current_section_end {
aim.save_current_peak();
aim.start_new_section_from(current_section_end);
speed.save_current_peak();
speed.start_new_section_from(current_section_end);
current_section_end += section_len;
}
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
}
aim.save_current_peak();
speed.save_current_peak();
let strains = aim
.strain_peaks
.into_iter()
.zip(speed.strain_peaks.into_iter())
.map(|(aim, speed)| aim + speed)
.collect();
Strains {
section_length: section_len,
strains,
}
}
#[cfg(test)]
mod tests {
use super::super::super::OsuPP;
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
current_section_peak: f32,
kind: SkillKind,
strain_peaks: Vec<f32>,
pub(crate) strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
+86 -1
View File
@@ -16,7 +16,7 @@ use skill::Skill;
use skill_kind::SkillKind;
use stamina_cheese::StaminaCheeseDetector;
use crate::{Beatmap, Mods, StarResult};
use crate::{Beatmap, Mods, StarResult, Strains};
use std::cmp::Ordering;
use std::f32::consts::PI;
@@ -117,6 +117,91 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
StarResult::Taiko { stars }
}
/// Essentially the same as the `stars` function but instead of
/// evaluating the final strains, it just returns them as is.
///
/// Suitable to plot the difficulty of a map over time.
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
if map.hit_objects.len() < 2 {
return Strains::default();
}
// True if the object at that index is stamina cheese
let cheese = map.find_cheese();
let mut skills = vec![
Skill::new(SkillKind::color()),
Skill::new(SkillKind::rhythm()),
Skill::new(SkillKind::stamina(true)),
Skill::new(SkillKind::stamina(false)),
];
let clock_rate = mods.speed();
let section_len = SECTION_LEN * clock_rate;
// No strain for first object
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut hit_objects = map
.hit_objects
.iter()
.enumerate()
.skip(2)
.zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter())
.map(|(((idx, base), prev), prev_prev)| {
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
});
// Handle second object separately to remove later if-branching
let h = hit_objects.next().unwrap();
while h.base.start_time > current_section_end {
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h, &cheese);
}
// Handle all other objects
for h in hit_objects {
while h.base.start_time > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h, &cheese);
}
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
let strains = skills[0]
.strain_peaks
.iter()
.zip(skills[1].strain_peaks.iter())
.zip(skills[2].strain_peaks.iter())
.zip(skills[3].strain_peaks.iter())
.map(|(((color, rhythm), stamina_right), stamina_left)| {
color + rhythm + stamina_right + stamina_left
})
.collect();
Strains {
section_length: section_len,
strains,
}
}
#[inline]
fn rescale(stars: f32) -> f32 {
if stars < 0.0 {