finished osu std + bunch of bug fixes
This commit is contained in:
+30
-24
@@ -31,10 +31,10 @@ pub struct BeatmapHitWindows {
|
|||||||
/// mods & co.
|
/// mods & co.
|
||||||
pub struct BeatmapAttributesBuilder {
|
pub struct BeatmapAttributesBuilder {
|
||||||
mode: GameMode,
|
mode: GameMode,
|
||||||
ar: f64,
|
ar: f32,
|
||||||
od: f64,
|
od: f32,
|
||||||
cs: f64,
|
cs: f32,
|
||||||
hp: f64,
|
hp: f32,
|
||||||
mods: Option<u32>,
|
mods: Option<u32>,
|
||||||
clock_rate: Option<f64>,
|
clock_rate: Option<f64>,
|
||||||
converted: bool,
|
converted: bool,
|
||||||
@@ -65,7 +65,7 @@ impl BeatmapAttributesBuilder {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Specify the approach rate.
|
/// Specify the approach rate.
|
||||||
pub fn ar(&mut self, ar: f64) -> &mut Self {
|
pub fn ar(&mut self, ar: f32) -> &mut Self {
|
||||||
self.ar = ar;
|
self.ar = ar;
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -73,7 +73,7 @@ impl BeatmapAttributesBuilder {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Specify the overall difficulty.
|
/// Specify the overall difficulty.
|
||||||
pub fn od(&mut self, od: f64) -> &mut Self {
|
pub fn od(&mut self, od: f32) -> &mut Self {
|
||||||
self.od = od;
|
self.od = od;
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -81,7 +81,7 @@ impl BeatmapAttributesBuilder {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Specify the circle size.
|
/// Specify the circle size.
|
||||||
pub fn cs(&mut self, cs: f64) -> &mut Self {
|
pub fn cs(&mut self, cs: f32) -> &mut Self {
|
||||||
self.cs = cs;
|
self.cs = cs;
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -89,7 +89,7 @@ impl BeatmapAttributesBuilder {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Specify the drain rate.
|
/// Specify the drain rate.
|
||||||
pub fn hp(&mut self, hp: f64) -> &mut Self {
|
pub fn hp(&mut self, hp: f32) -> &mut Self {
|
||||||
self.hp = hp;
|
self.hp = hp;
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -126,7 +126,7 @@ impl BeatmapAttributesBuilder {
|
|||||||
let mods = self.mods.unwrap_or(0);
|
let mods = self.mods.unwrap_or(0);
|
||||||
let clock_rate = self.clock_rate.unwrap_or_else(|| mods.clock_rate());
|
let clock_rate = self.clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||||
|
|
||||||
let mod_mult = |val: f64| {
|
let mod_mult = |val: f32| {
|
||||||
if mods.hr() {
|
if mods.hr() {
|
||||||
(val * 1.4).min(10.0)
|
(val * 1.4).min(10.0)
|
||||||
} else if mods.ez() {
|
} else if mods.ez() {
|
||||||
@@ -137,20 +137,27 @@ impl BeatmapAttributesBuilder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let raw_ar = mod_mult(self.ar);
|
let raw_ar = mod_mult(self.ar);
|
||||||
let preempt = difficulty_range(raw_ar, 1800.0, 1200.0, 450.0) / clock_rate;
|
let preempt = difficulty_range(raw_ar as f64, 1800.0, 1200.0, 450.0) / clock_rate;
|
||||||
|
|
||||||
// OD
|
// OD
|
||||||
let hit_window = match self.mode {
|
let hit_window = match self.mode {
|
||||||
GameMode::Osu | GameMode::Catch => {
|
GameMode::Osu | GameMode::Catch => {
|
||||||
let raw_od = mod_mult(self.od);
|
let raw_od = mod_mult(self.od);
|
||||||
|
|
||||||
difficulty_range(raw_od, Self::OSU_MIN, Self::OSU_AVG, Self::OSU_MAX) / clock_rate
|
difficulty_range(raw_od as f64, Self::OSU_MIN, Self::OSU_AVG, Self::OSU_MAX)
|
||||||
|
/ clock_rate
|
||||||
}
|
}
|
||||||
GameMode::Taiko => {
|
GameMode::Taiko => {
|
||||||
let raw_od = mod_mult(self.od);
|
let raw_od = mod_mult(self.od);
|
||||||
|
|
||||||
difficulty_range(raw_od, Self::TAIKO_MIN, Self::TAIKO_AVG, Self::TAIKO_MAX).floor()
|
let diff_range = difficulty_range(
|
||||||
/ clock_rate
|
raw_od as f64,
|
||||||
|
Self::TAIKO_MIN,
|
||||||
|
Self::TAIKO_AVG,
|
||||||
|
Self::TAIKO_MAX,
|
||||||
|
);
|
||||||
|
|
||||||
|
diff_range.floor() / clock_rate
|
||||||
}
|
}
|
||||||
GameMode::Mania => {
|
GameMode::Mania => {
|
||||||
let mut value = if !self.converted {
|
let mut value = if !self.converted {
|
||||||
@@ -167,7 +174,7 @@ impl BeatmapAttributesBuilder {
|
|||||||
value *= 1.4;
|
value *= 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
((value * clock_rate).floor() / clock_rate).ceil()
|
((value as f64 * clock_rate).floor() / clock_rate).ceil()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,10 +188,9 @@ impl BeatmapAttributesBuilder {
|
|||||||
pub fn build(&self) -> BeatmapAttributes {
|
pub fn build(&self) -> BeatmapAttributes {
|
||||||
let mods = self.mods.unwrap_or(0);
|
let mods = self.mods.unwrap_or(0);
|
||||||
let clock_rate = self.clock_rate.unwrap_or_else(|| mods.clock_rate());
|
let clock_rate = self.clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||||
let multiplier = mods.od_ar_hp_multiplier();
|
|
||||||
|
|
||||||
// HP
|
// HP
|
||||||
let hp = (self.hp * multiplier).min(10.0);
|
let hp = (self.hp * mods.od_ar_hp_multiplier() as f32).min(10.0);
|
||||||
|
|
||||||
// CS
|
// CS
|
||||||
let mut cs = self.cs;
|
let mut cs = self.cs;
|
||||||
@@ -207,16 +213,16 @@ impl BeatmapAttributesBuilder {
|
|||||||
|
|
||||||
// OD
|
// OD
|
||||||
let od = match self.mode {
|
let od = match self.mode {
|
||||||
GameMode::Osu => (Self::OSU_MIN - od) / (Self::OSU_MIN - Self::OSU_AVG) * 5.0,
|
GameMode::Osu => (Self::OSU_MIN - od) / 6.0,
|
||||||
GameMode::Taiko => (Self::TAIKO_MIN - od) / (Self::TAIKO_MIN - Self::TAIKO_AVG) * 5.0,
|
GameMode::Taiko => (Self::TAIKO_MIN - od) / (Self::TAIKO_MIN - Self::TAIKO_AVG) * 5.0,
|
||||||
GameMode::Catch | GameMode::Mania => self.od,
|
GameMode::Catch | GameMode::Mania => self.od as f64,
|
||||||
};
|
};
|
||||||
|
|
||||||
BeatmapAttributes {
|
BeatmapAttributes {
|
||||||
ar,
|
ar,
|
||||||
od,
|
od,
|
||||||
cs,
|
cs: cs as f64,
|
||||||
hp,
|
hp: hp as f64,
|
||||||
clock_rate,
|
clock_rate,
|
||||||
hit_windows,
|
hit_windows,
|
||||||
}
|
}
|
||||||
@@ -228,10 +234,10 @@ impl From<&Beatmap> for BeatmapAttributesBuilder {
|
|||||||
fn from(map: &Beatmap) -> Self {
|
fn from(map: &Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mode: map.mode,
|
mode: map.mode,
|
||||||
ar: map.ar as f64,
|
ar: map.ar,
|
||||||
od: map.od as f64,
|
od: map.od,
|
||||||
cs: map.cs as f64,
|
cs: map.cs,
|
||||||
hp: map.hp as f64,
|
hp: map.hp,
|
||||||
mods: None,
|
mods: None,
|
||||||
clock_rate: None,
|
clock_rate: None,
|
||||||
converted: false,
|
converted: false,
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ mod tests {
|
|||||||
n_droplets: 2,
|
n_droplets: 2,
|
||||||
n_tiny_droplets: 68,
|
n_tiny_droplets: 68,
|
||||||
n_tiny_droplet_misses: 0,
|
n_tiny_droplet_misses: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let next = gradual1.process_next_object(state.clone());
|
let next = gradual1.process_next_object(state.clone());
|
||||||
@@ -259,7 +259,7 @@ mod tests {
|
|||||||
n_droplets: 2,
|
n_droplets: 2,
|
||||||
n_tiny_droplets: 291,
|
n_tiny_droplets: 291,
|
||||||
n_tiny_droplet_misses: 0,
|
n_tiny_droplet_misses: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||||
@@ -283,7 +283,7 @@ mod tests {
|
|||||||
n_droplets: 2,
|
n_droplets: 2,
|
||||||
n_tiny_droplets: 68,
|
n_tiny_droplets: 68,
|
||||||
n_tiny_droplet_misses: 0,
|
n_tiny_droplet_misses: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@ pub enum GradualDifficultyAttributes<'map> {
|
|||||||
/// Gradual osu!catch difficulty attributes.
|
/// Gradual osu!catch difficulty attributes.
|
||||||
Catch(CatchGradualDifficultyAttributes<'map>),
|
Catch(CatchGradualDifficultyAttributes<'map>),
|
||||||
/// Gradual osu!mania difficulty attributes.
|
/// Gradual osu!mania difficulty attributes.
|
||||||
Mania(ManiaGradualDifficultyAttributes<'map>),
|
Mania(ManiaGradualDifficultyAttributes),
|
||||||
/// Gradual osu!standard difficulty attributes.
|
/// Gradual osu!standard difficulty attributes.
|
||||||
Osu(OsuGradualDifficultyAttributes),
|
Osu(OsuGradualDifficultyAttributes),
|
||||||
/// Gradual osu!taiko difficulty attributes.
|
/// Gradual osu!taiko difficulty attributes.
|
||||||
@@ -316,7 +316,7 @@ impl<'map> GradualPerformanceAttributes<'map> {
|
|||||||
.process_next_n_objects(state.into(), n)
|
.process_next_n_objects(state.into(), n)
|
||||||
.map(PerformanceAttributes::Catch),
|
.map(PerformanceAttributes::Catch),
|
||||||
GradualPerformanceAttributes::Mania(m) => m
|
GradualPerformanceAttributes::Mania(m) => m
|
||||||
.process_next_n_objects(state.score, n)
|
.process_next_n_objects(state.into(), n)
|
||||||
.map(PerformanceAttributes::Mania),
|
.map(PerformanceAttributes::Mania),
|
||||||
GradualPerformanceAttributes::Osu(o) => o
|
GradualPerformanceAttributes::Osu(o) => o
|
||||||
.process_next_n_objects(state.into(), n)
|
.process_next_n_objects(state.into(), n)
|
||||||
|
|||||||
+24
-3
@@ -520,11 +520,32 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom() {
|
fn custom() {
|
||||||
let path = "F:\\osu!\\beatmaps\\2536330.osu";
|
let path = "F:\\osu!\\beatmaps\\1529760.osu";
|
||||||
let map = Beatmap::from_path(path).unwrap();
|
let map = Beatmap::from_path(path).unwrap();
|
||||||
|
|
||||||
let attrs = OsuPP::new(&map).calculate();
|
let attrs = OsuPP::new(&map).mods(16).calculate();
|
||||||
|
|
||||||
println!("{:#?}", attrs);
|
println!(
|
||||||
|
"difficulty:\n\
|
||||||
|
aim={}\n\
|
||||||
|
speed={}\n\
|
||||||
|
flashlight={}\n\
|
||||||
|
stars={}\n\
|
||||||
|
performance:\n\
|
||||||
|
aim={}\n\
|
||||||
|
speed={}\n\
|
||||||
|
acc={}\n\
|
||||||
|
flashlight={}\n\
|
||||||
|
pp={}\n",
|
||||||
|
attrs.difficulty.aim,
|
||||||
|
attrs.difficulty.speed,
|
||||||
|
attrs.difficulty.flashlight,
|
||||||
|
attrs.difficulty.stars,
|
||||||
|
attrs.pp_aim,
|
||||||
|
attrs.pp_speed,
|
||||||
|
attrs.pp_acc,
|
||||||
|
attrs.pp_flashlight,
|
||||||
|
attrs.pp,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
use super::mania_object::ManiaObject;
|
use super::mania_object::ManiaObject;
|
||||||
|
|
||||||
pub(crate) struct ManiaDifficultyObject<'h> {
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct ManiaDifficultyObject {
|
||||||
pub(crate) idx: usize,
|
pub(crate) idx: usize,
|
||||||
pub(crate) base: ManiaObject<'h>,
|
pub(crate) base_column: usize,
|
||||||
pub(crate) delta_time: f64,
|
pub(crate) delta_time: f64,
|
||||||
pub(crate) start_time: f64,
|
pub(crate) start_time: f64,
|
||||||
pub(crate) end_time: f64,
|
pub(crate) end_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'h> ManiaDifficultyObject<'h> {
|
impl ManiaDifficultyObject {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
base: ManiaObject<'h>,
|
base: ManiaObject<'_>,
|
||||||
last: ManiaObject<'h>,
|
last: ManiaObject<'_>,
|
||||||
clock_rate: f64,
|
clock_rate: f64,
|
||||||
|
total_columns: f32,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let delta_time = (base.start_time() - last.start_time()) / clock_rate;
|
let delta_time = (base.start_time() - last.start_time()) / clock_rate;
|
||||||
let start_time = base.start_time() / clock_rate;
|
let start_time = base.start_time() / clock_rate;
|
||||||
let end_time = base.end_time() / clock_rate;
|
let end_time = base.end_time() / clock_rate;
|
||||||
|
let base_column = base.column(total_columns);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx,
|
idx,
|
||||||
base,
|
base_column,
|
||||||
delta_time,
|
delta_time,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
|
|||||||
+47
-103
@@ -1,16 +1,11 @@
|
|||||||
use std::{
|
use crate::{beatmap::BeatmapHitWindows, Beatmap, Mods};
|
||||||
iter::{self, Skip, Zip},
|
|
||||||
slice::Iter,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use super::{
|
||||||
mania::{strain::Strain, SECTION_LEN},
|
difficulty_object::ManiaDifficultyObject,
|
||||||
parse::HitObject,
|
skills::{Skill, Strain},
|
||||||
Beatmap, Mods,
|
ManiaDifficultyAttributes, ManiaObject, STAR_SCALING_FACTOR,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{DifficultyHitObject, ManiaDifficultyAttributes, STAR_SCALING_FACTOR};
|
|
||||||
|
|
||||||
/// Gradually calculate the difficulty attributes of an osu!mania map.
|
/// Gradually calculate the difficulty attributes of an osu!mania map.
|
||||||
///
|
///
|
||||||
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
|
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
|
||||||
@@ -42,70 +37,65 @@ use super::{DifficultyHitObject, ManiaDifficultyAttributes, STAR_SCALING_FACTOR}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ManiaGradualDifficultyAttributes<'map> {
|
pub struct ManiaGradualDifficultyAttributes {
|
||||||
pub(crate) idx: usize,
|
pub(crate) idx: usize,
|
||||||
difficulty_objects: ManiaObjectIter<'map>,
|
hit_window: f64,
|
||||||
strain: Strain,
|
strain: Strain,
|
||||||
curr_section_end: f64,
|
diff_objects: Vec<ManiaDifficultyObject>,
|
||||||
strain_peak_buf: Vec<f64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
impl ManiaGradualDifficultyAttributes {
|
||||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||||
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
pub fn new(map: &Beatmap, mods: u32) -> Self {
|
||||||
let columns = map.cs.round().max(1.0) as u8;
|
let total_columns = map.cs.round().max(1.0);
|
||||||
|
|
||||||
let clock_rate = mods.clock_rate();
|
let clock_rate = mods.clock_rate();
|
||||||
let strain = Strain::new(columns);
|
let strain = Strain::new(total_columns as usize);
|
||||||
let columns = columns as f32;
|
|
||||||
let difficulty_objects = ManiaObjectIter::new(&map.hit_objects, columns, clock_rate);
|
let BeatmapHitWindows { od: hit_window, .. } = map
|
||||||
|
.attributes()
|
||||||
|
.mods(mods)
|
||||||
|
// TODO: allow converts
|
||||||
|
// .converted(is_convert)
|
||||||
|
.clock_rate(clock_rate)
|
||||||
|
.hit_windows();
|
||||||
|
|
||||||
|
let diff_objects_iter = map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.map(ManiaObject::new)
|
||||||
|
.enumerate()
|
||||||
|
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
||||||
|
.map(|((i, base), prev)| {
|
||||||
|
ManiaDifficultyObject::new(base, prev, clock_rate, total_columns, i)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1));
|
||||||
|
diff_objects.extend(diff_objects_iter);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
difficulty_objects,
|
hit_window,
|
||||||
strain,
|
strain,
|
||||||
curr_section_end: 0.0,
|
diff_objects,
|
||||||
strain_peak_buf: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
impl Iterator for ManiaGradualDifficultyAttributes {
|
||||||
type Item = ManiaDifficultyAttributes;
|
type Item = ManiaDifficultyAttributes;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.idx = self.idx.saturating_add(1);
|
let curr = self.diff_objects.get(self.idx)?;
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
if self.idx == 1 {
|
self.strain.process(curr, &self.diff_objects);
|
||||||
return (!self.difficulty_objects.is_empty).then(ManiaDifficultyAttributes::default);
|
|
||||||
}
|
|
||||||
|
|
||||||
let h = self.difficulty_objects.next()?;
|
Some(ManiaDifficultyAttributes {
|
||||||
|
stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR,
|
||||||
if self.idx == 2 {
|
hit_window: self.hit_window,
|
||||||
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
})
|
||||||
} else {
|
|
||||||
while h.start_time > self.curr_section_end {
|
|
||||||
self.strain.save_current_peak();
|
|
||||||
self.strain.start_new_section_from(self.curr_section_end);
|
|
||||||
self.curr_section_end += SECTION_LEN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.strain.process(&h);
|
|
||||||
|
|
||||||
let missing = self.strain.strain_peaks.len() + 1 - self.strain_peak_buf.len();
|
|
||||||
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
|
|
||||||
|
|
||||||
self.strain_peak_buf[..self.strain.strain_peaks.len()]
|
|
||||||
.copy_from_slice(&self.strain.strain_peaks);
|
|
||||||
|
|
||||||
if let Some(last) = self.strain_peak_buf.last_mut() {
|
|
||||||
*last = self.strain.curr_section_peak;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stars = Strain::difficulty_value(&mut self.strain_peak_buf) * STAR_SCALING_FACTOR;
|
|
||||||
|
|
||||||
Some(ManiaDifficultyAttributes { stars })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -116,56 +106,10 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
impl ExactSizeIterator for ManiaGradualDifficultyAttributes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.difficulty_objects.len() + (self.idx == 0) as usize
|
self.diff_objects.len() - self.idx
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct ManiaObjectIter<'map> {
|
|
||||||
hit_objects: Zip<Skip<Iter<'map, HitObject>>, Iter<'map, HitObject>>,
|
|
||||||
columns: f32,
|
|
||||||
clock_rate: f64,
|
|
||||||
is_empty: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> ManiaObjectIter<'map> {
|
|
||||||
fn new(hit_objects: &'map [HitObject], columns: f32, clock_rate: f64) -> Self {
|
|
||||||
let is_empty = hit_objects.is_empty();
|
|
||||||
let hit_objects = hit_objects.iter().skip(1).zip(hit_objects);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
hit_objects,
|
|
||||||
columns,
|
|
||||||
clock_rate,
|
|
||||||
is_empty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> Iterator for ManiaObjectIter<'map> {
|
|
||||||
type Item = DifficultyHitObject<'map>;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let (base, prev) = self.hit_objects.next()?;
|
|
||||||
let obj = DifficultyHitObject::new(base, prev, self.columns, self.clock_rate);
|
|
||||||
|
|
||||||
Some(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
||||||
self.hit_objects.size_hint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExactSizeIterator for ManiaObjectIter<'_> {
|
|
||||||
#[inline]
|
|
||||||
fn len(&self) -> usize {
|
|
||||||
self.hit_objects.len()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct ManiaScoreState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ManiaScoreState {
|
impl ManiaScoreState {
|
||||||
|
/// Return the total amount of hits by adding everything up.
|
||||||
pub fn total_hits(&self) -> usize {
|
pub fn total_hits(&self) -> usize {
|
||||||
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
|
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ impl ManiaScoreState {
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ManiaGradualPerformanceAttributes<'map> {
|
pub struct ManiaGradualPerformanceAttributes<'map> {
|
||||||
difficulty: ManiaGradualDifficultyAttributes<'map>,
|
difficulty: ManiaGradualDifficultyAttributes,
|
||||||
performance: ManiaPP<'map>,
|
performance: ManiaPP<'map>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +119,11 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
|
|||||||
|
|
||||||
/// Process the next hit object and calculate the
|
/// Process the next hit object and calculate the
|
||||||
/// performance attributes for the resulting score.
|
/// performance attributes for the resulting score.
|
||||||
pub fn process_next_object(&mut self, score: u32) -> Option<ManiaPerformanceAttributes> {
|
pub fn process_next_object(
|
||||||
self.process_next_n_objects(score, 1)
|
&mut self,
|
||||||
|
state: ManiaScoreState,
|
||||||
|
) -> Option<ManiaPerformanceAttributes> {
|
||||||
|
self.process_next_n_objects(state, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
|
/// Same as [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
|
||||||
@@ -130,13 +134,18 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
|
|||||||
/// of remaining objects, `n` will be considered as the amount of remaining objects.
|
/// of remaining objects, `n` will be considered as the amount of remaining objects.
|
||||||
pub fn process_next_n_objects(
|
pub fn process_next_n_objects(
|
||||||
&mut self,
|
&mut self,
|
||||||
score: u32,
|
state: ManiaScoreState,
|
||||||
n: usize,
|
n: usize,
|
||||||
) -> Option<ManiaPerformanceAttributes> {
|
) -> Option<ManiaPerformanceAttributes> {
|
||||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||||
let difficulty = self.difficulty.nth(n)?;
|
let difficulty = self.difficulty.nth(n)?;
|
||||||
|
|
||||||
let _ = self.performance.score.insert(score as f64);
|
self.performance.n320 = Some(state.n320);
|
||||||
|
self.performance.n300 = Some(state.n300);
|
||||||
|
self.performance.n200 = Some(state.n200);
|
||||||
|
self.performance.n100 = Some(state.n100);
|
||||||
|
self.performance.n50 = Some(state.n50);
|
||||||
|
self.performance.n_misses = Some(state.n_misses);
|
||||||
|
|
||||||
let performance = self
|
let performance = self
|
||||||
.performance
|
.performance
|
||||||
@@ -161,10 +170,20 @@ mod tests {
|
|||||||
let mods = 64;
|
let mods = 64;
|
||||||
|
|
||||||
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
let score = 0;
|
|
||||||
|
|
||||||
assert!(gradual.process_next_n_objects(score, usize::MAX).is_some());
|
let state = ManiaScoreState {
|
||||||
assert!(gradual.process_next_object(score).is_none());
|
n320: 0,
|
||||||
|
n300: 0,
|
||||||
|
n200: 0,
|
||||||
|
n100: 0,
|
||||||
|
n50: 0,
|
||||||
|
n_misses: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(gradual
|
||||||
|
.process_next_n_objects(state.clone(), usize::MAX)
|
||||||
|
.is_some());
|
||||||
|
assert!(gradual.process_next_object(state).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
@@ -172,25 +191,34 @@ mod tests {
|
|||||||
fn next_and_next_n() {
|
fn next_and_next_n() {
|
||||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
let mods = 64;
|
let mods = 64;
|
||||||
let score = 0;
|
|
||||||
|
let mut state = ManiaScoreState {
|
||||||
|
n320: 0,
|
||||||
|
n300: 0,
|
||||||
|
n200: 0,
|
||||||
|
n100: 0,
|
||||||
|
n50: 0,
|
||||||
|
n_misses: 0,
|
||||||
|
};
|
||||||
|
|
||||||
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
let _ = gradual1.process_next_object(score);
|
let _ = gradual1.process_next_object(state.clone());
|
||||||
let _ = gradual2.process_next_object(score);
|
let _ = gradual2.process_next_object(state.clone());
|
||||||
|
state.n320 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let n = 80;
|
let n = 80;
|
||||||
|
|
||||||
for _ in 1..n {
|
for _ in 1..n {
|
||||||
let _ = gradual1.process_next_object(score);
|
let _ = gradual1.process_next_object(state.clone());
|
||||||
|
state.n320 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let score = 100_000;
|
let next = gradual1.process_next_object(state.clone());
|
||||||
let next = gradual1.process_next_object(score);
|
let next_n = gradual2.process_next_n_objects(state, n);
|
||||||
let next_n = gradual2.process_next_n_objects(score, n);
|
|
||||||
|
|
||||||
assert_eq!(next_n, next);
|
assert_eq!(next_n, next);
|
||||||
}
|
}
|
||||||
@@ -204,8 +232,16 @@ mod tests {
|
|||||||
|
|
||||||
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
|
|
||||||
let score = 1_000_000;
|
let state = ManiaScoreState {
|
||||||
let gradual_end = gradual.process_next_n_objects(score, usize::MAX).unwrap();
|
n320: 3238,
|
||||||
|
n300: 0,
|
||||||
|
n200: 0,
|
||||||
|
n100: 0,
|
||||||
|
n50: 0,
|
||||||
|
n_misses: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||||
|
|
||||||
assert_eq!(regular, gradual_end);
|
assert_eq!(regular, gradual_end);
|
||||||
}
|
}
|
||||||
@@ -216,16 +252,24 @@ mod tests {
|
|||||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||||
let mods = 64;
|
let mods = 64;
|
||||||
let n = 100;
|
let n = 100;
|
||||||
let score = 100_000;
|
|
||||||
|
let state = ManiaScoreState {
|
||||||
|
n320: 100,
|
||||||
|
n300: 0,
|
||||||
|
n200: 0,
|
||||||
|
n100: 0,
|
||||||
|
n50: 0,
|
||||||
|
n_misses: 0,
|
||||||
|
};
|
||||||
|
|
||||||
let regular = ManiaPP::new(&map)
|
let regular = ManiaPP::new(&map)
|
||||||
.mods(mods)
|
.mods(mods)
|
||||||
.passed_objects(n)
|
.passed_objects(n)
|
||||||
.score(score)
|
.state(state.clone())
|
||||||
.calculate();
|
.calculate();
|
||||||
|
|
||||||
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||||
let gradual = gradual.process_next_n_objects(score, n).unwrap();
|
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||||
|
|
||||||
assert_eq!(regular, gradual);
|
assert_eq!(regular, gradual);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-22
@@ -7,7 +7,7 @@ mod skills;
|
|||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, Beatmap, GameMode, Mods, OsuStars};
|
use crate::{beatmap::BeatmapHitWindows, Beatmap, GameMode, Mods, OsuStars};
|
||||||
|
|
||||||
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
|
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
|
||||||
|
|
||||||
@@ -105,12 +105,12 @@ impl<'map> ManiaStars<'map> {
|
|||||||
.clock_rate(clock_rate)
|
.clock_rate(clock_rate)
|
||||||
.hit_windows();
|
.hit_windows();
|
||||||
|
|
||||||
let (strain, mut attrs) = calculate_strain(self);
|
let strain = calculate_strain(self);
|
||||||
|
|
||||||
attrs.stars = strain.difficulty_value() * STAR_SCALING_FACTOR;
|
ManiaDifficultyAttributes {
|
||||||
attrs.hit_window = hit_window;
|
stars: strain.difficulty_value() * STAR_SCALING_FACTOR,
|
||||||
|
hit_window,
|
||||||
attrs
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the skill strains.
|
/// Calculate the skill strains.
|
||||||
@@ -119,7 +119,7 @@ impl<'map> ManiaStars<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn strains(self) -> ManiaStrains {
|
pub fn strains(self) -> ManiaStrains {
|
||||||
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
||||||
let (strain, _) = calculate_strain(self);
|
let strain = calculate_strain(self);
|
||||||
|
|
||||||
ManiaStrains {
|
ManiaStrains {
|
||||||
section_len: SECTION_LEN * clock_rate, // TODO: clock_rate correct here?
|
section_len: SECTION_LEN * clock_rate, // TODO: clock_rate correct here?
|
||||||
@@ -147,7 +147,7 @@ impl ManiaStrains {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_strain(params: ManiaStars<'_>) -> (Strain, ManiaDifficultyAttributes) {
|
fn calculate_strain(params: ManiaStars<'_>) -> Strain {
|
||||||
let ManiaStars {
|
let ManiaStars {
|
||||||
map,
|
map,
|
||||||
mods,
|
mods,
|
||||||
@@ -156,28 +156,22 @@ fn calculate_strain(params: ManiaStars<'_>) -> (Strain, ManiaDifficultyAttribute
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||||
let total_columns = map.cs.round().max(1.0) as usize;
|
let total_columns = map.cs.round().max(1.0);
|
||||||
|
|
||||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||||
let mut strain = Strain::new(total_columns);
|
let mut strain = Strain::new(total_columns as usize);
|
||||||
|
|
||||||
let mut attrs = ManiaDifficultyAttributes::default();
|
|
||||||
|
|
||||||
let diff_objects_iter = map
|
let diff_objects_iter = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.take(take)
|
.take(take)
|
||||||
.inspect(|h| match &h.kind {
|
|
||||||
HitObjectKind::Hold { end_time } => {
|
|
||||||
attrs.max_combo += 1 + ((*end_time - h.start_time) / 100.0) as usize
|
|
||||||
}
|
|
||||||
_ => attrs.max_combo += 1,
|
|
||||||
})
|
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.map(ManiaObject::new)
|
.map(ManiaObject::new)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
||||||
.map(|((i, base), prev)| ManiaDifficultyObject::new(base, prev, clock_rate, i));
|
.map(|((i, base), prev)| {
|
||||||
|
ManiaDifficultyObject::new(base, prev, clock_rate, total_columns, i)
|
||||||
|
});
|
||||||
|
|
||||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
|
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
|
||||||
diff_objects.extend(diff_objects_iter);
|
diff_objects.extend(diff_objects_iter);
|
||||||
@@ -186,7 +180,7 @@ fn calculate_strain(params: ManiaStars<'_>) -> (Strain, ManiaDifficultyAttribute
|
|||||||
strain.process(curr, &diff_objects);
|
strain.process(curr, &diff_objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
(strain, attrs)
|
strain
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a difficulty calculation on an osu!mania map.
|
/// The result of a difficulty calculation on an osu!mania map.
|
||||||
@@ -194,8 +188,6 @@ fn calculate_strain(params: ManiaStars<'_>) -> (Strain, ManiaDifficultyAttribute
|
|||||||
pub struct ManiaDifficultyAttributes {
|
pub struct ManiaDifficultyAttributes {
|
||||||
/// The final star rating.
|
/// The final star rating.
|
||||||
pub stars: f64,
|
pub stars: f64,
|
||||||
/// The maximum achievable combo.
|
|
||||||
pub max_combo: usize,
|
|
||||||
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc).
|
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
pub hit_window: f64,
|
pub hit_window: f64,
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-9
@@ -40,12 +40,12 @@ pub struct ManiaPP<'map> {
|
|||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
clock_rate: Option<f64>,
|
||||||
|
|
||||||
n320: Option<usize>,
|
pub(crate) n320: Option<usize>,
|
||||||
n300: Option<usize>,
|
pub(crate) n300: Option<usize>,
|
||||||
n200: Option<usize>,
|
pub(crate) n200: Option<usize>,
|
||||||
n100: Option<usize>,
|
pub(crate) n100: Option<usize>,
|
||||||
n50: Option<usize>,
|
pub(crate) n50: Option<usize>,
|
||||||
n_misses: Option<usize>,
|
pub(crate) n_misses: Option<usize>,
|
||||||
|
|
||||||
acc: Option<f64>,
|
acc: Option<f64>,
|
||||||
hitresult_priority: Option<ManiaHitResultPriority>,
|
hitresult_priority: Option<ManiaHitResultPriority>,
|
||||||
@@ -120,6 +120,8 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the accuracy of a play.
|
||||||
|
/// This will be used to generate matching hitresults.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||||
self.acc = Some(acc);
|
self.acc = Some(acc);
|
||||||
@@ -127,6 +129,9 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify how hitresults should be generated.
|
||||||
|
///
|
||||||
|
/// Defauls to [`ManiaHitResultPriority::BestCase`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hitresult_priority(mut self, priority: ManiaHitResultPriority) -> Self {
|
pub fn hitresult_priority(mut self, priority: ManiaHitResultPriority) -> Self {
|
||||||
self.hitresult_priority = Some(priority);
|
self.hitresult_priority = Some(priority);
|
||||||
@@ -134,6 +139,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the amount of 320s of a play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn n320(mut self, n320: usize) -> Self {
|
pub fn n320(mut self, n320: usize) -> Self {
|
||||||
self.n320 = Some(n320);
|
self.n320 = Some(n320);
|
||||||
@@ -141,6 +147,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the amount of 300s of a play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn n300(mut self, n300: usize) -> Self {
|
pub fn n300(mut self, n300: usize) -> Self {
|
||||||
self.n300 = Some(n300);
|
self.n300 = Some(n300);
|
||||||
@@ -148,6 +155,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the amount of 200s of a play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn n200(mut self, n200: usize) -> Self {
|
pub fn n200(mut self, n200: usize) -> Self {
|
||||||
self.n200 = Some(n200);
|
self.n200 = Some(n200);
|
||||||
@@ -155,6 +163,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the amount of 100s of a play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn n100(mut self, n100: usize) -> Self {
|
pub fn n100(mut self, n100: usize) -> Self {
|
||||||
self.n100 = Some(n100);
|
self.n100 = Some(n100);
|
||||||
@@ -162,6 +171,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the amount of 50s of a play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn n50(mut self, n50: usize) -> Self {
|
pub fn n50(mut self, n50: usize) -> Self {
|
||||||
self.n50 = Some(n50);
|
self.n50 = Some(n50);
|
||||||
@@ -169,6 +179,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify the amount of misses of a play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn n_misses(mut self, n_misses: usize) -> Self {
|
pub fn n_misses(mut self, n_misses: usize) -> Self {
|
||||||
self.n_misses = Some(n_misses);
|
self.n_misses = Some(n_misses);
|
||||||
@@ -176,6 +187,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provide parameters through an [`ManiaScoreState`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn state(mut self, state: ManiaScoreState) -> Self {
|
pub fn state(mut self, state: ManiaScoreState) -> Self {
|
||||||
let ManiaScoreState {
|
let ManiaScoreState {
|
||||||
@@ -217,7 +229,6 @@ impl<'map> ManiaPP<'map> {
|
|||||||
let inner = ManiaPpInner {
|
let inner = ManiaPpInner {
|
||||||
attrs,
|
attrs,
|
||||||
mods: self.mods,
|
mods: self.mods,
|
||||||
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
|
|
||||||
state: self.generate_hitresults(),
|
state: self.generate_hitresults(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -337,7 +348,6 @@ impl<'map> ManiaPP<'map> {
|
|||||||
struct ManiaPpInner {
|
struct ManiaPpInner {
|
||||||
attrs: ManiaDifficultyAttributes,
|
attrs: ManiaDifficultyAttributes,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
clock_rate: f64,
|
|
||||||
state: ManiaScoreState,
|
state: ManiaScoreState,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +395,7 @@ impl ManiaPpInner {
|
|||||||
n200,
|
n200,
|
||||||
n100,
|
n100,
|
||||||
n50,
|
n50,
|
||||||
n_misses,
|
n_misses: _,
|
||||||
} = &self.state;
|
} = &self.state;
|
||||||
|
|
||||||
let numerator = *n320 * 320 + *n300 * 300 + *n200 * 200 + *n100 * 100 + *n50 * 50;
|
let numerator = *n320 * 320 + *n300 * 300 + *n200 * 200 + *n100 * 100 + *n50 * 50;
|
||||||
|
|||||||
+14
-22
@@ -7,11 +7,7 @@ pub(crate) use self::strain::Strain;
|
|||||||
mod strain;
|
mod strain;
|
||||||
|
|
||||||
pub(crate) trait Skill {
|
pub(crate) trait Skill {
|
||||||
fn process(
|
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]);
|
||||||
&mut self,
|
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
|
||||||
);
|
|
||||||
fn difficulty_value(self) -> f64;
|
fn difficulty_value(self) -> f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,13 +22,9 @@ pub(crate) trait StrainSkill: Sized + Skill {
|
|||||||
|
|
||||||
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
|
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
|
||||||
|
|
||||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64;
|
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64;
|
||||||
|
|
||||||
fn process(
|
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]) {
|
||||||
&mut self,
|
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
|
||||||
) {
|
|
||||||
// The first object doesn't generate a strain, so we begin with an incremented section end
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
||||||
if curr.idx == 0 {
|
if curr.idx == 0 {
|
||||||
// currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
// currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
||||||
@@ -56,8 +48,8 @@ pub(crate) trait StrainSkill: Sized + Skill {
|
|||||||
fn start_new_section_from(
|
fn start_new_section_from(
|
||||||
&mut self,
|
&mut self,
|
||||||
time: f64,
|
time: f64,
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
curr: &ManiaDifficultyObject,
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
diff_objects: &[ManiaDifficultyObject],
|
||||||
) {
|
) {
|
||||||
*self.curr_section_peak_mut() = self.calculate_initial_strain(time, curr, diff_objects);
|
*self.curr_section_peak_mut() = self.calculate_initial_strain(time, curr, diff_objects);
|
||||||
}
|
}
|
||||||
@@ -65,8 +57,8 @@ pub(crate) trait StrainSkill: Sized + Skill {
|
|||||||
fn calculate_initial_strain(
|
fn calculate_initial_strain(
|
||||||
&self,
|
&self,
|
||||||
time: f64,
|
time: f64,
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
curr: &ManiaDifficultyObject,
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
diff_objects: &[ManiaDifficultyObject],
|
||||||
) -> f64;
|
) -> f64;
|
||||||
|
|
||||||
fn get_curr_strain_peaks(mut self) -> Vec<f64> {
|
fn get_curr_strain_peaks(mut self) -> Vec<f64> {
|
||||||
@@ -104,16 +96,16 @@ pub(crate) trait StrainDecaySkill: StrainSkill {
|
|||||||
fn curr_strain(&self) -> f64;
|
fn curr_strain(&self) -> f64;
|
||||||
fn curr_strain_mut(&mut self) -> &mut f64;
|
fn curr_strain_mut(&mut self) -> &mut f64;
|
||||||
|
|
||||||
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64;
|
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject) -> f64;
|
||||||
|
|
||||||
fn calculate_initial_strain(
|
fn calculate_initial_strain(
|
||||||
&self,
|
&self,
|
||||||
time: f64,
|
time: f64,
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
curr: &ManiaDifficultyObject,
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
diff_objects: &[ManiaDifficultyObject],
|
||||||
) -> f64;
|
) -> f64;
|
||||||
|
|
||||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64 {
|
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||||
*self.curr_strain_mut() *= self.strain_decay(curr.delta_time);
|
*self.curr_strain_mut() *= self.strain_decay(curr.delta_time);
|
||||||
*self.curr_strain_mut() += self.strain_value_of(curr) * Self::SKILL_MULTIPLIER;
|
*self.curr_strain_mut() += self.strain_value_of(curr) * Self::SKILL_MULTIPLIER;
|
||||||
|
|
||||||
@@ -125,11 +117,11 @@ pub(crate) trait StrainDecaySkill: StrainSkill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous<'map, 'objects>(
|
fn previous(
|
||||||
diff_objects: &'objects [ManiaDifficultyObject<'map>],
|
diff_objects: &[ManiaDifficultyObject],
|
||||||
curr: usize,
|
curr: usize,
|
||||||
backwards_idx: usize,
|
backwards_idx: usize,
|
||||||
) -> Option<&'objects ManiaDifficultyObject<'map>> {
|
) -> Option<&ManiaDifficultyObject> {
|
||||||
curr.checked_sub(backwards_idx + 1)
|
curr.checked_sub(backwards_idx + 1)
|
||||||
.and_then(|idx| diff_objects.get(idx))
|
.and_then(|idx| diff_objects.get(idx))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::mania::difficulty_object::ManiaDifficultyObject;
|
|||||||
|
|
||||||
use super::{previous, Skill, StrainDecaySkill, StrainSkill};
|
use super::{previous, Skill, StrainDecaySkill, StrainSkill};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Strain {
|
pub(crate) struct Strain {
|
||||||
start_times: Vec<f64>,
|
start_times: Vec<f64>,
|
||||||
end_times: Vec<f64>,
|
end_times: Vec<f64>,
|
||||||
@@ -15,8 +16,6 @@ pub(crate) struct Strain {
|
|||||||
curr_section_end: f64,
|
curr_section_end: f64,
|
||||||
|
|
||||||
pub(crate) strain_peaks: Vec<f64>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
|
||||||
total_columns: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strain {
|
impl Strain {
|
||||||
@@ -35,7 +34,6 @@ impl Strain {
|
|||||||
curr_section_peak: 0.0,
|
curr_section_peak: 0.0,
|
||||||
curr_section_end: 0.0,
|
curr_section_end: 0.0,
|
||||||
strain_peaks: Vec::new(),
|
strain_peaks: Vec::new(),
|
||||||
total_columns: total_columns as f32,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +43,7 @@ impl Strain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Skill for Strain {
|
impl Skill for Strain {
|
||||||
fn process(
|
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]) {
|
||||||
&mut self,
|
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
|
||||||
) {
|
|
||||||
<Self as StrainSkill>::process(self, curr, diff_objects)
|
<Self as StrainSkill>::process(self, curr, diff_objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,15 +75,15 @@ impl StrainSkill for Strain {
|
|||||||
&mut self.strain_peaks
|
&mut self.strain_peaks
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64 {
|
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||||
<Self as StrainDecaySkill>::strain_value_at(self, curr)
|
<Self as StrainDecaySkill>::strain_value_at(self, curr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_initial_strain(
|
fn calculate_initial_strain(
|
||||||
&self,
|
&self,
|
||||||
time: f64,
|
time: f64,
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
curr: &ManiaDifficultyObject,
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
diff_objects: &[ManiaDifficultyObject],
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr, diff_objects)
|
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr, diff_objects)
|
||||||
}
|
}
|
||||||
@@ -107,11 +101,11 @@ impl StrainDecaySkill for Strain {
|
|||||||
&mut self.curr_strain
|
&mut self.curr_strain
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64 {
|
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||||
let mania_curr = curr;
|
let mania_curr = curr;
|
||||||
let start_time = mania_curr.start_time;
|
let start_time = mania_curr.start_time;
|
||||||
let end_time = mania_curr.end_time;
|
let end_time = mania_curr.end_time;
|
||||||
let col = mania_curr.base.column(self.total_columns);
|
let col = mania_curr.base_column;
|
||||||
let mut is_overlapping = false;
|
let mut is_overlapping = false;
|
||||||
|
|
||||||
// Lowest value we can assume with the current information
|
// Lowest value we can assume with the current information
|
||||||
@@ -183,8 +177,8 @@ impl StrainDecaySkill for Strain {
|
|||||||
fn calculate_initial_strain(
|
fn calculate_initial_strain(
|
||||||
&self,
|
&self,
|
||||||
offset: f64,
|
offset: f64,
|
||||||
curr: &ManiaDifficultyObject<'_>,
|
curr: &ManiaDifficultyObject,
|
||||||
diff_objects: &[ManiaDifficultyObject<'_>],
|
diff_objects: &[ManiaDifficultyObject],
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
let prev_start = previous(diff_objects, curr.idx, 0).map_or(0.0, |h| h.start_time);
|
let prev_start = previous(diff_objects, curr.idx, 0).map_or(0.0, |h| h.start_time);
|
||||||
|
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
use super::DifficultyHitObject;
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Strain {
|
|
||||||
current_strain: f64,
|
|
||||||
pub(crate) curr_section_peak: f64,
|
|
||||||
|
|
||||||
individual_strain: f64,
|
|
||||||
overall_strain: f64,
|
|
||||||
|
|
||||||
hold_end_times: Vec<f64>,
|
|
||||||
individual_strains: Vec<f64>,
|
|
||||||
pub(crate) strain_peaks: Vec<f64>,
|
|
||||||
|
|
||||||
prev_time: Option<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const INDIVISUAL_DECAY_BASE: f64 = 0.125;
|
|
||||||
const OVERALL_DECAY_BASE: f64 = 0.3;
|
|
||||||
const STRAIN_DECAY_BASE: f64 = 1.0;
|
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f64 = 1.0;
|
|
||||||
const DECAY_WEIGHT: f64 = 0.9;
|
|
||||||
|
|
||||||
impl Strain {
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn new(column_count: u8) -> Self {
|
|
||||||
Self {
|
|
||||||
current_strain: 1.0,
|
|
||||||
curr_section_peak: 1.0,
|
|
||||||
|
|
||||||
individual_strain: 0.0,
|
|
||||||
overall_strain: 1.0,
|
|
||||||
|
|
||||||
hold_end_times: vec![0.0; column_count as usize],
|
|
||||||
individual_strains: vec![0.0; column_count as usize],
|
|
||||||
strain_peaks: Vec::with_capacity(128),
|
|
||||||
|
|
||||||
prev_time: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
|
||||||
self.strain_peaks.push(self.curr_section_peak);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
|
||||||
self.curr_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn peak_strain(&self, delta_time: f64) -> f64 {
|
|
||||||
apply_decay(self.individual_strain, delta_time, INDIVISUAL_DECAY_BASE)
|
|
||||||
+ apply_decay(self.overall_strain, delta_time, OVERALL_DECAY_BASE)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn strain_decay(&self, ms: f64) -> f64 {
|
|
||||||
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn process(&mut self, current: &DifficultyHitObject<'_>) {
|
|
||||||
self.current_strain *= self.strain_decay(current.delta);
|
|
||||||
self.current_strain += self.strain_value_of(current) * SKILL_MULTIPLIER;
|
|
||||||
self.curr_section_peak = self.current_strain.max(self.curr_section_peak);
|
|
||||||
self.prev_time.replace(current.start_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strain_value_of(&mut self, current: &DifficultyHitObject<'_>) -> f64 {
|
|
||||||
let end_time = current.base.end_time();
|
|
||||||
|
|
||||||
let mut hold_factor = 1.0;
|
|
||||||
let mut hold_addition = 0.0;
|
|
||||||
|
|
||||||
for col in 0..self.hold_end_times.len() {
|
|
||||||
let hold_end_time = self.hold_end_times[col];
|
|
||||||
|
|
||||||
if end_time > hold_end_time + 1.0 {
|
|
||||||
if hold_end_time > current.base.start_time + 1.0 {
|
|
||||||
hold_addition = 1.0;
|
|
||||||
}
|
|
||||||
} else if (end_time - hold_end_time).abs() < 1.0 {
|
|
||||||
hold_addition = 0.0;
|
|
||||||
} else if end_time < hold_end_time - 1.0 {
|
|
||||||
hold_factor = 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.individual_strains[col] = apply_decay(
|
|
||||||
self.individual_strains[col],
|
|
||||||
current.delta,
|
|
||||||
INDIVISUAL_DECAY_BASE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hold_end_times[current.column] = end_time;
|
|
||||||
self.individual_strains[current.column] += 2.0 * hold_factor;
|
|
||||||
self.individual_strain = self.individual_strains[current.column];
|
|
||||||
|
|
||||||
self.overall_strain = apply_decay(self.overall_strain, current.delta, OVERALL_DECAY_BASE)
|
|
||||||
+ (1.0 + hold_addition) * hold_factor;
|
|
||||||
|
|
||||||
self.individual_strain + self.overall_strain - self.current_strain
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn difficulty_value(strain_peaks: &mut [f64]) -> f64 {
|
|
||||||
let mut difficulty = 0.0;
|
|
||||||
let mut weight = 1.0;
|
|
||||||
|
|
||||||
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
for &strain in strain_peaks.iter() {
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= DECAY_WEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
difficulty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn apply_decay(value: f64, delta_time: f64, decay_base: f64) -> f64 {
|
|
||||||
value * decay_base.powf(delta_time / 1000.0)
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
parse::Pos2,
|
parse::Pos2,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{osu_object::NestedObject, OsuObject, ScalingFactor};
|
use super::{osu_object::OsuSlider, OsuObject, ScalingFactor};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct OsuDifficultyObject<'h> {
|
pub(crate) struct OsuDifficultyObject<'h> {
|
||||||
@@ -31,21 +31,6 @@ impl<'h> OsuDifficultyObject<'h> {
|
|||||||
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||||
let strain_time = delta_time.max(Self::MIN_DELTA_TIME as f64);
|
let strain_time = delta_time.max(Self::MIN_DELTA_TIME as f64);
|
||||||
|
|
||||||
// TODO: remove
|
|
||||||
// println!(
|
|
||||||
// "[{}] lazy_jump_dist={} | lazy_travel_dist={} | \
|
|
||||||
// min_jump_dist={} | min_jump_time={} \
|
|
||||||
// | travel_dist={} | travel_time={} | angle={:?}",
|
|
||||||
// base.start_time,
|
|
||||||
// dists.lazy_jump_dist,
|
|
||||||
// dists.lazy_travel_dist,
|
|
||||||
// dists.min_jump_dist,
|
|
||||||
// dists.min_jump_time,
|
|
||||||
// dists.travel_dist,
|
|
||||||
// dists.travel_time,
|
|
||||||
// dists.angle,
|
|
||||||
// );
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
start_time,
|
start_time,
|
||||||
delta_time,
|
delta_time,
|
||||||
@@ -56,7 +41,13 @@ impl<'h> OsuDifficultyObject<'h> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn opacity_at(&self, time: f64, hidden: bool) -> f64 {
|
pub(crate) fn opacity_at(
|
||||||
|
&self,
|
||||||
|
time: f64,
|
||||||
|
hidden: bool,
|
||||||
|
time_preempt: f64,
|
||||||
|
time_fade_in: f64,
|
||||||
|
) -> f64 {
|
||||||
if time > self.base.start_time {
|
if time > self.base.start_time {
|
||||||
// * Consider a hitobject as being invisible when its start time is passed.
|
// * Consider a hitobject as being invisible when its start time is passed.
|
||||||
// * In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
// * In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
||||||
@@ -64,15 +55,14 @@ impl<'h> OsuDifficultyObject<'h> {
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fade_in_start_time = self.base.start_time - self.base.time_preempt;
|
let fade_in_start_time = self.base.start_time - time_preempt;
|
||||||
let fade_in_duration = self.base.time_fade_in;
|
let fade_in_duration = time_fade_in;
|
||||||
|
|
||||||
if hidden {
|
if hidden {
|
||||||
// * Taken from OsuModHidden.
|
// * Taken from OsuModHidden.
|
||||||
let fade_out_start_time =
|
let fade_out_start_time = self.base.start_time - time_preempt + time_fade_in;
|
||||||
self.base.start_time - self.base.time_preempt + self.base.time_fade_in;
|
|
||||||
const FADE_OUT_DURATION_MULTIPLIER: f64 = 0.3;
|
const FADE_OUT_DURATION_MULTIPLIER: f64 = 0.3;
|
||||||
let fade_out_duration = self.base.time_preempt * FADE_OUT_DURATION_MULTIPLIER;
|
let fade_out_duration = time_preempt * FADE_OUT_DURATION_MULTIPLIER;
|
||||||
|
|
||||||
(((time - fade_in_start_time) / fade_in_duration).clamp(0.0, 1.0))
|
(((time - fade_in_start_time) / fade_in_duration).clamp(0.0, 1.0))
|
||||||
.min(1.0 - ((time - fade_out_start_time) / fade_out_duration).clamp(0.0, 1.0))
|
.min(1.0 - ((time - fade_out_start_time) / fade_out_duration).clamp(0.0, 1.0))
|
||||||
@@ -107,38 +97,28 @@ impl Distances {
|
|||||||
strain_time: f64,
|
strain_time: f64,
|
||||||
scaling_factor_: &ScalingFactor,
|
scaling_factor_: &ScalingFactor,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut this = if let OsuObjectKind::Slider {
|
let mut this =
|
||||||
lazy_end_pos,
|
if let Some(slider_values) = Self::compute_slider_cursor_pos(base, scaling_factor_) {
|
||||||
lazy_travel_time,
|
let SliderValues {
|
||||||
nested_objects,
|
lazy_travel_dist,
|
||||||
..
|
slider,
|
||||||
} = &mut base.kind
|
} = slider_values;
|
||||||
{
|
|
||||||
let lazy_travel_dist = Self::compute_slider_cursor_pos(
|
|
||||||
base.pos,
|
|
||||||
base.start_time,
|
|
||||||
lazy_end_pos,
|
|
||||||
lazy_travel_time,
|
|
||||||
nested_objects,
|
|
||||||
scaling_factor_,
|
|
||||||
);
|
|
||||||
|
|
||||||
let repeat_count = nested_objects.iter().fold(0, |repeats, nested| {
|
let repeat_count = slider.repeat_count();
|
||||||
repeats + matches!(nested.kind, NestedObjectKind::Repeat) as usize
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||||
travel_dist: (lazy_travel_dist
|
travel_dist: (lazy_travel_dist
|
||||||
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
|
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
|
||||||
as f64,
|
as f64,
|
||||||
travel_time: lazy_travel_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
|
travel_time: (base.lazy_travel_time() / clock_rate)
|
||||||
lazy_travel_dist,
|
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
|
||||||
..Default::default()
|
lazy_travel_dist,
|
||||||
}
|
..Default::default()
|
||||||
} else {
|
}
|
||||||
Self::default()
|
} else {
|
||||||
};
|
Self::default()
|
||||||
|
};
|
||||||
|
|
||||||
// * We don't need to calculate either angle or distance when
|
// * We don't need to calculate either angle or distance when
|
||||||
// * one of the last->curr objects is a spinner
|
// * one of the last->curr objects is a spinner
|
||||||
@@ -149,23 +129,19 @@ impl Distances {
|
|||||||
// * We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
// * We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
let scaling_factor = scaling_factor_.factor;
|
let scaling_factor = scaling_factor_.factor;
|
||||||
|
|
||||||
let last_cursor_pos = Self::get_end_cursor_pos(last, scaling_factor_);
|
let last_cursor_pos = Self::get_end_cursor_pos(last);
|
||||||
|
|
||||||
this.lazy_jump_dist =
|
this.lazy_jump_dist = (base.stacked_pos() * scaling_factor
|
||||||
(base.pos * scaling_factor - last_cursor_pos * scaling_factor).length() as f64;
|
- last_cursor_pos * scaling_factor)
|
||||||
|
.length() as f64;
|
||||||
this.min_jump_time = strain_time;
|
this.min_jump_time = strain_time;
|
||||||
this.min_jump_dist = this.lazy_jump_dist;
|
this.min_jump_dist = this.lazy_jump_dist;
|
||||||
|
|
||||||
if let OsuObjectKind::Slider {
|
if let OsuObjectKind::Slider(slider) = &last.kind {
|
||||||
end_pos,
|
let last_travel_time = (last.lazy_travel_time() / clock_rate)
|
||||||
lazy_travel_time,
|
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
||||||
..
|
|
||||||
} = &last.kind
|
|
||||||
{
|
|
||||||
let last_travel_dist =
|
|
||||||
(lazy_travel_time / clock_rate).max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
|
||||||
this.min_jump_time =
|
this.min_jump_time =
|
||||||
(strain_time - last_travel_dist).max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
(strain_time - last_travel_time).max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
||||||
|
|
||||||
// * There are two types of slider-to-object patterns to consider in order
|
// * There are two types of slider-to-object patterns to consider in order
|
||||||
// * to better approximate the real movement a player will take to jump between the hitobjects.
|
// * to better approximate the real movement a player will take to jump between the hitobjects.
|
||||||
@@ -190,19 +166,22 @@ impl Distances {
|
|||||||
// *
|
// *
|
||||||
// * Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
// * Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
||||||
|
|
||||||
let tail_jump_dist = (*end_pos - base.pos).length() * scaling_factor;
|
let stacked_tail_pos =
|
||||||
|
slider.tail().map_or_else(|| last.pos(), |tail| tail.pos) + last.stack_offset;
|
||||||
|
|
||||||
this.min_jump_dist = ((this.lazy_jump_dist
|
let tail_jump_dist = (stacked_tail_pos - base.stacked_pos()).length() * scaling_factor;
|
||||||
|
|
||||||
|
this.min_jump_dist = (this.lazy_jump_dist
|
||||||
- (Self::MAXIMUM_SLIDER_RADIUS - Self::ASSUMED_SLIDER_RADIUS) as f64)
|
- (Self::MAXIMUM_SLIDER_RADIUS - Self::ASSUMED_SLIDER_RADIUS) as f64)
|
||||||
.min((tail_jump_dist - Self::MAXIMUM_SLIDER_RADIUS) as f64))
|
.min((tail_jump_dist - Self::MAXIMUM_SLIDER_RADIUS) as f64)
|
||||||
.max(0.0);
|
.max(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(last_last) = last_last.filter(|obj| !obj.is_spinner()) {
|
if let Some(last_last) = last_last.filter(|obj| !obj.is_spinner()) {
|
||||||
let last_last_cursor_pos = Self::get_end_cursor_pos(last_last, scaling_factor_);
|
let last_last_cursor_pos = Self::get_end_cursor_pos(last_last);
|
||||||
|
|
||||||
let v1 = last_last_cursor_pos - last.pos;
|
let v1 = last_last_cursor_pos - last.stacked_pos();
|
||||||
let v2 = base.pos - last_cursor_pos;
|
let v2 = base.stacked_pos() - last_cursor_pos;
|
||||||
|
|
||||||
let dot = v1.dot(v2) as f64;
|
let dot = v1.dot(v2) as f64;
|
||||||
let det = (v1.x * v2.y - v1.y * v2.x) as f64;
|
let det = (v1.x * v2.y - v1.y * v2.x) as f64;
|
||||||
@@ -213,27 +192,32 @@ impl Distances {
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn compute_slider_cursor_pos(
|
pub(crate) fn compute_slider_cursor_pos<'h>(
|
||||||
stacked_pos: Pos2,
|
hit_object: &'h mut OsuObject,
|
||||||
start_time: f64,
|
|
||||||
lazy_end_pos: &mut Pos2,
|
|
||||||
lazy_travel_time: &mut f64,
|
|
||||||
nested_objects: &[NestedObject],
|
|
||||||
scaling_factor_: &ScalingFactor,
|
scaling_factor_: &ScalingFactor,
|
||||||
) -> f32 {
|
) -> Option<SliderValues<'h>> {
|
||||||
let mut curr_cursor_pos = stacked_pos;
|
let pos = hit_object.pos();
|
||||||
|
|
||||||
|
let slider = if let OsuObjectKind::Slider(slider) = &mut hit_object.kind {
|
||||||
|
slider
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut curr_cursor_pos = pos + hit_object.stack_offset;
|
||||||
let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64;
|
let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64;
|
||||||
|
|
||||||
let mut lazy_travel_dist: f32 = 0.0;
|
let mut lazy_travel_dist: f32 = 0.0;
|
||||||
|
|
||||||
for (curr_movement_obj, i) in nested_objects.iter().zip(1..) {
|
for (curr_movement_obj, i) in slider.nested_iter().zip(1..) {
|
||||||
let mut curr_movement = curr_movement_obj.pos - curr_cursor_pos;
|
let mut curr_movement =
|
||||||
|
(curr_movement_obj.pos + hit_object.stack_offset) - curr_cursor_pos;
|
||||||
let mut curr_movement_len = scaling_factor * curr_movement.length() as f64;
|
let mut curr_movement_len = scaling_factor * curr_movement.length() as f64;
|
||||||
|
|
||||||
// * Amount of movement required so that the cursor position needs to be updated.
|
// * Amount of movement required so that the cursor position needs to be updated.
|
||||||
let mut required_movement = Self::ASSUMED_SLIDER_RADIUS as f64;
|
let mut required_movement = Self::ASSUMED_SLIDER_RADIUS as f64;
|
||||||
|
|
||||||
if i == nested_objects.len() {
|
if i == slider.nested_len() {
|
||||||
// * The end of a slider has special aim rules due
|
// * The end of a slider has special aim rules due
|
||||||
// * to the relaxed time constraint on position.
|
// * to the relaxed time constraint on position.
|
||||||
// * There is both a lazy end position as well as the actual end slider position.
|
// * There is both a lazy end position as well as the actual end slider position.
|
||||||
@@ -242,7 +226,7 @@ impl Distances {
|
|||||||
// * may actually be farther away than the sliders true end.
|
// * may actually be farther away than the sliders true end.
|
||||||
// * This code is designed to prevent buffing situations
|
// * This code is designed to prevent buffing situations
|
||||||
// * where lazy end is actually a less efficient movement.
|
// * where lazy end is actually a less efficient movement.
|
||||||
let lazy_movement = *lazy_end_pos - curr_cursor_pos;
|
let lazy_movement = slider.lazy_end_pos - curr_cursor_pos;
|
||||||
|
|
||||||
if lazy_movement.length() < curr_movement.length() {
|
if lazy_movement.length() < curr_movement.length() {
|
||||||
curr_movement = lazy_movement;
|
curr_movement = lazy_movement;
|
||||||
@@ -261,26 +245,22 @@ impl Distances {
|
|||||||
curr_movement_len *= (curr_movement_len - required_movement) / curr_movement_len;
|
curr_movement_len *= (curr_movement_len - required_movement) / curr_movement_len;
|
||||||
lazy_travel_dist += curr_movement_len as f32;
|
lazy_travel_dist += curr_movement_len as f32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if i == nested_objects.len() {
|
|
||||||
*lazy_end_pos = curr_cursor_pos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*lazy_travel_time = nested_objects
|
slider.lazy_end_pos = curr_cursor_pos;
|
||||||
.last()
|
|
||||||
.map_or(0.0, |nested| nested.start_time - start_time);
|
|
||||||
|
|
||||||
lazy_travel_dist
|
Some(SliderValues {
|
||||||
|
lazy_travel_dist,
|
||||||
|
slider,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_end_cursor_pos(hit_object: &OsuObject, scaling_factor: &ScalingFactor) -> Pos2 {
|
fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 {
|
||||||
if hit_object.is_slider() {
|
hit_object.lazy_end_pos()
|
||||||
let stack_offset = scaling_factor.stack_offset(hit_object.stack_height);
|
|
||||||
|
|
||||||
hit_object.lazy_end_pos(stack_offset)
|
|
||||||
} else {
|
|
||||||
hit_object.pos
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SliderValues<'s> {
|
||||||
|
lazy_travel_dist: f32,
|
||||||
|
slider: &'s OsuSlider,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::{Debug, Formatter, Result as FmtResult},
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
mem,
|
mem,
|
||||||
vec::IntoIter,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{curve::CurveBuffers, Beatmap, Mods};
|
use crate::{curve::CurveBuffers, Beatmap, Mods};
|
||||||
@@ -13,7 +12,8 @@ use super::{
|
|||||||
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
||||||
scaling_factor::ScalingFactor,
|
scaling_factor::ScalingFactor,
|
||||||
skills::{Aim, Flashlight, Skill, Speed},
|
skills::{Aim, Flashlight, Skill, Speed},
|
||||||
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, PERFORMANCE_BASE_MULTIPLIER,
|
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
|
||||||
|
PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gradually calculate the difficulty attributes of an osu!standard map.
|
/// Gradually calculate the difficulty attributes of an osu!standard map.
|
||||||
@@ -74,8 +74,22 @@ impl OsuGradualDifficultyAttributes {
|
|||||||
let map_attrs = map.attributes().mods(mods).build();
|
let map_attrs = map.attributes().mods(mods).build();
|
||||||
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||||
let hr = mods.hr();
|
let hr = mods.hr();
|
||||||
let time_preempt = map_attrs.hit_windows.ar;
|
|
||||||
let hit_window = 2.0 * map_attrs.hit_windows.od;
|
let hit_window = 2.0 * map_attrs.hit_windows.od;
|
||||||
|
let time_preempt = map_attrs.hit_windows.ar;
|
||||||
|
|
||||||
|
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||||
|
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||||
|
// * This uniform speedup is hard to match 1:1, however we can at least make
|
||||||
|
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
||||||
|
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
||||||
|
// * classically known, but it feels good.
|
||||||
|
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
||||||
|
// * become smaller leading to hitcircles not fully fading in.
|
||||||
|
let time_fade_in = if mods.hd() {
|
||||||
|
time_preempt * FADE_IN_DURATION_MULTIPLIER
|
||||||
|
} else {
|
||||||
|
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
|
||||||
|
};
|
||||||
|
|
||||||
let mut attrs = OsuDifficultyAttributes {
|
let mut attrs = OsuDifficultyAttributes {
|
||||||
ar: map_attrs.ar,
|
ar: map_attrs.ar,
|
||||||
@@ -94,7 +108,7 @@ impl OsuGradualDifficultyAttributes {
|
|||||||
let hit_objects_iter = map
|
let hit_objects_iter = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
.filter_map(|h| OsuObject::new(h, &mut params));
|
||||||
|
|
||||||
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||||
hit_objects.extend(hit_objects_iter);
|
hit_objects.extend(hit_objects_iter);
|
||||||
@@ -113,13 +127,12 @@ impl OsuGradualDifficultyAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut hit_objects_iter = hit_objects.iter_mut().map(|h| {
|
let mut hit_objects_iter = hit_objects.iter_mut().map(|h| {
|
||||||
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
h.post_process(hr, &scaling_factor);
|
||||||
h.pos += stack_offset;
|
|
||||||
|
|
||||||
h
|
h
|
||||||
});
|
});
|
||||||
|
|
||||||
let skills = create_skills(mods, scaling_factor.radius);
|
let skills = create_skills(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||||
|
|
||||||
let last = match hit_objects_iter.next() {
|
let last = match hit_objects_iter.next() {
|
||||||
Some(prev) => prev,
|
Some(prev) => prev,
|
||||||
@@ -139,22 +152,7 @@ impl OsuGradualDifficultyAttributes {
|
|||||||
let mut last_last = None;
|
let mut last_last = None;
|
||||||
|
|
||||||
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
||||||
if let OsuObjectKind::Slider {
|
Distances::compute_slider_cursor_pos(last, &scaling_factor);
|
||||||
lazy_travel_time,
|
|
||||||
lazy_end_pos,
|
|
||||||
nested_objects,
|
|
||||||
..
|
|
||||||
} = &mut last.kind
|
|
||||||
{
|
|
||||||
Distances::compute_slider_cursor_pos(
|
|
||||||
last.pos,
|
|
||||||
last.start_time,
|
|
||||||
lazy_end_pos,
|
|
||||||
lazy_travel_time,
|
|
||||||
nested_objects,
|
|
||||||
&scaling_factor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut last = &*last;
|
let mut last = &*last;
|
||||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2));
|
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2));
|
||||||
@@ -218,9 +216,9 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
|||||||
|
|
||||||
match &curr.base.kind {
|
match &curr.base.kind {
|
||||||
OsuObjectKind::Circle => attrs.n_circles += 1,
|
OsuObjectKind::Circle => attrs.n_circles += 1,
|
||||||
OsuObjectKind::Slider { nested_objects, .. } => {
|
OsuObjectKind::Slider(slider) => {
|
||||||
attrs.n_sliders += 1;
|
attrs.n_sliders += 1;
|
||||||
attrs.max_combo += nested_objects.len();
|
attrs.max_combo += slider.nested_len();
|
||||||
}
|
}
|
||||||
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
|
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
|
||||||
}
|
}
|
||||||
@@ -316,37 +314,6 @@ impl ExactSizeIterator for OsuGradualDifficultyAttributes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ mod tests {
|
|||||||
n300: 88,
|
n300: 88,
|
||||||
n100: 8,
|
n100: 8,
|
||||||
n50: 2,
|
n50: 2,
|
||||||
misses: 2,
|
n_misses: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
let next = gradual1.process_next_object(state.clone());
|
let next = gradual1.process_next_object(state.clone());
|
||||||
@@ -236,7 +236,7 @@ mod tests {
|
|||||||
n300: 601,
|
n300: 601,
|
||||||
n100: 0,
|
n100: 0,
|
||||||
n50: 0,
|
n50: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||||
@@ -259,7 +259,7 @@ mod tests {
|
|||||||
n300: 100,
|
n300: 100,
|
||||||
n100: 0,
|
n100: 0,
|
||||||
n50: 0,
|
n50: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||||
|
|||||||
+54
-44
@@ -6,11 +6,11 @@ mod pp;
|
|||||||
mod scaling_factor;
|
mod scaling_factor;
|
||||||
mod skills;
|
mod skills;
|
||||||
|
|
||||||
use crate::{curve::CurveBuffers, AnyStars, Beatmap, GameMode, Mods};
|
use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
difficulty_object::{Distances, OsuDifficultyObject},
|
difficulty_object::{Distances, OsuDifficultyObject},
|
||||||
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
osu_object::{ObjectParameters, OsuObject},
|
||||||
scaling_factor::ScalingFactor,
|
scaling_factor::ScalingFactor,
|
||||||
skills::{Aim, Flashlight, Skill, Speed},
|
skills::{Aim, Flashlight, Skill, Speed},
|
||||||
};
|
};
|
||||||
@@ -24,6 +24,9 @@ const NORMALIZED_RADIUS: f32 = 50.0;
|
|||||||
const STACK_DISTANCE: f32 = 3.0;
|
const STACK_DISTANCE: f32 = 3.0;
|
||||||
// * This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
// * This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
|
const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
|
||||||
|
const PREEMPT_MIN: f64 = 450.0;
|
||||||
|
const FADE_IN_DURATION_MULTIPLIER: f64 = 0.4;
|
||||||
|
const PLAYFIELD_BASE_SIZE: Pos2 = Pos2 { x: 512.0, y: 384.0 };
|
||||||
|
|
||||||
/// Difficulty calculator on osu!standard maps.
|
/// Difficulty calculator on osu!standard maps.
|
||||||
///
|
///
|
||||||
@@ -237,16 +240,30 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
|||||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods).clock_rate(clock_rate).build();
|
let map_attrs = map.attributes().mods(mods).clock_rate(clock_rate).build();
|
||||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||||
let hr = mods.hr();
|
let hr = mods.hr();
|
||||||
let time_preempt = map_attributes.hit_windows.ar;
|
let hit_window = 2.0 * map_attrs.hit_windows.od;
|
||||||
let hit_window = 2.0 * map_attributes.hit_windows.od;
|
let time_preempt = (map_attrs.hit_windows.ar * clock_rate) as f32 as f64;
|
||||||
|
|
||||||
|
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||||
|
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||||
|
// * This uniform speedup is hard to match 1:1, however we can at least make
|
||||||
|
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
||||||
|
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
||||||
|
// * classically known, but it feels good.
|
||||||
|
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
||||||
|
// * become smaller leading to hitcircles not fully fading in.
|
||||||
|
let time_fade_in = if mods.hd() {
|
||||||
|
time_preempt * FADE_IN_DURATION_MULTIPLIER
|
||||||
|
} else {
|
||||||
|
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
|
||||||
|
};
|
||||||
|
|
||||||
let mut attributes = OsuDifficultyAttributes {
|
let mut attributes = OsuDifficultyAttributes {
|
||||||
ar: map_attributes.ar,
|
ar: map_attrs.ar,
|
||||||
hp: map_attributes.hp,
|
hp: map_attrs.hp,
|
||||||
od: map_attributes.od,
|
od: map_attrs.od,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,7 +278,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
|||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.take(take)
|
.take(take)
|
||||||
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
.filter_map(|h| OsuObject::new(h, &mut params));
|
||||||
|
|
||||||
let mut hit_objects = Vec::with_capacity(take.min(map.hit_objects.len()));
|
let mut hit_objects = Vec::with_capacity(take.min(map.hit_objects.len()));
|
||||||
hit_objects.extend(hit_objects_iter);
|
hit_objects.extend(hit_objects_iter);
|
||||||
@@ -275,13 +292,12 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut hit_objects = hit_objects.iter_mut().map(|h| {
|
let mut hit_objects = hit_objects.iter_mut().map(|h| {
|
||||||
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
h.post_process(hr, &scaling_factor);
|
||||||
h.pos += stack_offset;
|
|
||||||
|
|
||||||
h
|
h
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut skills = create_skills(mods, scaling_factor.radius);
|
let mut skills = create_skills(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||||
|
|
||||||
let last = match hit_objects.next() {
|
let last = match hit_objects.next() {
|
||||||
Some(prev) => prev,
|
Some(prev) => prev,
|
||||||
@@ -291,25 +307,10 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
|||||||
let mut last_last = None;
|
let mut last_last = None;
|
||||||
|
|
||||||
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
||||||
if let OsuObjectKind::Slider {
|
Distances::compute_slider_cursor_pos(last, &scaling_factor);
|
||||||
lazy_travel_time,
|
|
||||||
lazy_end_pos,
|
|
||||||
nested_objects,
|
|
||||||
..
|
|
||||||
} = &mut last.kind
|
|
||||||
{
|
|
||||||
Distances::compute_slider_cursor_pos(
|
|
||||||
last.pos,
|
|
||||||
last.start_time,
|
|
||||||
lazy_end_pos,
|
|
||||||
lazy_travel_time,
|
|
||||||
nested_objects,
|
|
||||||
&scaling_factor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut last = &*last;
|
let mut last = &*last;
|
||||||
let mut diff_objects = Vec::with_capacity(hit_objects.len().saturating_sub(2));
|
let mut diff_objects = Vec::with_capacity(hit_objects.len());
|
||||||
|
|
||||||
for (i, curr) in hit_objects.enumerate() {
|
for (i, curr) in hit_objects.enumerate() {
|
||||||
let delta_time = (curr.start_time - last.start_time) / clock_rate;
|
let delta_time = (curr.start_time - last.start_time) / clock_rate;
|
||||||
@@ -399,8 +400,8 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
// * o <- hitCircle has stack of -2
|
// * o <- hitCircle has stack of -2
|
||||||
if hit_objects[n].is_slider()
|
if hit_objects[n].is_slider()
|
||||||
&& hit_objects[n]
|
&& hit_objects[n]
|
||||||
.end_pos()
|
.pre_stacked_end_pos()
|
||||||
.distance(hit_objects[obj_i_idx].pos)
|
.distance(hit_objects[obj_i_idx].pos())
|
||||||
< STACK_DISTANCE
|
< STACK_DISTANCE
|
||||||
{
|
{
|
||||||
let offset =
|
let offset =
|
||||||
@@ -409,7 +410,11 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
for j in n + 1..=i {
|
for j in n + 1..=i {
|
||||||
// * For each object which was declared under this slider, we will offset
|
// * For each object which was declared under this slider, we will offset
|
||||||
// * it to appear *below* the slider end (rather than above).
|
// * it to appear *below* the slider end (rather than above).
|
||||||
if hit_objects[n].end_pos().distance(hit_objects[j].pos) < STACK_DISTANCE {
|
if hit_objects[n]
|
||||||
|
.pre_stacked_end_pos()
|
||||||
|
.distance(hit_objects[j].pos())
|
||||||
|
< STACK_DISTANCE
|
||||||
|
{
|
||||||
hit_objects[j].stack_height -= offset;
|
hit_objects[j].stack_height -= offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,7 +425,7 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if hit_objects[n].pos.distance(hit_objects[obj_i_idx].pos) < STACK_DISTANCE {
|
if hit_objects[n].pos().distance(hit_objects[obj_i_idx].pos()) < STACK_DISTANCE {
|
||||||
// * Keep processing as if there are no sliders.
|
// * Keep processing as if there are no sliders.
|
||||||
// * If we come across a slider, this gets cancelled out.
|
// * If we come across a slider, this gets cancelled out.
|
||||||
// * NOTE: Sliders with start positions stacking
|
// * NOTE: Sliders with start positions stacking
|
||||||
@@ -441,15 +446,15 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
|
|
||||||
if hit_objects[n].is_spinner() {
|
if hit_objects[n].is_spinner() {
|
||||||
continue;
|
continue;
|
||||||
} else if hit_objects[obj_i_idx].start_time - hit_objects[n].start_time
|
}
|
||||||
> stack_threshold
|
|
||||||
{
|
if hit_objects[obj_i_idx].start_time - hit_objects[n].start_time > stack_threshold {
|
||||||
break; // * We are no longer within stacking range of the previous object.
|
break; // * We are no longer within stacking range of the previous object.
|
||||||
}
|
}
|
||||||
|
|
||||||
if hit_objects[n]
|
if hit_objects[n]
|
||||||
.end_pos()
|
.pre_stacked_end_pos()
|
||||||
.distance(hit_objects[obj_i_idx].pos)
|
.distance(hit_objects[obj_i_idx].pos())
|
||||||
< STACK_DISTANCE
|
< STACK_DISTANCE
|
||||||
{
|
{
|
||||||
hit_objects[n].stack_height = hit_objects[obj_i_idx].stack_height + 1.0;
|
hit_objects[n].stack_height = hit_objects[obj_i_idx].stack_height + 1.0;
|
||||||
@@ -467,7 +472,7 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut start_time = hit_objects[i].end_time();
|
let mut start_time = hit_objects[i].end_time();
|
||||||
let end_pos = hit_objects[i].end_pos();
|
let pos2 = hit_objects[i].old_stacking_pos2();
|
||||||
|
|
||||||
let mut slider_stack = 0.0;
|
let mut slider_stack = 0.0;
|
||||||
|
|
||||||
@@ -476,10 +481,10 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if hit_objects[j].pos.distance(hit_objects[i].pos) < STACK_DISTANCE {
|
if hit_objects[j].pos().distance(hit_objects[i].pos()) < STACK_DISTANCE {
|
||||||
hit_objects[i].stack_height += 1.0;
|
hit_objects[i].stack_height += 1.0;
|
||||||
start_time = hit_objects[j].end_time();
|
start_time = hit_objects[j].end_time();
|
||||||
} else if hit_objects[j].pos.distance(end_pos) < STACK_DISTANCE {
|
} else if hit_objects[j].pos().distance(pos2) < STACK_DISTANCE {
|
||||||
slider_stack += 1.0;
|
slider_stack += 1.0;
|
||||||
hit_objects[j].stack_height -= slider_stack;
|
hit_objects[j].stack_height -= slider_stack;
|
||||||
start_time = hit_objects[j].end_time();
|
start_time = hit_objects[j].end_time();
|
||||||
@@ -488,12 +493,17 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_skills(mods: u32, radius: f32) -> [Box<dyn Skill>; 4] {
|
fn create_skills(
|
||||||
|
mods: u32,
|
||||||
|
radius: f32,
|
||||||
|
time_preempt: f64,
|
||||||
|
time_fade_in: f64,
|
||||||
|
) -> [Box<dyn Skill>; 4] {
|
||||||
[
|
[
|
||||||
Box::new(Aim::new(true)) as Box<dyn Skill>,
|
Box::new(Aim::new(true)) as Box<dyn Skill>,
|
||||||
Box::new(Aim::new(false)) as Box<dyn Skill>,
|
Box::new(Aim::new(false)) as Box<dyn Skill>,
|
||||||
Box::new(Speed::new()) as Box<dyn Skill>,
|
Box::new(Speed::new()) as Box<dyn Skill>,
|
||||||
Box::new(Flashlight::new(mods, radius)) as Box<dyn Skill>,
|
Box::new(Flashlight::new(mods, radius, time_preempt, time_fade_in)) as Box<dyn Skill>,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+198
-139
@@ -1,9 +1,8 @@
|
|||||||
use std::{cmp::Ordering, convert::identity};
|
use std::slice::Iter;
|
||||||
|
|
||||||
use super::OsuDifficultyAttributes;
|
use super::{scaling_factor::ScalingFactor, OsuDifficultyAttributes, PLAYFIELD_BASE_SIZE};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
beatmap::DifficultyPoint,
|
|
||||||
curve::{Curve, CurveBuffers},
|
curve::{Curve, CurveBuffers},
|
||||||
parse::{HitObject, HitObjectKind, Pos2},
|
parse::{HitObject, HitObjectKind, Pos2},
|
||||||
Beatmap,
|
Beatmap,
|
||||||
@@ -14,31 +13,65 @@ const BASE_SCORING_DISTANCE: f64 = 100.0;
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct OsuObject {
|
pub(crate) struct OsuObject {
|
||||||
|
pos: Pos2,
|
||||||
pub(crate) start_time: f64,
|
pub(crate) start_time: f64,
|
||||||
pub(crate) pos: Pos2,
|
pub(crate) stack_offset: Pos2,
|
||||||
pub(crate) stack_height: f32,
|
pub(crate) stack_height: f32,
|
||||||
pub(crate) time_preempt: f64,
|
|
||||||
pub(crate) time_fade_in: f64,
|
|
||||||
pub(crate) kind: OsuObjectKind,
|
pub(crate) kind: OsuObjectKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum OsuObjectKind {
|
pub(crate) enum OsuObjectKind {
|
||||||
Circle,
|
Circle,
|
||||||
Slider {
|
Slider(OsuSlider),
|
||||||
end_time: f64,
|
Spinner { end_time: f64 },
|
||||||
end_pos: Pos2,
|
}
|
||||||
lazy_travel_time: f64,
|
|
||||||
lazy_end_pos: Pos2,
|
#[derive(Clone, Debug)]
|
||||||
nested_objects: Vec<NestedObject>,
|
pub(crate) struct OsuSlider {
|
||||||
},
|
pub(crate) end_time: f64,
|
||||||
Spinner {
|
pub(crate) lazy_end_pos: Pos2,
|
||||||
end_time: f64,
|
nested_objects: Vec<NestedObject>,
|
||||||
},
|
}
|
||||||
|
|
||||||
|
impl OsuSlider {
|
||||||
|
pub(crate) fn nested_len(&self) -> usize {
|
||||||
|
self.nested_objects.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn nested_iter(&self) -> Iter<'_, NestedObject> {
|
||||||
|
self.nested_objects.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn repeat_count(&self) -> usize {
|
||||||
|
self.nested_objects.iter().fold(0, |count, nested| {
|
||||||
|
count + matches!(nested.kind, NestedObjectKind::Repeat) as usize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn end_pos(&self) -> Option<Pos2> {
|
||||||
|
self.tail().map(|tail| tail.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tail(&self) -> Option<&NestedObject> {
|
||||||
|
self.nested_objects
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|nested| matches!(nested.kind, NestedObjectKind::Tail))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tail_mut(&mut self) -> Option<(usize, &mut NestedObject)> {
|
||||||
|
self.nested_objects
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find(|(_, nested)| matches!(nested.kind, NestedObjectKind::Tail))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct NestedObject {
|
pub(crate) struct NestedObject {
|
||||||
|
/// Note: `pos` does not include stacking!
|
||||||
pub(crate) pos: Pos2,
|
pub(crate) pos: Pos2,
|
||||||
pub(crate) start_time: f64,
|
pub(crate) start_time: f64,
|
||||||
pub(crate) kind: NestedObjectKind,
|
pub(crate) kind: NestedObjectKind,
|
||||||
@@ -59,11 +92,7 @@ pub(crate) struct ObjectParameters<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OsuObject {
|
impl OsuObject {
|
||||||
const PREEMPT_MIN: f64 = 450.0;
|
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||||
const TIME_PREEMPT: f64 = 600.0;
|
|
||||||
const TIME_FADE_IN: f64 = 400.0;
|
|
||||||
|
|
||||||
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
|
||||||
let ObjectParameters {
|
let ObjectParameters {
|
||||||
map,
|
map,
|
||||||
attributes,
|
attributes,
|
||||||
@@ -72,37 +101,17 @@ impl OsuObject {
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||||
let mut pos = h.pos;
|
let pos = h.pos;
|
||||||
|
|
||||||
if hr {
|
|
||||||
pos.y = 384.0 - pos.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
let obj = match &h.kind {
|
let obj = match &h.kind {
|
||||||
HitObjectKind::Circle => {
|
HitObjectKind::Circle => {
|
||||||
attributes.n_circles += 1;
|
attributes.n_circles += 1;
|
||||||
|
|
||||||
// TODO: check if ar needs to be adjusted
|
|
||||||
let tmp_preempt =
|
|
||||||
difficulty_range(map.ar as f64, 1800.0, 1200.0, Self::PREEMPT_MIN) as f32;
|
|
||||||
let time_preempt = tmp_preempt as f64;
|
|
||||||
|
|
||||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
|
||||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
|
||||||
// * This uniform speedup is hard to match 1:1, however we can at least make
|
|
||||||
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
|
||||||
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
|
||||||
// * classically known, but it feels good.
|
|
||||||
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
|
||||||
// * become smaller leading to hitcircles not fully fading in.
|
|
||||||
let time_fade_in = 400.0 * (time_preempt / Self::PREEMPT_MIN).min(1.0);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
start_time: h.start_time,
|
start_time: h.start_time,
|
||||||
pos,
|
pos,
|
||||||
|
stack_offset: Pos2::default(),
|
||||||
stack_height: 0.0,
|
stack_height: 0.0,
|
||||||
time_preempt,
|
|
||||||
time_fade_in,
|
|
||||||
kind: OsuObjectKind::Circle,
|
kind: OsuObjectKind::Circle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,14 +134,7 @@ impl OsuObject {
|
|||||||
// * prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
// * prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||||
// * this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
// * this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||||
let tick_dist_mult = if map.version < 8 {
|
let tick_dist_mult = if map.version < 8 {
|
||||||
let first_slider_vel = map
|
difficulty_point.slider_vel.recip()
|
||||||
.difficulty_points
|
|
||||||
.first()
|
|
||||||
.map_or(DifficultyPoint::DEFAULT_SLIDER_VEL, |point| {
|
|
||||||
point.slider_vel
|
|
||||||
});
|
|
||||||
|
|
||||||
first_slider_vel.recip()
|
|
||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
@@ -163,21 +165,6 @@ impl OsuObject {
|
|||||||
|
|
||||||
let mut curr_dist = tick_dist;
|
let mut curr_dist = tick_dist;
|
||||||
|
|
||||||
// TODO: check if ar needs to be adjusted
|
|
||||||
let tmp_preempt =
|
|
||||||
difficulty_range(map.ar as f64, 1800.0, 1200.0, Self::PREEMPT_MIN) as f32;
|
|
||||||
let head_time_preempt = tmp_preempt as f64;
|
|
||||||
|
|
||||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
|
||||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
|
||||||
// * This uniform speedup is hard to match 1:1, however we can at least make
|
|
||||||
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
|
||||||
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
|
||||||
// * classically known, but it feels good.
|
|
||||||
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
|
||||||
// * become smaller leading to hitcircles not fully fading in.
|
|
||||||
let head_time_fade_in = 400.0 * (head_time_preempt / Self::PREEMPT_MIN).min(1.0);
|
|
||||||
|
|
||||||
ticks.clear();
|
ticks.clear();
|
||||||
|
|
||||||
let mut nested_objects = if tick_dist != 0.0 {
|
let mut nested_objects = if tick_dist != 0.0 {
|
||||||
@@ -190,11 +177,7 @@ impl OsuObject {
|
|||||||
let progress = curr_dist / len;
|
let progress = curr_dist / len;
|
||||||
|
|
||||||
let curr_time = h.start_time + progress * span_duration;
|
let curr_time = h.start_time + progress * span_duration;
|
||||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
let curr_pos = h.pos + curve.position_at(progress);
|
||||||
|
|
||||||
if hr {
|
|
||||||
curr_pos.y = 384.0 - curr_pos.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tick = NestedObject {
|
let tick = NestedObject {
|
||||||
pos: curr_pos,
|
pos: curr_pos,
|
||||||
@@ -215,11 +198,7 @@ impl OsuObject {
|
|||||||
|
|
||||||
// Repeat point
|
// Repeat point
|
||||||
let curr_time = h.start_time + span_duration * span_idx_f64;
|
let curr_time = h.start_time + span_duration * span_idx_f64;
|
||||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
let curr_pos = h.pos + curve.position_at(progress);
|
||||||
|
|
||||||
if hr {
|
|
||||||
curr_pos.y = 384.0 - curr_pos.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
let repeat = NestedObject {
|
let repeat = NestedObject {
|
||||||
pos: curr_pos,
|
pos: curr_pos,
|
||||||
@@ -228,6 +207,7 @@ impl OsuObject {
|
|||||||
};
|
};
|
||||||
|
|
||||||
nested_objects.push(repeat);
|
nested_objects.push(repeat);
|
||||||
|
let span_offset = span_idx_f64 * span_duration;
|
||||||
|
|
||||||
// Ticks
|
// Ticks
|
||||||
if span_idx & 1 == 1 {
|
if span_idx & 1 == 1 {
|
||||||
@@ -246,30 +226,20 @@ impl OsuObject {
|
|||||||
// 30 = 24 + 6
|
// 30 = 24 + 6
|
||||||
// 32 = 24 + 8
|
// 32 = 24 + 8
|
||||||
|
|
||||||
let offset = span_idx_f64 * span_duration;
|
let base = h.start_time + h.start_time + span_duration;
|
||||||
|
|
||||||
let tick_iter = ticks.iter().rev().zip(ticks.iter()).map(
|
let tick_iter = ticks.iter().rev().map(|(pos, time)| NestedObject {
|
||||||
|((rev_pos, _), (_, time))| {
|
pos: *pos,
|
||||||
let start_time = offset + time;
|
start_time: span_offset + base - time,
|
||||||
|
kind: NestedObjectKind::Tick,
|
||||||
NestedObject {
|
});
|
||||||
pos: *rev_pos,
|
|
||||||
start_time,
|
|
||||||
kind: NestedObjectKind::Tick,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
nested_objects.extend(tick_iter);
|
nested_objects.extend(tick_iter);
|
||||||
} else {
|
} else {
|
||||||
let tick_iter = ticks.iter().map(|(pos, time)| {
|
let tick_iter = ticks.iter().map(|(pos, time)| NestedObject {
|
||||||
let start_time = time + span_duration * span_idx_f64;
|
pos: *pos,
|
||||||
|
start_time: time + span_offset,
|
||||||
NestedObject {
|
kind: NestedObjectKind::Tick,
|
||||||
pos: *pos,
|
|
||||||
start_time,
|
|
||||||
kind: NestedObjectKind::Tick,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
nested_objects.extend(tick_iter);
|
nested_objects.extend(tick_iter);
|
||||||
@@ -287,11 +257,7 @@ impl OsuObject {
|
|||||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||||
|
|
||||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||||
let mut end_pos = h.pos + curve.position_at(progress);
|
let end_pos = curve.position_at(progress);
|
||||||
|
|
||||||
if hr {
|
|
||||||
end_pos.y = 384.0 - end_pos.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * we need to use the LegacyLastTick here for compatibility reasons (difficulty).
|
// * we need to use the LegacyLastTick here for compatibility reasons (difficulty).
|
||||||
// * it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
|
// * it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
|
||||||
@@ -307,13 +273,10 @@ impl OsuObject {
|
|||||||
match nested_objects.last() {
|
match nested_objects.last() {
|
||||||
Some(last) if last.start_time > final_span_end_time => {
|
Some(last) if last.start_time > final_span_end_time => {
|
||||||
let idx = nested_objects
|
let idx = nested_objects
|
||||||
.binary_search_by(|nested| {
|
.iter()
|
||||||
nested
|
.rev()
|
||||||
.start_time
|
.position(|nested| nested.start_time <= final_span_end_time)
|
||||||
.partial_cmp(&final_span_end_time)
|
.map_or(0, |i| nested_objects.len() - i);
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
})
|
|
||||||
.map_or_else(identity, identity);
|
|
||||||
|
|
||||||
nested_objects.insert(idx, legacy_last_tick);
|
nested_objects.insert(idx, legacy_last_tick);
|
||||||
}
|
}
|
||||||
@@ -336,25 +299,22 @@ impl OsuObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// * temporary lazy end position until a real result can be derived.
|
// * temporary lazy end position until a real result can be derived.
|
||||||
let mut lazy_end_pos = h.pos + curve.position_at(end_time_min);
|
// The position is added after the stacking for the correct order of
|
||||||
|
// floating point operations.
|
||||||
|
let lazy_end_pos = curve.position_at(end_time_min);
|
||||||
|
|
||||||
if hr {
|
let slider = OsuSlider {
|
||||||
lazy_end_pos.y = 384.0 - lazy_end_pos.y;
|
end_time,
|
||||||
}
|
lazy_end_pos,
|
||||||
|
nested_objects,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
start_time: h.start_time,
|
start_time: h.start_time,
|
||||||
pos,
|
pos,
|
||||||
|
stack_offset: Pos2::default(),
|
||||||
stack_height: 0.0,
|
stack_height: 0.0,
|
||||||
time_preempt: head_time_preempt,
|
kind: OsuObjectKind::Slider(slider),
|
||||||
time_fade_in: head_time_fade_in,
|
|
||||||
kind: OsuObjectKind::Slider {
|
|
||||||
end_time,
|
|
||||||
end_pos,
|
|
||||||
lazy_end_pos,
|
|
||||||
lazy_travel_time,
|
|
||||||
nested_objects,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HitObjectKind::Spinner { end_time } => {
|
HitObjectKind::Spinner { end_time } => {
|
||||||
@@ -363,9 +323,8 @@ impl OsuObject {
|
|||||||
Self {
|
Self {
|
||||||
start_time: h.start_time,
|
start_time: h.start_time,
|
||||||
pos,
|
pos,
|
||||||
|
stack_offset: Pos2::default(),
|
||||||
stack_height: 0.0,
|
stack_height: 0.0,
|
||||||
time_preempt: Self::TIME_PREEMPT,
|
|
||||||
time_fade_in: Self::TIME_FADE_IN,
|
|
||||||
kind: OsuObjectKind::Spinner {
|
kind: OsuObjectKind::Spinner {
|
||||||
end_time: *end_time,
|
end_time: *end_time,
|
||||||
},
|
},
|
||||||
@@ -377,28 +336,78 @@ impl OsuObject {
|
|||||||
Some(obj)
|
Some(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn end_time(&self) -> f64 {
|
pub(crate) fn end_time(&self) -> f64 {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
OsuObjectKind::Circle => self.start_time,
|
OsuObjectKind::Circle => self.start_time,
|
||||||
OsuObjectKind::Slider { end_time, .. } => *end_time,
|
OsuObjectKind::Slider(slider) => slider.end_time,
|
||||||
OsuObjectKind::Spinner { end_time } => *end_time,
|
OsuObjectKind::Spinner { end_time } => *end_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn end_pos(&self) -> Pos2 {
|
pub(crate) fn end_pos(&self) -> Pos2 {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||||
OsuObjectKind::Slider { end_pos, .. } => *end_pos,
|
OsuObjectKind::Slider(slider) => slider.end_pos().unwrap_or(self.pos),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
pub(crate) fn pre_stacked_end_pos(&self) -> Pos2 {
|
||||||
pub(crate) fn lazy_end_pos(&self, stack_offset: Pos2) -> Pos2 {
|
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||||
OsuObjectKind::Slider { lazy_end_pos, .. } => *lazy_end_pos + stack_offset,
|
OsuObjectKind::Slider(slider) => slider
|
||||||
|
.end_pos()
|
||||||
|
.map_or(self.pos, |end_pos| self.pos + end_pos),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn old_stacking_pos2(&self) -> Pos2 {
|
||||||
|
match &self.kind {
|
||||||
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||||
|
OsuObjectKind::Slider(slider) => {
|
||||||
|
// Old stacking requires the path end position
|
||||||
|
// instead of slider end position
|
||||||
|
let repeat_count = slider.repeat_count();
|
||||||
|
|
||||||
|
if repeat_count % 2 == 0 {
|
||||||
|
slider
|
||||||
|
.end_pos()
|
||||||
|
.map_or(self.pos, |end_pos| self.pos + end_pos)
|
||||||
|
} else {
|
||||||
|
slider
|
||||||
|
.nested_iter()
|
||||||
|
.find(|nested| matches!(nested.kind, NestedObjectKind::Repeat))
|
||||||
|
.map_or(self.pos, |repeat| repeat.pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn pos(&self) -> Pos2 {
|
||||||
|
self.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stacked_pos(&self) -> Pos2 {
|
||||||
|
self.pos + self.stack_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stacked_end_pos(&self) -> Pos2 {
|
||||||
|
self.end_pos() + self.stack_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lazy_end_pos(&self) -> Pos2 {
|
||||||
|
match &self.kind {
|
||||||
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.stacked_pos(),
|
||||||
|
OsuObjectKind::Slider(slider) => slider.lazy_end_pos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lazy_travel_time(&self) -> f64 {
|
||||||
|
match &self.kind {
|
||||||
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
|
||||||
|
OsuObjectKind::Slider(slider) => slider
|
||||||
|
.nested_objects
|
||||||
|
.last()
|
||||||
|
.map_or(0.0, |nested| nested.start_time - self.start_time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,15 +425,65 @@ impl OsuObject {
|
|||||||
pub(crate) fn is_spinner(&self) -> bool {
|
pub(crate) fn is_spinner(&self) -> bool {
|
||||||
matches!(self.kind, OsuObjectKind::Spinner { .. })
|
matches!(self.kind, OsuObjectKind::Spinner { .. })
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: cleanup
|
/// Applies stack offset, flips playfield for HR,
|
||||||
fn difficulty_range(difficulty: f64, min: f64, mid: f64, max: f64) -> f64 {
|
/// and adjusts slider tails and lazy_end_positions.
|
||||||
if difficulty > 5.0 {
|
pub(crate) fn post_process(&mut self, hr: bool, scaling_factor: &ScalingFactor) {
|
||||||
mid + (max - mid) * (difficulty - 5.0) / 5.0
|
self.stack_offset = scaling_factor.stack_offset(self.stack_height);
|
||||||
} else if difficulty < 5.0 {
|
let pos = self.pos();
|
||||||
mid - (mid - min) * (5.0 - difficulty) / 5.0
|
|
||||||
} else {
|
if let OsuObjectKind::Slider(slider) = &mut self.kind {
|
||||||
mid
|
if hr {
|
||||||
|
let mut lazy_end_pos = pos;
|
||||||
|
lazy_end_pos.y = PLAYFIELD_BASE_SIZE.y - lazy_end_pos.y;
|
||||||
|
lazy_end_pos += self.stack_offset;
|
||||||
|
|
||||||
|
lazy_end_pos += Pos2 {
|
||||||
|
x: slider.lazy_end_pos.x,
|
||||||
|
y: -slider.lazy_end_pos.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
slider.lazy_end_pos = lazy_end_pos;
|
||||||
|
|
||||||
|
let tail_idx = slider.tail_mut().map(|(tail_idx, tail)| {
|
||||||
|
let mut tail_pos = pos;
|
||||||
|
tail_pos.y = PLAYFIELD_BASE_SIZE.y - tail_pos.y;
|
||||||
|
|
||||||
|
tail_pos += Pos2 {
|
||||||
|
x: tail.pos.x,
|
||||||
|
y: -tail.pos.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
tail.pos = tail_pos;
|
||||||
|
|
||||||
|
tail_idx
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(tail_idx) = tail_idx {
|
||||||
|
for nested in slider.nested_objects[..tail_idx].iter_mut() {
|
||||||
|
nested.pos.y = PLAYFIELD_BASE_SIZE.y - nested.pos.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
for nested in slider.nested_objects[tail_idx + 1..].iter_mut() {
|
||||||
|
nested.pos.y = PLAYFIELD_BASE_SIZE.y - nested.pos.y;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Should never happen since sliders are bound to have a tail
|
||||||
|
for nested in slider.nested_objects.iter_mut() {
|
||||||
|
nested.pos.y = PLAYFIELD_BASE_SIZE.y - nested.pos.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slider.lazy_end_pos += pos + self.stack_offset;
|
||||||
|
|
||||||
|
if let Some((_, tail)) = slider.tail_mut() {
|
||||||
|
tail.pos += pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hr {
|
||||||
|
self.pos.y = PLAYFIELD_BASE_SIZE.y - pos.y
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ impl ScalingFactor {
|
|||||||
Self {
|
Self {
|
||||||
factor,
|
factor,
|
||||||
radius,
|
radius,
|
||||||
scale: scale * -6.4,
|
scale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn stack_offset(&self, stack_height: f32) -> Pos2 {
|
pub(crate) fn stack_offset(&self, stack_height: f32) -> Pos2 {
|
||||||
Pos2::new(stack_height * self.scale)
|
Pos2::new(stack_height * self.scale * -6.4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
f64::consts::{FRAC_PI_2, FRAC_PI_6, PI},
|
f64::consts::{FRAC_PI_2, PI},
|
||||||
mem,
|
mem,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ impl AimEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn calc_wide_angle_bonus(angle: f64) -> f64 {
|
fn calc_wide_angle_bonus(angle: f64) -> f64 {
|
||||||
let base = (3.0 / 4.0 * ((5.0 / 6.0 * PI).min(angle.max(FRAC_PI_6)) - FRAC_PI_6)).sin();
|
let base = (3.0 / 4.0 * ((5.0 / 6.0 * PI).min(angle.max(PI / 6.0)) - PI / 6.0)).sin();
|
||||||
|
|
||||||
base * base
|
base * base
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
use std::{any::Any, mem};
|
use std::{any::Any, mem};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
osu::{
|
osu::{difficulty_object::OsuDifficultyObject, osu_object::OsuObjectKind},
|
||||||
difficulty_object::OsuDifficultyObject,
|
|
||||||
osu_object::{NestedObjectKind, OsuObjectKind},
|
|
||||||
},
|
|
||||||
Mods,
|
Mods,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,13 +15,15 @@ pub(crate) struct Flashlight {
|
|||||||
strain_peaks: Vec<f64>,
|
strain_peaks: Vec<f64>,
|
||||||
has_hidden_mod: bool,
|
has_hidden_mod: bool,
|
||||||
scaling_factor: f64,
|
scaling_factor: f64,
|
||||||
|
time_preempt: f64,
|
||||||
|
time_fade_in: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flashlight {
|
impl Flashlight {
|
||||||
const SKILL_MULTIPLIER: f64 = 0.052;
|
const SKILL_MULTIPLIER: f64 = 0.052;
|
||||||
const STRAIN_DECAY_BASE: f64 = 0.15;
|
const STRAIN_DECAY_BASE: f64 = 0.15;
|
||||||
|
|
||||||
pub(crate) fn new(mods: u32, radius: f32) -> Self {
|
pub(crate) fn new(mods: u32, radius: f32, time_preempt: f64, time_fade_in: f64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
curr_strain: 0.0,
|
curr_strain: 0.0,
|
||||||
curr_section_peak: 0.0,
|
curr_section_peak: 0.0,
|
||||||
@@ -32,6 +31,8 @@ impl Flashlight {
|
|||||||
strain_peaks: Vec::new(),
|
strain_peaks: Vec::new(),
|
||||||
has_hidden_mod: mods.hd(),
|
has_hidden_mod: mods.hd(),
|
||||||
scaling_factor: 52.0 / radius as f64,
|
scaling_factor: 52.0 / radius as f64,
|
||||||
|
time_preempt,
|
||||||
|
time_fade_in,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,8 @@ impl StrainSkill for Flashlight {
|
|||||||
diff_objects,
|
diff_objects,
|
||||||
self.has_hidden_mod,
|
self.has_hidden_mod,
|
||||||
self.scaling_factor,
|
self.scaling_factor,
|
||||||
|
self.time_preempt,
|
||||||
|
self.time_fade_in,
|
||||||
) * Self::SKILL_MULTIPLIER;
|
) * Self::SKILL_MULTIPLIER;
|
||||||
|
|
||||||
self.curr_strain
|
self.curr_strain
|
||||||
@@ -131,6 +134,8 @@ impl FlashlightEvaluator {
|
|||||||
diff_objects: &[OsuDifficultyObject<'_>],
|
diff_objects: &[OsuDifficultyObject<'_>],
|
||||||
hidden: bool,
|
hidden: bool,
|
||||||
scaling_factor: f64,
|
scaling_factor: f64,
|
||||||
|
time_preempt: f64,
|
||||||
|
time_fade_in: f64,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
if curr.base.is_spinner() {
|
if curr.base.is_spinner() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -159,7 +164,8 @@ impl FlashlightEvaluator {
|
|||||||
let curr_hit_obj = curr_obj.base;
|
let curr_hit_obj = curr_obj.base;
|
||||||
|
|
||||||
if !curr_obj.base.is_spinner() {
|
if !curr_obj.base.is_spinner() {
|
||||||
let jump_dist = (osu_hit_obj.pos - curr_hit_obj.end_pos()).length() as f64;
|
let jump_dist =
|
||||||
|
(osu_hit_obj.stacked_pos() - curr_hit_obj.stacked_end_pos()).length() as f64;
|
||||||
cumulative_strain_time += last_obj.strain_time;
|
cumulative_strain_time += last_obj.strain_time;
|
||||||
|
|
||||||
// * We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
// * We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||||
@@ -173,7 +179,13 @@ impl FlashlightEvaluator {
|
|||||||
// * Bonus based on how visible the object is.
|
// * Bonus based on how visible the object is.
|
||||||
let opacity_bonus = 1.0
|
let opacity_bonus = 1.0
|
||||||
+ Self::MAX_OPACITY_BONUS
|
+ Self::MAX_OPACITY_BONUS
|
||||||
* (1.0 - osu_curr.opacity_at(curr_hit_obj.start_time, hidden));
|
* (1.0
|
||||||
|
- osu_curr.opacity_at(
|
||||||
|
curr_hit_obj.start_time,
|
||||||
|
hidden,
|
||||||
|
time_preempt,
|
||||||
|
time_fade_in,
|
||||||
|
));
|
||||||
|
|
||||||
result += stack_nerf * opacity_bonus * scaling_factor * jump_dist
|
result += stack_nerf * opacity_bonus * scaling_factor * jump_dist
|
||||||
/ cumulative_strain_time;
|
/ cumulative_strain_time;
|
||||||
@@ -205,7 +217,7 @@ impl FlashlightEvaluator {
|
|||||||
|
|
||||||
let mut slider_bonus = 0.0;
|
let mut slider_bonus = 0.0;
|
||||||
|
|
||||||
if let OsuObjectKind::Slider { nested_objects, .. } = &osu_curr.base.kind {
|
if let OsuObjectKind::Slider(slider) = &osu_curr.base.kind {
|
||||||
// * Invert the scaling factor to determine the true travel distance independent of circle size.
|
// * Invert the scaling factor to determine the true travel distance independent of circle size.
|
||||||
let pixel_travel_dist = osu_curr.dists.lazy_travel_dist as f64 / scaling_factor;
|
let pixel_travel_dist = osu_curr.dists.lazy_travel_dist as f64 / scaling_factor;
|
||||||
|
|
||||||
@@ -219,9 +231,7 @@ impl FlashlightEvaluator {
|
|||||||
slider_bonus *= pixel_travel_dist;
|
slider_bonus *= pixel_travel_dist;
|
||||||
|
|
||||||
// * Nerf sliders with repeats, as less memorisation is required.
|
// * Nerf sliders with repeats, as less memorisation is required.
|
||||||
let repeat_count = nested_objects.iter().fold(0, |count, nested| {
|
let repeat_count = slider.repeat_count();
|
||||||
count + matches!(nested.kind, NestedObjectKind::Repeat) as usize
|
|
||||||
});
|
|
||||||
|
|
||||||
if repeat_count > 0 {
|
if repeat_count > 0 {
|
||||||
slider_bonus /= (repeat_count + 1) as f64;
|
slider_bonus /= (repeat_count + 1) as f64;
|
||||||
|
|||||||
@@ -249,13 +249,13 @@ impl RhythmEvaluator {
|
|||||||
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
||||||
let curr_ratio = 1.0 + 6.0 * (base * base).min(0.5);
|
let curr_ratio = 1.0 + 6.0 * (base * base).min(0.5);
|
||||||
|
|
||||||
|
let hit_window = !curr_obj.base.is_spinner() as u64 as f64 * hit_window;
|
||||||
|
|
||||||
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
||||||
.max(0.0))
|
.max(0.0))
|
||||||
/ (hit_window * 0.3))
|
/ (hit_window * 0.3))
|
||||||
.min(1.0);
|
.min(1.0);
|
||||||
|
|
||||||
println!("window_penalty: prev={prev_delta} | curr={curr_delta} | window={hit_window} => {window_penalty}");
|
|
||||||
|
|
||||||
window_penalty = window_penalty.min(1.0);
|
window_penalty = window_penalty.min(1.0);
|
||||||
|
|
||||||
let mut effective_ratio = window_penalty * curr_ratio;
|
let mut effective_ratio = window_penalty * curr_ratio;
|
||||||
@@ -322,10 +322,6 @@ impl RhythmEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
let res = (4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0;
|
(4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||||
|
|
||||||
println!("res={res}");
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-59
@@ -14,10 +14,7 @@ pub use slider_parsing::*;
|
|||||||
use reader::FileReader;
|
use reader::FileReader;
|
||||||
pub(crate) use sort::legacy_sort;
|
pub(crate) use sort::legacy_sort;
|
||||||
|
|
||||||
use std::{
|
use std::{cmp::Ordering, ops::Neg};
|
||||||
cmp::Ordering,
|
|
||||||
ops::{ControlFlow, Neg},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
@@ -128,7 +125,9 @@ macro_rules! parse_general_body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if key == b"StackLeniency" {
|
if key == b"StackLeniency" {
|
||||||
stack_leniency = Some(value.parse()?);
|
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
||||||
|
stack_leniency = Some(val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,12 +159,36 @@ macro_rules! parse_difficulty_body {
|
|||||||
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
|
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
b"ApproachRate" => ar = Some(value.parse()?),
|
b"ApproachRate" => {
|
||||||
b"OverallDifficulty" => od = Some(value.parse()?),
|
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
||||||
b"CircleSize" => cs = Some(value.parse()?),
|
ar = Some(val);
|
||||||
b"HPDrainRate" => hp = Some(value.parse()?),
|
}
|
||||||
b"SliderTickRate" => tick_rate = Some(value.parse()?),
|
}
|
||||||
b"SliderMultiplier" => sv = Some(value.parse()?),
|
b"OverallDifficulty" => {
|
||||||
|
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
||||||
|
od = Some(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"CircleSize" => {
|
||||||
|
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
||||||
|
cs = Some(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"HPDrainRate" => {
|
||||||
|
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
||||||
|
hp = Some(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"SliderTickRate" => {
|
||||||
|
if let Some(val) = value.parse().ok().filter(f64::is_in_range) {
|
||||||
|
tick_rate = Some(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"SliderMultiplier" => {
|
||||||
|
if let Some(val) = value.parse().ok().filter(f64::is_in_range) {
|
||||||
|
sv = Some(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,8 +199,8 @@ macro_rules! parse_difficulty_body {
|
|||||||
$self.cs = cs.unwrap_or(DEFAULT_DIFFICULTY);
|
$self.cs = cs.unwrap_or(DEFAULT_DIFFICULTY);
|
||||||
$self.hp = hp.unwrap_or(DEFAULT_DIFFICULTY);
|
$self.hp = hp.unwrap_or(DEFAULT_DIFFICULTY);
|
||||||
$self.ar = ar.unwrap_or($self.od);
|
$self.ar = ar.unwrap_or($self.od);
|
||||||
$self.slider_mult = sv.next_field("sv")?;
|
$self.slider_mult = sv.unwrap_or(1.0);
|
||||||
$self.tick_rate = tick_rate.next_field("tick rate")?;
|
$self.tick_rate = tick_rate.unwrap_or(1.0);
|
||||||
|
|
||||||
Ok(empty)
|
Ok(empty)
|
||||||
}};
|
}};
|
||||||
@@ -203,13 +226,26 @@ macro_rules! parse_events_body {
|
|||||||
|
|
||||||
// We're only interested in breaks
|
// We're only interested in breaks
|
||||||
if let Some(b'2') = split.next().and_then(|value| value.bytes().next()) {
|
if let Some(b'2') = split.next().and_then(|value| value.bytes().next()) {
|
||||||
let start_time = split.next().next_field("break start")?.parse()?;
|
let start_time = split
|
||||||
let end_time = split.next().next_field("break end")?.parse()?;
|
.next()
|
||||||
|
.next_field("break start")?
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(f64::is_in_range);
|
||||||
|
|
||||||
$self.breaks.push(Break {
|
let end_time = split
|
||||||
start_time,
|
.next()
|
||||||
end_time,
|
.next_field("break end")?
|
||||||
});
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(f64::is_in_range);
|
||||||
|
|
||||||
|
if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
|
||||||
|
$self.breaks.push(Break {
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,16 +359,6 @@ macro_rules! parse_timingpoints_body {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if timing_change {
|
|
||||||
let point = TimingPoint {
|
|
||||||
time,
|
|
||||||
beat_len: beat_len.clamp(6.0, 60_000.0),
|
|
||||||
kiai,
|
|
||||||
};
|
|
||||||
|
|
||||||
$self.timing_points.push(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
// * If beatLength is NaN, speedMultiplier should still be 1
|
// * If beatLength is NaN, speedMultiplier should still be 1
|
||||||
// * because all comparisons against NaN are false.
|
// * because all comparisons against NaN are false.
|
||||||
let speed_multiplier = if beat_len < 0.0 {
|
let speed_multiplier = if beat_len < 0.0 {
|
||||||
@@ -347,10 +373,28 @@ macro_rules! parse_timingpoints_body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pending_diff_point = Some(DifficultyPoint::new(time, beat_len, speed_multiplier, kiai));
|
if timing_change {
|
||||||
|
let point = TimingPoint {
|
||||||
|
time,
|
||||||
|
beat_len: beat_len.clamp(6.0, 60_000.0),
|
||||||
|
kiai,
|
||||||
|
};
|
||||||
|
|
||||||
|
$self.timing_points.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !timing_change || pending_diff_point.is_none() {
|
||||||
|
pending_diff_point =
|
||||||
|
Some(DifficultyPoint::new(time, beat_len, speed_multiplier, kiai));
|
||||||
|
}
|
||||||
|
|
||||||
pending_diff_points_time = time;
|
pending_diff_points_time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(point) = pending_diff_point {
|
||||||
|
$self.difficulty_points.push_if_not_redundant(point);
|
||||||
|
}
|
||||||
|
|
||||||
$self.timing_points.dedup_by_key(|point| point.time);
|
$self.timing_points.dedup_by_key(|point| point.time);
|
||||||
$self.difficulty_points.dedup_by_key(|point| point.time);
|
$self.difficulty_points.dedup_by_key(|point| point.time);
|
||||||
|
|
||||||
@@ -367,8 +411,8 @@ macro_rules! parse_hitobjects_body {
|
|||||||
// `point_split` will be of type `Vec<&str>
|
// `point_split` will be of type `Vec<&str>
|
||||||
// with each element having its lifetime bound to `buf`.
|
// with each element having its lifetime bound to `buf`.
|
||||||
// To circumvent this, `point_split_raw` will contain
|
// To circumvent this, `point_split_raw` will contain
|
||||||
// the actual `&str` elements transmuted into `usize`.
|
// the actual `&str` elements transmuted into `(usize, usize)`.
|
||||||
let mut point_split_raw: Vec<usize> = Vec::new();
|
let mut point_split_raw: Vec<(usize, usize)> = Vec::new();
|
||||||
|
|
||||||
// Buffer to re-use for all sliders
|
// Buffer to re-use for all sliders
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
@@ -383,22 +427,40 @@ macro_rules! parse_hitobjects_body {
|
|||||||
let line = $reader.get_line()?;
|
let line = $reader.get_line()?;
|
||||||
let mut split = line.split(',');
|
let mut split = line.split(',');
|
||||||
|
|
||||||
let x: f32 = split.next().next_field("x pos")?.parse()?;
|
let x = split
|
||||||
let y: f32 = split.next().next_field("y pos")?.parse()?;
|
.next()
|
||||||
|
.next_field("x pos")?
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(|x| f32::is_in_custom_range(x, MAX_COORDINATE_VALUE as f32))
|
||||||
|
.map(|x| x as i32 as f32);
|
||||||
|
|
||||||
if !(x.is_in_custom_range(MAX_COORDINATE_VALUE as f32)
|
let y = split
|
||||||
&& y.is_in_custom_range(MAX_COORDINATE_VALUE as f32))
|
.next()
|
||||||
{
|
.next_field("y pos")?
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(|x| f32::is_in_custom_range(x, MAX_COORDINATE_VALUE as f32))
|
||||||
|
.map(|x| x as i32 as f32);
|
||||||
|
|
||||||
|
let pos = if let (Some(x), Some(y)) = (x, y) {
|
||||||
|
Pos2 { x, y }
|
||||||
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
let pos = Pos2 { x, y };
|
let time_opt = split
|
||||||
|
.next()
|
||||||
|
.next_field("hitobject time")?
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(f64::is_in_range);
|
||||||
|
|
||||||
let time: f64 = split.next().next_field("hitobject time")?.trim().parse()?;
|
let time = match time_opt {
|
||||||
|
Some(time) => time,
|
||||||
if !time.is_in_range() {
|
None => continue,
|
||||||
continue;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if !$self.hit_objects.is_empty() && time < prev_time {
|
if !$self.hit_objects.is_empty() && time < prev_time {
|
||||||
unsorted = true;
|
unsorted = true;
|
||||||
@@ -436,7 +498,7 @@ macro_rules! parse_hitobjects_body {
|
|||||||
let mut end_idx = 0;
|
let mut end_idx = 0;
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
|
|
||||||
// SAFETY: `Vec<usize>` and `Vec<&str>` have the same size and layout.
|
// SAFETY: `Vec<(usize, usize)>` and `Vec<&str>` have the same size and layout.
|
||||||
let point_split: &mut Vec<&str> =
|
let point_split: &mut Vec<&str> =
|
||||||
unsafe { std::mem::transmute(&mut point_split_raw) };
|
unsafe { std::mem::transmute(&mut point_split_raw) };
|
||||||
|
|
||||||
@@ -489,7 +551,7 @@ macro_rules! parse_hitobjects_body {
|
|||||||
} else {
|
} else {
|
||||||
let pixel_len = match split.next().map(str::parse::<f64>) {
|
let pixel_len = match split.next().map(str::parse::<f64>) {
|
||||||
Some(Ok(len)) if len.is_in_custom_range(MAX_COORDINATE_VALUE as f64) => {
|
Some(Ok(len)) if len.is_in_custom_range(MAX_COORDINATE_VALUE as f64) => {
|
||||||
(len != 0.0).then_some(len)
|
(len > 0.0).then_some(len)
|
||||||
}
|
}
|
||||||
Some(_) => continue,
|
Some(_) => continue,
|
||||||
None => None,
|
None => None,
|
||||||
@@ -573,19 +635,13 @@ macro_rules! parse_hitobjects_body {
|
|||||||
|
|
||||||
// Required for maps with slider edge sound values above 255 e.g. map /b/80799
|
// Required for maps with slider edge sound values above 255 e.g. map /b/80799
|
||||||
fn parse_custom_sound(sound: &str) -> u8 {
|
fn parse_custom_sound(sound: &str) -> u8 {
|
||||||
fn fold_str(sound: &str) -> ControlFlow<u8, u8> {
|
sound
|
||||||
sound.bytes().try_fold(0_u8, |sound, byte| match byte {
|
.bytes()
|
||||||
b'0'..=b'9' => {
|
.try_fold(0_u8, |sound, byte| match byte {
|
||||||
ControlFlow::Continue(sound.wrapping_mul(10).wrapping_add((byte & 0xF) as u8))
|
b'0'..=b'9' => Some(sound.wrapping_mul(10).wrapping_add((byte & 0xF) as u8)),
|
||||||
}
|
_ => None,
|
||||||
_ => ControlFlow::Break(0),
|
|
||||||
})
|
})
|
||||||
}
|
.unwrap_or(0)
|
||||||
|
|
||||||
match fold_str(sound) {
|
|
||||||
ControlFlow::Continue(n) => n,
|
|
||||||
ControlFlow::Break(n) => n,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_body {
|
macro_rules! parse_body {
|
||||||
@@ -599,7 +655,7 @@ macro_rules! parse_body {
|
|||||||
|
|
||||||
let mut map = Beatmap {
|
let mut map = Beatmap {
|
||||||
version: reader.version()?,
|
version: reader.version()?,
|
||||||
hit_objects: Vec::with_capacity(256),
|
hit_objects: Vec::with_capacity(256), // TODO: test avg length, same for control points
|
||||||
sounds: Vec::with_capacity(256),
|
sounds: Vec::with_capacity(256),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -710,6 +766,15 @@ mod slider_parsing {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * Legacy Catmull sliders don't support multiple segments,
|
||||||
|
// * so adjacent Catmull segments should be treated as a single one.
|
||||||
|
// * Importantly, this is not applied to the first control point,
|
||||||
|
// * which may duplicate the slider path's position
|
||||||
|
// * resulting in a duplicate (0,0) control point in the resultant list.
|
||||||
|
if path_kind == PathType::Catmull && end_idx > 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// * The last control point of each segment is not
|
// * The last control point of each segment is not
|
||||||
// * allowed to start a new implicit segment.
|
// * allowed to start a new implicit segment.
|
||||||
if end_idx == vertices.len() - end_point_len - 1 {
|
if end_idx == vertices.len() - end_point_len - 1 {
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ mod tests {
|
|||||||
max_combo: 246,
|
max_combo: 246,
|
||||||
n300: 200,
|
n300: 200,
|
||||||
n100: 40,
|
n100: 40,
|
||||||
misses: 6,
|
n_misses: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
let next = gradual1.process_next_object(state.clone());
|
let next = gradual1.process_next_object(state.clone());
|
||||||
@@ -230,7 +230,7 @@ mod tests {
|
|||||||
max_combo: 289,
|
max_combo: 289,
|
||||||
n300: 289,
|
n300: 289,
|
||||||
n100: 0,
|
n100: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||||
@@ -252,7 +252,7 @@ mod tests {
|
|||||||
max_combo: 246,
|
max_combo: 246,
|
||||||
n300: 246,
|
n300: 246,
|
||||||
n100: 0,
|
n100: 0,
|
||||||
misses: 0,
|
n_misses: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user