added OsuDifficultyAttributesIter struct
adjusted changelog
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
- Added method `Beatmap::bpm`
|
||||
- Added method `max_combo` for `DifficultyAttributes`, `PerformanceAttributes`, and all `{Mode}PerformanceAttributes`
|
||||
- [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
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ impl<'p> ControlPointIter<'p> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ControlPoint {
|
||||
Timing { time: f64, beat_len: f64 },
|
||||
Difficulty { time: f64, slider_velocity: f64 },
|
||||
|
||||
@@ -470,33 +470,3 @@ impl From<FruitsPerformanceAttributes> for FruitsDifficultyAttributes {
|
||||
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")]
|
||||
|
||||
mod difficulty_iter;
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
mod pp;
|
||||
@@ -8,6 +9,9 @@ mod skill;
|
||||
mod skill_kind;
|
||||
mod slider_state;
|
||||
|
||||
use std::mem;
|
||||
|
||||
pub use difficulty_iter::OsuDifficultyAttributesIter;
|
||||
use difficulty_object::DifficultyObject;
|
||||
use osu_object::{ObjectParameters, OsuObject};
|
||||
pub use pp::*;
|
||||
@@ -18,6 +22,8 @@ use slider_state::SliderState;
|
||||
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||
|
||||
use self::skill::Skills;
|
||||
|
||||
const SECTION_LEN: f64 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
||||
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 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
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let speed_rating = if mods.rx() {
|
||||
0.0
|
||||
let (speed, flashlight) = skills.speed_flashlight();
|
||||
|
||||
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 {
|
||||
skills[2].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
0.0
|
||||
};
|
||||
|
||||
let flashlight_rating = skills.get_mut(3).map_or(0.0, |skill| {
|
||||
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
});
|
||||
let flashlight_rating = if let Some(flashlight) = flashlight {
|
||||
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 = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
|
||||
|
||||
@@ -79,32 +115,20 @@ pub fn stars(
|
||||
base * base * base / 100_000.0
|
||||
};
|
||||
|
||||
let base_flashlight_performance = if mods.fl() {
|
||||
flashlight_rating * flashlight_rating * 25.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let base_flashlight_performance = flashlight_rating * flashlight_rating * 25.0;
|
||||
|
||||
let base_performance = (base_aim_performance.powf(1.1)
|
||||
+ base_speed_performance.powf(1.1)
|
||||
+ base_flashlight_performance.powf(1.1))
|
||||
.powf(1.0 / 1.1);
|
||||
|
||||
let star_rating = if base_performance > 0.00001 {
|
||||
if base_performance > 0.00001 {
|
||||
1.12_f64.cbrt()
|
||||
* 0.027
|
||||
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
} else {
|
||||
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
|
||||
@@ -117,25 +141,29 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
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 aim_strains = skills.pop().unwrap().strain_peaks; // no sliders
|
||||
let speed_strains = skills.pop().unwrap().strain_peaks;
|
||||
let strains = match tuple {
|
||||
(Some(speed), Some(flashlight)) => {
|
||||
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_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.zip(flashlight_strains)
|
||||
.map(|((aim, speed), flashlight)| aim + speed + flashlight)
|
||||
.collect()
|
||||
} else {
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.map(|(aim, speed)| aim + speed)
|
||||
.collect()
|
||||
aim
|
||||
}
|
||||
(Some(strains), None) | (None, Some(strains)) => {
|
||||
for (aim, strain) in aim.iter_mut().zip(&strains.strain_peaks) {
|
||||
*aim += strain;
|
||||
}
|
||||
|
||||
aim
|
||||
}
|
||||
(None, None) => aim,
|
||||
};
|
||||
|
||||
Strains {
|
||||
@@ -148,7 +176,7 @@ fn calculate_skills(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> Option<(Vec<Skill>, OsuDifficultyAttributes)> {
|
||||
) -> Option<(Skills, OsuDifficultyAttributes)> {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
@@ -203,23 +231,13 @@ fn calculate_skills(
|
||||
h
|
||||
});
|
||||
|
||||
let fl = 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 skills = Skills::new(hit_window, mods.rx(), scaling_factor.radius(), mods.fl());
|
||||
|
||||
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 current_section_end =
|
||||
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
|
||||
@@ -234,20 +252,13 @@ fn calculate_skills(
|
||||
|
||||
let base_time = h.base.time / map_attributes.clock_rate;
|
||||
|
||||
while base_time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
while base_time > curr_section_end {
|
||||
skills.start_new_section_from(curr_section_end);
|
||||
curr_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev = curr;
|
||||
skills.process(&h);
|
||||
prev_prev = Some(mem::replace(&mut prev, curr));
|
||||
|
||||
// Handle all other objects
|
||||
for curr in hit_objects {
|
||||
@@ -261,26 +272,16 @@ fn calculate_skills(
|
||||
|
||||
let base_time = h.base.time / map_attributes.clock_rate;
|
||||
|
||||
while base_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;
|
||||
while base_time > curr_section_end {
|
||||
skills.save_peak_and_start_new_section(curr_section_end);
|
||||
curr_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev = curr;
|
||||
skills.process(&h);
|
||||
prev_prev = Some(mem::replace(&mut prev, curr));
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
skills.save_current_peak();
|
||||
|
||||
let attributes = OsuDifficultyAttributes {
|
||||
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.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct OsuDifficultyAttributes {
|
||||
/// The aim portion of the total strain.
|
||||
pub aim_strain: f64,
|
||||
@@ -520,50 +521,3 @@ impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
|
||||
fn difficulty_range_od(od: f64) -> f64 {
|
||||
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 BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f64,
|
||||
pub(crate) pos: Pos2,
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::NORMALIZED_RADIUS;
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ScalingFactor {
|
||||
adjusted_factor: f32,
|
||||
factor: f32,
|
||||
|
||||
+103
-10
@@ -1,9 +1,92 @@
|
||||
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;
|
||||
|
||||
#[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 {
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
@@ -62,7 +145,7 @@ impl Skill {
|
||||
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
|
||||
// println!("---");
|
||||
|
||||
@@ -72,15 +155,14 @@ impl Skill {
|
||||
|
||||
let mut difficulty = 0.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;
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
strain_peaks.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() {
|
||||
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);
|
||||
}
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
strain_peaks.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;
|
||||
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::{
|
||||
collections::VecDeque,
|
||||
f64::consts::{FRAC_PI_2, PI},
|
||||
iter,
|
||||
fmt, iter,
|
||||
};
|
||||
|
||||
use crate::parse::Pos2;
|
||||
@@ -565,3 +565,13 @@ fn calculate_wide_angle_bonus(angle: f64) -> f64 {
|
||||
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
|
||||
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