fixes for osu_fast feature

This commit is contained in:
MaxOhn
2021-11-09 04:13:13 +01:00
parent cb4a3d76d9
commit 1ba6cfc44c
9 changed files with 263 additions and 95 deletions
+6 -3
View File
@@ -5,6 +5,7 @@ pub(crate) struct DifficultyObject<'h> {
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
pub(crate) jump_dist: f32,
pub(crate) travel_dist: f32,
pub(crate) angle: Option<f32>,
pub(crate) delta: f32,
@@ -20,19 +21,20 @@ impl<'h> DifficultyObject<'h> {
scaling_factor: f32,
) -> Self {
let delta = base.time - prev.time;
let travel_dist = prev.travel_dist();
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
let strain_time = delta.max(25.0);
// We don't need to calculate either angle or distance
// when one of the last->curr objects is a spinner
let (jump_dist, angle) = if base.is_spinner {
let (jump_dist, angle) = if base.is_spinner() || prev.is_spinner() {
(0.0, None)
} else {
let jump_dist = ((base.pos - prev.pos) * scaling_factor).length();
let jump_dist = ((base.pos - prev.end_pos()) * scaling_factor).length();
let angle = prev_prev.map(|prev_prev| {
let v1 = prev_prev.pos - prev.pos;
let v1 = prev_prev.end_pos() - prev.pos;
let v2 = base.pos - prev.pos;
let dot = v1.dot(v2);
@@ -49,6 +51,7 @@ impl<'h> DifficultyObject<'h> {
prev: prev_vals,
jump_dist,
travel_dist,
angle,
delta,
+58 -8
View File
@@ -76,19 +76,44 @@ pub fn stars(
HitObjectKind::Circle => {
max_combo += 1;
OsuObject::from(h, clock_rate)
Some(OsuObject::circle(h, clock_rate))
}
#[cfg(feature = "sliders")]
HitObjectKind::Slider {
pixel_len, repeats, ..
pixel_len,
repeats,
control_points,
} => {
max_combo += state.count_ticks(h.start_time, *pixel_len, *repeats, map);
OsuObject::from(h, clock_rate)
Some(OsuObject::slider(
h,
clock_rate,
radius,
*repeats,
control_points,
))
}
#[cfg(not(feature = "sliders"))]
HitObjectKind::Slider {
pixel_len,
span_count,
last_control_point,
} => {
max_combo += state.count_ticks(h.start_time, *pixel_len, *span_count, map);
Some(OsuObject::slider(
h,
clock_rate,
radius,
*span_count,
*last_control_point,
))
}
HitObjectKind::Spinner { .. } => {
max_combo += 1;
OsuObject::from(h, clock_rate)
Some(OsuObject::spinner(h, clock_rate))
}
HitObjectKind::Hold { .. } => None,
});
@@ -236,10 +261,35 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let clock_rate = attributes.clock_rate;
let mut hit_objects = map
.hit_objects
.iter()
.filter_map(|h| OsuObject::from(h, clock_rate));
let mut hit_objects = map.hit_objects.iter().filter_map(|h| match &h.kind {
HitObjectKind::Circle => Some(OsuObject::circle(h, clock_rate)),
#[cfg(feature = "sliders")]
HitObjectKind::Slider {
repeats,
control_points,
..
} => Some(OsuObject::slider(
h,
clock_rate,
radius,
*repeats,
control_points,
)),
#[cfg(not(feature = "sliders"))]
HitObjectKind::Slider {
span_count,
last_control_point,
..
} => Some(OsuObject::slider(
h,
clock_rate,
radius,
*span_count,
*last_control_point,
)),
HitObjectKind::Spinner { .. } => Some(OsuObject::spinner(h, clock_rate)),
HitObjectKind::Hold { .. } => None,
});
let fl = mods.fl();
let mut skills = Vec::with_capacity(2 + fl as usize);
+115 -25
View File
@@ -1,35 +1,125 @@
use crate::parse::{HitObject, HitObjectKind, Pos2};
use crate::parse::{HitObject, Pos2};
pub(crate) struct OsuObject {
pub(crate) pos: Pos2,
pub(crate) time: f32,
pub(crate) is_spinner: bool,
pub(crate) is_slider: bool,
kind: OsuObjectKind,
}
pub(crate) enum OsuObjectKind {
Circle,
Slider { end_pos: Pos2, travel_dist: f32 },
Spinner,
}
impl OsuObject {
#[inline]
pub(crate) fn from(h: &HitObject, clock_rate: f32) -> Option<Self> {
match &h.kind {
HitObjectKind::Circle => Some(Self {
pos: h.pos,
time: h.start_time / clock_rate,
is_spinner: false,
is_slider: false,
}),
HitObjectKind::Slider { .. } => Some(Self {
pos: h.pos,
time: h.start_time / clock_rate,
is_spinner: false,
is_slider: true,
}),
HitObjectKind::Spinner { .. } => Some(Self {
pos: h.pos,
time: h.start_time / clock_rate,
is_spinner: true,
is_slider: false,
}),
HitObjectKind::Hold { .. } => None,
pub(crate) fn circle(h: &HitObject, clock_rate: f32) -> Self {
Self {
pos: h.pos,
time: h.start_time / clock_rate,
kind: OsuObjectKind::Circle,
}
}
#[cfg(feature = "sliders")]
pub(crate) fn slider(
h: &HitObject,
clock_rate: f32,
radius: f32,
repeats: usize,
control_points: &[crate::parse::PathControlPoint],
) -> Self {
match control_points.last() {
Some(point) => {
let travel_dist =
Self::approximate_travel_dist(radius, repeats + 1, h.pos, point.pos + h.pos);
let mut end_pos = h.pos;
if repeats % 2 == 0 {
end_pos += point.pos
}
Self {
pos: h.pos,
time: h.start_time / clock_rate,
kind: OsuObjectKind::Slider {
end_pos,
travel_dist,
},
}
}
None => Self::circle(h, clock_rate),
}
}
#[cfg(not(feature = "sliders"))]
pub(crate) fn slider(
h: &HitObject,
clock_rate: f32,
radius: f32,
span_count: usize,
last_control_point: Pos2,
) -> Self {
let travel_dist = Self::approximate_travel_dist(radius, span_count, h.pos, point.pos);
let end_pos = if span_count % 2 == 1 {
last_control_point
} else {
h.pos
};
Self {
pos: h.pos,
time: h.start_time / clock_rate,
kind: OsuObjectKind::Slider {
end_pos,
travel_dist,
},
}
}
pub(crate) fn spinner(h: &HitObject, clock_rate: f32) -> Self {
Self {
pos: h.pos,
time: h.start_time / clock_rate,
kind: OsuObjectKind::Spinner,
}
}
pub(crate) fn is_slider(&self) -> bool {
matches!(self.kind, OsuObjectKind::Slider { .. })
}
pub(crate) fn is_spinner(&self) -> bool {
matches!(self.kind, OsuObjectKind::Spinner)
}
pub(crate) fn end_pos(&self) -> Pos2 {
match &self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
OsuObjectKind::Slider { end_pos, .. } => *end_pos,
}
}
pub(crate) fn travel_dist(&self) -> f32 {
match &self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner => 0.0,
OsuObjectKind::Slider { travel_dist, .. } => *travel_dist,
}
}
// Approximating lower bound for lazy travel distance
fn approximate_travel_dist(
radius: f32,
span_count: usize,
pos: Pos2,
last_control_point: Pos2,
) -> f32 {
let approx_follow_circle_radius = radius * 3.0;
let lazy_end_point_dist = approx_follow_circle_radius * (span_count + 1) as f32;
let dist = (pos - last_control_point).length();
(dist * span_count as f32 - lazy_end_point_dist).max(0.0)
}
}
+38 -35
View File
@@ -47,8 +47,8 @@ pub(crate) struct FlashlightHistoryEntry {
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
fn from(h: &DifficultyObject<'_>) -> Self {
Self {
end_pos: h.base.pos,
is_spinner: h.base.is_spinner,
end_pos: h.base.end_pos(),
is_spinner: h.base.is_spinner(),
jump_dist: h.jump_dist,
strain_time: h.strain_time,
}
@@ -64,7 +64,7 @@ pub(crate) struct SpeedHistoryEntry {
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
fn from(h: &DifficultyObject<'_>) -> Self {
Self {
is_slider: h.base.is_slider,
is_slider: h.base.is_slider(),
start_time: h.base.time,
strain_time: h.strain_time,
}
@@ -119,7 +119,7 @@ impl SkillKind {
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f32 {
match self {
Self::Aim => {
if curr.base.is_spinner {
if curr.base.is_spinner() {
return 0.0;
}
@@ -140,15 +140,19 @@ impl SkillKind {
}
let jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist);
(aim_strain + jump_dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(jump_dist_exp / curr.strain_time)
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / curr.strain_time)
}
Self::Flashlight {
history,
scaling_factor,
} => {
if curr.base.is_spinner {
if curr.base.is_spinner() {
return 0.0;
}
@@ -163,13 +167,11 @@ impl SkillKind {
let jump_dist = (curr.base.pos - prev.end_pos).length();
cumulative_strain_time += prev.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
small_dist_nerf = (jump_dist / 75.0).min(1.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for
// -- since jump distance is 0 on stacked notes in this version, approximate value as 0.2
let stack_nerf =
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
// * We also want to nerf stacks so that only the first object of the stack is accounted for
let stack_nerf = ((prev.jump_dist / scaling_factor) / 25.0).min(1.0);
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
}
@@ -180,8 +182,9 @@ impl SkillKind {
if !prev.is_spinner {
let jump_dist = (curr.base.pos - prev.end_pos).length();
cumulative_strain_time += prev.strain_time;
let stack_nerf =
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
// * We also want to nerf stacks so that only the first object of the stack is accounted for
let stack_nerf = ((prev.jump_dist / scaling_factor) / 25.0).min(1.0);
result += factor * stack_nerf * scaling_factor * jump_dist
/ cumulative_strain_time;
@@ -198,7 +201,7 @@ impl SkillKind {
hit_window,
..
} => {
if curr.base.is_spinner {
if curr.base.is_spinner() {
return 0.0;
}
@@ -207,7 +210,7 @@ impl SkillKind {
let speed_window_ratio = strain_time / hit_window_full;
let prev = history.front();
// Aim to nerf cheesy rhythms (very fast consecutive doubles with large delta times between)
// * Aim to nerf cheesy rhythms (very fast consecutive doubles with large delta times between)
if let Some(prev) =
prev.filter(|p| strain_time < hit_window_full && p.strain_time > strain_time)
{
@@ -215,12 +218,12 @@ impl SkillKind {
math_util::lerp(prev.strain_time, strain_time, speed_window_ratio);
}
// Cap delta time to the OD 300 hit window
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly,
// whilst 0.92 limits the effect of the cap
// * Cap delta time to the OD 300 hit window
// * 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly,
// * whilst 0.92 limits the effect of the cap
strain_time /= (strain_time / hit_window_full / 0.93).clamp(0.92, 1.0);
// Derive speed bonus for calculation
// * Derive speed bonus for calculation
let mut speed_bonus = 1.0;
if strain_time < MIN_SPEED_BONUS {
@@ -228,7 +231,7 @@ impl SkillKind {
speed_bonus = 1.0 + 0.75 * base * base;
}
let dist = SINGLE_SPACING_TRESHOLD.min(curr.jump_dist);
let dist = SINGLE_SPACING_TRESHOLD.min(curr.travel_dist + curr.jump_dist);
(speed_bonus + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ strain_time
@@ -286,7 +289,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
history: &VecDeque<SpeedHistoryEntry>,
hit_window: f32,
) -> f32 {
if current.base.is_spinner {
if current.base.is_spinner() {
return 0.0;
}
@@ -297,7 +300,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
let adjusted_hit_window = hit_window * 0.6;
let history_len = history.len() as f32;
// Store the ratio of the current start of an island to buff for tighter rhythms
// * Store the ratio of the current start of an island to buff for tighter rhythms
let mut start_ratio = 0.0;
let currs = history.iter();
@@ -310,14 +313,14 @@ pub(crate) fn calculate_speed_rhythm_bonus(
/ SPEED_HISTORY_TIME_MAX;
if curr_historical_decay.abs() > f32::EPSILON {
// Either we're limited by time or limited by object count
// * Either we're limited by time or limited by object count
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
let curr_delta = curr.strain_time;
let prev_delta = prev.strain_time;
let last_delta = last.strain_time;
// Fancy function to calculate rhythm bonuses
// * Fancy function to calculate rhythm bonuses
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);
@@ -333,27 +336,27 @@ pub(crate) fn calculate_speed_rhythm_bonus(
}
} else {
if curr.is_slider {
// bpm change is into slider, this is easy acc window
// * bpm change is into slider, this is easy acc window
effective_ratio *= 0.125;
}
if prev.is_slider {
// bpm change was from a slider, this is easier typically than circle -> circle
// * bpm change was from a slider, this is easier typically than circle -> circle
effective_ratio *= 0.25;
}
if prev_island_size == island_size {
// repeated island size (ex: triplet -> triplet)
// * repeated island size (ex: triplet -> triplet)
effective_ratio *= 0.25;
}
if prev_island_size % 2 == island_size % 2 {
// repeated island polarity (2 -> 4, 3 -> 5)
// * repeated island polarity (2 -> 4, 3 -> 5)
effective_ratio *= 0.5;
}
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
// previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
// * previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
effective_ratio *= 0.125;
}
@@ -367,15 +370,15 @@ pub(crate) fn calculate_speed_rhythm_bonus(
prev_island_size = island_size;
island_size = 1;
// we're slowing down, stop counting
// * we're slowing down, stop counting
if prev_delta * 1.25 < curr_delta {
// if we're speeding up, this stays true and we keep counting island size
// * if we're speeding up, this stays true and we keep counting island size
first_delta_switch = false;
}
}
} else if prev_delta > 1.25 * curr_delta {
// we want to be speeding up
// begin counting island until we change speed again
// * we want to be speeding up
// * begin counting island until we change speed again
first_delta_switch = true;
start_ratio = effective_ratio;
island_size = 1;
@@ -383,7 +386,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
}
}
// 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)
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
}
+3 -3
View File
@@ -24,7 +24,7 @@ impl<'p> SliderState<'p> {
&mut self,
time: f32,
pixel_len: f32,
repeats: usize,
span_count: usize,
map: &Beatmap,
) -> usize {
while time >= self.next_time {
@@ -46,12 +46,12 @@ impl<'p> SliderState<'p> {
}
}
let spans = repeats as f32;
let spans = span_count as f32;
let beats = pixel_len * spans / self.px_per_beat;
let ticks = ((beats - 0.1) / spans * map.tick_rate).ceil() as usize;
ticks
.checked_sub(1)
.map_or(0, |ticks| ticks * repeats + repeats + 1)
.map_or(0, |ticks| ticks * span_count + span_count + 1)
}
}
+3 -3
View File
@@ -66,13 +66,13 @@ fn difficulty_range_od(od: f32) -> f32 {
}
#[test]
#[ignore]
// #[ignore]
fn custom_osu() {
use std::{fs::File, time::Instant};
use crate::{Beatmap, OsuPP};
let path = "E:Games/osu!/beatmaps/809469_.osu";
let path = "E:Games/osu!/beatmaps/1402167_.osu";
let file = File::open(path).unwrap();
let start = Instant::now();
@@ -94,7 +94,7 @@ fn custom_osu() {
println!("Parsing average: {:?}", accum / iters);
let start = Instant::now();
let result = OsuPP::new(&map).mods(66).calculate();
let result = OsuPP::new(&map).mods(1024).calculate();
let iters = 100;
let accum = start.elapsed();
+15 -14
View File
@@ -171,7 +171,6 @@ impl SkillKind {
small_dist_nerf = (jump_dist / 75.0).min(1.0);
// * We also want to nerf stacks so that only the first object of the stack is accounted for
// -- since jump distance is 0 on stacked notes in this version, approximate value as 0.2
let stack_nerf = ((prev.jump_dist / scaling_factor) / 25.0).min(1.0);
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
@@ -183,6 +182,8 @@ impl SkillKind {
if !prev.is_spinner {
let jump_dist = (curr.base.pos - prev.end_pos).length();
cumulative_strain_time += prev.strain_time;
// * We also want to nerf stacks so that only the first object of the stack is accounted for
let stack_nerf = ((prev.jump_dist / scaling_factor) / 25.0).min(1.0);
result += factor * stack_nerf * scaling_factor * jump_dist
@@ -299,7 +300,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
let adjusted_hit_window = hit_window * 0.6;
let history_len = history.len() as f32;
// Store the ratio of the current start of an island to buff for tighter rhythms
// * Store the ratio of the current start of an island to buff for tighter rhythms
let mut start_ratio = 0.0;
let currs = history.iter();
@@ -312,14 +313,14 @@ pub(crate) fn calculate_speed_rhythm_bonus(
/ SPEED_HISTORY_TIME_MAX;
if curr_historical_decay.abs() > f32::EPSILON {
// Either we're limited by time or limited by object count
// * Either we're limited by time or limited by object count
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
let curr_delta = curr.strain_time;
let prev_delta = prev.strain_time;
let last_delta = last.strain_time;
// Fancy function to calculate rhythm bonuses
// * Fancy function to calculate rhythm bonuses
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);
@@ -335,27 +336,27 @@ pub(crate) fn calculate_speed_rhythm_bonus(
}
} else {
if curr.is_slider {
// bpm change is into slider, this is easy acc window
// * bpm change is into slider, this is easy acc window
effective_ratio *= 0.125;
}
if prev.is_slider {
// bpm change was from a slider, this is easier typically than circle -> circle
// * bpm change was from a slider, this is easier typically than circle -> circle
effective_ratio *= 0.25;
}
if prev_island_size == island_size {
// repeated island size (ex: triplet -> triplet)
// * repeated island size (ex: triplet -> triplet)
effective_ratio *= 0.25;
}
if prev_island_size % 2 == island_size % 2 {
// repeated island polarity (2 -> 4, 3 -> 5)
// * repeated island polarity (2 -> 4, 3 -> 5)
effective_ratio *= 0.5;
}
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
// previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
// * previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
effective_ratio *= 0.125;
}
@@ -369,15 +370,15 @@ pub(crate) fn calculate_speed_rhythm_bonus(
prev_island_size = island_size;
island_size = 1;
// we're slowing down, stop counting
// * we're slowing down, stop counting
if prev_delta * 1.25 < curr_delta {
// if we're speeding up, this stays true and we keep counting island size
// * if we're speeding up, this stays true and we keep counting island size
first_delta_switch = false;
}
}
} else if prev_delta > 1.25 * curr_delta {
// we want to be speeding up
// begin counting island until we change speed again
// * we want to be speeding up
// * begin counting island until we change speed again
first_delta_switch = true;
start_ratio = effective_ratio;
island_size = 1;
@@ -385,7 +386,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
}
}
// 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)
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
}
+2 -1
View File
@@ -59,7 +59,8 @@ pub enum HitObjectKind {
#[cfg(not(feature = "sliders"))]
Slider {
pixel_len: f32,
repeats: usize,
span_count: usize,
last_control_point: Pos2,
},
Spinner {
end_time: f32,
+23 -3
View File
@@ -533,10 +533,30 @@ macro_rules! parse_hitobjects_body {
#[cfg(not(feature = "sliders"))]
{
let repeats = split.nth(1).next_field("repeats")?.parse()?;
let pixel_len = split.next().next_field("pixel len")?.parse()?;
let last_control_point = split
.next()
.next_field("control points")?
.split('|')
.next_back();
HitObjectKind::Slider { repeats, pixel_len }
match last_control_point.map(|v| v.split(':').map(str::parse)) {
Some(mut coords) => {
let last_control_point = match (coords.next(), coords.next()) {
(Some(Ok(x)), Some(Ok(y))) => Pos2 { x, y },
_ => return Err(ParseError::InvalidCurvePoints),
};
let span_count = split.next().next_field("repeats")?.parse()?;
let pixel_len = split.next().next_field("pixel len")?.parse()?;
HitObjectKind::Slider {
span_count,
pixel_len,
last_control_point,
}
}
None => HitObjectKind::Circle,
}
}
} else if kind & Self::SPINNER_FLAG > 0 {
$self.n_spinners += 1;