added OsuDifficultyAttributesIter struct
adjusted changelog
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
- Added method `Beatmap::bpm`
|
- Added method `Beatmap::bpm`
|
||||||
- Added method `max_combo` for `DifficultyAttributes`, `PerformanceAttributes`, and all `{Mode}PerformanceAttributes`
|
- Added method `max_combo` for `DifficultyAttributes`, `PerformanceAttributes`, and all `{Mode}PerformanceAttributes`
|
||||||
- [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 `OsuDifficultyAttributesIter`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ impl<'p> ControlPointIter<'p> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) enum ControlPoint {
|
pub(crate) enum ControlPoint {
|
||||||
Timing { time: f64, beat_len: f64 },
|
Timing { time: f64, beat_len: f64 },
|
||||||
Difficulty { time: f64, slider_velocity: f64 },
|
Difficulty { time: f64, slider_velocity: f64 },
|
||||||
|
|||||||
@@ -470,33 +470,3 @@ impl From<FruitsPerformanceAttributes> for FruitsDifficultyAttributes {
|
|||||||
attributes.difficulty
|
attributes.difficulty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
|
||||||
#[test]
|
|
||||||
// #[ignore]
|
|
||||||
fn custom_fruits() {
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use crate::{Beatmap, FruitsPP};
|
|
||||||
|
|
||||||
let path = "E:Games/osu!/beatmaps/2919116_.osu";
|
|
||||||
let map = Beatmap::from_path(path).unwrap();
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
let result = FruitsPP::new(&map).mods(256).calculate();
|
|
||||||
|
|
||||||
let iters = 100;
|
|
||||||
let accum = start.elapsed();
|
|
||||||
|
|
||||||
// * Tiny benchmark for pp calculation
|
|
||||||
// let mut accum = accum;
|
|
||||||
|
|
||||||
// for _ in 0..iters {
|
|
||||||
// let start = Instant::now();
|
|
||||||
// let _result = OsuPP::new(&map).mods(0).calculate();
|
|
||||||
// accum += start.elapsed();
|
|
||||||
// }
|
|
||||||
|
|
||||||
println!("{:#?}", result);
|
|
||||||
println!("Calculation average: {:?}", accum / iters);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
use std::{iter, mem, vec::IntoIter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
curve::CurveBuffers, osu::difficulty_object::DifficultyObject, parse::Pos2, Beatmap, Mods,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
calculate_star_rating, difficulty_range_ar, difficulty_range_od, old_stacking,
|
||||||
|
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
||||||
|
scaling_factor::ScalingFactor,
|
||||||
|
skill::{Skill, Skills},
|
||||||
|
slider_state::SliderState,
|
||||||
|
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, SECTION_LEN,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Iterate over a map's hit objects and update the difficulty attributes each time.
|
||||||
|
///
|
||||||
|
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
|
||||||
|
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
|
||||||
|
/// be processed and the [`OsuDifficultyAttributes`](`crate::osu::OsuDifficultyAttributes`)
|
||||||
|
/// will be updated and returned.
|
||||||
|
///
|
||||||
|
/// TODO: Mention struct that does the same for performance
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use rosu_pp::{Beatmap, osu::OsuDifficultyAttributesIter};
|
||||||
|
///
|
||||||
|
/// # /*
|
||||||
|
/// let map: Beatmap = ...
|
||||||
|
/// # */
|
||||||
|
/// # let map = Beatmap::default();
|
||||||
|
///
|
||||||
|
/// let mods = 64; // DT
|
||||||
|
/// let mut iter = OsuDifficultyAttributesIter::new(&map, mods);
|
||||||
|
///
|
||||||
|
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
|
||||||
|
/// let attrs2 = iter.next(); // after the second hit object
|
||||||
|
///
|
||||||
|
/// // Remaining hit objects
|
||||||
|
/// for difficulty in iter {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OsuDifficultyAttributesIter {
|
||||||
|
idx: usize,
|
||||||
|
attributes: OsuDifficultyAttributes,
|
||||||
|
clock_rate: f64,
|
||||||
|
hit_objects: OsuObjectIter,
|
||||||
|
skills: Skills,
|
||||||
|
prev_prev: Option<OsuObject>,
|
||||||
|
prev: OsuObject,
|
||||||
|
curr_section_end: f64,
|
||||||
|
strain_peak_buf: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OsuDifficultyAttributesIter {
|
||||||
|
/// Create a new difficulty attributes iterator for osu!standard maps.
|
||||||
|
pub fn new(map: &Beatmap, mods: impl Mods) -> Self {
|
||||||
|
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;
|
||||||
|
|
||||||
|
let mut raw_ar = map.ar as f64;
|
||||||
|
let hr = mods.hr();
|
||||||
|
|
||||||
|
if hr {
|
||||||
|
raw_ar = (raw_ar * 1.4).min(10.0);
|
||||||
|
} else if mods.ez() {
|
||||||
|
raw_ar *= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_preempt = difficulty_range_ar(raw_ar);
|
||||||
|
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||||
|
|
||||||
|
let mut params = ObjectParameters {
|
||||||
|
map,
|
||||||
|
max_combo: 0,
|
||||||
|
slider_state: SliderState::new(map),
|
||||||
|
ticks: Vec::new(),
|
||||||
|
curve_bufs: CurveBuffers::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let hit_objects_iter = map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
||||||
|
|
||||||
|
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||||
|
hit_objects.extend(hit_objects_iter);
|
||||||
|
|
||||||
|
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 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 {
|
||||||
|
hit_objects: hit_objects.into_iter(),
|
||||||
|
scaling_factor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_prev = None;
|
||||||
|
|
||||||
|
let prev = OsuObject {
|
||||||
|
time: 0.0,
|
||||||
|
pos: Pos2::zero(),
|
||||||
|
stack_height: 0.0,
|
||||||
|
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,
|
||||||
|
prev_prev,
|
||||||
|
prev,
|
||||||
|
strain_peak_buf: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for OsuDifficultyAttributesIter {
|
||||||
|
type Item = OsuDifficultyAttributes;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let curr = self.hit_objects.next()?;
|
||||||
|
self.attributes.max_combo += 1;
|
||||||
|
|
||||||
|
match &curr.kind {
|
||||||
|
OsuObjectKind::Circle => self.attributes.n_circles += 1,
|
||||||
|
OsuObjectKind::Slider { nested_objects, .. } => {
|
||||||
|
self.attributes.max_combo += nested_objects.len();
|
||||||
|
self.attributes.n_sliders += 1
|
||||||
|
}
|
||||||
|
OsuObjectKind::Spinner { .. } => self.attributes.n_spinners += 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
|
if self.idx == 1 {
|
||||||
|
self.prev = curr;
|
||||||
|
|
||||||
|
return Some(self.attributes.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&mut self.prev,
|
||||||
|
self.prev_prev.as_ref(),
|
||||||
|
&self.hit_objects.scaling_factor,
|
||||||
|
self.clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
let base_time = h.base.time / self.clock_rate;
|
||||||
|
|
||||||
|
if self.idx == 2 {
|
||||||
|
while base_time > self.curr_section_end {
|
||||||
|
self.skills.start_new_section_from(self.curr_section_end);
|
||||||
|
self.curr_section_end += SECTION_LEN;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while base_time > self.curr_section_end {
|
||||||
|
self.skills
|
||||||
|
.save_peak_and_start_new_section(self.curr_section_end);
|
||||||
|
self.curr_section_end += SECTION_LEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
|
||||||
|
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
|
||||||
|
.copy_from_slice(&aim_no_sliders.strain_peaks);
|
||||||
|
let aim_rating_no_sliders =
|
||||||
|
Skill::difficulty_value(&mut self.strain_peak_buf, aim_no_sliders).sqrt()
|
||||||
|
* DIFFICULTY_MULTIPLIER;
|
||||||
|
|
||||||
|
aim_rating_no_sliders / aim_rating
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
Skill::difficulty_value(&mut self.strain_peak_buf, speed).sqrt() * DIFFICULTY_MULTIPLIER
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let flashlight_rating = if let Some(flashlight) = flashlight {
|
||||||
|
self.strain_peak_buf
|
||||||
|
.copy_from_slice(&flashlight.strain_peaks);
|
||||||
|
|
||||||
|
Skill::difficulty_value(&mut self.strain_peak_buf, flashlight).sqrt()
|
||||||
|
* DIFFICULTY_MULTIPLIER
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let star_rating = calculate_star_rating(aim_rating, speed_rating, flashlight_rating);
|
||||||
|
|
||||||
|
self.attributes.aim_strain = aim_rating;
|
||||||
|
self.attributes.speed_strain = speed_rating;
|
||||||
|
self.attributes.flashlight_rating = flashlight_rating;
|
||||||
|
self.attributes.slider_factor = slider_factor;
|
||||||
|
self.attributes.stars = star_rating;
|
||||||
|
|
||||||
|
Some(self.attributes.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.hit_objects.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for OsuDifficultyAttributesIter {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.hit_objects.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OsuObjectIter {
|
||||||
|
hit_objects: IntoIter<OsuObject>,
|
||||||
|
scaling_factor: ScalingFactor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for OsuObjectIter {
|
||||||
|
type Item = OsuObject;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let mut h = self.hit_objects.next()?;
|
||||||
|
let stack_offset = self.scaling_factor.stack_offset(h.stack_height);
|
||||||
|
h.pos += stack_offset;
|
||||||
|
|
||||||
|
Some(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.hit_objects.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for OsuObjectIter {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.hit_objects.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_map() {
|
||||||
|
let map = Beatmap::default();
|
||||||
|
assert!(OsuDifficultyAttributesIter::new(&map, 0).next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn iter_end_eq_regular() {
|
||||||
|
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let regular = crate::osu::stars(&map, mods, None);
|
||||||
|
|
||||||
|
let iter_end = OsuDifficultyAttributesIter::new(&map, mods)
|
||||||
|
.reduce(|_, next| next)
|
||||||
|
.expect("empty iter");
|
||||||
|
|
||||||
|
assert_eq!(regular, iter_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
-129
@@ -1,5 +1,6 @@
|
|||||||
#![cfg(feature = "osu")]
|
#![cfg(feature = "osu")]
|
||||||
|
|
||||||
|
mod difficulty_iter;
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
mod osu_object;
|
mod osu_object;
|
||||||
mod pp;
|
mod pp;
|
||||||
@@ -8,6 +9,9 @@ mod skill;
|
|||||||
mod skill_kind;
|
mod skill_kind;
|
||||||
mod slider_state;
|
mod slider_state;
|
||||||
|
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
pub use difficulty_iter::OsuDifficultyAttributesIter;
|
||||||
use difficulty_object::DifficultyObject;
|
use difficulty_object::DifficultyObject;
|
||||||
use osu_object::{ObjectParameters, OsuObject};
|
use osu_object::{ObjectParameters, OsuObject};
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
@@ -18,6 +22,8 @@ use slider_state::SliderState;
|
|||||||
|
|
||||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||||
|
|
||||||
|
use self::skill::Skills;
|
||||||
|
|
||||||
const SECTION_LEN: f64 = 400.0;
|
const SECTION_LEN: f64 = 400.0;
|
||||||
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
||||||
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
|
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
|
||||||
@@ -47,26 +53,56 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
let aim_rating = {
|
||||||
|
let aim = skills.aim();
|
||||||
|
let mut aim_strains = mem::take(&mut aim.strain_peaks);
|
||||||
|
|
||||||
|
Skill::difficulty_value(&mut aim_strains, aim).sqrt() * DIFFICULTY_MULTIPLIER
|
||||||
|
};
|
||||||
|
|
||||||
let slider_factor = if aim_rating > 0.0 {
|
let slider_factor = if aim_rating > 0.0 {
|
||||||
let aim_rating_no_sliders = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
let aim_no_sliders = skills.aim_no_sliders();
|
||||||
|
|
||||||
|
let mut aim_strains_no_sliders = mem::take(&mut aim_no_sliders.strain_peaks);
|
||||||
|
let aim_rating_no_sliders =
|
||||||
|
Skill::difficulty_value(&mut aim_strains_no_sliders, aim_no_sliders).sqrt()
|
||||||
|
* DIFFICULTY_MULTIPLIER;
|
||||||
|
|
||||||
aim_rating_no_sliders / aim_rating
|
aim_rating_no_sliders / aim_rating
|
||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let speed_rating = if mods.rx() {
|
let (speed, flashlight) = skills.speed_flashlight();
|
||||||
0.0
|
|
||||||
|
let speed_rating = if let Some(speed) = speed {
|
||||||
|
let mut speed_strains = mem::take(&mut speed.strain_peaks);
|
||||||
|
|
||||||
|
Skill::difficulty_value(&mut speed_strains, speed).sqrt() * DIFFICULTY_MULTIPLIER
|
||||||
} else {
|
} else {
|
||||||
skills[2].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let flashlight_rating = skills.get_mut(3).map_or(0.0, |skill| {
|
let flashlight_rating = if let Some(flashlight) = flashlight {
|
||||||
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
let mut flashlight_strains = mem::take(&mut flashlight.strain_peaks);
|
||||||
});
|
|
||||||
|
|
||||||
|
Skill::difficulty_value(&mut flashlight_strains, flashlight).sqrt() * DIFFICULTY_MULTIPLIER
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let star_rating = calculate_star_rating(aim_rating, speed_rating, flashlight_rating);
|
||||||
|
|
||||||
|
attributes.aim_strain = aim_rating;
|
||||||
|
attributes.speed_strain = speed_rating;
|
||||||
|
attributes.flashlight_rating = flashlight_rating;
|
||||||
|
attributes.slider_factor = slider_factor;
|
||||||
|
attributes.stars = star_rating;
|
||||||
|
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_star_rating(aim_rating: f64, speed_rating: f64, flashlight_rating: f64) -> f64 {
|
||||||
let base_aim_performance = {
|
let base_aim_performance = {
|
||||||
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
|
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
|
||||||
|
|
||||||
@@ -79,32 +115,20 @@ pub fn stars(
|
|||||||
base * base * base / 100_000.0
|
base * base * base / 100_000.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let base_flashlight_performance = if mods.fl() {
|
let base_flashlight_performance = flashlight_rating * flashlight_rating * 25.0;
|
||||||
flashlight_rating * flashlight_rating * 25.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let base_performance = (base_aim_performance.powf(1.1)
|
let base_performance = (base_aim_performance.powf(1.1)
|
||||||
+ base_speed_performance.powf(1.1)
|
+ base_speed_performance.powf(1.1)
|
||||||
+ base_flashlight_performance.powf(1.1))
|
+ base_flashlight_performance.powf(1.1))
|
||||||
.powf(1.0 / 1.1);
|
.powf(1.0 / 1.1);
|
||||||
|
|
||||||
let star_rating = if base_performance > 0.00001 {
|
if base_performance > 0.00001 {
|
||||||
1.12_f64.cbrt()
|
1.12_f64.cbrt()
|
||||||
* 0.027
|
* 0.027
|
||||||
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
}
|
||||||
|
|
||||||
attributes.aim_strain = aim_rating;
|
|
||||||
attributes.speed_strain = speed_rating;
|
|
||||||
attributes.flashlight_rating = flashlight_rating;
|
|
||||||
attributes.slider_factor = slider_factor;
|
|
||||||
attributes.stars = star_rating;
|
|
||||||
|
|
||||||
attributes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the [`stars`] function but instead of
|
/// Essentially the same as the [`stars`] function but instead of
|
||||||
@@ -117,25 +141,29 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
None => return Strains::default(),
|
None => return Strains::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
skills.reverse();
|
let mut aim = mem::take(&mut skills.aim().strain_peaks);
|
||||||
|
let tuple = skills.speed_flashlight();
|
||||||
|
|
||||||
let _ = skills.pop();
|
let strains = match tuple {
|
||||||
let aim_strains = skills.pop().unwrap().strain_peaks; // no sliders
|
(Some(speed), Some(flashlight)) => {
|
||||||
let speed_strains = skills.pop().unwrap().strain_peaks;
|
for ((aim, speed), flashlight) in aim
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&speed.strain_peaks)
|
||||||
|
.zip(&flashlight.strain_peaks)
|
||||||
|
{
|
||||||
|
*aim += speed + flashlight;
|
||||||
|
}
|
||||||
|
|
||||||
let strains = if let Some(flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
|
aim
|
||||||
aim_strains
|
}
|
||||||
.into_iter()
|
(Some(strains), None) | (None, Some(strains)) => {
|
||||||
.zip(speed_strains)
|
for (aim, strain) in aim.iter_mut().zip(&strains.strain_peaks) {
|
||||||
.zip(flashlight_strains)
|
*aim += strain;
|
||||||
.map(|((aim, speed), flashlight)| aim + speed + flashlight)
|
}
|
||||||
.collect()
|
|
||||||
} else {
|
aim
|
||||||
aim_strains
|
}
|
||||||
.into_iter()
|
(None, None) => aim,
|
||||||
.zip(speed_strains)
|
|
||||||
.map(|(aim, speed)| aim + speed)
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Strains {
|
Strains {
|
||||||
@@ -148,7 +176,7 @@ fn calculate_skills(
|
|||||||
map: &Beatmap,
|
map: &Beatmap,
|
||||||
mods: impl Mods,
|
mods: impl Mods,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
) -> Option<(Vec<Skill>, OsuDifficultyAttributes)> {
|
) -> Option<(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);
|
||||||
@@ -203,23 +231,13 @@ fn calculate_skills(
|
|||||||
h
|
h
|
||||||
});
|
});
|
||||||
|
|
||||||
let fl = mods.fl();
|
let mut skills = Skills::new(hit_window, mods.rx(), scaling_factor.radius(), mods.fl());
|
||||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
|
||||||
|
|
||||||
skills.push(Skill::aim(true));
|
|
||||||
skills.push(Skill::aim(false));
|
|
||||||
skills.push(Skill::speed(hit_window));
|
|
||||||
|
|
||||||
if fl {
|
|
||||||
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
|
||||||
skills.push(Skill::flashlight(52.0 / scaling_factor.radius() as f64));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prev_prev = None;
|
let mut prev_prev = None;
|
||||||
let mut prev = hit_objects.next().unwrap();
|
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 current_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
|
||||||
@@ -234,20 +252,13 @@ fn calculate_skills(
|
|||||||
|
|
||||||
let base_time = h.base.time / map_attributes.clock_rate;
|
let base_time = h.base.time / map_attributes.clock_rate;
|
||||||
|
|
||||||
while base_time > current_section_end {
|
while base_time > curr_section_end {
|
||||||
for skill in skills.iter_mut() {
|
skills.start_new_section_from(curr_section_end);
|
||||||
skill.start_new_section_from(current_section_end);
|
curr_section_end += SECTION_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
current_section_end += SECTION_LEN;
|
skills.process(&h);
|
||||||
}
|
prev_prev = Some(mem::replace(&mut prev, curr));
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
|
||||||
skill.process(&h);
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
|
||||||
prev = curr;
|
|
||||||
|
|
||||||
// Handle all other objects
|
// Handle all other objects
|
||||||
for curr in hit_objects {
|
for curr in hit_objects {
|
||||||
@@ -261,26 +272,16 @@ fn calculate_skills(
|
|||||||
|
|
||||||
let base_time = h.base.time / map_attributes.clock_rate;
|
let base_time = h.base.time / map_attributes.clock_rate;
|
||||||
|
|
||||||
while base_time > current_section_end {
|
while base_time > curr_section_end {
|
||||||
for skill in skills.iter_mut() {
|
skills.save_peak_and_start_new_section(curr_section_end);
|
||||||
skill.save_current_peak();
|
curr_section_end += SECTION_LEN;
|
||||||
skill.start_new_section_from(current_section_end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_section_end += SECTION_LEN;
|
skills.process(&h);
|
||||||
|
prev_prev = Some(mem::replace(&mut prev, curr));
|
||||||
}
|
}
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
skills.save_current_peak();
|
||||||
skill.process(&h);
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
|
||||||
prev = curr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
|
||||||
skill.save_current_peak();
|
|
||||||
}
|
|
||||||
|
|
||||||
let attributes = OsuDifficultyAttributes {
|
let attributes = OsuDifficultyAttributes {
|
||||||
ar: map_attributes.ar,
|
ar: map_attributes.ar,
|
||||||
@@ -445,7 +446,7 @@ fn lerp(start: f64, end: f64, percent: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a difficulty calculation on an osu!standard map.
|
/// The result of a difficulty calculation on an osu!standard map.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct OsuDifficultyAttributes {
|
pub struct OsuDifficultyAttributes {
|
||||||
/// The aim portion of the total strain.
|
/// The aim portion of the total strain.
|
||||||
pub aim_strain: f64,
|
pub aim_strain: f64,
|
||||||
@@ -520,50 +521,3 @@ impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
|
|||||||
fn difficulty_range_od(od: f64) -> f64 {
|
fn difficulty_range_od(od: f64) -> f64 {
|
||||||
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
|
||||||
#[test]
|
|
||||||
// #[ignore]
|
|
||||||
fn custom_osu() {
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use crate::{Beatmap, OsuPP};
|
|
||||||
|
|
||||||
let path = "E:Games/osu!/beatmaps/116169_.osu";
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
let map = Beatmap::from_path(path).unwrap();
|
|
||||||
|
|
||||||
let iters = 100;
|
|
||||||
let accum = start.elapsed();
|
|
||||||
|
|
||||||
// * Tiny benchmark for map parsing
|
|
||||||
// let mut accum = accum;
|
|
||||||
|
|
||||||
// for _ in 0..iters {
|
|
||||||
// let file = File::open(path).unwrap();
|
|
||||||
// let start = Instant::now();
|
|
||||||
// let _map = Beatmap::parse(file).unwrap();
|
|
||||||
// accum += start.elapsed();
|
|
||||||
// }
|
|
||||||
|
|
||||||
println!("Parsing average: {:?}", accum / iters);
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
let result = OsuPP::new(&map).mods(2 + 64).calculate();
|
|
||||||
|
|
||||||
let iters = 100;
|
|
||||||
let accum = start.elapsed();
|
|
||||||
|
|
||||||
// * Tiny benchmark for pp calculation
|
|
||||||
// let mut accum = accum;
|
|
||||||
|
|
||||||
// for _ in 0..iters {
|
|
||||||
// let start = Instant::now();
|
|
||||||
// let _result = OsuPP::new(&map).mods(0).calculate();
|
|
||||||
// accum += start.elapsed();
|
|
||||||
// }
|
|
||||||
|
|
||||||
println!("{:#?}", result);
|
|
||||||
println!("Calculation average: {:?}", accum / iters);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::{
|
|||||||
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
||||||
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) struct OsuObject {
|
pub(crate) struct OsuObject {
|
||||||
pub(crate) time: f64,
|
pub(crate) time: f64,
|
||||||
pub(crate) pos: Pos2,
|
pub(crate) pos: Pos2,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use super::NORMALIZED_RADIUS;
|
|||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) struct ScalingFactor {
|
pub(crate) struct ScalingFactor {
|
||||||
adjusted_factor: f32,
|
adjusted_factor: f32,
|
||||||
factor: f32,
|
factor: f32,
|
||||||
|
|||||||
+103
-10
@@ -1,9 +1,92 @@
|
|||||||
use super::{lerp, skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
|
use super::{lerp, skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::{cmp::Ordering, fmt};
|
||||||
|
|
||||||
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Skills {
|
||||||
|
skills: Box<[Skill]>,
|
||||||
|
mask: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skills {
|
||||||
|
const RX: u8 = 1 << 0;
|
||||||
|
const FL: u8 = 1 << 1;
|
||||||
|
|
||||||
|
pub(crate) fn new(hit_window: f64, rx: bool, radius: f32, fl: bool) -> Self {
|
||||||
|
let mut skills = Vec::with_capacity(2 + !rx as usize + fl as usize);
|
||||||
|
|
||||||
|
skills.push(Skill::aim(true));
|
||||||
|
skills.push(Skill::aim(false));
|
||||||
|
|
||||||
|
if !rx {
|
||||||
|
skills.push(Skill::speed(hit_window));
|
||||||
|
}
|
||||||
|
|
||||||
|
if fl {
|
||||||
|
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
||||||
|
let scaling_factor = 52.0 / radius as f64;
|
||||||
|
skills.push(Skill::flashlight(scaling_factor));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask = rx as u8 * Self::RX + fl as u8 * Self::FL;
|
||||||
|
let skills = skills.into_boxed_slice();
|
||||||
|
|
||||||
|
Self { skills, mask }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start_new_section_from(&mut self, curr_section_end: f64) {
|
||||||
|
for skill in self.skills.iter_mut() {
|
||||||
|
skill.start_new_section_from(curr_section_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_peak_and_start_new_section(&mut self, curr_section_end: f64) {
|
||||||
|
for skill in self.skills.iter_mut() {
|
||||||
|
skill.save_current_peak();
|
||||||
|
skill.start_new_section_from(curr_section_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
|
for skill in self.skills.iter_mut() {
|
||||||
|
skill.save_current_peak();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process(&mut self, h: &DifficultyObject<'_>) {
|
||||||
|
for skill in self.skills.iter_mut() {
|
||||||
|
skill.process(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn aim(&mut self) -> &mut Skill {
|
||||||
|
&mut self.skills[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn aim_no_sliders(&mut self) -> &mut Skill {
|
||||||
|
&mut self.skills[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn speed_flashlight(&mut self) -> (Option<&mut Skill>, Option<&mut Skill>) {
|
||||||
|
match (self.mask & Self::RX, self.mask & Self::FL) {
|
||||||
|
// only speed
|
||||||
|
(0, 0) => (Some(&mut self.skills[2]), None),
|
||||||
|
// both speed and flashlight
|
||||||
|
(0, _) => {
|
||||||
|
let (left, right) = self.skills.split_at_mut(3);
|
||||||
|
|
||||||
|
(Some(&mut left[2]), Some(&mut right[0]))
|
||||||
|
}
|
||||||
|
// neither
|
||||||
|
(_, 0) => (None, None),
|
||||||
|
// only flashlight
|
||||||
|
(_, _) => (None, Some(&mut self.skills[2])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct Skill {
|
pub(crate) struct Skill {
|
||||||
curr_strain: f64,
|
curr_strain: f64,
|
||||||
curr_section_peak: f64,
|
curr_section_peak: f64,
|
||||||
@@ -62,7 +145,7 @@ impl Skill {
|
|||||||
self.curr_section_peak = self.calculate_initial_strain(time);
|
self.curr_section_peak = self.calculate_initial_strain(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
pub(crate) fn difficulty_value(strain_peaks: &mut [f64], this: &Self) -> f64 {
|
||||||
// ? Common values to debug
|
// ? Common values to debug
|
||||||
// println!("---");
|
// println!("---");
|
||||||
|
|
||||||
@@ -72,15 +155,14 @@ impl Skill {
|
|||||||
|
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
let decay_weight = self.kind.decay_weight();
|
let decay_weight = this.kind.decay_weight();
|
||||||
|
|
||||||
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
let (reduced_section_count, difficulty_multiplier) = this.kind.difficulty_values();
|
||||||
let reduced_section_count_f64 = reduced_section_count as f64;
|
let reduced_section_count_f64 = reduced_section_count as f64;
|
||||||
|
|
||||||
self.strain_peaks
|
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
let peaks = self.strain_peaks.iter_mut();
|
let peaks = strain_peaks.iter_mut();
|
||||||
|
|
||||||
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
||||||
let clamped = (i as f64 / reduced_section_count_f64).clamp(0.0, 1.0);
|
let clamped = (i as f64 / reduced_section_count_f64).clamp(0.0, 1.0);
|
||||||
@@ -88,10 +170,9 @@ impl Skill {
|
|||||||
*strain *= lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
*strain *= lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.strain_peaks
|
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
for &strain in self.strain_peaks.iter() {
|
for &strain in strain_peaks.iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= decay_weight;
|
weight *= decay_weight;
|
||||||
}
|
}
|
||||||
@@ -127,3 +208,15 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Skill {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Skill")
|
||||||
|
.field("curr_strain", &self.curr_strain)
|
||||||
|
.field("curr_section_peak", &self.curr_section_peak)
|
||||||
|
.field("kind", &self.kind)
|
||||||
|
.field("strain_peaks_len", &self.strain_peaks.len())
|
||||||
|
.field("prev_time", &self.prev_time)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-1
@@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
f64::consts::{FRAC_PI_2, PI},
|
f64::consts::{FRAC_PI_2, PI},
|
||||||
iter,
|
fmt, iter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::parse::Pos2;
|
use crate::parse::Pos2;
|
||||||
@@ -565,3 +565,13 @@ fn calculate_wide_angle_bonus(angle: f64) -> f64 {
|
|||||||
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
|
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
|
||||||
1.0 - calculate_wide_angle_bonus(angle)
|
1.0 - calculate_wide_angle_bonus(angle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for SkillKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Aim { .. } => f.debug_struct("Aim").finish(),
|
||||||
|
Self::Flashlight { .. } => f.debug_struct("Flashlight").finish(),
|
||||||
|
Self::Speed { .. } => f.debug_struct("Speed").finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user