added FruitsGradualDifficultyAttributes + bunch of fruits restructuring
This commit is contained in:
+1
-3
@@ -9,15 +9,13 @@
|
|||||||
- 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 `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.
|
||||||
- Fixed incorrect attributes on maps with only 1 or 2 hit objects for all modes
|
- Fixed incorrect attributes on maps with only 1 or 2 hit objects for all modes
|
||||||
- [BREAKING] Replaced field `FruitsDifficultyAttributes::max_combo` by a method with the same name
|
- [BREAKING] Replaced field `FruitsDifficultyAttributes::max_combo` by a method with the same name
|
||||||
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
|
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
|
||||||
- Added `ManiaGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
|
||||||
- Added `ManiaGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `ManiaPP` over and over.
|
- Added `ManiaGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `ManiaPP` over and over.
|
||||||
- Added `TaikoGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
|
||||||
- Added `TaikoGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `TaikoPP` over and over.
|
- Added `TaikoGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `TaikoPP` over and over.
|
||||||
|
- Added structs `{Mode}GradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the mode's `stars` function over and over.
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ macro_rules! next_tuple {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct ControlPointIter<'p> {
|
pub(crate) struct ControlPointIter<'p> {
|
||||||
timing_points: Iter<'p, TimingPoint>,
|
timing_points: Iter<'p, TimingPoint>,
|
||||||
difficulty_points: Iter<'p, DifficultyPoint>,
|
difficulty_points: Iter<'p, DifficultyPoint>,
|
||||||
@@ -37,7 +38,7 @@ impl<'p> ControlPointIter<'p> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Copy, Clone, 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 },
|
||||||
|
|||||||
+2
-2
@@ -8,13 +8,13 @@ const BEZIER_TOLERANCE: f32 = 0.25;
|
|||||||
const CATMULL_DETAIL: usize = 50;
|
const CATMULL_DETAIL: usize = 50;
|
||||||
const CIRCULAR_ARC_TOLERANCE: f32 = 0.1;
|
const CIRCULAR_ARC_TOLERANCE: f32 = 0.1;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct CurveBuffers {
|
pub(crate) struct CurveBuffers {
|
||||||
vertices: Vec<Pos2>,
|
vertices: Vec<Pos2>,
|
||||||
bezier: BezierBuffers,
|
bezier: BezierBuffers,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct BezierBuffers {
|
struct BezierBuffers {
|
||||||
buf1: Vec<Pos2>,
|
buf1: Vec<Pos2>,
|
||||||
buf2: Vec<Pos2>,
|
buf2: Vec<Pos2>,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use crate::parse::Pos2;
|
use crate::parse::Pos2;
|
||||||
|
|
||||||
|
use super::fruit_or_juice::FruitParams;
|
||||||
|
|
||||||
const PLAYFIELD_WIDTH: f32 = 512.0;
|
const PLAYFIELD_WIDTH: f32 = 512.0;
|
||||||
const BASE_SPEED: f64 = 1.0;
|
const BASE_SPEED: f64 = 1.0;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CatchObject {
|
pub struct CatchObject {
|
||||||
pub(crate) pos: f32,
|
pub(crate) pos: f32,
|
||||||
pub(crate) time: f64,
|
pub(crate) time: f64,
|
||||||
@@ -23,11 +25,11 @@ impl CatchObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f64) -> Self {
|
pub(crate) fn with_hr(mut self, params: &mut FruitParams<'_>) -> Self {
|
||||||
let mut offset_pos = self.pos;
|
let mut offset_pos = self.pos;
|
||||||
let time_diff = self.time - *last_time;
|
let time_diff = self.time - params.last_time;
|
||||||
|
|
||||||
if let Some(last_pos_ref) = last_pos.filter(|_| time_diff <= 1000.0) {
|
if let Some(last_pos_ref) = params.last_pos.filter(|_| time_diff <= 1000.0) {
|
||||||
let pos_diff = offset_pos - last_pos_ref;
|
let pos_diff = offset_pos - last_pos_ref;
|
||||||
|
|
||||||
if pos_diff.abs() > f32::EPSILON {
|
if pos_diff.abs() > f32::EPSILON {
|
||||||
@@ -41,14 +43,14 @@ impl CatchObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
last_pos.replace(offset_pos);
|
params.last_pos.replace(offset_pos);
|
||||||
*last_time = self.time;
|
params.last_time = self.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pos = offset_pos;
|
self.pos = offset_pos;
|
||||||
} else {
|
} else {
|
||||||
last_pos.replace(offset_pos);
|
params.last_pos.replace(offset_pos);
|
||||||
*last_time = self.time;
|
params.last_time = self.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
use std::{iter::Map, vec::IntoIter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
curve::{Curve, CurveBuffers},
|
||||||
|
parse::{HitObject, HitObjectKind, Pos2},
|
||||||
|
Beatmap,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{catch_object::CatchObject, slider_state::SliderState, FruitsDifficultyAttributes};
|
||||||
|
|
||||||
|
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
||||||
|
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct FruitParams<'a> {
|
||||||
|
pub(crate) attributes: FruitsDifficultyAttributes,
|
||||||
|
pub(crate) curve_bufs: CurveBuffers,
|
||||||
|
pub(crate) last_pos: Option<f32>,
|
||||||
|
pub(crate) last_time: f64,
|
||||||
|
pub(crate) map: &'a Beatmap,
|
||||||
|
pub(crate) slider_state: SliderState<'a>,
|
||||||
|
pub(crate) ticks: Vec<(Pos2, f64)>,
|
||||||
|
pub(crate) with_hr: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
type JuiceStream = Map<IntoIter<(Pos2, f64)>, fn((Pos2, f64)) -> CatchObject>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum FruitOrJuice {
|
||||||
|
Fruit(Option<CatchObject>),
|
||||||
|
Juice(JuiceStream),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FruitOrJuice {
|
||||||
|
pub(crate) fn new(h: &HitObject, params: &mut FruitParams<'_>) -> Option<Self> {
|
||||||
|
match &h.kind {
|
||||||
|
HitObjectKind::Circle => {
|
||||||
|
let mut h = CatchObject::new((h.pos, h.start_time));
|
||||||
|
|
||||||
|
if params.with_hr {
|
||||||
|
h = h.with_hr(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.attributes.n_fruits += 1;
|
||||||
|
|
||||||
|
Some(FruitOrJuice::Fruit(Some(h)))
|
||||||
|
}
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
pixel_len,
|
||||||
|
repeats,
|
||||||
|
control_points,
|
||||||
|
} => {
|
||||||
|
// HR business
|
||||||
|
params.last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x);
|
||||||
|
params.last_time = h.start_time;
|
||||||
|
|
||||||
|
// Responsible for timing point values
|
||||||
|
params.slider_state.update(h.start_time);
|
||||||
|
|
||||||
|
let span_count = (*repeats + 1) as f64;
|
||||||
|
|
||||||
|
let mut tick_dist = 100.0 * params.map.slider_mult / params.map.tick_rate;
|
||||||
|
|
||||||
|
if params.map.version >= 8 {
|
||||||
|
tick_dist /= (100.0 / params.slider_state.slider_velocity)
|
||||||
|
.max(10.0)
|
||||||
|
.min(1000.0)
|
||||||
|
/ 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the curve w.r.t. the control points
|
||||||
|
let curve = Curve::new(control_points, *pixel_len, &mut params.curve_bufs);
|
||||||
|
|
||||||
|
let velocity = (BASE_SCORING_DISTANCE
|
||||||
|
* params.map.slider_mult
|
||||||
|
* params.slider_state.slider_velocity)
|
||||||
|
/ params.slider_state.beat_len;
|
||||||
|
|
||||||
|
let end_time = h.start_time + span_count * curve.dist() / velocity;
|
||||||
|
let duration = end_time - h.start_time;
|
||||||
|
let span_duration = duration / span_count;
|
||||||
|
|
||||||
|
// * A very lenient maximum length of a slider for ticks to be generated.
|
||||||
|
// * This exists for edge cases such as /b/1573664 where the beatmap has
|
||||||
|
// * been edited by the user, and should never be reached in normal usage.
|
||||||
|
let max_len = 100_000.0;
|
||||||
|
|
||||||
|
let len = curve.dist().min(max_len);
|
||||||
|
tick_dist = tick_dist.clamp(0.0, len);
|
||||||
|
let min_dist_from_end = velocity * 10.0;
|
||||||
|
|
||||||
|
let mut curr_dist = tick_dist;
|
||||||
|
let time_add = duration * tick_dist / (*pixel_len * span_count);
|
||||||
|
|
||||||
|
let target = *pixel_len - tick_dist / 8.0;
|
||||||
|
|
||||||
|
params.ticks.reserve((target / tick_dist) as usize);
|
||||||
|
|
||||||
|
// Tick of the first span
|
||||||
|
while curr_dist < len - min_dist_from_end {
|
||||||
|
let progress = curr_dist / len;
|
||||||
|
let pos = h.pos + curve.position_at(progress);
|
||||||
|
let time = h.start_time + progress * span_duration;
|
||||||
|
params.ticks.push((pos, time));
|
||||||
|
curr_dist += tick_dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.attributes.n_tiny_droplets += tiny_droplet_count(
|
||||||
|
h.start_time,
|
||||||
|
time_add,
|
||||||
|
duration,
|
||||||
|
span_count as usize,
|
||||||
|
¶ms.ticks,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut slider_objects =
|
||||||
|
Vec::with_capacity(span_count as usize * (params.ticks.len() + 1));
|
||||||
|
slider_objects.push((h.pos, h.start_time));
|
||||||
|
|
||||||
|
// Other spans
|
||||||
|
if *repeats == 0 {
|
||||||
|
slider_objects.append(&mut params.ticks); // automatically empties buffer for next slider
|
||||||
|
} else {
|
||||||
|
slider_objects.extend(¶ms.ticks);
|
||||||
|
|
||||||
|
for span_idx in 1..=*repeats {
|
||||||
|
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||||
|
let pos = h.pos + curve.position_at(progress);
|
||||||
|
let time_offset = span_duration * span_idx as f64;
|
||||||
|
|
||||||
|
// Reverse tick
|
||||||
|
slider_objects.push((pos, h.start_time + time_offset));
|
||||||
|
|
||||||
|
let new_ticks = params.ticks.iter().enumerate().map(|(i, (pos, time))| {
|
||||||
|
(*pos, *time + time_offset + time_add * i as f64)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actual ticks
|
||||||
|
if span_idx & 1 == 1 {
|
||||||
|
slider_objects.extend(new_ticks.rev());
|
||||||
|
} else {
|
||||||
|
slider_objects.extend(new_ticks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.ticks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slider tail
|
||||||
|
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||||
|
let pos = h.pos + curve.position_at(progress);
|
||||||
|
slider_objects.push((pos, h.start_time + duration));
|
||||||
|
|
||||||
|
let new_fruits = *repeats + 2;
|
||||||
|
params.attributes.n_fruits += new_fruits;
|
||||||
|
params.attributes.n_droplets += slider_objects.len() - new_fruits;
|
||||||
|
|
||||||
|
let iter = slider_objects
|
||||||
|
.into_iter()
|
||||||
|
.map(CatchObject::new as fn(_) -> _);
|
||||||
|
|
||||||
|
Some(FruitOrJuice::Juice(iter))
|
||||||
|
}
|
||||||
|
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for FruitOrJuice {
|
||||||
|
type Item = CatchObject;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self {
|
||||||
|
Self::Fruit(fruit) => fruit.take(),
|
||||||
|
Self::Juice(slider) => slider.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let len = self.len();
|
||||||
|
|
||||||
|
(len, Some(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for FruitOrJuice {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
FruitOrJuice::Fruit(Some(_)) => 1,
|
||||||
|
FruitOrJuice::Fruit(None) => 0,
|
||||||
|
FruitOrJuice::Juice(slider) => slider.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUG: Sometimes there are off-by-one errors,
|
||||||
|
// presumably caused by floating point inaccuracies
|
||||||
|
fn tiny_droplet_count(
|
||||||
|
start_time: f64,
|
||||||
|
time_between_ticks: f64,
|
||||||
|
duration: f64,
|
||||||
|
span_count: usize,
|
||||||
|
ticks: &[(Pos2, f64)],
|
||||||
|
) -> usize {
|
||||||
|
// tiny droplets preceeding a _tick_
|
||||||
|
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
|
||||||
|
let time_between_tiny = shrink_down(time_between_ticks);
|
||||||
|
|
||||||
|
// add a little for floating point inaccuracies
|
||||||
|
let start = time_between_tiny + 0.001;
|
||||||
|
|
||||||
|
count_iterations(start, time_between_tiny, time_between_ticks)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// tiny droplets preceeding a _reverse_
|
||||||
|
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||||
|
let repeat_time = start_time + duration / span_count as f64;
|
||||||
|
let since_last_tick = repeat_time - last;
|
||||||
|
|
||||||
|
let span_last_section = if since_last_tick > 80.0 {
|
||||||
|
let time_between_tiny = shrink_down(since_last_tick);
|
||||||
|
|
||||||
|
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// tiny droplets preceeding the slider tail
|
||||||
|
// necessary to handle distinctly because of the legacy last tick
|
||||||
|
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||||
|
let end_time = start_time + duration / span_count as f64 - LEGACY_LAST_TICK_OFFSET;
|
||||||
|
let since_last_tick = end_time - last;
|
||||||
|
|
||||||
|
let last_section = if since_last_tick > 80.0 {
|
||||||
|
let time_between_tiny = shrink_down(since_last_tick);
|
||||||
|
|
||||||
|
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine tiny droplets counts
|
||||||
|
per_tick * ticks.len() * span_count
|
||||||
|
+ span_last_section * (span_count.saturating_sub(1))
|
||||||
|
+ last_section
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn shrink_down(mut val: f64) -> f64 {
|
||||||
|
while val > 100.0 {
|
||||||
|
val /= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
val
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn count_iterations(mut start: f64, step: f64, end: f64) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
while start < end {
|
||||||
|
count += 1;
|
||||||
|
start += step;
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
use std::{iter, slice::Iter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
curve::CurveBuffers,
|
||||||
|
fruits::{
|
||||||
|
difficulty_object::DifficultyObject, slider_state::SliderState, SECTION_LENGTH,
|
||||||
|
STAR_SCALING_FACTOR,
|
||||||
|
},
|
||||||
|
parse::{HitObject, Pos2},
|
||||||
|
Beatmap, Mods,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
calculate_catch_width,
|
||||||
|
catch_object::CatchObject,
|
||||||
|
fruit_or_juice::{FruitOrJuice, FruitParams},
|
||||||
|
movement::Movement,
|
||||||
|
FruitsDifficultyAttributes, ALLOWED_CATCH_RANGE,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Gradually calculate the difficulty attributes of an osu!ctb map.
|
||||||
|
///
|
||||||
|
/// 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 [`FruitsDifficultyAttributes`] will be updated and returned.
|
||||||
|
///
|
||||||
|
/// If you want to calculate performance attributes, use
|
||||||
|
/// [`FruitsGradualPerformanceAttributes`](crate::fruits::FruitsGradualPerformanceAttributes) instead.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use rosu_pp::{Beatmap, fruits::FruitsGradualDifficultyAttributes};
|
||||||
|
///
|
||||||
|
/// # /*
|
||||||
|
/// let map: Beatmap = ...
|
||||||
|
/// # */
|
||||||
|
/// # let map = Beatmap::default();
|
||||||
|
///
|
||||||
|
/// let mods = 64; // DT
|
||||||
|
/// let mut iter = FruitsGradualDifficultyAttributes::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(Clone, Debug)]
|
||||||
|
pub struct FruitsGradualDifficultyAttributes<'map> {
|
||||||
|
pub(crate) idx: usize,
|
||||||
|
clock_rate: f64,
|
||||||
|
hit_objects: FruitsObjectIter<'map>,
|
||||||
|
movement: Movement,
|
||||||
|
prev: CatchObject,
|
||||||
|
half_catcher_width: f64,
|
||||||
|
last_direction: i8,
|
||||||
|
last_excess: f64,
|
||||||
|
curr_section_end: f64,
|
||||||
|
strain_peak_buf: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> FruitsGradualDifficultyAttributes<'map> {
|
||||||
|
/// Create a new difficulty attributes iterator for osu!ctb maps.
|
||||||
|
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
||||||
|
let map_attributes = map.attributes().mods(mods);
|
||||||
|
|
||||||
|
let attributes = FruitsDifficultyAttributes {
|
||||||
|
ar: map_attributes.ar,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let hit_objects = FruitsObjectIter::new(map, mods, attributes);
|
||||||
|
|
||||||
|
let half_catcher_width =
|
||||||
|
(calculate_catch_width(map_attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||||
|
let last_direction = 0;
|
||||||
|
let last_excess = half_catcher_width;
|
||||||
|
|
||||||
|
let movement = Movement::new(map_attributes.cs as f32);
|
||||||
|
let prev = CatchObject::new((Pos2::zero(), 0.0));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
idx: 0,
|
||||||
|
clock_rate: mods.speed(),
|
||||||
|
hit_objects,
|
||||||
|
movement,
|
||||||
|
prev,
|
||||||
|
half_catcher_width,
|
||||||
|
last_direction,
|
||||||
|
last_excess,
|
||||||
|
curr_section_end: 0.0,
|
||||||
|
strain_peak_buf: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_hyper_dash(&mut self, next: &CatchObject) {
|
||||||
|
self.prev.init_hyper_dash(
|
||||||
|
self.half_catcher_width,
|
||||||
|
next,
|
||||||
|
&mut self.last_direction,
|
||||||
|
&mut self.last_excess,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for FruitsGradualDifficultyAttributes<'_> {
|
||||||
|
type Item = FruitsDifficultyAttributes;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let curr = self.hit_objects.next()?;
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
|
if self.idx == 1 {
|
||||||
|
self.prev = curr;
|
||||||
|
|
||||||
|
return Some(self.hit_objects.attributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init_hyper_dash(&curr);
|
||||||
|
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&self.prev,
|
||||||
|
self.movement.half_catcher_width,
|
||||||
|
self.clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.idx == 2 {
|
||||||
|
self.curr_section_end =
|
||||||
|
(h.base.time / self.clock_rate / SECTION_LENGTH).ceil() * SECTION_LENGTH;
|
||||||
|
} else {
|
||||||
|
let base_time = h.base.time / self.clock_rate;
|
||||||
|
|
||||||
|
while base_time > self.curr_section_end {
|
||||||
|
self.movement.save_current_peak();
|
||||||
|
self.movement.start_new_section_from(self.curr_section_end);
|
||||||
|
self.curr_section_end += SECTION_LENGTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.movement.process(&h);
|
||||||
|
self.prev = curr;
|
||||||
|
|
||||||
|
let len = self.movement.strain_peaks.len();
|
||||||
|
let missing = len + 1 - self.strain_peak_buf.len();
|
||||||
|
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
|
||||||
|
|
||||||
|
self.strain_peak_buf[..len].copy_from_slice(&self.movement.strain_peaks);
|
||||||
|
|
||||||
|
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||||
|
*last = self.movement.curr_section_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut attributes = self.hit_objects.attributes();
|
||||||
|
attributes.stars =
|
||||||
|
Movement::difficulty_value(&mut self.strain_peak_buf).sqrt() * STAR_SCALING_FACTOR;
|
||||||
|
|
||||||
|
Some(attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct FruitsObjectIter<'map> {
|
||||||
|
last_object: Option<FruitOrJuice>,
|
||||||
|
hit_objects: Iter<'map, HitObject>,
|
||||||
|
params: FruitParams<'map>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> FruitsObjectIter<'map> {
|
||||||
|
fn new(map: &'map Beatmap, mods: impl Mods, attributes: FruitsDifficultyAttributes) -> Self {
|
||||||
|
let params = FruitParams {
|
||||||
|
attributes,
|
||||||
|
curve_bufs: CurveBuffers::default(),
|
||||||
|
last_pos: None,
|
||||||
|
last_time: 0.0,
|
||||||
|
map,
|
||||||
|
slider_state: SliderState::new(map),
|
||||||
|
ticks: Vec::new(),
|
||||||
|
with_hr: mods.hr(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
last_object: None,
|
||||||
|
hit_objects: map.hit_objects.iter(),
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attributes(&self) -> FruitsDifficultyAttributes {
|
||||||
|
self.params.attributes.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for FruitsObjectIter<'_> {
|
||||||
|
type Item = CatchObject;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if let Some(h) = self.last_object.as_mut().and_then(Iterator::next) {
|
||||||
|
return Some(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(h) = self.hit_objects.next() {
|
||||||
|
if let Some(h) = FruitOrJuice::new(h, &mut self.params) {
|
||||||
|
return self.last_object.insert(h).next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_map() {
|
||||||
|
let map = Beatmap::default();
|
||||||
|
let mut attributes = FruitsGradualDifficultyAttributes::new(&map, 0);
|
||||||
|
assert!(attributes.next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn iter_end_eq_regular() {
|
||||||
|
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let regular = crate::fruits::stars(&map, mods, None);
|
||||||
|
|
||||||
|
let iter_end = FruitsGradualDifficultyAttributes::new(&map, mods)
|
||||||
|
.last()
|
||||||
|
.expect("empty iter");
|
||||||
|
|
||||||
|
assert_eq!(regular, iter_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-287
@@ -2,21 +2,21 @@
|
|||||||
|
|
||||||
mod catch_object;
|
mod catch_object;
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
|
mod fruit_or_juice;
|
||||||
|
mod gradual_difficulty;
|
||||||
mod movement;
|
mod movement;
|
||||||
mod pp;
|
mod pp;
|
||||||
mod slider_state;
|
mod slider_state;
|
||||||
|
|
||||||
use catch_object::CatchObject;
|
use catch_object::CatchObject;
|
||||||
use difficulty_object::DifficultyObject;
|
use difficulty_object::DifficultyObject;
|
||||||
|
use fruit_or_juice::FruitOrJuice;
|
||||||
|
pub use gradual_difficulty::*;
|
||||||
use movement::Movement;
|
use movement::Movement;
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{
|
use crate::{curve::CurveBuffers, fruits::fruit_or_juice::FruitParams, Beatmap, Mods, Strains};
|
||||||
curve::{Curve, CurveBuffers},
|
|
||||||
parse::{HitObjectKind, Pos2},
|
|
||||||
Beatmap, Mods, Strains,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SECTION_LENGTH: f64 = 750.0;
|
const SECTION_LENGTH: f64 = 750.0;
|
||||||
const STAR_SCALING_FACTOR: f64 = 0.153;
|
const STAR_SCALING_FACTOR: f64 = 0.153;
|
||||||
@@ -24,9 +24,6 @@ const STAR_SCALING_FACTOR: f64 = 0.153;
|
|||||||
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
||||||
const CATCHER_SIZE: f32 = 106.75;
|
const CATCHER_SIZE: f32 = 106.75;
|
||||||
|
|
||||||
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
|
||||||
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
|
||||||
|
|
||||||
/// Difficulty calculation for osu!ctb maps.
|
/// Difficulty calculation for osu!ctb maps.
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||||
@@ -36,7 +33,8 @@ pub fn stars(
|
|||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
) -> FruitsDifficultyAttributes {
|
) -> FruitsDifficultyAttributes {
|
||||||
let (mut movement, mut attributes) = calculate_movement(map, mods, passed_objects);
|
let (mut movement, mut attributes) = calculate_movement(map, mods, passed_objects);
|
||||||
attributes.stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
attributes.stars =
|
||||||
|
Movement::difficulty_value(&mut movement.strain_peaks).sqrt() * STAR_SCALING_FACTOR;
|
||||||
|
|
||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
@@ -62,146 +60,28 @@ fn calculate_movement(
|
|||||||
let take = passed_objects.unwrap_or(usize::MAX);
|
let take = passed_objects.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_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);
|
|
||||||
let mut curve_bufs = CurveBuffers::default();
|
|
||||||
|
|
||||||
let mut attributes = FruitsDifficultyAttributes {
|
let attributes = FruitsDifficultyAttributes {
|
||||||
ar: map_attributes.ar,
|
ar: map_attributes.ar,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut params = FruitParams {
|
||||||
|
attributes,
|
||||||
|
curve_bufs: CurveBuffers::default(),
|
||||||
|
last_pos: None,
|
||||||
|
last_time: 0.0,
|
||||||
|
map,
|
||||||
|
slider_state: SliderState::new(map),
|
||||||
|
ticks: Vec::new(), // using the same buffer for all sliders
|
||||||
|
with_hr: mods.hr(),
|
||||||
|
};
|
||||||
|
|
||||||
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
||||||
let mut hit_objects = map
|
let mut hit_objects = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
|
.filter_map(|h| FruitOrJuice::new(h, &mut params))
|
||||||
HitObjectKind::Circle => {
|
|
||||||
let mut h = CatchObject::new((h.pos, h.start_time));
|
|
||||||
|
|
||||||
if with_hr {
|
|
||||||
h = h.with_hr(last_pos, last_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.n_fruits += 1;
|
|
||||||
|
|
||||||
Some(Some(FruitOrJuice::Fruit(Some(h))))
|
|
||||||
}
|
|
||||||
HitObjectKind::Slider {
|
|
||||||
pixel_len,
|
|
||||||
repeats,
|
|
||||||
control_points,
|
|
||||||
} => {
|
|
||||||
// HR business
|
|
||||||
*last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x);
|
|
||||||
*last_time = h.start_time;
|
|
||||||
|
|
||||||
// Responsible for timing point values
|
|
||||||
slider_state.update(h.start_time);
|
|
||||||
|
|
||||||
let span_count = (*repeats + 1) as f64;
|
|
||||||
|
|
||||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
|
||||||
|
|
||||||
if map.version >= 8 {
|
|
||||||
tick_dist /=
|
|
||||||
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the curve w.r.t. the control points
|
|
||||||
let curve = Curve::new(control_points, *pixel_len, &mut curve_bufs);
|
|
||||||
|
|
||||||
let velocity =
|
|
||||||
(BASE_SCORING_DISTANCE * map.slider_mult * slider_state.slider_velocity)
|
|
||||||
/ slider_state.beat_len;
|
|
||||||
|
|
||||||
let end_time = h.start_time + span_count * curve.dist() / velocity;
|
|
||||||
let duration = end_time - h.start_time;
|
|
||||||
let span_duration = duration / span_count;
|
|
||||||
|
|
||||||
// * A very lenient maximum length of a slider for ticks to be generated.
|
|
||||||
// * This exists for edge cases such as /b/1573664 where the beatmap has
|
|
||||||
// * been edited by the user, and should never be reached in normal usage.
|
|
||||||
let max_len = 100_000.0;
|
|
||||||
|
|
||||||
let len = curve.dist().min(max_len);
|
|
||||||
tick_dist = tick_dist.clamp(0.0, len);
|
|
||||||
let min_dist_from_end = velocity * 10.0;
|
|
||||||
|
|
||||||
let mut curr_dist = tick_dist;
|
|
||||||
let time_add = duration * tick_dist / (*pixel_len * span_count);
|
|
||||||
|
|
||||||
let target = *pixel_len - tick_dist / 8.0;
|
|
||||||
|
|
||||||
ticks.reserve((target / tick_dist) as usize);
|
|
||||||
|
|
||||||
// Tick of the first span
|
|
||||||
while curr_dist < len - min_dist_from_end {
|
|
||||||
let progress = curr_dist / len;
|
|
||||||
let pos = h.pos + curve.position_at(progress);
|
|
||||||
let time = h.start_time + progress * span_duration;
|
|
||||||
ticks.push((pos, time));
|
|
||||||
curr_dist += tick_dist;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.n_tiny_droplets += tiny_droplet_count(
|
|
||||||
h.start_time,
|
|
||||||
time_add,
|
|
||||||
duration,
|
|
||||||
span_count as usize,
|
|
||||||
&ticks,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut slider_objects =
|
|
||||||
Vec::with_capacity(span_count as usize * (ticks.len() + 1));
|
|
||||||
slider_objects.push((h.pos, h.start_time));
|
|
||||||
|
|
||||||
// Other spans
|
|
||||||
if *repeats == 0 {
|
|
||||||
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
|
|
||||||
} else {
|
|
||||||
slider_objects.extend(&ticks);
|
|
||||||
|
|
||||||
for span_idx in 1..=*repeats {
|
|
||||||
let progress = (span_idx % 2 == 1) as u8 as f64;
|
|
||||||
let pos = h.pos + curve.position_at(progress);
|
|
||||||
let time_offset = span_duration * span_idx as f64;
|
|
||||||
|
|
||||||
// Reverse tick
|
|
||||||
slider_objects.push((pos, h.start_time + time_offset));
|
|
||||||
|
|
||||||
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
|
||||||
(*pos, *time + time_offset + time_add * i as f64)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Actual ticks
|
|
||||||
if span_idx & 1 == 1 {
|
|
||||||
slider_objects.extend(new_ticks.rev());
|
|
||||||
} else {
|
|
||||||
slider_objects.extend(new_ticks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ticks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slider tail
|
|
||||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
|
||||||
let pos = h.pos + curve.position_at(progress);
|
|
||||||
slider_objects.push((pos, h.start_time + duration));
|
|
||||||
|
|
||||||
let new_fruits = *repeats + 2;
|
|
||||||
attributes.n_fruits += new_fruits;
|
|
||||||
attributes.n_droplets += slider_objects.len() - new_fruits;
|
|
||||||
|
|
||||||
let iter = slider_objects.into_iter().map(CatchObject::new);
|
|
||||||
|
|
||||||
Some(Some(FruitOrJuice::Juice(iter)))
|
|
||||||
}
|
|
||||||
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => Some(None),
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.flatten()
|
.flatten()
|
||||||
.take(take);
|
.take(take);
|
||||||
|
|
||||||
@@ -213,16 +93,15 @@ fn calculate_movement(
|
|||||||
|
|
||||||
// Strain business
|
// Strain business
|
||||||
let mut movement = Movement::new(map_attributes.cs as f32);
|
let mut movement = Movement::new(map_attributes.cs as f32);
|
||||||
let section_len = SECTION_LENGTH * map_attributes.clock_rate;
|
|
||||||
|
|
||||||
let (mut prev, mut curr) = match (hit_objects.next(), hit_objects.next()) {
|
let (mut prev, curr) = match (hit_objects.next(), hit_objects.next()) {
|
||||||
(Some(prev), Some(curr)) => (prev, curr),
|
(Some(prev), Some(curr)) => (prev, curr),
|
||||||
(Some(_), None) | (None, None) => return (movement, attributes),
|
(Some(_), None) | (None, None) => return (movement, params.attributes),
|
||||||
(None, Some(_)) => unreachable!(),
|
(None, Some(_)) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: time of second second object instead?
|
let mut curr_section_end =
|
||||||
let mut curr_section_end = (prev.time / section_len).ceil() * section_len;
|
(curr.time / map_attributes.clock_rate / SECTION_LENGTH).ceil() * SECTION_LENGTH;
|
||||||
|
|
||||||
prev.init_hyper_dash(
|
prev.init_hyper_dash(
|
||||||
half_catcher_width,
|
half_catcher_width,
|
||||||
@@ -231,15 +110,7 @@ fn calculate_movement(
|
|||||||
&mut last_excess,
|
&mut last_excess,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle second object separately to remove later if-branching
|
// Handle first object distinctly
|
||||||
let next = hit_objects.next().unwrap();
|
|
||||||
curr.init_hyper_dash(
|
|
||||||
half_catcher_width,
|
|
||||||
&next,
|
|
||||||
&mut last_direction,
|
|
||||||
&mut last_excess,
|
|
||||||
);
|
|
||||||
|
|
||||||
let h = DifficultyObject::new(
|
let h = DifficultyObject::new(
|
||||||
&curr,
|
&curr,
|
||||||
&prev,
|
&prev,
|
||||||
@@ -247,20 +118,14 @@ fn calculate_movement(
|
|||||||
map_attributes.clock_rate,
|
map_attributes.clock_rate,
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > curr_section_end {
|
|
||||||
curr_section_end += section_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
movement.process(&h);
|
movement.process(&h);
|
||||||
|
|
||||||
prev = curr;
|
prev = curr;
|
||||||
curr = next;
|
|
||||||
|
|
||||||
// Handle all other objects
|
// Handle all other objects
|
||||||
for next in hit_objects {
|
for curr in hit_objects {
|
||||||
curr.init_hyper_dash(
|
prev.init_hyper_dash(
|
||||||
half_catcher_width,
|
half_catcher_width,
|
||||||
&next,
|
&curr,
|
||||||
&mut last_direction,
|
&mut last_direction,
|
||||||
&mut last_excess,
|
&mut last_excess,
|
||||||
);
|
);
|
||||||
@@ -272,112 +137,21 @@ fn calculate_movement(
|
|||||||
map_attributes.clock_rate,
|
map_attributes.clock_rate,
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > curr_section_end {
|
let base_time = h.base.time / map_attributes.clock_rate;
|
||||||
|
|
||||||
|
while base_time > curr_section_end {
|
||||||
movement.save_current_peak();
|
movement.save_current_peak();
|
||||||
movement.start_new_section_from(curr_section_end / map_attributes.clock_rate);
|
movement.start_new_section_from(curr_section_end);
|
||||||
curr_section_end += section_len;
|
curr_section_end += SECTION_LENGTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
movement.process(&h);
|
movement.process(&h);
|
||||||
|
|
||||||
prev = curr;
|
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,
|
|
||||||
map_attributes.clock_rate,
|
|
||||||
);
|
|
||||||
|
|
||||||
while h.base.time > curr_section_end {
|
|
||||||
movement.save_current_peak();
|
|
||||||
movement.start_new_section_from(curr_section_end / map_attributes.clock_rate);
|
|
||||||
|
|
||||||
curr_section_end += section_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
movement.process(&h);
|
|
||||||
movement.save_current_peak();
|
movement.save_current_peak();
|
||||||
|
|
||||||
(movement, attributes)
|
(movement, params.attributes)
|
||||||
}
|
|
||||||
|
|
||||||
// BUG: Sometimes there are off-by-one errors,
|
|
||||||
// presumably caused by floating point inaccuracies
|
|
||||||
fn tiny_droplet_count(
|
|
||||||
start_time: f64,
|
|
||||||
time_between_ticks: f64,
|
|
||||||
duration: f64,
|
|
||||||
span_count: usize,
|
|
||||||
ticks: &[(Pos2, f64)],
|
|
||||||
) -> usize {
|
|
||||||
// tiny droplets preceeding a _tick_
|
|
||||||
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
|
|
||||||
let time_between_tiny = shrink_down(time_between_ticks);
|
|
||||||
|
|
||||||
// add a little for floating point inaccuracies
|
|
||||||
let start = time_between_tiny + 0.001;
|
|
||||||
|
|
||||||
count_iterations(start, time_between_tiny, time_between_ticks)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// tiny droplets preceeding a _reverse_
|
|
||||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
|
||||||
let repeat_time = start_time + duration / span_count as f64;
|
|
||||||
let since_last_tick = repeat_time - last;
|
|
||||||
|
|
||||||
let span_last_section = if since_last_tick > 80.0 {
|
|
||||||
let time_between_tiny = shrink_down(since_last_tick);
|
|
||||||
|
|
||||||
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// tiny droplets preceeding the slider tail
|
|
||||||
// necessary to handle distinctly because of the legacy last tick
|
|
||||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
|
||||||
let end_time = start_time + duration / span_count as f64 - LEGACY_LAST_TICK_OFFSET;
|
|
||||||
let since_last_tick = end_time - last;
|
|
||||||
|
|
||||||
let last_section = if since_last_tick > 80.0 {
|
|
||||||
let time_between_tiny = shrink_down(since_last_tick);
|
|
||||||
|
|
||||||
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Combine tiny droplets counts
|
|
||||||
per_tick * ticks.len() * span_count
|
|
||||||
+ span_last_section * (span_count.saturating_sub(1))
|
|
||||||
+ last_section
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn shrink_down(mut val: f64) -> f64 {
|
|
||||||
while val > 100.0 {
|
|
||||||
val /= 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
val
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn count_iterations(mut start: f64, step: f64, end: f64) -> usize {
|
|
||||||
let mut count = 0;
|
|
||||||
|
|
||||||
while start < end {
|
|
||||||
count += 1;
|
|
||||||
start += step;
|
|
||||||
}
|
|
||||||
|
|
||||||
count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -387,34 +161,8 @@ pub(crate) fn calculate_catch_width(cs: f32) -> f32 {
|
|||||||
CATCHER_SIZE * scale.abs() * ALLOWED_CATCH_RANGE
|
CATCHER_SIZE * scale.abs() * ALLOWED_CATCH_RANGE
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FruitOrJuice<I> {
|
|
||||||
Fruit(Option<CatchObject>),
|
|
||||||
Juice(I),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
|
|
||||||
type Item = CatchObject;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
match self {
|
|
||||||
Self::Fruit(fruit) => fruit.take(),
|
|
||||||
Self::Juice(slider) => slider.next(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
||||||
match self {
|
|
||||||
Self::Fruit(Some(_)) => (1, Some(1)),
|
|
||||||
Self::Fruit(None) => (0, Some(0)),
|
|
||||||
Self::Juice(slider) => slider.size_hint(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of a difficulty calculation on an osu!ctb map.
|
/// The result of a difficulty calculation on an osu!ctb map.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct FruitsDifficultyAttributes {
|
pub struct FruitsDifficultyAttributes {
|
||||||
/// The final star rating
|
/// The final star rating
|
||||||
pub stars: f64,
|
pub stars: f64,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const SKILL_MULTIPLIER: f64 = 900.0;
|
|||||||
const STRAIN_DECAY_BASE: f64 = 0.2;
|
const STRAIN_DECAY_BASE: f64 = 0.2;
|
||||||
const DECAY_WEIGHT: f64 = 0.94;
|
const DECAY_WEIGHT: f64 = 0.94;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Movement {
|
pub(crate) struct Movement {
|
||||||
pub(crate) half_catcher_width: f32,
|
pub(crate) half_catcher_width: f32,
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ pub(crate) struct Movement {
|
|||||||
last_strain_time: f64,
|
last_strain_time: f64,
|
||||||
|
|
||||||
current_strain: f64,
|
current_strain: f64,
|
||||||
current_section_peak: f64,
|
pub(crate) curr_section_peak: f64,
|
||||||
|
|
||||||
pub(crate) strain_peaks: Vec<f64>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
prev_time: Option<f64>,
|
prev_time: Option<f64>,
|
||||||
@@ -38,7 +39,7 @@ impl Movement {
|
|||||||
last_strain_time: 0.0,
|
last_strain_time: 0.0,
|
||||||
|
|
||||||
current_strain: 1.0,
|
current_strain: 1.0,
|
||||||
current_section_peak: 1.0,
|
curr_section_peak: 1.0,
|
||||||
|
|
||||||
strain_peaks: Vec::with_capacity(128),
|
strain_peaks: Vec::with_capacity(128),
|
||||||
prev_time: None,
|
prev_time: None,
|
||||||
@@ -47,29 +48,28 @@ impl Movement {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
self.strain_peaks.push(self.current_section_peak);
|
self.strain_peaks.push(self.curr_section_peak);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
self.curr_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process(&mut self, current: &DifficultyObject<'_>) {
|
pub(crate) fn process(&mut self, current: &DifficultyObject<'_>) {
|
||||||
self.current_strain *= strain_decay(current.delta);
|
self.current_strain *= strain_decay(current.delta);
|
||||||
self.current_strain += self.strain_value_of(current) * SKILL_MULTIPLIER;
|
self.current_strain += self.strain_value_of(current) * SKILL_MULTIPLIER;
|
||||||
self.current_section_peak = self.current_strain.max(self.current_section_peak);
|
self.curr_section_peak = self.current_strain.max(self.curr_section_peak);
|
||||||
self.prev_time.replace(current.start_time);
|
self.prev_time.replace(current.start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
pub(crate) fn difficulty_value(strain_peaks: &mut [f64]) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{Beatmap, ControlPoint, ControlPointIter};
|
use crate::{Beatmap, ControlPoint, ControlPointIter};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct SliderState<'p> {
|
pub(crate) struct SliderState<'p> {
|
||||||
control_points: ControlPointIter<'p>,
|
control_points: ControlPointIter<'p>,
|
||||||
next: Option<ControlPoint>,
|
next: Option<ControlPoint>,
|
||||||
|
|||||||
Reference in New Issue
Block a user