implemented hotfix & various bug fixes

This commit is contained in:
MaxOhn
2021-11-13 14:47:28 +01:00
parent 26e11843c6
commit 15cefe78d5
8 changed files with 130 additions and 49 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
- [BREAKING] Instead of returning `StarResult`, difficulty calculations now return `DifficultyAttributes` depending on the mode.
- [BREAKING] Various fields and methods now include `f64` instead of `f32` to stay true to osu!'s original code
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
- osu: Updated up to commit [baa5285b59911efa1433a298f365133254a96874](https://github.com/ppy/osu/commit/baa5285b59911efa1433a298f365133254a96874) (2021-11-09)
- osu: Updated up to commit [9fb2402781ad91c197d51aeec716b0000f52c4d1](https://github.com/ppy/osu/commit/9fb2402781ad91c197d51aeec716b0000f52c4d1) (2021-11-12)
# v0.2.3
+18 -4
View File
@@ -7,10 +7,11 @@ use super::{OsuObject, ScalingFactor, NORMALIZED_RADIUS};
const MIN_DELTA_TIME: f64 = 25.0;
const MAXIMUM_SLIDER_RADIUS: f32 = NORMALIZED_RADIUS * 2.4;
const ASSUMED_SLIDER_RADIUS: f32 = NORMALIZED_RADIUS * 1.65;
const ASSUMED_SLIDER_RADIUS: f32 = NORMALIZED_RADIUS * 1.8;
pub(crate) struct DifficultyObject<'h> {
pub(crate) base: &'h OsuObject,
pub(crate) clock_rate: f64,
pub(crate) delta: f64,
pub(crate) strain_time: f64,
@@ -33,7 +34,7 @@ impl<'h> DifficultyObject<'h> {
scaling_factor: &ScalingFactor,
clock_rate: f64,
) -> Self {
let delta = base.time - prev.time;
let delta = (base.time - prev.time) / clock_rate;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
let strain_time = delta.max(MIN_DELTA_TIME);
@@ -95,8 +96,21 @@ impl<'h> DifficultyObject<'h> {
)
};
// ? Common values to debug
// println!("travel_dist={} | travel_time={}", travel_dist, travel_time);
// println!(
// "movement_dist={} | movement_time={}",
// movement_dist, movement_time
// );
// println!(
// "jump_dist={} | strain_time={} | angle={:?}",
// jump_dist, strain_time, angle
// );
// println!("--");
Self {
base,
clock_rate,
delta,
strain_time,
jump_dist,
@@ -188,9 +202,9 @@ impl<'h> DifficultyObject<'h> {
let lazy_travel_time = nested_objects
.last()
.map_or(0.0, |nested| nested.time / clock_rate - prev_time);
.map_or(0.0, |nested| nested.time - prev_time);
let travel_time = MIN_DELTA_TIME.max(lazy_travel_time);
let travel_time = MIN_DELTA_TIME.max(lazy_travel_time / clock_rate);
(travel_dist, travel_time)
}
+39 -27
View File
@@ -16,8 +16,6 @@ use skill::Skill;
use skill_kind::SkillKind;
use slider_state::SliderState;
use std::mem;
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
const SECTION_LEN: f64 = 400.0;
@@ -88,13 +86,9 @@ pub fn stars(
old_stacking(&mut hit_objects, stack_threshold);
}
// let scale_factor = (scaling_factor.scale * -6.4) as f32;
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
// let stack_offset = Pos2::new(h.stack_height * scale_factor);
let stack_offset = scaling_factor.stack_offset(h.stack_height);
h.pos += stack_offset;
h.time /= map_attributes.clock_rate;
h
});
@@ -102,7 +96,8 @@ pub fn stars(
let fl = mods.fl();
let mut skills = Vec::with_capacity(2 + fl as usize);
skills.push(Skill::aim());
skills.push(Skill::aim(true));
skills.push(Skill::aim(false));
skills.push(Skill::speed(hit_window));
if fl {
@@ -114,7 +109,8 @@ pub fn stars(
let mut prev = hit_objects.next().unwrap();
// First object has no predecessor and thus no strain, handle distinctly
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
let mut current_section_end =
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
// Handle second object separately to remove later if-branching
let curr = hit_objects.next().unwrap();
@@ -126,7 +122,9 @@ pub fn stars(
map_attributes.clock_rate,
);
while h.base.time > current_section_end {
let base_time = h.base.time / map_attributes.clock_rate;
while base_time > current_section_end {
for skill in skills.iter_mut() {
skill.start_new_section_from(current_section_end);
}
@@ -151,7 +149,9 @@ pub fn stars(
map_attributes.clock_rate,
);
while h.base.time > current_section_end {
let base_time = h.base.time / map_attributes.clock_rate;
while base_time > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
@@ -174,13 +174,21 @@ pub fn stars(
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let slider_factor = if aim_rating > 0.0 {
let aim_rating_no_sliders = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
aim_rating_no_sliders / aim_rating
} else {
1.0
};
let speed_rating = if mods.rx() {
0.0
} else {
skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
skills[2].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
};
let flashlight_rating = skills.get_mut(2).map_or(0.0, |skill| {
let flashlight_rating = skills.get_mut(3).map_or(0.0, |skill| {
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
});
@@ -222,6 +230,7 @@ pub fn stars(
aim_strain: aim_rating,
speed_strain: speed_rating,
flashlight_rating,
slider_factor,
n_circles: map.n_circles as usize,
n_sliders: map.n_sliders as usize,
n_spinners: map.n_spinners as usize,
@@ -278,13 +287,9 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
old_stacking(&mut hit_objects, stack_threshold);
}
// let scale_factor = (scaling_factor.scale * -6.4) as f32;
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
// let stack_offset = Pos2::new(h.stack_height * scale_factor);
let stack_offset = scaling_factor.stack_offset(h.stack_height);
h.pos += stack_offset;
h.time /= map_attributes.clock_rate;
h
});
@@ -292,7 +297,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let fl = mods.fl();
let mut skills = Vec::with_capacity(2 + fl as usize);
skills.push(Skill::aim());
skills.push(Skill::aim(true));
skills.push(Skill::aim(false));
skills.push(Skill::speed(hit_window));
if fl {
@@ -304,7 +310,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let mut prev = hit_objects.next().unwrap();
// First object has no predecessor and thus no strain, handle distinctly
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
let mut current_section_end =
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
// Handle second object separately to remove later if-branching
let curr = hit_objects.next().unwrap();
@@ -316,7 +323,9 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
map_attributes.clock_rate,
);
while h.base.time > current_section_end {
let base_time = h.base.time / map_attributes.clock_rate;
while base_time > current_section_end {
for skill in skills.iter_mut() {
skill.start_new_section_from(current_section_end);
}
@@ -341,7 +350,9 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
map_attributes.clock_rate,
);
while h.base.time > current_section_end {
let base_time = h.base.time / map_attributes.clock_rate;
while base_time > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
@@ -362,13 +373,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
skill.save_current_peak();
}
let mut speed_strains = skills.pop().unwrap().strain_peaks;
let mut aim_strains = skills.pop().unwrap().strain_peaks;
skills.reverse();
let strains = if let Some(mut flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
mem::swap(&mut speed_strains, &mut aim_strains);
mem::swap(&mut aim_strains, &mut flashlight_strains);
let _ = skills.pop();
let aim_strains = skills.pop().unwrap().strain_peaks; // no sliders
let speed_strains = skills.pop().unwrap().strain_peaks;
let strains = if let Some(flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
aim_strains
.into_iter()
.zip(speed_strains)
@@ -540,6 +551,7 @@ pub struct DifficultyAttributes {
pub aim_strain: f64,
pub speed_strain: f64,
pub flashlight_rating: f64,
pub slider_factor: f64,
pub ar: f64,
pub od: f64,
pub hp: f64,
@@ -587,7 +599,7 @@ fn custom_osu() {
use crate::{Beatmap, OsuPP};
let path = "E:Games/osu!/beatmaps/70090_.osu";
let path = "E:Games/osu!/beatmaps/116169_.osu";
let file = File::open(path).unwrap();
let start = Instant::now();
@@ -609,7 +621,7 @@ fn custom_osu() {
println!("Parsing average: {:?}", accum / iters);
let start = Instant::now();
let result = OsuPP::new(&map).mods(16 + 64).calculate();
let result = OsuPP::new(&map).mods(2 + 64).calculate();
let iters = 100;
let accum = start.elapsed();
+22 -2
View File
@@ -1,3 +1,5 @@
use std::{cmp::Ordering, convert::identity};
use super::slider_state::SliderState;
use crate::{
@@ -16,6 +18,7 @@ pub(crate) struct OsuObject {
pub(crate) kind: OsuObjectKind,
}
#[derive(Debug)]
pub(crate) enum OsuObjectKind {
Circle,
Slider {
@@ -226,7 +229,24 @@ impl OsuObject {
kind: NestedObjectKind::Tail,
};
nested_objects.push(legacy_last_tick);
// On very short buzz sliders it can happen that the
// legacy last tick is not the last object time-wise
match nested_objects.last() {
Some(last) if last.time > final_span_end_time => {
let idx = nested_objects
.binary_search_by(|nested| {
nested
.time
.partial_cmp(&final_span_end_time)
.unwrap_or(Ordering::Equal)
})
.map_or_else(identity, identity);
nested_objects.insert(idx, legacy_last_tick);
}
_ => nested_objects.push(legacy_last_tick),
};
*max_combo += nested_objects.len();
let lazy_travel_time = final_span_end_time - h.start_time;
@@ -250,7 +270,7 @@ impl OsuObject {
pos,
stack_height: 0.0,
kind: OsuObjectKind::Slider {
end_time: final_span_end_time,
end_time,
end_pos,
lazy_end_pos,
nested_objects,
+19
View File
@@ -400,6 +400,25 @@ impl OsuPPInner {
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
}
if attributes.n_sliders > 0 {
// * We assume 15% of sliders in a map are difficult since
// * there's no way to tell from the performance calculator.
let estimate_difficult_sliders = attributes.n_sliders as f64 * 0.15;
let non_300s = self.total_hits - self.n300 as f64;
let missing_combo = attributes.max_combo - self.combo.unwrap_or(attributes.max_combo);
let estimate_slider_ends_dropped = non_300s
.min(missing_combo as f64)
.clamp(0.0, estimate_difficult_sliders);
let base = 1.0 - estimate_slider_ends_dropped / estimate_difficult_sliders;
let slider_nerf_factor =
(1.0 - attributes.slider_factor) * base * base * base + attributes.slider_factor;
aim_value *= slider_nerf_factor;
}
aim_value *= self.acc;
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
+10 -3
View File
@@ -18,8 +18,8 @@ pub(crate) struct Skill {
impl Skill {
#[inline]
pub(crate) fn aim() -> Self {
Self::new(SkillKind::aim())
pub(crate) fn aim(with_sliders: bool) -> Self {
Self::new(SkillKind::aim(with_sliders))
}
#[inline]
@@ -49,7 +49,7 @@ impl Skill {
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>) {
self.kind.pre_process();
self.curr_section_peak = self.strain_value_at(curr).max(self.curr_section_peak);
self.prev_time = Some(curr.base.time);
self.prev_time = Some(curr.base.time / curr.clock_rate);
self.kind.post_process(curr);
}
@@ -65,6 +65,13 @@ impl Skill {
}
pub(crate) fn difficulty_value(&mut self) -> f64 {
// ? Common values to debug
// println!("---");
// for (i, strain) in self.strain_peaks.iter().enumerate() {
// println!("[{}] {}", i, strain);
// }
let mut difficulty = 0.0;
let mut weight = 1.0;
let decay_weight = self.kind.decay_weight();
+20 -11
View File
@@ -89,6 +89,7 @@ impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
}
}
#[derive(Debug)]
pub(crate) struct SpeedHistoryEntry {
is_slider: bool,
start_time: f64,
@@ -99,7 +100,7 @@ impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
fn from(h: &DifficultyObject<'_>) -> Self {
Self {
is_slider: h.base.is_slider(),
start_time: h.base.time,
start_time: h.base.time / h.clock_rate,
strain_time: h.strain_time,
}
}
@@ -108,6 +109,7 @@ impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
pub(crate) enum SkillKind {
Aim {
history: VecDeque<AimHistoryEntry>,
with_sliders: bool,
},
Flashlight {
history: VecDeque<FlashlightHistoryEntry>,
@@ -121,9 +123,10 @@ pub(crate) enum SkillKind {
}
impl SkillKind {
pub(crate) fn aim() -> Self {
pub(crate) fn aim(with_sliders: bool) -> Self {
Self::Aim {
history: VecDeque::with_capacity(AIM_HISTORY_LENGTH + 1),
with_sliders,
}
}
@@ -144,7 +147,7 @@ impl SkillKind {
pub(crate) fn pre_process(&mut self) {
match self {
Self::Aim { history } => history.truncate(AIM_HISTORY_LENGTH),
Self::Aim { history, .. } => history.truncate(AIM_HISTORY_LENGTH),
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
}
@@ -152,7 +155,7 @@ impl SkillKind {
pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
match self {
Self::Aim { history } => history.push_front(current.into()),
Self::Aim { history, .. } => history.push_front(current.into()),
Self::Flashlight { history, .. } => history.push_front(current.into()),
Self::Speed { history, .. } => history.push_front(current.into()),
}
@@ -160,7 +163,10 @@ impl SkillKind {
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f64 {
match self {
Self::Aim { history } => {
Self::Aim {
history,
with_sliders,
} => {
if curr.base.is_spinner() || history.len() < 2 || history[0].is_spinner {
return 0.0;
}
@@ -174,7 +180,7 @@ impl SkillKind {
// * But if the last object is a slider, then we extend the
// * travel velocity through the slider into the current object.
if prev.is_slider {
if prev.is_slider && *with_sliders {
// * calculate the movement velocity from slider end to current object
let movement_velocity = curr.movement_dist / curr.movement_time;
@@ -188,7 +194,7 @@ impl SkillKind {
// * As above, do the same for the previous hitobject.
let mut prev_velocity = prev.jump_dist / prev.strain_time;
if prev_prev.is_slider {
if prev_prev.is_slider && *with_sliders {
let movement_velocity = prev.movement_dist / prev.movement_time;
let travel_velocity = prev.travel_dist / prev.travel_time;
prev_velocity = prev_velocity.max(movement_velocity + travel_velocity);
@@ -302,7 +308,9 @@ impl SkillKind {
);
// * Add in additional slider velocity bonus.
aim_strain += slider_bonus * AIM_SLIDER_MULTIPLIER;
if *with_sliders {
aim_strain += slider_bonus * AIM_SLIDER_MULTIPLIER;
}
aim_strain
}
@@ -466,9 +474,10 @@ pub(crate) fn calculate_speed_rhythm_bonus(
let lasts = history.iter().skip(2);
for (((prev, curr), last), i) in prevs.zip(currs).zip(lasts).rev().zip(2..) {
let mut curr_historical_decay =
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
/ SPEED_HISTORY_TIME_MAX;
let mut curr_historical_decay = (SPEED_HISTORY_TIME_MAX
- (current.base.time / current.clock_rate - curr.start_time))
.max(0.0)
/ SPEED_HISTORY_TIME_MAX;
if curr_historical_decay.abs() > f64::EPSILON {
// * Either we're limited by time or limited by object count
+1 -1
View File
@@ -320,7 +320,7 @@ macro_rules! parse_timingpoints_body {
if beat_len < 0.0 {
let point = DifficultyPoint {
time,
speed_multiplier: -100.0 / beat_len,
speed_multiplier: (-100.0 / beat_len).max(0.1).min(10.0),
};
$self.difficulty_points.push(point);