implemented hotfix & various bug fixes
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user