f32 -> f64 & osu_precise fixes + update
This commit is contained in:
+2
-1
@@ -7,8 +7,9 @@
|
||||
Additionally, instead of importing through `rosu_pp::osu::{version}`, you now have to import through `rosu_pp::osu`
|
||||
- [BREAKING] Instead of returning `PpResult`, performance calculations now return `PerformanceAttributes` depending on the mode.
|
||||
- [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 [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
|
||||
- osu: Updated up to commit [baa5285b59911efa1433a298f365133254a96874](https://github.com/ppy/osu/commit/baa5285b59911efa1433a298f365133254a96874) (2021-11-09)
|
||||
|
||||
# v0.2.3
|
||||
|
||||
|
||||
+9
-9
@@ -229,22 +229,22 @@ impl Data {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct GenericData {
|
||||
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")]
|
||||
aim: Option<f32>,
|
||||
aim: Option<f64>,
|
||||
#[serde(default, alias = "Speed", skip_serializing_if = "Option::is_none")]
|
||||
speed: Option<f32>,
|
||||
speed: Option<f64>,
|
||||
#[serde(default, alias = "Accuracy", skip_serializing_if = "Option::is_none")]
|
||||
accuracy: Option<f32>,
|
||||
accuracy: Option<f64>,
|
||||
#[serde(default, alias = "Flashlight", skip_serializing_if = "Option::is_none")]
|
||||
flashlight: Option<f32>,
|
||||
flashlight: Option<f64>,
|
||||
#[serde(default, alias = "Strain", skip_serializing_if = "Option::is_none")]
|
||||
strain: Option<f32>,
|
||||
strain: Option<f64>,
|
||||
#[serde(default, alias = "OD", skip_serializing_if = "Option::is_none")]
|
||||
od: Option<f32>,
|
||||
od: Option<f64>,
|
||||
#[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")]
|
||||
ar: Option<f32>,
|
||||
ar: Option<f64>,
|
||||
#[serde(alias = "Mods")]
|
||||
mods: String,
|
||||
#[serde(alias = "Stars")]
|
||||
stars: f32,
|
||||
pp: f32,
|
||||
stars: f64,
|
||||
pp: f64,
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ pub(crate) struct ControlPointIter<'p> {
|
||||
timing_points: Iter<'p, TimingPoint>,
|
||||
difficulty_points: Iter<'p, DifficultyPoint>,
|
||||
|
||||
next_timing: Option<(f32, f32)>,
|
||||
next_difficulty: Option<(f32, f32)>,
|
||||
next_timing: Option<(f64, f64)>,
|
||||
next_difficulty: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl<'p> ControlPointIter<'p> {
|
||||
@@ -39,20 +39,20 @@ impl<'p> ControlPointIter<'p> {
|
||||
|
||||
pub(crate) enum ControlPoint {
|
||||
Timing {
|
||||
time: f32,
|
||||
time: f64,
|
||||
#[allow(dead_code)] // not used in `osu_fast` feature
|
||||
beat_len: f32,
|
||||
beat_len: f64,
|
||||
},
|
||||
Difficulty {
|
||||
time: f32,
|
||||
slider_velocity: f32,
|
||||
time: f64,
|
||||
slider_velocity: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "osu_precise", feature = "fruits"))]
|
||||
impl ControlPoint {
|
||||
#[inline]
|
||||
pub(crate) fn time(&self) -> f32 {
|
||||
pub(crate) fn time(&self) -> f64 {
|
||||
match self {
|
||||
Self::Timing { time, .. } => *time,
|
||||
Self::Difficulty { time, .. } => *time,
|
||||
@@ -70,13 +70,13 @@ impl<'p> Iterator for ControlPointIter<'p> {
|
||||
|
||||
Some(ControlPoint::Timing { time, beat_len })
|
||||
}
|
||||
(_, Some((time, speed_mult))) => {
|
||||
(_, Some((time, slider_velocity))) => {
|
||||
self.next_difficulty =
|
||||
next_tuple!(self.difficulty_points, (time, speed_multiplier));
|
||||
|
||||
Some(ControlPoint::Difficulty {
|
||||
time,
|
||||
slider_velocity: speed_mult,
|
||||
slider_velocity,
|
||||
})
|
||||
}
|
||||
(Some((time, beat_len)), None) => {
|
||||
|
||||
+41
-41
@@ -19,6 +19,7 @@ struct BezierBuffers {
|
||||
buf1: Vec<Pos2>,
|
||||
buf2: Vec<Pos2>,
|
||||
buf3: Vec<Pos2>,
|
||||
buf4: Vec<Pos2>,
|
||||
}
|
||||
|
||||
impl BezierBuffers {
|
||||
@@ -38,26 +39,28 @@ impl BezierBuffers {
|
||||
.extend(iter::repeat(Pos2::zero()).take(additional));
|
||||
self.buf3
|
||||
.extend(iter::repeat(Pos2::zero()).take(additional));
|
||||
self.buf4
|
||||
.extend(iter::repeat(Pos2::zero()).take(additional));
|
||||
}
|
||||
}
|
||||
|
||||
struct CircularArcProperties {
|
||||
theta_start: f32,
|
||||
theta_range: f32,
|
||||
direction: f32,
|
||||
theta_start: f64,
|
||||
theta_range: f64,
|
||||
direction: f64,
|
||||
radius: f32,
|
||||
centre: Pos2,
|
||||
}
|
||||
|
||||
pub(crate) struct Curve {
|
||||
path: Vec<Pos2>,
|
||||
lengths: Vec<f32>,
|
||||
lengths: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
pub(crate) fn new(
|
||||
points: &[PathControlPoint],
|
||||
expected_len: f32,
|
||||
expected_len: f64,
|
||||
bufs: &mut CurveBuffers,
|
||||
) -> Self {
|
||||
let mut path = Self::calculate_path(points, bufs);
|
||||
@@ -66,28 +69,28 @@ impl Curve {
|
||||
Self { path, lengths }
|
||||
}
|
||||
|
||||
pub(crate) fn position_at(&self, progress: f32) -> Pos2 {
|
||||
pub(crate) fn position_at(&self, progress: f64) -> Pos2 {
|
||||
let d = self.progress_to_dist(progress);
|
||||
let i = self.idx_of_dist(d);
|
||||
|
||||
self.interpolate_vertices(i, d)
|
||||
}
|
||||
|
||||
fn progress_to_dist(&self, progress: f32) -> f32 {
|
||||
fn progress_to_dist(&self, progress: f64) -> f64 {
|
||||
progress.clamp(0.0, 1.0) * self.dist()
|
||||
}
|
||||
|
||||
pub(crate) fn dist(&self) -> f32 {
|
||||
pub(crate) fn dist(&self) -> f64 {
|
||||
self.lengths.last().copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn idx_of_dist(&self, d: f32) -> usize {
|
||||
fn idx_of_dist(&self, d: f64) -> usize {
|
||||
self.lengths
|
||||
.binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal))
|
||||
.map_or_else(identity, identity)
|
||||
}
|
||||
|
||||
fn interpolate_vertices(&self, i: usize, d: f32) -> Pos2 {
|
||||
fn interpolate_vertices(&self, i: usize, d: f64) -> Pos2 {
|
||||
if self.path.is_empty() {
|
||||
return Pos2::zero();
|
||||
}
|
||||
@@ -107,13 +110,13 @@ impl Curve {
|
||||
|
||||
// * Avoid division by an almost-zero number in case
|
||||
// * two points are extremely close to each other
|
||||
if (d0 - d1).abs() <= f32::EPSILON {
|
||||
if (d0 - d1).abs() <= f64::EPSILON {
|
||||
return p0;
|
||||
}
|
||||
|
||||
let w = (d - d0) / (d1 - d0);
|
||||
|
||||
p0 + (p1 - p0) * w
|
||||
p0 + (p1 - p0) * w as f32
|
||||
}
|
||||
|
||||
fn calculate_path(points: &[PathControlPoint], bufs: &mut CurveBuffers) -> Vec<Pos2> {
|
||||
@@ -152,18 +155,18 @@ impl Curve {
|
||||
fn calculate_length(
|
||||
points: &[PathControlPoint],
|
||||
path: &mut Vec<Pos2>,
|
||||
expected_len: f32,
|
||||
) -> Vec<f32> {
|
||||
expected_len: f64,
|
||||
) -> Vec<f64> {
|
||||
let mut calculated_len = 0.0;
|
||||
let mut cumulative_len = vec![0.0];
|
||||
|
||||
for (&curr, &next) in path.iter().zip(path.iter().skip(1)) {
|
||||
let diff = next - curr;
|
||||
calculated_len += diff.length();
|
||||
calculated_len += diff.length() as f64;
|
||||
cumulative_len.push(calculated_len);
|
||||
}
|
||||
|
||||
if (expected_len - calculated_len).abs() > f32::EPSILON {
|
||||
if (expected_len - calculated_len).abs() > f64::EPSILON {
|
||||
// * In osu-stable, if the last two control points of a slider are equal, extension is not performed
|
||||
let condition_opt = points
|
||||
.len()
|
||||
@@ -207,7 +210,7 @@ impl Curve {
|
||||
// * The direction of the segment to shorten or lengthen
|
||||
let dir = (path[end_idx] - path[prev_idx]).normalize();
|
||||
|
||||
path[end_idx] = path[prev_idx] + dir * (expected_len - cumulative_len[prev_idx]);
|
||||
path[end_idx] = path[prev_idx] + dir * (expected_len - cumulative_len[prev_idx]) as f32;
|
||||
cumulative_len.push(expected_len);
|
||||
}
|
||||
|
||||
@@ -286,18 +289,22 @@ impl Curve {
|
||||
} else {
|
||||
let divisor = 2.0 * (1.0 - CIRCULAR_ARC_TOLERANCE / pr.radius).acos();
|
||||
|
||||
((pr.theta_range / divisor).ceil() as usize).max(2)
|
||||
((pr.theta_range / divisor as f64).ceil() as usize).max(2)
|
||||
};
|
||||
|
||||
path.reserve_exact(amount_points);
|
||||
let divisor = (amount_points - 1) as f32;
|
||||
let divisor = (amount_points - 1) as f64;
|
||||
let directed_range = pr.direction * pr.theta_range;
|
||||
|
||||
let subpath = (0..amount_points).map(|i| {
|
||||
let fract = i as f32 / divisor;
|
||||
let fract = i as f64 / divisor;
|
||||
let theta = pr.theta_start + fract * directed_range;
|
||||
let (sin, cos) = theta.sin_cos();
|
||||
let origin = Pos2 { x: cos, y: sin };
|
||||
|
||||
let origin = Pos2 {
|
||||
x: cos as f32,
|
||||
y: sin as f32,
|
||||
};
|
||||
|
||||
pr.centre + origin * pr.radius
|
||||
});
|
||||
@@ -323,7 +330,7 @@ impl Curve {
|
||||
// * <a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a>
|
||||
// * over the tree resulting from the subdivisions we make.)
|
||||
|
||||
let mut left_child = bufs.buf2.to_owned();
|
||||
// bufs.buf4 will serve as left_child
|
||||
|
||||
while let Some(mut parent) = to_flatten.pop() {
|
||||
if Self::bezier_is_flat_enough(&parent) {
|
||||
@@ -345,13 +352,13 @@ impl Curve {
|
||||
|
||||
Self::bezier_subdivide(
|
||||
&parent,
|
||||
&mut left_child,
|
||||
&mut bufs.buf4,
|
||||
right_child.to_mut(),
|
||||
&mut bufs.buf1,
|
||||
);
|
||||
|
||||
// * We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
|
||||
parent.to_mut().copy_from_slice(&left_child[..p]);
|
||||
parent.to_mut().copy_from_slice(&bufs.buf4[..p]);
|
||||
|
||||
to_flatten.push(right_child);
|
||||
to_flatten.push(parent);
|
||||
@@ -395,6 +402,7 @@ impl Curve {
|
||||
buf1: l,
|
||||
buf2: r,
|
||||
buf3: midpoints,
|
||||
..
|
||||
} = bufs;
|
||||
|
||||
Self::bezier_subdivide(points, l, r, midpoints);
|
||||
@@ -403,22 +411,13 @@ impl Curve {
|
||||
let l = &l[..count];
|
||||
let r = &r[1..count];
|
||||
|
||||
let left_iter = l
|
||||
let subpath = l
|
||||
.iter()
|
||||
.chain(r)
|
||||
.skip(1)
|
||||
.zip(l.iter().skip(2))
|
||||
.zip(l.iter().skip(3))
|
||||
.step_by(2);
|
||||
|
||||
let right_iter = r
|
||||
.iter()
|
||||
.skip(1)
|
||||
.zip(r.iter().skip(2))
|
||||
.zip(r.iter().skip(3))
|
||||
.step_by(2);
|
||||
|
||||
let subpath = left_iter
|
||||
.chain(right_iter)
|
||||
.zip(l.iter().chain(r).skip(2))
|
||||
.zip(l.iter().chain(r).skip(3))
|
||||
.step_by(2)
|
||||
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
|
||||
|
||||
path.extend(subpath);
|
||||
@@ -472,6 +471,7 @@ impl Curve {
|
||||
return None;
|
||||
}
|
||||
|
||||
// * See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2
|
||||
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
|
||||
let a_sq = a.length_squared();
|
||||
let b_sq = b.length_squared();
|
||||
@@ -479,7 +479,7 @@ impl Curve {
|
||||
|
||||
let centre = Pos2 {
|
||||
x: (a_sq * (b - c).y + b_sq * (c - a).y + c_sq * (a - b).y) / d,
|
||||
y: ((c - b).x + b_sq * (a - c).x + c_sq * (b - a).x) / d,
|
||||
y: (a_sq * (c - b).x + b_sq * (a - c).x + c_sq * (b - a).x) / d,
|
||||
};
|
||||
|
||||
let d_a = a - centre;
|
||||
@@ -512,8 +512,8 @@ impl Curve {
|
||||
}
|
||||
|
||||
Some(CircularArcProperties {
|
||||
theta_start,
|
||||
theta_range,
|
||||
theta_start: theta_start as f64,
|
||||
theta_range: theta_range as f64,
|
||||
direction,
|
||||
radius,
|
||||
centre,
|
||||
|
||||
+10
-10
@@ -1,12 +1,12 @@
|
||||
use crate::parse::Pos2;
|
||||
|
||||
const PLAYFIELD_WIDTH: f32 = 512.0;
|
||||
const BASE_SPEED: f32 = 1.0;
|
||||
const BASE_SPEED: f64 = 1.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CatchObject {
|
||||
pub(crate) pos: f32,
|
||||
pub(crate) time: f32,
|
||||
pub(crate) time: f64,
|
||||
|
||||
pub(crate) hyper_dash: bool,
|
||||
pub(crate) hyper_dist: f32,
|
||||
@@ -14,7 +14,7 @@ pub struct CatchObject {
|
||||
|
||||
impl CatchObject {
|
||||
#[inline]
|
||||
pub(crate) fn new((pos, time): (Pos2, f32)) -> Self {
|
||||
pub(crate) fn new((pos, time): (Pos2, f64)) -> Self {
|
||||
Self {
|
||||
pos: pos.x,
|
||||
time,
|
||||
@@ -23,7 +23,7 @@ impl CatchObject {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f32) -> Self {
|
||||
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f64) -> Self {
|
||||
let mut offset_pos = self.pos;
|
||||
let time_diff = self.time - *last_time;
|
||||
|
||||
@@ -31,7 +31,7 @@ impl CatchObject {
|
||||
let pos_diff = offset_pos - last_pos_ref;
|
||||
|
||||
if pos_diff.abs() > f32::EPSILON {
|
||||
if pos_diff.abs() < (time_diff / 3.0).floor() {
|
||||
if pos_diff.abs() < (time_diff as f32 / 3.0).floor() {
|
||||
if pos_diff > 0.0 {
|
||||
if offset_pos + pos_diff < PLAYFIELD_WIDTH {
|
||||
offset_pos += pos_diff;
|
||||
@@ -56,10 +56,10 @@ impl CatchObject {
|
||||
|
||||
pub(crate) fn init_hyper_dash(
|
||||
&mut self,
|
||||
half_catcher_width: f32,
|
||||
half_catcher_width: f64,
|
||||
next: &CatchObject,
|
||||
last_direction: &mut i8,
|
||||
last_excess: &mut f32,
|
||||
last_excess: &mut f64,
|
||||
) {
|
||||
let next_x = next.pos;
|
||||
let curr_x = self.pos;
|
||||
@@ -73,15 +73,15 @@ impl CatchObject {
|
||||
half_catcher_width
|
||||
};
|
||||
|
||||
let dist_to_next = (next_x - curr_x).abs() - sub;
|
||||
let hyper_dist = time_to_next * BASE_SPEED - dist_to_next;
|
||||
let dist_to_next = (next_x - curr_x).abs() as f64 - sub;
|
||||
let hyper_dist = (time_to_next * BASE_SPEED - dist_to_next) as f32;
|
||||
|
||||
if hyper_dist < 0.0 {
|
||||
self.hyper_dash = true;
|
||||
*last_excess = half_catcher_width;
|
||||
} else {
|
||||
self.hyper_dist = hyper_dist;
|
||||
*last_excess = hyper_dist.max(0.0).min(half_catcher_width);
|
||||
*last_excess = (hyper_dist as f64).max(0.0).min(half_catcher_width);
|
||||
}
|
||||
|
||||
*last_direction = this_direction;
|
||||
|
||||
@@ -6,14 +6,14 @@ pub(crate) struct DifficultyObject<'o> {
|
||||
pub(crate) base: &'o CatchObject,
|
||||
pub(crate) last: &'o CatchObject,
|
||||
|
||||
pub(crate) delta: f32,
|
||||
pub(crate) start_time: f32,
|
||||
pub(crate) delta: f64,
|
||||
pub(crate) start_time: f64,
|
||||
|
||||
pub(crate) normalized_pos: f32,
|
||||
pub(crate) last_normalized_pos: f32,
|
||||
|
||||
pub(crate) strain_time: f32,
|
||||
pub(crate) clock_rate: f32,
|
||||
pub(crate) strain_time: f64,
|
||||
pub(crate) clock_rate: f64,
|
||||
}
|
||||
|
||||
impl<'o> DifficultyObject<'o> {
|
||||
@@ -22,7 +22,7 @@ impl<'o> DifficultyObject<'o> {
|
||||
base: &'o CatchObject,
|
||||
last: &'o CatchObject,
|
||||
half_catcher_width: f32,
|
||||
clock_rate: f32,
|
||||
clock_rate: f64,
|
||||
) -> Self {
|
||||
let delta = (base.time - last.time) / clock_rate;
|
||||
let start_time = base.time / clock_rate;
|
||||
|
||||
+33
-31
@@ -18,14 +18,14 @@ use crate::{
|
||||
Beatmap, Mods, Strains,
|
||||
};
|
||||
|
||||
const SECTION_LENGTH: f32 = 750.0;
|
||||
const STAR_SCALING_FACTOR: f32 = 0.153;
|
||||
const SECTION_LENGTH: f64 = 750.0;
|
||||
const STAR_SCALING_FACTOR: f64 = 0.153;
|
||||
|
||||
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
||||
const CATCHER_SIZE: f32 = 106.75;
|
||||
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
||||
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||
|
||||
/// Star calculation for osu!ctb maps
|
||||
///
|
||||
@@ -79,7 +79,7 @@ pub fn stars(
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f32;
|
||||
let span_count = (*repeats + 1) as f64;
|
||||
|
||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||
|
||||
@@ -143,15 +143,15 @@ pub fn stars(
|
||||
slider_objects.extend(&ticks);
|
||||
|
||||
for span_idx in 1..=*repeats {
|
||||
let progress = (span_idx % 2 == 1) as u8 as f32;
|
||||
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
let time_offset = span_duration * span_idx as f32;
|
||||
let time_offset = span_duration * span_idx as f64;
|
||||
|
||||
// Reverse tick
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
|
||||
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
||||
(*pos, *time + time_offset + time_add * i as f32)
|
||||
(*pos, *time + time_offset + time_add * i as f64)
|
||||
});
|
||||
|
||||
// Actual ticks
|
||||
@@ -166,7 +166,7 @@ pub fn stars(
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let progress = (*repeats % 2 == 0) as u8 as f32;
|
||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
@@ -185,12 +185,13 @@ pub fn stars(
|
||||
.take(take);
|
||||
|
||||
// Hyper dash business
|
||||
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
|
||||
let half_catcher_width =
|
||||
(calculate_catch_width(attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||
let mut last_direction = 0;
|
||||
let mut last_excess = half_catcher_width;
|
||||
|
||||
// Strain business
|
||||
let mut movement = Movement::new(attributes.cs);
|
||||
let mut movement = Movement::new(attributes.cs as f32);
|
||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
@@ -329,7 +330,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f32;
|
||||
let span_count = (*repeats + 1) as f64;
|
||||
|
||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||
|
||||
@@ -385,15 +386,15 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
slider_objects.extend(&ticks);
|
||||
|
||||
for span_idx in 1..=*repeats {
|
||||
let progress = (span_idx % 2 == 1) as u8 as f32;
|
||||
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
let time_offset = span_duration * span_idx as f32;
|
||||
let time_offset = span_duration * span_idx as f64;
|
||||
|
||||
// Reverse tick
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
|
||||
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
||||
(*pos, *time + time_offset + time_add * i as f32)
|
||||
(*pos, *time + time_offset + time_add * i as f64)
|
||||
});
|
||||
|
||||
// Actual ticks
|
||||
@@ -408,7 +409,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let progress = (*repeats % 2 == 0) as u8 as f32;
|
||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
@@ -422,12 +423,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
.flatten();
|
||||
|
||||
// Hyper dash business
|
||||
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
|
||||
let half_catcher_width =
|
||||
(calculate_catch_width(attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||
let mut last_direction = 0;
|
||||
let mut last_excess = half_catcher_width;
|
||||
|
||||
// Strain business
|
||||
let mut movement = Movement::new(attributes.cs);
|
||||
let mut movement = Movement::new(attributes.cs as f32);
|
||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
@@ -522,11 +524,11 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
// BUG: Sometimes there are off-by-one errors,
|
||||
// presumably caused by floating point inaccuracies
|
||||
fn tiny_droplet_count(
|
||||
start_time: f32,
|
||||
time_between_ticks: f32,
|
||||
duration: f32,
|
||||
start_time: f64,
|
||||
time_between_ticks: f64,
|
||||
duration: f64,
|
||||
span_count: usize,
|
||||
ticks: &[(Pos2, f32)],
|
||||
ticks: &[(Pos2, f64)],
|
||||
) -> usize {
|
||||
// tiny droplets preceeding a _tick_
|
||||
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
|
||||
@@ -542,7 +544,7 @@ fn tiny_droplet_count(
|
||||
|
||||
// tiny droplets preceeding a _reverse_
|
||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||
let repeat_time = start_time + duration / span_count as f32;
|
||||
let repeat_time = start_time + duration / span_count as f64;
|
||||
let since_last_tick = repeat_time - last;
|
||||
|
||||
let span_last_section = if since_last_tick > 80.0 {
|
||||
@@ -556,7 +558,7 @@ fn tiny_droplet_count(
|
||||
// tiny droplets preceeding the slider tail
|
||||
// necessary to handle distinctly because of the legacy last tick
|
||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||
let end_time = start_time + duration / span_count as f32 - LEGACY_LAST_TICK_OFFSET;
|
||||
let end_time = start_time + duration / span_count as f64 - LEGACY_LAST_TICK_OFFSET;
|
||||
let since_last_tick = end_time - last;
|
||||
|
||||
let last_section = if since_last_tick > 80.0 {
|
||||
@@ -574,7 +576,7 @@ fn tiny_droplet_count(
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn shrink_down(mut val: f32) -> f32 {
|
||||
fn shrink_down(mut val: f64) -> f64 {
|
||||
while val > 100.0 {
|
||||
val /= 2.0;
|
||||
}
|
||||
@@ -583,7 +585,7 @@ fn shrink_down(mut val: f32) -> f32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn count_iterations(mut start: f32, step: f32, end: f32) -> usize {
|
||||
fn count_iterations(mut start: f64, step: f64, end: f64) -> usize {
|
||||
let mut count = 0;
|
||||
|
||||
while start < end {
|
||||
@@ -631,9 +633,9 @@ impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
|
||||
/// This data is necessary to calculate PP.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DifficultyAttributes {
|
||||
pub stars: f32,
|
||||
pub stars: f64,
|
||||
pub max_combo: usize,
|
||||
pub ar: f32,
|
||||
pub ar: f64,
|
||||
pub n_fruits: usize,
|
||||
pub n_droplets: usize,
|
||||
pub n_tiny_droplets: usize,
|
||||
@@ -643,19 +645,19 @@ pub struct DifficultyAttributes {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PerformanceAttributes {
|
||||
pub attributes: DifficultyAttributes,
|
||||
pub pp: f32,
|
||||
pub pp: f64,
|
||||
}
|
||||
|
||||
impl PerformanceAttributes {
|
||||
/// Return the star value.
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f32 {
|
||||
pub fn stars(&self) -> f64 {
|
||||
self.attributes.stars
|
||||
}
|
||||
|
||||
/// Return the performance point value.
|
||||
#[inline]
|
||||
pub fn pp(&self) -> f32 {
|
||||
pub fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
}
|
||||
|
||||
+21
-20
@@ -5,23 +5,23 @@ use std::cmp::Ordering;
|
||||
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
|
||||
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
|
||||
const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR;
|
||||
const DIRECTION_CHANGE_BONUS: f32 = 21.0;
|
||||
const SKILL_MULTIPLIER: f32 = 900.0;
|
||||
const STRAIN_DECAY_BASE: f32 = 0.2;
|
||||
const DECAY_WEIGHT: f32 = 0.94;
|
||||
const DIRECTION_CHANGE_BONUS: f64 = 21.0;
|
||||
const SKILL_MULTIPLIER: f64 = 900.0;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.2;
|
||||
const DECAY_WEIGHT: f64 = 0.94;
|
||||
|
||||
pub(crate) struct Movement {
|
||||
pub(crate) half_catcher_width: f32,
|
||||
|
||||
last_player_position: Option<f32>,
|
||||
last_distance_moved: f32,
|
||||
last_strain_time: f32,
|
||||
last_strain_time: f64,
|
||||
|
||||
current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
current_strain: f64,
|
||||
current_section_peak: f64,
|
||||
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
prev_time: Option<f32>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
prev_time: Option<f64>,
|
||||
}
|
||||
|
||||
impl Movement {
|
||||
@@ -51,7 +51,7 @@ impl Movement {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl Movement {
|
||||
self.prev_time.replace(current.start_time);
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
@@ -77,7 +77,7 @@ impl Movement {
|
||||
difficulty
|
||||
}
|
||||
|
||||
fn strain_value_of(&mut self, current: &DifficultyObject<'_>) -> f32 {
|
||||
fn strain_value_of(&mut self, current: &DifficultyObject<'_>) -> f64 {
|
||||
let last_player_pos = self
|
||||
.last_player_position
|
||||
.unwrap_or(current.last_normalized_pos);
|
||||
@@ -89,14 +89,15 @@ impl Movement {
|
||||
let dist_moved = pos - last_player_pos;
|
||||
let weighted_strain_time = current.strain_time + 13.0 + (3.0 / current.clock_rate);
|
||||
|
||||
let mut dist_addition = dist_moved.abs().powf(1.3) / 510.0;
|
||||
let mut dist_addition = (dist_moved.abs().powf(1.3) / 510.0) as f64;
|
||||
|
||||
if dist_moved.abs() > 0.1 {
|
||||
if self.last_distance_moved.abs() > 0.1
|
||||
&& dist_moved.signum() != self.last_distance_moved.signum()
|
||||
{
|
||||
let bonus_factor = dist_moved.abs().min(50.0) / 50.0;
|
||||
let anti_flow_factor = (self.last_distance_moved.abs().min(70.0) / 70.0).max(0.38);
|
||||
let bonus_factor = (dist_moved.abs().min(50.0) / 50.0) as f64;
|
||||
let anti_flow_factor =
|
||||
(self.last_distance_moved.abs().min(70.0) / 70.0).max(0.38) as f64;
|
||||
|
||||
dist_addition += DIRECTION_CHANGE_BONUS / (self.last_strain_time + 16.0).sqrt()
|
||||
* bonus_factor
|
||||
@@ -104,8 +105,8 @@ impl Movement {
|
||||
* (1.0 - (weighted_strain_time / 1000.0).powi(3)).max(0.0);
|
||||
}
|
||||
|
||||
dist_addition += 12.5 * dist_moved.abs().min(NORMALIZED_HITOBJECT_RADIUS * 2.0)
|
||||
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0)
|
||||
dist_addition += (12.5 * dist_moved.abs().min(NORMALIZED_HITOBJECT_RADIUS * 2.0)
|
||||
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0)) as f64
|
||||
/ weighted_strain_time.sqrt();
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ impl Movement {
|
||||
|
||||
dist_addition *= 1.0
|
||||
+ edge_dash_bonus
|
||||
* ((20.0 - current.last.hyper_dist) / 20.0)
|
||||
* ((20.0 - current.last.hyper_dist) / 20.0) as f64
|
||||
* ((current.strain_time * current.clock_rate).min(265.0) / 265.0).powf(1.5);
|
||||
}
|
||||
|
||||
@@ -132,12 +133,12 @@ impl Movement {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||
fn peak_strain(&self, delta_time: f64) -> f64 {
|
||||
self.current_strain * strain_decay(delta_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay(ms: f32) -> f32 {
|
||||
fn strain_decay(ms: f64) -> f64 {
|
||||
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||
}
|
||||
|
||||
+11
-11
@@ -145,7 +145,7 @@ impl<'m> FruitsPP<'m> {
|
||||
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
||||
///
|
||||
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
||||
pub fn accuracy(mut self, mut acc: f32) -> Self {
|
||||
pub fn accuracy(mut self, mut acc: f64) -> Self {
|
||||
if self.attributes.is_none() {
|
||||
self.attributes = Some(stars(self.map, self.mods, self.passed_objects));
|
||||
}
|
||||
@@ -167,7 +167,7 @@ impl<'m> FruitsPP<'m> {
|
||||
acc /= 100.0;
|
||||
|
||||
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
||||
((acc * (attributes.max_combo + max_tiny_droplets) as f32).round() as usize)
|
||||
((acc * (attributes.max_combo + max_tiny_droplets) as f64).round() as usize)
|
||||
.saturating_sub(n_fruits)
|
||||
.saturating_sub(n_droplets)
|
||||
});
|
||||
@@ -290,17 +290,17 @@ impl FruitsPPInner {
|
||||
|
||||
// Longer maps are worth more
|
||||
let len_bonus = 0.95
|
||||
+ 0.3 * (combo_hits as f32 / 2500.0).min(1.0)
|
||||
+ (combo_hits > 2500) as u8 as f32 * (combo_hits as f32 / 2500.0).log10() * 0.475;
|
||||
+ 0.3 * (combo_hits as f64 / 2500.0).min(1.0)
|
||||
+ (combo_hits > 2500) as u8 as f64 * (combo_hits as f64 / 2500.0).log10() * 0.475;
|
||||
|
||||
pp *= len_bonus;
|
||||
|
||||
// Penalize misses exponentially
|
||||
pp *= 0.97_f32.powi(self.n_misses as i32);
|
||||
pp *= 0.97_f64.powi(self.n_misses as i32);
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
pp *= (combo as f32 / attributes.max_combo as f32)
|
||||
pp *= (combo as f64 / attributes.max_combo as f64)
|
||||
.powf(0.8)
|
||||
.min(1.0);
|
||||
}
|
||||
@@ -309,7 +309,7 @@ impl FruitsPPInner {
|
||||
let ar = attributes.ar;
|
||||
let mut ar_factor = 1.0;
|
||||
if ar > 9.0 {
|
||||
ar_factor += 0.1 * (ar - 9.0) + (ar > 10.0) as u8 as f32 * 0.1 * (ar - 10.0);
|
||||
ar_factor += 0.1 * (ar - 9.0) + (ar > 10.0) as u8 as f64 * 0.1 * (ar - 10.0);
|
||||
} else if ar < 8.0 {
|
||||
ar_factor += 0.025 * (8.0 - ar);
|
||||
}
|
||||
@@ -359,13 +359,13 @@ impl FruitsPPInner {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn acc(&self) -> f32 {
|
||||
fn acc(&self) -> f64 {
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
1.0
|
||||
} else {
|
||||
(self.successful_hits() as f32 / total_hits as f32)
|
||||
(self.successful_hits() as f64 / total_hits as f64)
|
||||
.max(0.0)
|
||||
.min(1.0)
|
||||
}
|
||||
@@ -447,7 +447,7 @@ mod test {
|
||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||
let denominator =
|
||||
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
(target_acc - acc).abs() < 1.0,
|
||||
@@ -487,7 +487,7 @@ mod test {
|
||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||
let denominator =
|
||||
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
(target_acc - acc).abs() < 1.0,
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::{Beatmap, ControlPoint, ControlPointIter};
|
||||
pub(crate) struct SliderState<'p> {
|
||||
control_points: ControlPointIter<'p>,
|
||||
next: Option<ControlPoint>,
|
||||
pub(crate) beat_len: f32,
|
||||
pub(crate) slider_velocity: f32,
|
||||
pub(crate) beat_len: f64,
|
||||
pub(crate) slider_velocity: f64,
|
||||
}
|
||||
|
||||
impl<'p> SliderState<'p> {
|
||||
@@ -29,7 +29,7 @@ impl<'p> SliderState<'p> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn update(&mut self, time: f32) {
|
||||
pub(crate) fn update(&mut self, time: f64) {
|
||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||
match next {
|
||||
ControlPoint::Timing { beat_len, .. } => {
|
||||
@@ -88,14 +88,14 @@ mod test {
|
||||
let mut state = SliderState::new(&map);
|
||||
|
||||
state.update(2.0);
|
||||
assert!((state.beat_len - 10.0).abs() <= f32::EPSILON);
|
||||
assert!((state.beat_len - 10.0).abs() <= f64::EPSILON);
|
||||
|
||||
state.update(3.0);
|
||||
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON);
|
||||
assert!((state.beat_len - 20.0).abs() <= f64::EPSILON);
|
||||
assert!((state.slider_velocity - 1.0).abs() <= f64::EPSILON);
|
||||
|
||||
state.update(5.0);
|
||||
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON);
|
||||
assert!((state.beat_len - 30.0).abs() <= f64::EPSILON);
|
||||
assert!((state.slider_velocity - 45.0).abs() <= f64::EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -298,8 +298,8 @@ impl BeatmapExt for Beatmap {
|
||||
/// `section_length` is the time in ms inbetween two strains.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Strains {
|
||||
pub section_length: f32,
|
||||
pub strains: Vec<f32>,
|
||||
pub section_length: f64,
|
||||
pub strains: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Basic enum containing the result of a star calculation based on the mode.
|
||||
@@ -318,7 +318,7 @@ pub enum StarResult {
|
||||
impl StarResult {
|
||||
/// The final star value.
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f32 {
|
||||
pub fn stars(&self) -> f64 {
|
||||
match self {
|
||||
#[cfg(feature = "fruits")]
|
||||
Self::Fruits(attributes) => attributes.stars,
|
||||
@@ -348,7 +348,7 @@ pub enum PpResult {
|
||||
impl PpResult {
|
||||
/// The final pp value.
|
||||
#[inline]
|
||||
pub fn pp(&self) -> f32 {
|
||||
pub fn pp(&self) -> f64 {
|
||||
match self {
|
||||
#[cfg(feature = "fruits")]
|
||||
Self::Fruits(attributes) => attributes.pp,
|
||||
@@ -363,7 +363,7 @@ impl PpResult {
|
||||
|
||||
/// The final star value.
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f32 {
|
||||
pub fn stars(&self) -> f64 {
|
||||
match self {
|
||||
#[cfg(feature = "fruits")]
|
||||
Self::Fruits(attributes) => attributes.stars(),
|
||||
@@ -379,7 +379,7 @@ impl PpResult {
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "taiko"))]
|
||||
#[inline]
|
||||
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
||||
fn difficulty_range(val: f64, max: f64, avg: f64, min: f64) -> f64 {
|
||||
if val > 5.0 {
|
||||
avg + (max - avg) * (val - 5.0) / 5.0
|
||||
} else if val < 5.0 {
|
||||
|
||||
+11
-11
@@ -8,8 +8,8 @@ use strain::Strain;
|
||||
|
||||
use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
|
||||
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const STAR_SCALING_FACTOR: f32 = 0.018;
|
||||
const SECTION_LEN: f64 = 400.0;
|
||||
const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||
|
||||
/// Star calculation for osu!mania maps
|
||||
///
|
||||
@@ -150,13 +150,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
pub(crate) struct DifficultyHitObject<'o> {
|
||||
base: &'o HitObject,
|
||||
column: usize,
|
||||
delta: f32,
|
||||
start_time: f32,
|
||||
delta: f64,
|
||||
start_time: f64,
|
||||
}
|
||||
|
||||
impl<'o> DifficultyHitObject<'o> {
|
||||
#[inline]
|
||||
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f32) -> Self {
|
||||
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f64) -> Self {
|
||||
let x_divisor = 512.0 / columns;
|
||||
let column = (base.pos.x / x_divisor).floor().min(columns - 1.0) as usize;
|
||||
|
||||
@@ -173,28 +173,28 @@ impl<'o> DifficultyHitObject<'o> {
|
||||
/// This data is necessary to calculate PP.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct DifficultyAttributes {
|
||||
pub stars: f32,
|
||||
pub stars: f64,
|
||||
}
|
||||
|
||||
/// Various data created through the pp calculation.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct PerformanceAttributes {
|
||||
pub attributes: DifficultyAttributes,
|
||||
pub pp_acc: f32,
|
||||
pub pp_strain: f32,
|
||||
pub pp: f32,
|
||||
pub pp_acc: f64,
|
||||
pub pp_strain: f64,
|
||||
pub pp: f64,
|
||||
}
|
||||
|
||||
impl PerformanceAttributes {
|
||||
/// Return the star value.
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f32 {
|
||||
pub fn stars(&self) -> f64 {
|
||||
self.attributes.stars
|
||||
}
|
||||
|
||||
/// Return the performance point value.
|
||||
#[inline]
|
||||
pub fn pp(&self) -> f32 {
|
||||
pub fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
}
|
||||
|
||||
+17
-17
@@ -30,9 +30,9 @@ use crate::{Beatmap, Mods, PpResult, StarResult};
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct ManiaPP<'m> {
|
||||
map: &'m Beatmap,
|
||||
stars: Option<f32>,
|
||||
stars: Option<f64>,
|
||||
mods: u32,
|
||||
score: Option<f32>,
|
||||
score: Option<f64>,
|
||||
passed_objects: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl<'m> ManiaPP<'m> {
|
||||
#[inline]
|
||||
pub fn attributes(mut self, attributes: impl ManiaAttributeProvider) -> Self {
|
||||
if let Some(stars) = attributes.attributes() {
|
||||
self.stars.replace(stars);
|
||||
self.stars = Some(stars);
|
||||
}
|
||||
|
||||
self
|
||||
@@ -76,7 +76,7 @@ impl<'m> ManiaPP<'m> {
|
||||
/// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
|
||||
#[inline]
|
||||
pub fn score(mut self, score: u32) -> Self {
|
||||
self.score.replace(score as f32);
|
||||
self.score = Some(score as f64);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -100,17 +100,17 @@ impl<'m> ManiaPP<'m> {
|
||||
let ht = self.mods.ht();
|
||||
|
||||
let mut scaled_score = self.score.map_or(1_000_000.0, |score| {
|
||||
score / 0.5_f32.powi(ez as i32 + nf as i32 + ht as i32)
|
||||
score / 0.5_f64.powi(ez as i32 + nf as i32 + ht as i32)
|
||||
});
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
let percent_passed =
|
||||
passed_objects as f32 / (self.map.n_circles + self.map.n_sliders) as f32;
|
||||
passed_objects as f64 / (self.map.n_circles + self.map.n_sliders) as f64;
|
||||
|
||||
scaled_score /= percent_passed;
|
||||
}
|
||||
|
||||
let mut od = 34.0 + 3.0 * (10.0 - self.map.od).max(0.0).min(10.0);
|
||||
let mut od = 34.0 + 3.0 * (10.0 - self.map.od as f64).max(0.0).min(10.0);
|
||||
let clock_rate = self.mods.speed();
|
||||
|
||||
let mut multiplier = 0.8;
|
||||
@@ -139,10 +139,10 @@ impl<'m> ManiaPP<'m> {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_strain(&self, score: f32, stars: f32) -> f32 {
|
||||
fn compute_strain(&self, score: f64, stars: f64) -> f64 {
|
||||
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
|
||||
|
||||
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f32 / 1500.0).min(1.0);
|
||||
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f64 / 1500.0).min(1.0);
|
||||
|
||||
if score <= 500_000.0 {
|
||||
strain_value = 0.0;
|
||||
@@ -162,7 +162,7 @@ impl<'m> ManiaPP<'m> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn compute_accuracy_value(&self, score: f32, strain: f32, hit_window: f32) -> f32 {
|
||||
fn compute_accuracy_value(&self, score: f64, strain: f64, hit_window: f64) -> f64 {
|
||||
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
|
||||
* strain
|
||||
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
|
||||
@@ -170,33 +170,33 @@ impl<'m> ManiaPP<'m> {
|
||||
}
|
||||
|
||||
pub trait ManiaAttributeProvider {
|
||||
fn attributes(self) -> Option<f32>;
|
||||
fn attributes(self) -> Option<f64>;
|
||||
}
|
||||
|
||||
impl ManiaAttributeProvider for f32 {
|
||||
impl ManiaAttributeProvider for f64 {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManiaAttributeProvider for DifficultyAttributes {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
Some(self.stars)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManiaAttributeProvider for PerformanceAttributes {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
Some(self.attributes.stars)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManiaAttributeProvider for StarResult {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Mania(attributes) = self {
|
||||
Some(attributes.stars)
|
||||
@@ -208,7 +208,7 @@ impl ManiaAttributeProvider for StarResult {
|
||||
|
||||
impl ManiaAttributeProvider for PpResult {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Mania(attributes) = self {
|
||||
Some(attributes.attributes.stars)
|
||||
|
||||
+19
-19
@@ -3,25 +3,25 @@ use super::DifficultyHitObject;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
pub(crate) struct Strain {
|
||||
current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
current_strain: f64,
|
||||
current_section_peak: f64,
|
||||
|
||||
individual_strain: f32,
|
||||
overall_strain: f32,
|
||||
individual_strain: f64,
|
||||
overall_strain: f64,
|
||||
|
||||
hold_end_times: Vec<f32>,
|
||||
individual_strains: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
hold_end_times: Vec<f64>,
|
||||
individual_strains: Vec<f64>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
prev_time: Option<f64>,
|
||||
}
|
||||
|
||||
const INDIVISUAL_DECAY_BASE: f32 = 0.125;
|
||||
const OVERALL_DECAY_BASE: f32 = 0.3;
|
||||
const STRAIN_DECAY_BASE: f32 = 1.0;
|
||||
const INDIVISUAL_DECAY_BASE: f64 = 0.125;
|
||||
const OVERALL_DECAY_BASE: f64 = 0.3;
|
||||
const STRAIN_DECAY_BASE: f64 = 1.0;
|
||||
|
||||
const SKILL_MULTIPLIER: f32 = 1.0;
|
||||
const DECAY_WEIGHT: f32 = 0.9;
|
||||
const SKILL_MULTIPLIER: f64 = 1.0;
|
||||
const DECAY_WEIGHT: f64 = 0.9;
|
||||
|
||||
impl Strain {
|
||||
#[inline]
|
||||
@@ -47,18 +47,18 @@ impl Strain {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||
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: f32) -> f32 {
|
||||
fn strain_decay(&self, ms: f64) -> f64 {
|
||||
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ impl Strain {
|
||||
self.prev_time.replace(current.start_time);
|
||||
}
|
||||
|
||||
fn strain_value_of(&mut self, current: &DifficultyHitObject<'_>) -> f32 {
|
||||
fn strain_value_of(&mut self, current: &DifficultyHitObject<'_>) -> f64 {
|
||||
let end_time = current.base.end_time();
|
||||
|
||||
let mut hold_factor = 1.0;
|
||||
@@ -107,7 +107,7 @@ impl Strain {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
@@ -124,6 +124,6 @@ impl Strain {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn apply_decay(value: f32, delta_time: f32, decay_base: f32) -> f32 {
|
||||
fn apply_decay(value: f64, delta_time: f64, decay_base: f64) -> f64 {
|
||||
value * decay_base.powf(delta_time / 1000.0)
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,6 +10,6 @@ pub(crate) fn is_linear(
|
||||
|
||||
#[cfg(feature = "osu")]
|
||||
#[inline]
|
||||
pub(crate) fn lerp(start: f32, end: f32, percent: f32) -> f32 {
|
||||
pub(crate) fn lerp(start: f64, end: f64, percent: f64) -> f64 {
|
||||
start + (end - start) * percent
|
||||
}
|
||||
|
||||
+4
-4
@@ -21,8 +21,8 @@ pub trait Mods: Copy {
|
||||
|
||||
fn change_speed(self) -> bool;
|
||||
fn change_map(self) -> bool;
|
||||
fn speed(self) -> f32;
|
||||
fn od_ar_hp_multiplier(self) -> f32;
|
||||
fn speed(self) -> f64;
|
||||
fn od_ar_hp_multiplier(self) -> f64;
|
||||
fn nf(self) -> bool;
|
||||
fn ez(self) -> bool;
|
||||
fn td(self) -> bool;
|
||||
@@ -47,7 +47,7 @@ impl Mods for u32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn speed(self) -> f32 {
|
||||
fn speed(self) -> f64 {
|
||||
if self & Self::DT > 0 {
|
||||
1.5
|
||||
} else if self & Self::HT > 0 {
|
||||
@@ -58,7 +58,7 @@ impl Mods for u32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn od_ar_hp_multiplier(self) -> f32 {
|
||||
fn od_ar_hp_multiplier(self) -> f64 {
|
||||
if self & Self::HR > 0 {
|
||||
1.4
|
||||
} else if self & Self::EZ > 0 {
|
||||
|
||||
+17
-17
@@ -22,16 +22,16 @@ pub use fast::*;
|
||||
/// This data is necessary to calculate PP.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DifficultyAttributes {
|
||||
pub aim_strain: f32,
|
||||
pub speed_strain: f32,
|
||||
pub flashlight_rating: f32,
|
||||
pub ar: f32,
|
||||
pub od: f32,
|
||||
pub hp: f32,
|
||||
pub aim_strain: f64,
|
||||
pub speed_strain: f64,
|
||||
pub flashlight_rating: f64,
|
||||
pub ar: f64,
|
||||
pub od: f64,
|
||||
pub hp: f64,
|
||||
pub n_circles: usize,
|
||||
pub n_sliders: usize,
|
||||
pub n_spinners: usize,
|
||||
pub stars: f32,
|
||||
pub stars: f64,
|
||||
pub max_combo: usize,
|
||||
}
|
||||
|
||||
@@ -39,29 +39,29 @@ pub struct DifficultyAttributes {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PerformanceAttributes {
|
||||
pub attributes: DifficultyAttributes,
|
||||
pub pp_acc: f32,
|
||||
pub pp_aim: f32,
|
||||
pub pp_flashlight: f32,
|
||||
pub pp_speed: f32,
|
||||
pub pp: f32,
|
||||
pub pp_acc: f64,
|
||||
pub pp_aim: f64,
|
||||
pub pp_flashlight: f64,
|
||||
pub pp_speed: f64,
|
||||
pub pp: f64,
|
||||
}
|
||||
|
||||
impl PerformanceAttributes {
|
||||
/// Return the star value.
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f32 {
|
||||
pub fn stars(&self) -> f64 {
|
||||
self.attributes.stars
|
||||
}
|
||||
|
||||
/// Return the performance point value.
|
||||
#[inline]
|
||||
pub fn pp(&self) -> f32 {
|
||||
pub fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
fn difficulty_range_od(od: f64) -> f64 {
|
||||
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ fn custom_osu() {
|
||||
|
||||
use crate::{Beatmap, OsuPP};
|
||||
|
||||
let path = "E:Games/osu!/beatmaps/1402167_.osu";
|
||||
let path = "E:Games/osu!/beatmaps/70090_.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(0).calculate();
|
||||
let result = OsuPP::new(&map).mods(16 + 64).calculate();
|
||||
|
||||
let iters = 100;
|
||||
let accum = start.elapsed();
|
||||
|
||||
+58
-59
@@ -35,7 +35,7 @@ pub struct OsuPP<'m> {
|
||||
attributes: Option<DifficultyAttributes>,
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
acc: Option<f32>,
|
||||
acc: Option<f64>,
|
||||
|
||||
n300: Option<usize>,
|
||||
n100: Option<usize>,
|
||||
@@ -138,7 +138,7 @@ impl<'m> OsuPP<'m> {
|
||||
///
|
||||
/// Be sure to set `misses` beforehand!
|
||||
/// In case of a partial play, be also sure to set `passed_objects` beforehand!
|
||||
pub fn accuracy(mut self, acc: f32) -> Self {
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
let n_objects = self
|
||||
.passed_objects
|
||||
.unwrap_or_else(|| self.map.hit_objects.len());
|
||||
@@ -152,7 +152,7 @@ impl<'m> OsuPP<'m> {
|
||||
let placed_points = 2 * n100 + n50 + self.n_misses;
|
||||
let missing_objects = n_objects - n100 - n50 - self.n_misses;
|
||||
let missing_points =
|
||||
((6.0 * acc * n_objects as f32).round() as usize).saturating_sub(placed_points);
|
||||
((6.0 * acc * n_objects as f64).round() as usize).saturating_sub(placed_points);
|
||||
|
||||
let mut n300 = missing_objects.min(missing_points / 6);
|
||||
n50 += missing_objects - n300;
|
||||
@@ -171,10 +171,10 @@ impl<'m> OsuPP<'m> {
|
||||
self.n100 = Some(n100);
|
||||
self.n50 = Some(n50);
|
||||
|
||||
acc = (6 * n300 + 2 * n100 + n50) as f32 / (6 * n_objects) as f32;
|
||||
acc = (6 * n300 + 2 * n100 + n50) as f64 / (6 * n_objects) as f64;
|
||||
} else {
|
||||
let misses = self.n_misses.min(n_objects);
|
||||
let target_total = (acc * n_objects as f32 * 6.0).round() as usize;
|
||||
let target_total = (acc * n_objects as f64 * 6.0).round() as usize;
|
||||
let delta = target_total - (n_objects - misses);
|
||||
|
||||
let mut n300 = delta / 5;
|
||||
@@ -191,7 +191,7 @@ impl<'m> OsuPP<'m> {
|
||||
self.n100 = Some(n100);
|
||||
self.n50 = Some(n50);
|
||||
|
||||
acc = (6 * n300 + 2 * n100 + n50) as f32 / (6 * n_objects) as f32;
|
||||
acc = (6 * n300 + 2 * n100 + n50) as f64 / (6 * n_objects) as f64;
|
||||
}
|
||||
|
||||
self.acc = Some(acc);
|
||||
@@ -213,7 +213,7 @@ impl<'m> OsuPP<'m> {
|
||||
let n100 = n100.unwrap_or(0);
|
||||
let n50 = n50.unwrap_or(0);
|
||||
|
||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f32;
|
||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64;
|
||||
|
||||
let effective_misses =
|
||||
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
||||
@@ -259,9 +259,9 @@ impl<'m> OsuPP<'m> {
|
||||
let n50 = n50.unwrap_or(0);
|
||||
|
||||
let numerator = n300 * 6 + n100 * 2 + n50;
|
||||
let acc = numerator as f32 / n_objects as f32 / 6.0;
|
||||
let acc = numerator as f64 / n_objects as f64 / 6.0;
|
||||
|
||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f32;
|
||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64;
|
||||
|
||||
let effective_misses =
|
||||
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
||||
@@ -295,13 +295,13 @@ struct OsuPPInner {
|
||||
attributes: DifficultyAttributes,
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
acc: f32,
|
||||
acc: f64,
|
||||
|
||||
n300: usize,
|
||||
n100: usize,
|
||||
n50: usize,
|
||||
|
||||
total_hits: f32,
|
||||
total_hits: f64,
|
||||
effective_misses: usize,
|
||||
}
|
||||
|
||||
@@ -311,18 +311,23 @@ impl OsuPPInner {
|
||||
|
||||
// NF penalty
|
||||
if self.mods.nf() {
|
||||
multiplier *= (1.0 - 0.02 * (self.effective_misses as f32)).max(0.9);
|
||||
multiplier *= (1.0 - 0.02 * (self.effective_misses as f64)).max(0.9);
|
||||
}
|
||||
|
||||
// SO penalty
|
||||
if self.mods.so() {
|
||||
let n_spinners = self.attributes.n_spinners;
|
||||
multiplier *= 1.0 - (n_spinners as f32 / self.total_hits).powf(0.85);
|
||||
multiplier *= 1.0 - (n_spinners as f64 / self.total_hits).powf(0.85);
|
||||
}
|
||||
|
||||
// Relax penalty
|
||||
if self.mods.rx() {
|
||||
self.effective_misses += self.n100 + self.n50;
|
||||
// * As we're adding 100s and 50s to an approximated number of combo breaks\
|
||||
// * the result can be higher than total hits in specific scenarios
|
||||
// * (which breaks some calculations) so we need to clamp it.
|
||||
self.effective_misses =
|
||||
(self.effective_misses + self.n100 + self.n50).min(self.total_hits as usize);
|
||||
|
||||
multiplier *= 0.6;
|
||||
}
|
||||
|
||||
@@ -348,7 +353,7 @@ impl OsuPPInner {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_aim_value(&self) -> f32 {
|
||||
fn compute_aim_value(&self) -> f64 {
|
||||
let attributes = &self.attributes;
|
||||
let total_hits = self.total_hits;
|
||||
|
||||
@@ -364,48 +369,44 @@ impl OsuPPInner {
|
||||
// Longer maps are worth more
|
||||
let len_bonus = 0.95
|
||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
||||
+ (total_hits > 2000.0) as u8 as f64 * 0.5 * (total_hits / 2000.0).log10();
|
||||
aim_value *= len_bonus;
|
||||
|
||||
// Penalize misses
|
||||
let effective_misses = self.effective_misses as i32;
|
||||
if effective_misses > 0 {
|
||||
aim_value *= 0.97
|
||||
* (1.0 - (effective_misses as f32 / total_hits).powf(0.775)).powi(effective_misses);
|
||||
* (1.0 - (effective_misses as f64 / total_hits).powf(0.775)).powi(effective_misses);
|
||||
}
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
aim_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||
aim_value *= ((combo as f64 / attributes.max_combo as f64).powf(0.8)).min(1.0);
|
||||
}
|
||||
|
||||
// AR bonus
|
||||
let ar_factor = if attributes.ar > 10.33 {
|
||||
attributes.ar - 10.33
|
||||
0.3 * (attributes.ar - 10.33)
|
||||
} else if attributes.ar < 8.0 {
|
||||
0.025 * (8.0 - attributes.ar)
|
||||
0.1 * (8.0 - attributes.ar)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip();
|
||||
let ar_bonus = 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
|
||||
aim_value *= 1.0 + ar_factor * len_bonus; // * Buff for longer maps with high AR.
|
||||
|
||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||
if self.mods.hd() {
|
||||
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||
}
|
||||
|
||||
aim_value *= ar_bonus;
|
||||
|
||||
// Scale with accuracy
|
||||
aim_value *= 0.5 + self.acc / 2.0;
|
||||
aim_value *= self.acc;
|
||||
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||
|
||||
aim_value
|
||||
}
|
||||
|
||||
fn compute_speed_value(&self) -> f32 {
|
||||
fn compute_speed_value(&self) -> f64 {
|
||||
let attributes = &self.attributes;
|
||||
let total_hits = self.total_hits;
|
||||
|
||||
@@ -415,11 +416,11 @@ impl OsuPPInner {
|
||||
// Longer maps are worth more
|
||||
let len_bonus = 0.95
|
||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
||||
+ (total_hits > 2000.0) as u8 as f64 * 0.5 * (total_hits / 2000.0).log10();
|
||||
speed_value *= len_bonus;
|
||||
|
||||
// Penalize misses
|
||||
let effective_misses = self.effective_misses as f32;
|
||||
let effective_misses = self.effective_misses as f64;
|
||||
if effective_misses > 0.0 {
|
||||
speed_value *= 0.97
|
||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||
@@ -428,19 +429,17 @@ impl OsuPPInner {
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
speed_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||
speed_value *= ((combo as f64 / attributes.max_combo as f64).powf(0.8)).min(1.0);
|
||||
}
|
||||
|
||||
// AR bonus
|
||||
let ar_factor = if attributes.ar > 10.33 {
|
||||
attributes.ar - 10.33
|
||||
0.3 * (attributes.ar - 10.33)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip();
|
||||
|
||||
speed_value *= 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
|
||||
speed_value *= 1.0 + ar_factor * len_bonus; // * Buff for longer maps with high AR.
|
||||
|
||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||
if self.mods.hd() {
|
||||
@@ -453,34 +452,34 @@ impl OsuPPInner {
|
||||
speed_value *= od_factor * acc_factor;
|
||||
|
||||
// Penalize n50s
|
||||
speed_value *= 0.98_f32.powf(
|
||||
(self.n50 as f32 >= total_hits / 500.0) as u8 as f32
|
||||
* (self.n50 as f32 - total_hits / 500.0),
|
||||
speed_value *= 0.98_f64.powf(
|
||||
(self.n50 as f64 >= total_hits / 500.0) as u8 as f64
|
||||
* (self.n50 as f64 - total_hits / 500.0),
|
||||
);
|
||||
|
||||
speed_value
|
||||
}
|
||||
|
||||
fn compute_accuracy_value(&self) -> f32 {
|
||||
fn compute_accuracy_value(&self) -> f64 {
|
||||
if self.mods.rx() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let attributes = &self.attributes;
|
||||
let total_hits = self.total_hits;
|
||||
let n_circles = attributes.n_circles as f32;
|
||||
let n300 = self.n300 as f32;
|
||||
let n100 = self.n100 as f32;
|
||||
let n50 = self.n50 as f32;
|
||||
let n_circles = attributes.n_circles as f64;
|
||||
let n300 = self.n300 as f64;
|
||||
let n100 = self.n100 as f64;
|
||||
let n50 = self.n50 as f64;
|
||||
|
||||
let better_acc_percentage = (n_circles > 0.0) as u8 as f32
|
||||
let better_acc_percentage = (n_circles > 0.0) as u8 as f64
|
||||
* (((n300 - (total_hits - n_circles)) * 6.0 + n100 * 2.0 + n50) / (n_circles * 6.0))
|
||||
.max(0.0);
|
||||
|
||||
let mut acc_value = 1.52163_f32.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
||||
let mut acc_value = 1.52163_f64.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
||||
|
||||
// Bonus for many hitcircles
|
||||
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
|
||||
acc_value *= ((n_circles as f64 / 1000.0).powf(0.3)).min(1.15);
|
||||
|
||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||
if self.mods.hd() {
|
||||
@@ -495,7 +494,7 @@ impl OsuPPInner {
|
||||
acc_value
|
||||
}
|
||||
|
||||
fn compute_flashlight_value(&self) -> f32 {
|
||||
fn compute_flashlight_value(&self) -> f64 {
|
||||
if !self.mods.fl() {
|
||||
return 0.0;
|
||||
}
|
||||
@@ -519,7 +518,7 @@ impl OsuPPInner {
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// Default a 3% reduction for any # of misses
|
||||
let effective_misses = self.effective_misses as f32;
|
||||
let effective_misses = self.effective_misses as f64;
|
||||
if effective_misses > 0.0 {
|
||||
flashlight_value *= 0.97
|
||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||
@@ -528,13 +527,13 @@ impl OsuPPInner {
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
flashlight_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||
flashlight_value *= ((combo as f64 / attributes.max_combo as f64).powf(0.8)).min(1.0);
|
||||
}
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
|
||||
flashlight_value *= 0.7
|
||||
+ 0.1 * (total_hits / 200.0).min(1.0)
|
||||
+ (total_hits > 200.0) as u8 as f32 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
||||
+ (total_hits > 200.0) as u8 as f64 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
||||
|
||||
// Scale the aim value with accuracy _slightly_
|
||||
flashlight_value *= 0.5 + self.acc / 2.0;
|
||||
@@ -550,23 +549,23 @@ fn calculate_effective_misses(
|
||||
attributes: &DifficultyAttributes,
|
||||
combo: Option<usize>,
|
||||
n_misses: usize,
|
||||
total_hits: f32,
|
||||
total_hits: f64,
|
||||
) -> usize {
|
||||
// Guess the number of misses + slider breaks from combo
|
||||
let mut combo_based_misses: f32 = 0.0;
|
||||
// * Guess the number of misses + slider breaks from combo
|
||||
let mut combo_based_misses: f64 = 0.0;
|
||||
|
||||
if attributes.n_sliders > 0 {
|
||||
let full_combo_threshold = attributes.max_combo as f32 - 0.1 * attributes.n_sliders as f32;
|
||||
let full_combo_threshold = attributes.max_combo as f64 - 0.1 * attributes.n_sliders as f64;
|
||||
|
||||
let f32_combo = combo.map(|c| c as f32);
|
||||
let f64_combo = combo.map(|c| c as f64);
|
||||
|
||||
if let Some(combo) = f32_combo.filter(|&c| c < full_combo_threshold) {
|
||||
if let Some(combo) = f64_combo.filter(|&c| c < full_combo_threshold) {
|
||||
combo_based_misses = full_combo_threshold / combo.max(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// We're clamping misses because since it's derived from combo it
|
||||
// can be higher than total hits and that breaks some calculations
|
||||
// * Clamp misscount since it's derived from combo and can be
|
||||
// * higher than total hits and that breaks some calculations
|
||||
combo_based_misses = combo_based_misses.min(total_hits);
|
||||
|
||||
n_misses.max(combo_based_misses.floor() as usize)
|
||||
@@ -634,7 +633,7 @@ mod test {
|
||||
+ 2 * calculator.n100.unwrap_or(0)
|
||||
+ calculator.n50.unwrap_or(0);
|
||||
let denominator = 6 * total_objects;
|
||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
(target_acc - acc).abs() < 1.0,
|
||||
@@ -668,7 +667,7 @@ mod test {
|
||||
+ 2 * calculator.n100.unwrap_or(0)
|
||||
+ calculator.n50.unwrap_or(0);
|
||||
let denominator = 6 * total_objects;
|
||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
(target_acc - acc).abs() < 1.0,
|
||||
|
||||
@@ -1,67 +1,238 @@
|
||||
use super::OsuObject;
|
||||
use crate::{
|
||||
osu::precise::osu_object::{NestedObjectKind, OsuObjectKind},
|
||||
parse::Pos2,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
pub(crate) struct DifficultyObject<'h> {
|
||||
pub(crate) base: &'h OsuObject,
|
||||
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: f64,
|
||||
pub(crate) strain_time: f64,
|
||||
|
||||
pub(crate) delta: f32,
|
||||
pub(crate) strain_time: f32,
|
||||
pub(crate) angle: Option<f64>,
|
||||
pub(crate) jump_dist: f64,
|
||||
|
||||
pub(crate) movement_dist: f64,
|
||||
pub(crate) movement_time: f64,
|
||||
|
||||
pub(crate) travel_dist: f64,
|
||||
pub(crate) travel_time: f64,
|
||||
}
|
||||
|
||||
impl<'h> DifficultyObject<'h> {
|
||||
pub(crate) fn new(
|
||||
pub(super) fn new(
|
||||
base: &'h OsuObject,
|
||||
prev: &OsuObject,
|
||||
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||
prev_prev: Option<OsuObject>,
|
||||
scale_factor: f32,
|
||||
scaling_factor: f32,
|
||||
prev: &mut OsuObject,
|
||||
prev_prev: Option<&OsuObject>,
|
||||
scaling_factor: &ScalingFactor,
|
||||
clock_rate: f64,
|
||||
) -> Self {
|
||||
let delta = base.time - prev.time;
|
||||
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
|
||||
let strain_time = delta.max(25.0);
|
||||
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
|
||||
let strain_time = delta.max(MIN_DELTA_TIME);
|
||||
|
||||
let pos = base.pos; // stacked position
|
||||
let travel_dist = prev.travel_dist();
|
||||
let prev_cursor_pos = prev.lazy_end_pos(scale_factor);
|
||||
// * We don't need to calculate either angle or distances
|
||||
// * when one of the last->curr objects is a spinner
|
||||
let (travel_dist, travel_time, movement_dist, movement_time, jump_dist, angle) =
|
||||
if base.is_spinner() || prev.is_spinner() {
|
||||
(0.0, 0.0, 0.0, 0.0, 0.0, None)
|
||||
} else {
|
||||
let prev_stack_offset = scaling_factor.stack_offset(prev.stack_height);
|
||||
|
||||
// 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() || prev.is_spinner() {
|
||||
(0.0, None)
|
||||
} else {
|
||||
let jump_dist = ((pos - prev_cursor_pos) * scaling_factor).length();
|
||||
// Important to call `Self::compute_slider_cursor_pos` before using `prev.lazy_end_pos`
|
||||
// because the lazy end position is being calculated in that function
|
||||
let (travel_dist, travel_time) = Self::compute_slider_cursor_pos(
|
||||
prev,
|
||||
prev_stack_offset,
|
||||
scaling_factor.raw(),
|
||||
clock_rate,
|
||||
);
|
||||
|
||||
let angle = prev_prev.map(|prev_prev| {
|
||||
let prev_prev_cursor_pos = prev_prev.lazy_end_pos(scale_factor);
|
||||
let prev_cursor_pos = prev.lazy_end_pos(prev_stack_offset);
|
||||
|
||||
let v1 = prev_prev_cursor_pos - prev.pos;
|
||||
let v2 = pos - prev_cursor_pos;
|
||||
let jump_dist =
|
||||
((base.pos - prev_cursor_pos) * scaling_factor.adjusted()).length() as f64;
|
||||
|
||||
let dot = v1.dot(v2);
|
||||
let det = v1.x * v2.y - v1.y * v2.x;
|
||||
let angle =
|
||||
prev_prev
|
||||
.filter(|prev_prev| !prev_prev.is_spinner())
|
||||
.map(|prev_prev| {
|
||||
let prev_prev_cursor_pos = prev_prev
|
||||
.lazy_end_pos(scaling_factor.stack_offset(prev_prev.stack_height));
|
||||
|
||||
det.atan2(dot).abs()
|
||||
});
|
||||
let v1 = prev_prev_cursor_pos - prev.pos;
|
||||
let v2 = base.pos - prev_cursor_pos;
|
||||
|
||||
(jump_dist, angle)
|
||||
};
|
||||
let dot = (v1.dot(v2)) as f64;
|
||||
let det = (v1.x * v2.y - v1.y * v2.x) as f64;
|
||||
|
||||
det.atan2(dot).abs()
|
||||
});
|
||||
|
||||
let (movement_dist, movement_time) = Self::compute_movement_values(
|
||||
prev,
|
||||
base.pos,
|
||||
jump_dist,
|
||||
strain_time,
|
||||
travel_time,
|
||||
scaling_factor.adjusted(),
|
||||
);
|
||||
|
||||
(
|
||||
travel_dist,
|
||||
travel_time,
|
||||
movement_dist,
|
||||
movement_time,
|
||||
jump_dist,
|
||||
angle,
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
base,
|
||||
prev: prev_vals,
|
||||
|
||||
jump_dist,
|
||||
travel_dist,
|
||||
angle,
|
||||
|
||||
delta,
|
||||
strain_time,
|
||||
jump_dist,
|
||||
angle,
|
||||
movement_dist,
|
||||
movement_time,
|
||||
travel_dist,
|
||||
travel_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_slider_cursor_pos(
|
||||
prev: &mut OsuObject,
|
||||
stack_offset: Pos2,
|
||||
scaling_factor: f64,
|
||||
clock_rate: f64,
|
||||
) -> (f64, f64) {
|
||||
match &mut prev.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => (0.0, 0.0),
|
||||
OsuObjectKind::Slider {
|
||||
lazy_end_pos,
|
||||
nested_objects,
|
||||
..
|
||||
} => {
|
||||
let mut travel_dist = 0.0;
|
||||
let pos = prev.pos - stack_offset; // stack offset is ignored everywhere
|
||||
let mut curr_cursor_pos = pos;
|
||||
|
||||
let last_idx = nested_objects.len() - 1;
|
||||
|
||||
for (i, nested) in nested_objects.iter_mut().enumerate() {
|
||||
let mut curr_movement = nested.pos - curr_cursor_pos;
|
||||
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.
|
||||
let mut required_movement = ASSUMED_SLIDER_RADIUS as f64;
|
||||
|
||||
if i == last_idx {
|
||||
// * The end of a slider has special aim rules due
|
||||
// * to the relaxed time constraint on position.
|
||||
// * There is both a lazy end position as well as the actual end slider position.
|
||||
// * We assume the player takes the simpler movement.
|
||||
// * For sliders that are circular, the lazy end position
|
||||
// * may actually be farther away than the sliders true end.
|
||||
// * This code is designed to prevent buffing situations
|
||||
// * where lazy end is actually a less efficient movement.
|
||||
let lazy_movement = *lazy_end_pos - curr_cursor_pos;
|
||||
|
||||
if lazy_movement.length() < curr_movement.length() {
|
||||
curr_movement = lazy_movement;
|
||||
}
|
||||
|
||||
curr_movement_len = scaling_factor * curr_movement.length() as f64;
|
||||
} else if let NestedObjectKind::Repeat = nested.kind {
|
||||
// * For a slider repeat, assume a tighter movement
|
||||
// * threshold to better assess repeat sliders.
|
||||
required_movement = NORMALIZED_RADIUS as f64;
|
||||
}
|
||||
|
||||
if curr_movement_len > required_movement {
|
||||
// * this finds the positional delta from the required
|
||||
// * radius and the current position, and updates the
|
||||
// * currCursorPosition accordingly, as well as rewarding distance.
|
||||
curr_cursor_pos = curr_cursor_pos
|
||||
+ curr_movement
|
||||
* ((curr_movement_len - required_movement) / curr_movement_len)
|
||||
as f32;
|
||||
|
||||
curr_movement_len *=
|
||||
(curr_movement_len - required_movement) / curr_movement_len;
|
||||
|
||||
travel_dist += curr_movement_len;
|
||||
}
|
||||
|
||||
if i == last_idx {
|
||||
*lazy_end_pos = curr_cursor_pos;
|
||||
}
|
||||
}
|
||||
|
||||
let repeats = nested_objects
|
||||
.iter()
|
||||
.filter(|nested| matches!(nested.kind, NestedObjectKind::Repeat))
|
||||
.count();
|
||||
|
||||
// * Bonus for repeat sliders until a better per
|
||||
// * nested object strain system can be achieved.
|
||||
travel_dist *= (1.0 + repeats as f64 / 2.5).powf(1.0 / 2.5);
|
||||
let prev_time = prev.time;
|
||||
|
||||
let lazy_travel_time = nested_objects
|
||||
.last()
|
||||
.map_or(0.0, |nested| nested.time / clock_rate - prev_time);
|
||||
|
||||
let travel_time = MIN_DELTA_TIME.max(lazy_travel_time);
|
||||
|
||||
(travel_dist, travel_time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_movement_values(
|
||||
prev: &OsuObject,
|
||||
base_pos: Pos2,
|
||||
jump_dist: f64,
|
||||
strain_time: f64,
|
||||
travel_time: f64,
|
||||
scaling_factor: f32,
|
||||
) -> (f64, f64) {
|
||||
match &prev.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => (jump_dist, strain_time),
|
||||
OsuObjectKind::Slider { end_pos, .. } => {
|
||||
let movement_time = MIN_DELTA_TIME.max(strain_time - travel_time);
|
||||
|
||||
// * Jump distance from the slider tail to the next object,
|
||||
// * as opposed to the lazy position of JumpDistance.
|
||||
let tail_jump_dist = (*end_pos - base_pos).length() * scaling_factor;
|
||||
|
||||
// * For hitobjects which continue in the direction of the slider,
|
||||
// * the player will normally follow through the slider,
|
||||
// * such that they're not jumping from the lazy position but
|
||||
// * rather from very close to (or the end of) the slider.
|
||||
// * In such cases, a leniency is applied by also considering the
|
||||
// * jump distance from the tail of the slider,
|
||||
// * and taking the minimum jump distance.
|
||||
// * Additional distance is removed based on position of jump
|
||||
// * relative to slider follow circle radius.
|
||||
// * JumpDistance is the leniency distance beyond the assumed_slider_radius.
|
||||
// * tailJumpDistance is maximum_slider_radius since
|
||||
// * the full distance of radial leniency is still possible.
|
||||
let movement_dist = (jump_dist
|
||||
- (MAXIMUM_SLIDER_RADIUS - ASSUMED_SLIDER_RADIUS) as f64)
|
||||
.min((tail_jump_dist - MAXIMUM_SLIDER_RADIUS) as f64)
|
||||
.max(0.0);
|
||||
|
||||
(movement_dist, movement_time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-79
@@ -8,24 +8,25 @@ use std::mem;
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
mod scaling_factor;
|
||||
mod skill;
|
||||
mod skill_kind;
|
||||
mod slider_state;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
use osu_object::{ObjectParameters, OsuObject};
|
||||
use scaling_factor::ScalingFactor;
|
||||
use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{curve::CurveBuffers, parse::Pos2, Beatmap, Mods, Strains};
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||
|
||||
use super::DifficultyAttributes;
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||
const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
const SECTION_LEN: f64 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
||||
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
|
||||
const STACK_DISTANCE: f32 = 3.0;
|
||||
|
||||
/// Star calculation for osu!standard maps.
|
||||
@@ -54,7 +55,7 @@ pub fn stars(
|
||||
};
|
||||
}
|
||||
|
||||
let mut raw_ar = map.ar;
|
||||
let mut raw_ar = map.ar as f64;
|
||||
let hr = mods.hr();
|
||||
|
||||
if hr {
|
||||
@@ -64,19 +65,10 @@ pub fn stars(
|
||||
}
|
||||
|
||||
let time_preempt = difficulty_range_ar(raw_ar);
|
||||
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let radius = OBJECT_RADIUS * scale;
|
||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
if radius < 30.0 {
|
||||
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||
scaling_factor *= 1.0 + small_circle_bonus;
|
||||
}
|
||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
max_combo: 0,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
@@ -92,7 +84,7 @@ pub fn stars(
|
||||
let mut hit_objects = Vec::with_capacity(take);
|
||||
hit_objects.extend(hit_objects_iter);
|
||||
|
||||
let stack_threshold = time_preempt * map.stack_leniency;
|
||||
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||
|
||||
if map.version >= 6 {
|
||||
stacking(&mut hit_objects, stack_threshold);
|
||||
@@ -100,13 +92,13 @@ pub fn stars(
|
||||
old_stacking(&mut hit_objects, stack_threshold);
|
||||
}
|
||||
|
||||
let scale_factor = scale * -6.4;
|
||||
// let scale_factor = (scaling_factor.scale * -6.4) as f32;
|
||||
|
||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||
let stack_offset = h.stack_height * scale_factor;
|
||||
|
||||
// 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.pos += Pos2::new(stack_offset);
|
||||
|
||||
h
|
||||
});
|
||||
@@ -114,16 +106,16 @@ pub fn stars(
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
skills.push(Skill::aim());
|
||||
skills.push(Skill::speed(hit_window));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
||||
skills.push(Skill::flashlight(52.0 / scaling_factor.radius() as f64));
|
||||
}
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut prev_vals = None;
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
@@ -132,11 +124,10 @@ pub fn stars(
|
||||
let curr = hit_objects.next().unwrap();
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
prev_vals,
|
||||
prev_prev,
|
||||
scale_factor,
|
||||
scaling_factor,
|
||||
&mut prev,
|
||||
prev_prev.as_ref(),
|
||||
&scaling_factor,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
@@ -152,18 +143,16 @@ pub fn stars(
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
|
||||
// Handle all other objects
|
||||
for curr in hit_objects {
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
prev_vals,
|
||||
prev_prev,
|
||||
scale_factor,
|
||||
scaling_factor,
|
||||
&mut prev,
|
||||
prev_prev.as_ref(),
|
||||
&scaling_factor,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
@@ -180,7 +169,6 @@ pub fn stars(
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
@@ -224,9 +212,9 @@ pub fn stars(
|
||||
.powf(1.0 / 1.1);
|
||||
|
||||
let star_rating = if base_performance > 0.00001 {
|
||||
1.12_f32.cbrt()
|
||||
1.12_f64.cbrt()
|
||||
* 0.027
|
||||
* ((100_000.0 / (1.0_f32 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -258,7 +246,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
let mut raw_ar = map.ar;
|
||||
let mut raw_ar = map.ar as f64;
|
||||
let hr = mods.hr();
|
||||
|
||||
if hr {
|
||||
@@ -268,19 +256,10 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
let time_preempt = difficulty_range_ar(raw_ar);
|
||||
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let radius = OBJECT_RADIUS * scale;
|
||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
if radius < 30.0 {
|
||||
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||
scaling_factor *= 1.0 + small_circle_bonus;
|
||||
}
|
||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
max_combo: 0,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
@@ -295,7 +274,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||
hit_objects.extend(hit_objects_iter);
|
||||
|
||||
let stack_threshold = time_preempt * map.stack_leniency;
|
||||
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||
|
||||
if map.version >= 6 {
|
||||
stacking(&mut hit_objects, stack_threshold);
|
||||
@@ -303,13 +282,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
old_stacking(&mut hit_objects, stack_threshold);
|
||||
}
|
||||
|
||||
let scale_factor = scale * -6.4;
|
||||
// let scale_factor = (scaling_factor.scale * -6.4) as f32;
|
||||
|
||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||
let stack_offset = h.stack_height * scale_factor;
|
||||
|
||||
// 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.pos += Pos2::new(stack_offset);
|
||||
|
||||
h
|
||||
});
|
||||
@@ -317,16 +296,16 @@ 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::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
skills.push(Skill::aim());
|
||||
skills.push(Skill::speed(hit_window));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
||||
skills.push(Skill::flashlight(52.0 / scaling_factor.radius() as f64));
|
||||
}
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut prev_vals = None;
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
@@ -335,11 +314,10 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let curr = hit_objects.next().unwrap();
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
prev_vals,
|
||||
prev_prev,
|
||||
scale_factor,
|
||||
scaling_factor,
|
||||
&mut prev,
|
||||
prev_prev.as_ref(),
|
||||
&scaling_factor,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
@@ -355,18 +333,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
|
||||
// Handle all other objects
|
||||
for curr in hit_objects {
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
prev_vals,
|
||||
prev_prev,
|
||||
scale_factor,
|
||||
scaling_factor,
|
||||
&mut prev,
|
||||
prev_prev.as_ref(),
|
||||
&scaling_factor,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
@@ -383,7 +359,6 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
@@ -418,7 +393,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
}
|
||||
|
||||
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
||||
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
let mut extended_start_idx = 0;
|
||||
let extended_end_idx = hit_objects.len() - 1;
|
||||
|
||||
@@ -529,7 +504,7 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
||||
fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
for i in 0..hit_objects.len() {
|
||||
if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() {
|
||||
continue;
|
||||
@@ -557,11 +532,7 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
const OSU_AR_MAX: f32 = 450.0;
|
||||
const OSU_AR_AVG: f32 = 1200.0;
|
||||
const OSU_AR_MIN: f32 = 1800.0;
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_ar(ar: f32) -> f32 {
|
||||
crate::difficulty_range(ar, OSU_AR_MAX, OSU_AR_AVG, OSU_AR_MIN)
|
||||
fn difficulty_range_ar(ar: f64) -> f64 {
|
||||
crate::difficulty_range(ar, 450.0, 1200.0, 1800.0)
|
||||
}
|
||||
|
||||
+130
-86
@@ -6,35 +6,47 @@ use crate::{
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
||||
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) time: f64,
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) stack_height: f32,
|
||||
kind: OsuObjectKind,
|
||||
pub(crate) kind: OsuObjectKind,
|
||||
}
|
||||
|
||||
enum OsuObjectKind {
|
||||
pub(crate) enum OsuObjectKind {
|
||||
Circle,
|
||||
Slider {
|
||||
end_time: f32,
|
||||
end_time: f64,
|
||||
end_pos: Pos2,
|
||||
lazy_end_pos: Pos2,
|
||||
travel_dist: f32,
|
||||
nested_objects: Vec<NestedObject>,
|
||||
},
|
||||
Spinner {
|
||||
end_time: f32,
|
||||
end_time: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct NestedObject {
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) time: f64,
|
||||
pub(crate) kind: NestedObjectKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum NestedObjectKind {
|
||||
Repeat,
|
||||
Tail,
|
||||
Tick,
|
||||
}
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) radius: f32,
|
||||
pub(crate) scaling_factor: f32,
|
||||
pub(crate) max_combo: usize,
|
||||
pub(crate) ticks: Vec<f32>,
|
||||
pub(crate) ticks: Vec<(Pos2, f64)>,
|
||||
pub(crate) slider_state: SliderState<'a>,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
}
|
||||
@@ -44,8 +56,6 @@ impl OsuObject {
|
||||
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
max_combo,
|
||||
ticks,
|
||||
slider_state,
|
||||
@@ -71,18 +81,15 @@ impl OsuObject {
|
||||
repeats,
|
||||
control_points,
|
||||
} => {
|
||||
// Key values which are computed here
|
||||
let mut lazy_end_pos = pos;
|
||||
let mut travel_dist = 0.0;
|
||||
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f32;
|
||||
let span_count = (*repeats + 1) as f64;
|
||||
|
||||
let approx_follow_circle_radius = *radius * 3.0;
|
||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||
|
||||
// * 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.
|
||||
if map.version >= 8 {
|
||||
tick_dist /=
|
||||
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
||||
@@ -99,35 +106,6 @@ impl OsuObject {
|
||||
let duration = end_time - h.start_time;
|
||||
let span_duration = duration / span_count;
|
||||
|
||||
// Called on each slider object except for the head.
|
||||
// Increases combo and adjusts `lazy_end_pos` and `travel_dist`
|
||||
// w.r.t. the object position at the given time on the slider curve.
|
||||
let mut compute_vertex = |mut progress: f32| {
|
||||
*max_combo += 1;
|
||||
|
||||
if progress % 2.0 >= 1.0 {
|
||||
progress = 1.0 - progress % 1.0;
|
||||
} else {
|
||||
progress %= 1.0;
|
||||
}
|
||||
|
||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
if hr {
|
||||
curr_pos.y = 384.0 - curr_pos.y;
|
||||
}
|
||||
|
||||
let diff = curr_pos - lazy_end_pos;
|
||||
let mut dist = diff.length();
|
||||
|
||||
if dist > approx_follow_circle_radius {
|
||||
// * The cursor would be outside the follow circle, we need to move it
|
||||
dist -= approx_follow_circle_radius;
|
||||
lazy_end_pos += diff.normalize() * dist;
|
||||
travel_dist += dist;
|
||||
}
|
||||
};
|
||||
|
||||
// * A very lenient maximum length of a slider for ticks to be generated.
|
||||
// * This exists for edge cases such as /b/1573664 where the beatmap has
|
||||
// * been edited by the user, and should never be reached in normal usage.
|
||||
@@ -141,54 +119,132 @@ impl OsuObject {
|
||||
|
||||
ticks.clear();
|
||||
ticks.reserve((len / tick_dist) as usize);
|
||||
let mut nested_objects =
|
||||
Vec::with_capacity((len * span_count / tick_dist) as usize);
|
||||
|
||||
// Tick of the first span
|
||||
// Ticks of the first span
|
||||
while curr_dist < len - min_dist_from_end {
|
||||
let progress = curr_dist / len;
|
||||
|
||||
compute_vertex(progress);
|
||||
ticks.push(progress);
|
||||
let curr_time = h.start_time + progress * span_duration;
|
||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
if hr {
|
||||
curr_pos.y = 384.0 - curr_pos.y;
|
||||
}
|
||||
|
||||
let tick = NestedObject {
|
||||
pos: curr_pos,
|
||||
time: curr_time,
|
||||
kind: NestedObjectKind::Tick,
|
||||
};
|
||||
|
||||
nested_objects.push(tick);
|
||||
ticks.push((curr_pos, curr_time));
|
||||
|
||||
curr_dist += tick_dist;
|
||||
}
|
||||
|
||||
// Other spans
|
||||
for span_idx in 1..=*repeats {
|
||||
let progress = (span_idx % 2 == 1) as u8 as f32;
|
||||
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||
let span_idx_f64 = span_idx as f64;
|
||||
|
||||
// Reverse tick
|
||||
compute_vertex(progress);
|
||||
// Repeat point
|
||||
let curr_time = h.start_time + span_duration * span_idx_f64;
|
||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
// Actual ticks
|
||||
if hr {
|
||||
curr_pos.y = 384.0 - curr_pos.y;
|
||||
}
|
||||
|
||||
let repeat = NestedObject {
|
||||
pos: curr_pos,
|
||||
time: curr_time,
|
||||
kind: NestedObjectKind::Repeat,
|
||||
};
|
||||
|
||||
nested_objects.push(repeat);
|
||||
|
||||
// Ticks
|
||||
if span_idx & 1 == 1 {
|
||||
ticks
|
||||
.iter()
|
||||
.rev()
|
||||
.for_each(|&tick_progress| compute_vertex(tick_progress + progress));
|
||||
// S-------->R | Span 0
|
||||
// 2 4 6 8 | => span_duration = 8
|
||||
// R<--------- | Span 1
|
||||
// 16 14 12 10 | => offset = 1 * span_duration
|
||||
// --------->R | Span 2
|
||||
// 18 20 22 24 | => not reverse; simple case
|
||||
// T<--------- | Span 3
|
||||
// 32 30 28 26 | => offset = 3 * span_duration
|
||||
//
|
||||
// n = offset + tick
|
||||
// 26 = 24 + 2
|
||||
// 28 = 24 + 4
|
||||
// 30 = 24 + 6
|
||||
// 32 = 24 + 8
|
||||
|
||||
let offset = span_idx_f64 * span_duration;
|
||||
|
||||
let tick_iter = ticks.iter().rev().zip(ticks.iter()).map(
|
||||
|((rev_pos, _), (_, time))| NestedObject {
|
||||
pos: *rev_pos,
|
||||
time: offset + time,
|
||||
kind: NestedObjectKind::Tick,
|
||||
},
|
||||
);
|
||||
|
||||
nested_objects.extend(tick_iter);
|
||||
} else {
|
||||
ticks
|
||||
.iter()
|
||||
.for_each(|&tick_progress| compute_vertex(tick_progress + progress));
|
||||
let tick_iter = ticks.iter().map(|(pos, time)| NestedObject {
|
||||
pos: *pos,
|
||||
time: time + span_duration * span_idx_f64,
|
||||
kind: NestedObjectKind::Tick,
|
||||
});
|
||||
|
||||
nested_objects.extend(tick_iter);
|
||||
}
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let final_span_start_time = h.start_time + *repeats as f32 * span_duration;
|
||||
let final_span_start_time = h.start_time + *repeats as f64 * span_duration;
|
||||
let final_span_end_time = (h.start_time + duration / 2.0)
|
||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||
let progress = (*repeats % 2 == 1) as u8 as f32;
|
||||
let final_progress =
|
||||
(final_span_end_time - final_span_start_time) / span_duration + progress;
|
||||
|
||||
compute_vertex(final_progress);
|
||||
|
||||
let mut end_pos = h.pos + curve.position_at(1.0 - progress);
|
||||
travel_dist *= *scaling_factor;
|
||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||
let mut end_pos = h.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).
|
||||
// * it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
|
||||
// * if this is to change, we should revisit this.
|
||||
let legacy_last_tick = NestedObject {
|
||||
pos: end_pos,
|
||||
time: final_span_end_time,
|
||||
kind: NestedObjectKind::Tail,
|
||||
};
|
||||
|
||||
nested_objects.push(legacy_last_tick);
|
||||
*max_combo += nested_objects.len();
|
||||
|
||||
let lazy_travel_time = final_span_end_time - h.start_time;
|
||||
let mut end_time_min = lazy_travel_time / span_duration;
|
||||
|
||||
if end_time_min % 2.0 >= 1.0 {
|
||||
end_time_min = 1.0 - end_time_min % 1.0;
|
||||
} else {
|
||||
end_time_min %= 1.0;
|
||||
}
|
||||
|
||||
// * temporary lazy end position until a real result can be derived.
|
||||
let mut lazy_end_pos = h.pos + curve.position_at(end_time_min);
|
||||
|
||||
if hr {
|
||||
lazy_end_pos.y = 384.0 - lazy_end_pos.y;
|
||||
}
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos,
|
||||
@@ -197,7 +253,7 @@ impl OsuObject {
|
||||
end_time: final_span_end_time,
|
||||
end_pos,
|
||||
lazy_end_pos,
|
||||
travel_dist,
|
||||
nested_objects,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -216,15 +272,7 @@ impl OsuObject {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn travel_dist(&self) -> f32 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Slider { travel_dist, .. } => *travel_dist,
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end_time(&self) -> f32 {
|
||||
pub(crate) fn end_time(&self) -> f64 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle => self.time,
|
||||
OsuObjectKind::Slider { end_time, .. } => *end_time,
|
||||
@@ -241,14 +289,10 @@ impl OsuObject {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn lazy_end_pos(&self, scale_factor: f32) -> Pos2 {
|
||||
pub(crate) fn lazy_end_pos(&self, stack_offset: Pos2) -> Pos2 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||
OsuObjectKind::Slider { lazy_end_pos, .. } => {
|
||||
let stack_offset = scale_factor * self.stack_height;
|
||||
|
||||
*lazy_end_pos + Pos2::new(stack_offset)
|
||||
}
|
||||
OsuObjectKind::Slider { lazy_end_pos, .. } => *lazy_end_pos + stack_offset,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::parse::Pos2;
|
||||
|
||||
use super::NORMALIZED_RADIUS;
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
|
||||
pub(crate) struct ScalingFactor {
|
||||
adjusted_factor: f32,
|
||||
factor: f32,
|
||||
radius: f32,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
impl ScalingFactor {
|
||||
pub(crate) fn new(cs: f64) -> Self {
|
||||
let scale = (1.0 - 0.7 * (cs as f32 - 5.0) / 5.0) / 2.0;
|
||||
|
||||
let radius = OBJECT_RADIUS * scale;
|
||||
let factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
let adjusted_factor = if radius < 30.0 {
|
||||
factor * (1.0 + (30.0 - radius).min(5.0) / 50.0)
|
||||
} else {
|
||||
factor
|
||||
};
|
||||
|
||||
Self {
|
||||
adjusted_factor,
|
||||
factor,
|
||||
radius,
|
||||
scale: scale * -6.4,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn raw(&self) -> f64 {
|
||||
self.factor as f64
|
||||
}
|
||||
|
||||
pub(crate) fn adjusted(&self) -> f32 {
|
||||
self.adjusted_factor
|
||||
}
|
||||
|
||||
pub(crate) fn radius(&self) -> f32 {
|
||||
self.radius
|
||||
}
|
||||
|
||||
pub(crate) fn stack_offset(&self, stack_height: f32) -> Pos2 {
|
||||
Pos2::new(stack_height * self.scale)
|
||||
}
|
||||
}
|
||||
+30
-15
@@ -4,23 +4,38 @@ use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKin
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
||||
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
curr_strain: f32,
|
||||
curr_section_peak: f32,
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
prev_time: Option<f64>,
|
||||
}
|
||||
|
||||
impl Skill {
|
||||
#[inline]
|
||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
||||
pub(crate) fn aim() -> Self {
|
||||
Self::new(SkillKind::aim())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn flashlight(scaling_factor: f64) -> Self {
|
||||
Self::new(SkillKind::flashlight(scaling_factor))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn speed(hit_window: f64) -> Self {
|
||||
Self::new(SkillKind::speed(hit_window))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn new(kind: SkillKind) -> Self {
|
||||
Self {
|
||||
curr_strain: 1.0,
|
||||
curr_strain: 0.0,
|
||||
curr_section_peak: 0.0,
|
||||
|
||||
kind,
|
||||
@@ -44,18 +59,18 @@ impl Skill {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||
// The maximum strain of the new section is not zero by default
|
||||
self.curr_section_peak = self.calculate_initial_strain(time);
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
let decay_weight = self.kind.decay_weight();
|
||||
|
||||
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
||||
let reduced_section_count_f32 = reduced_section_count as f32;
|
||||
let reduced_section_count_f64 = reduced_section_count as f64;
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
@@ -63,7 +78,7 @@ impl Skill {
|
||||
let peaks = self.strain_peaks.iter_mut();
|
||||
|
||||
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
||||
let clamped = (i as f32 / reduced_section_count_f32).clamp(0.0, 1.0);
|
||||
let clamped = (i as f64 / reduced_section_count_f64).clamp(0.0, 1.0);
|
||||
let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
|
||||
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||
}
|
||||
@@ -79,22 +94,22 @@ impl Skill {
|
||||
difficulty * difficulty_multiplier
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_initial_strain(&self, time: f32) -> f32 {
|
||||
pub(crate) fn calculate_initial_strain(&self, time: f64) -> f64 {
|
||||
let prev_time = self.prev_time.unwrap_or(0.0);
|
||||
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
|
||||
|
||||
match &self.kind {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => decayed_strain,
|
||||
SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => decayed_strain,
|
||||
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f32 {
|
||||
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f64 {
|
||||
self.curr_strain *= self.kind.strain_decay(curr.delta);
|
||||
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
|
||||
|
||||
match &mut self.kind {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => self.curr_strain,
|
||||
SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => self.curr_strain,
|
||||
SkillKind::Speed {
|
||||
curr_rhythm,
|
||||
history,
|
||||
|
||||
+238
-75
@@ -1,47 +1,81 @@
|
||||
use std::{collections::VecDeque, f32::consts::PI, iter};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
f64::consts::{FRAC_PI_2, PI},
|
||||
iter,
|
||||
};
|
||||
|
||||
use crate::{math_util, parse::Pos2};
|
||||
|
||||
use super::DifficultyObject;
|
||||
|
||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
||||
const SINGLE_SPACING_TRESHOLD: f64 = 125.0;
|
||||
|
||||
const MIN_SPEED_BONUS: f32 = 75.0;
|
||||
const SPEED_BALANCING_FACTOR: f32 = 40.0;
|
||||
const SPEED_BALANCING_FACTOR: f64 = 40.0;
|
||||
|
||||
const TIMING_THRESHOLD: f32 = 107.0;
|
||||
|
||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const AIM_DECAY_WEIGHT: f32 = 0.9;
|
||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const AIM_SKILL_MULTIPLIER: f64 = 23.25;
|
||||
const AIM_STRAIN_DECAY_BASE: f64 = 0.15;
|
||||
const AIM_DECAY_WEIGHT: f64 = 0.9;
|
||||
const AIM_DIFFICULTY_MULTIPLIER: f64 = 1.06;
|
||||
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
|
||||
const AIM_HISTORY_LENGTH: usize = 2;
|
||||
const AIM_WIDE_ANGLE_MULTIPLIER: f64 = 1.5;
|
||||
const AIM_ACUTE_ANGLE_MULTIPLIER: f64 = 2.0;
|
||||
const AIM_SLIDER_MULTIPLIER: f64 = 1.5;
|
||||
const AIM_VELOCITY_CHANGE_MULTIPLIER: f64 = 0.75;
|
||||
|
||||
const SPEED_SKILL_MULTIPLIER: f32 = 1375.0;
|
||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
||||
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
||||
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
||||
const SPEED_SKILL_MULTIPLIER: f64 = 1375.0;
|
||||
const SPEED_STRAIN_DECAY_BASE: f64 = 0.3;
|
||||
const SPEED_DECAY_WEIGHT: f64 = 0.9;
|
||||
const SPEED_DIFFICULTY_MULTIPLIER: f64 = 1.04;
|
||||
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
||||
|
||||
const SPEED_HISTORY_LENGTH: usize = 32;
|
||||
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0;
|
||||
const SPEED_RHYTHM_MULTIPLIER: f32 = 0.75;
|
||||
const SPEED_RHYTHM_MULTIPLIER: f64 = 0.75;
|
||||
const SPEED_HISTORY_TIME_MAX: f64 = 5000.0; // * 5 seconds of calculate_speed_rhythm_bonus max
|
||||
const MIN_SPEED_BONUS: f64 = 75.0; // * ~200BPM
|
||||
|
||||
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.15;
|
||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
|
||||
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const FLASHLIGHT_SKILL_MULTIPLIER: f64 = 0.15;
|
||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f64 = 0.15;
|
||||
const FLASHLIGHT_DECAY_WEIGHT: f64 = 1.0;
|
||||
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f64 = 1.06;
|
||||
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
||||
|
||||
pub(crate) struct AimHistoryEntry {
|
||||
angle: Option<f64>,
|
||||
is_slider: bool,
|
||||
is_spinner: bool,
|
||||
strain_time: f64,
|
||||
jump_dist: f64,
|
||||
movement_dist: f64,
|
||||
movement_time: f64,
|
||||
travel_dist: f64,
|
||||
travel_time: f64,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for AimHistoryEntry {
|
||||
fn from(h: &DifficultyObject<'_>) -> Self {
|
||||
Self {
|
||||
angle: h.angle,
|
||||
is_slider: h.base.is_slider(),
|
||||
is_spinner: h.base.is_spinner(),
|
||||
strain_time: h.strain_time,
|
||||
jump_dist: h.jump_dist,
|
||||
movement_dist: h.movement_dist,
|
||||
movement_time: h.movement_time,
|
||||
travel_dist: h.travel_dist,
|
||||
travel_time: h.travel_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FlashlightHistoryEntry {
|
||||
end_pos: Pos2,
|
||||
is_spinner: bool,
|
||||
jump_dist: f32,
|
||||
strain_time: f32,
|
||||
jump_dist: f64,
|
||||
strain_time: f64,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
||||
@@ -57,8 +91,8 @@ impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
||||
|
||||
pub(crate) struct SpeedHistoryEntry {
|
||||
is_slider: bool,
|
||||
start_time: f32,
|
||||
strain_time: f32,
|
||||
start_time: f64,
|
||||
strain_time: f64,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
||||
@@ -72,37 +106,45 @@ impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
||||
}
|
||||
|
||||
pub(crate) enum SkillKind {
|
||||
Aim,
|
||||
Aim {
|
||||
history: VecDeque<AimHistoryEntry>,
|
||||
},
|
||||
Flashlight {
|
||||
history: VecDeque<FlashlightHistoryEntry>,
|
||||
scaling_factor: f32,
|
||||
scaling_factor: f64,
|
||||
},
|
||||
Speed {
|
||||
curr_rhythm: f32,
|
||||
curr_rhythm: f64,
|
||||
history: VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
hit_window: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl SkillKind {
|
||||
pub(crate) fn flashlight(scaling_factor: f32) -> Self {
|
||||
pub(crate) fn aim() -> Self {
|
||||
Self::Aim {
|
||||
history: VecDeque::with_capacity(AIM_HISTORY_LENGTH + 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn flashlight(scaling_factor: f64) -> Self {
|
||||
Self::Flashlight {
|
||||
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH),
|
||||
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH + 1),
|
||||
scaling_factor,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn speed(hit_window: f32) -> Self {
|
||||
pub(crate) fn speed(hit_window: f64) -> Self {
|
||||
Self::Speed {
|
||||
curr_rhythm: 1.0,
|
||||
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH),
|
||||
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH + 1),
|
||||
hit_window,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pre_process(&mut self) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Aim { history } => history.truncate(AIM_HISTORY_LENGTH),
|
||||
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
|
||||
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
|
||||
}
|
||||
@@ -110,43 +152,159 @@ impl SkillKind {
|
||||
|
||||
pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Aim { history } => history.push_front(current.into()),
|
||||
Self::Flashlight { history, .. } => history.push_front(current.into()),
|
||||
Self::Speed { history, .. } => history.push_front(current.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f32 {
|
||||
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f64 {
|
||||
match self {
|
||||
Self::Aim => {
|
||||
if curr.base.is_spinner() {
|
||||
Self::Aim { history } => {
|
||||
if curr.base.is_spinner() || history.len() < 2 || history[0].is_spinner {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut aim_strain = 0.0;
|
||||
let prev = &history[0];
|
||||
let prev_prev = &history[1];
|
||||
|
||||
if let Some((prev_jump_dist, prev_strain_time)) = curr.prev {
|
||||
if let Some(angle) = curr.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
|
||||
let scale = 90.0;
|
||||
// * Calculate the velocity to the current hitobject,
|
||||
// * which starts with a base distance / time assuming the last object is a hitcircle.
|
||||
let mut curr_velocity = curr.jump_dist / curr.strain_time;
|
||||
|
||||
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
|
||||
* (prev_jump_dist - scale).max(0.0)
|
||||
* (curr.jump_dist - scale).max(0.0))
|
||||
.sqrt();
|
||||
// * 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 {
|
||||
// * calculate the movement velocity from slider end to current object
|
||||
let movement_velocity = curr.movement_dist / curr.movement_time;
|
||||
|
||||
aim_strain = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
|
||||
/ (TIMING_THRESHOLD).max(prev_strain_time)
|
||||
// * calculate the slider velocity from slider head to slider end.
|
||||
let travel_velocity = curr.travel_dist / curr.travel_time;
|
||||
|
||||
// * take the larger total combined velocity.
|
||||
curr_velocity = curr_velocity.max(movement_velocity + travel_velocity);
|
||||
}
|
||||
|
||||
// * As above, do the same for the previous hitobject.
|
||||
let mut prev_velocity = prev.jump_dist / prev.strain_time;
|
||||
|
||||
if prev_prev.is_slider {
|
||||
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);
|
||||
}
|
||||
|
||||
let mut wide_angle_bonus = 0.0;
|
||||
let mut acute_angle_bonus = 0.0;
|
||||
let mut slider_bonus = 0.0;
|
||||
let mut velocity_change_bonus = 0.0;
|
||||
|
||||
// * Start strain with regular velocity
|
||||
let mut aim_strain = curr_velocity;
|
||||
|
||||
// * If rhythms are the same.
|
||||
if curr.strain_time.max(prev.strain_time)
|
||||
< 1.25 * curr.strain_time.min(prev.strain_time)
|
||||
{
|
||||
if let (Some(curr_angle), Some(prev_angle), Some(prev_prev_angle)) =
|
||||
(curr.angle, prev.angle, prev_prev.angle)
|
||||
{
|
||||
// * Rewarding angles, take the smaller velocity as base.
|
||||
let angle_bonus = curr_velocity.min(prev_velocity);
|
||||
|
||||
wide_angle_bonus = calculate_wide_angle_bonus(curr_angle);
|
||||
|
||||
// * Only bufff delta_time exceeding 300 bpm 1/2.
|
||||
if curr.strain_time <= 100.0 {
|
||||
let curr_bonus = calculate_acute_angle_bonus(curr_angle);
|
||||
|
||||
// * Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
let prev_bonus = calculate_acute_angle_bonus(prev_angle);
|
||||
|
||||
// * The maximum velocity we buff is equal to 125 / strainTime
|
||||
let angle_bonus = angle_bonus.min(125.0 / curr.strain_time);
|
||||
|
||||
// * scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
let base1 =
|
||||
(FRAC_PI_2 * ((100.0 - curr.strain_time) / 25.0).min(1.0)).sin();
|
||||
|
||||
// * Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||
let base2 = (FRAC_PI_2 * (curr.jump_dist.clamp(50.0, 100.0) - 50.0)
|
||||
/ 50.0)
|
||||
.sin();
|
||||
|
||||
acute_angle_bonus = curr_bonus
|
||||
* prev_bonus
|
||||
* angle_bonus
|
||||
* base1
|
||||
* base1
|
||||
* base2
|
||||
* base2
|
||||
}
|
||||
|
||||
// * Penalize wide angles if they're repeated,
|
||||
// * reducing the penalty as the lastAngle gets more acute.
|
||||
let base = calculate_wide_angle_bonus(prev_angle);
|
||||
wide_angle_bonus *=
|
||||
angle_bonus * (1.0 - wide_angle_bonus.min(base * base * base));
|
||||
|
||||
// * Penalize acute angles if they're repeated,
|
||||
// * reducing the penalty as the lastLastAngle gets more obtuse.
|
||||
let base = calculate_acute_angle_bonus(prev_prev_angle);
|
||||
acute_angle_bonus *=
|
||||
0.5 + 0.5 * (1.0 - acute_angle_bonus.min(base * base * base));
|
||||
}
|
||||
}
|
||||
|
||||
let jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
|
||||
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist);
|
||||
if prev_velocity.max(curr_velocity).abs() > f64::EPSILON {
|
||||
// * We want to use the average velocity over the whole object when
|
||||
// * awarding differences, not the individual jump and slider path velocities.
|
||||
prev_velocity = (prev.jump_dist + prev.travel_dist) / prev.strain_time;
|
||||
curr_velocity = (curr.jump_dist + curr.travel_dist) / curr.strain_time;
|
||||
|
||||
let dist_exp =
|
||||
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
|
||||
let velocity_diff = (prev_velocity - curr_velocity).abs();
|
||||
|
||||
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
|
||||
.max(dist_exp / curr.strain_time)
|
||||
// * Scale with ratio of difference compared to 0.5 * max dist.
|
||||
let base = (FRAC_PI_2 * velocity_diff / prev_velocity.max(curr_velocity)).sin();
|
||||
let dist_ratio = base * base;
|
||||
|
||||
// * Reward for % distance up to 125 / strainTime
|
||||
// * for overlaps where velocity is still changing.
|
||||
let overlap_velocity_buff =
|
||||
velocity_diff.min(125.0 / curr.strain_time.min(prev.strain_time));
|
||||
|
||||
// * Reward for % distance slowed down compared to previous,
|
||||
// * paying attention to not award overlap
|
||||
let base =
|
||||
(FRAC_PI_2 * (curr.jump_dist.min(prev.jump_dist) / 100.0).min(1.0)).sin();
|
||||
let non_overlap_velocity_buff = velocity_diff * base * base;
|
||||
|
||||
// * Choose the largest bonus, multiplied by ratio.
|
||||
velocity_change_bonus =
|
||||
overlap_velocity_buff.max(non_overlap_velocity_buff) * dist_ratio;
|
||||
|
||||
// * Penalize for rhythm changes.
|
||||
let base = curr.strain_time.min(prev.strain_time)
|
||||
/ curr.strain_time.max(prev.strain_time);
|
||||
velocity_change_bonus *= base * base;
|
||||
}
|
||||
|
||||
if curr.travel_time.abs() > f64::EPSILON {
|
||||
// * Reward sliders based on velocity
|
||||
slider_bonus = curr.travel_dist / curr.travel_time;
|
||||
}
|
||||
|
||||
// * Add in acute angle bonus or wide angle bonus + velocity change bonus,
|
||||
// * whichever is larger
|
||||
aim_strain += (acute_angle_bonus * AIM_ACUTE_ANGLE_MULTIPLIER).max(
|
||||
wide_angle_bonus * AIM_WIDE_ANGLE_MULTIPLIER
|
||||
+ velocity_change_bonus * AIM_VELOCITY_CHANGE_MULTIPLIER,
|
||||
);
|
||||
|
||||
// * Add in additional slider velocity bonus.
|
||||
aim_strain += slider_bonus * AIM_SLIDER_MULTIPLIER;
|
||||
|
||||
aim_strain
|
||||
}
|
||||
Self::Flashlight {
|
||||
history,
|
||||
@@ -164,7 +322,7 @@ impl SkillKind {
|
||||
if let Some(prev) = history.next() {
|
||||
// Handle first entry distinctly for slight optimization
|
||||
if !prev.is_spinner {
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length() as f64;
|
||||
cumulative_strain_time += prev.strain_time;
|
||||
|
||||
// * We want to nerf objects that can be easily seen within the Flashlight circle radius
|
||||
@@ -180,7 +338,7 @@ impl SkillKind {
|
||||
|
||||
for (factor, prev) in factors.zip(history) {
|
||||
if !prev.is_spinner {
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length() as f64;
|
||||
cumulative_strain_time += prev.strain_time;
|
||||
|
||||
// * We also want to nerf stacks so that only the first object of the stack is accounted for
|
||||
@@ -240,9 +398,9 @@ impl SkillKind {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
||||
pub(crate) fn difficulty_values(&self) -> (usize, f64) {
|
||||
match self {
|
||||
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
||||
Self::Aim { .. } => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
||||
Self::Flashlight { .. } => (
|
||||
FLASHLIGHT_REDUCED_SECTION_COUNT,
|
||||
FLASHLIGHT_DIFFICULTY_MULTIPLIER,
|
||||
@@ -252,34 +410,34 @@ impl SkillKind {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn skill_multiplier(&self) -> f32 {
|
||||
pub(crate) fn skill_multiplier(&self) -> f64 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
||||
SkillKind::Aim { .. } => AIM_SKILL_MULTIPLIER,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn strain_decay_base(&self) -> f32 {
|
||||
pub(crate) fn strain_decay_base(&self) -> f64 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Aim { .. } => AIM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn decay_weight(&self) -> f32 {
|
||||
pub(crate) fn decay_weight(&self) -> f64 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_DECAY_WEIGHT,
|
||||
SkillKind::Aim { .. } => AIM_DECAY_WEIGHT,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
||||
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn strain_decay(&self, ms: f32) -> f32 {
|
||||
pub(crate) fn strain_decay(&self, ms: f64) -> f64 {
|
||||
self.strain_decay_base().powf(ms / 1000.0)
|
||||
}
|
||||
}
|
||||
@@ -287,8 +445,8 @@ impl SkillKind {
|
||||
pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
current: &DifficultyObject<'_>,
|
||||
history: &VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
) -> f32 {
|
||||
hit_window: f64,
|
||||
) -> f64 {
|
||||
if current.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
@@ -298,7 +456,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
let mut island_size = 1;
|
||||
let mut first_delta_switch = false;
|
||||
let adjusted_hit_window = hit_window * 0.6;
|
||||
let history_len = history.len() as f32;
|
||||
let history_len = history.len() as f64;
|
||||
|
||||
// * Store the ratio of the current start of an island to buff for tighter rhythms
|
||||
let mut start_ratio = 0.0;
|
||||
@@ -312,9 +470,9 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
|
||||
/ SPEED_HISTORY_TIME_MAX;
|
||||
|
||||
if curr_historical_decay.abs() > f32::EPSILON {
|
||||
if curr_historical_decay.abs() > f64::EPSILON {
|
||||
// * Either we're limited by time or limited by object count
|
||||
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
|
||||
curr_historical_decay = curr_historical_decay.min(i as f64 / history_len);
|
||||
|
||||
let curr_delta = curr.strain_time;
|
||||
let prev_delta = prev.strain_time;
|
||||
@@ -362,8 +520,8 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
|
||||
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
||||
* curr_historical_decay
|
||||
* ((4 + island_size) as f32).sqrt()
|
||||
* ((4 + prev_island_size) as f32).sqrt()
|
||||
* ((4 + island_size) as f64).sqrt()
|
||||
* ((4 + prev_island_size) as f64).sqrt()
|
||||
/ 4.0;
|
||||
|
||||
start_ratio = effective_ratio;
|
||||
@@ -390,7 +548,12 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn apply_diminishing_exp(val: f32) -> f32 {
|
||||
val.powf(0.99)
|
||||
fn calculate_wide_angle_bonus(angle: f64) -> f64 {
|
||||
let base = (3.0 / 4.0 * ((PI / 6.0).max(angle).min(5.0 / 6.0 * PI) - PI / 6.0)).sin();
|
||||
|
||||
base * base
|
||||
}
|
||||
|
||||
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
|
||||
1.0 - calculate_wide_angle_bonus(angle)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::{Beatmap, ControlPoint, ControlPointIter};
|
||||
pub(crate) struct SliderState<'p> {
|
||||
control_points: ControlPointIter<'p>,
|
||||
next: Option<ControlPoint>,
|
||||
pub(crate) beat_len: f32,
|
||||
pub(crate) slider_velocity: f32,
|
||||
pub(crate) beat_len: f64,
|
||||
pub(crate) slider_velocity: f64,
|
||||
}
|
||||
|
||||
impl<'p> SliderState<'p> {
|
||||
@@ -29,7 +29,7 @@ impl<'p> SliderState<'p> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn update(&mut self, time: f32) {
|
||||
pub(crate) fn update(&mut self, time: f64) {
|
||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||
match next {
|
||||
ControlPoint::Timing { beat_len, .. } => {
|
||||
@@ -88,14 +88,14 @@ mod test {
|
||||
let mut state = SliderState::new(&map);
|
||||
|
||||
state.update(2.0);
|
||||
assert!((state.beat_len - 10.0).abs() <= f32::EPSILON);
|
||||
assert!((state.beat_len - 10.0).abs() <= f64::EPSILON);
|
||||
|
||||
state.update(3.0);
|
||||
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON);
|
||||
assert!((state.beat_len - 20.0).abs() <= f64::EPSILON);
|
||||
assert!((state.slider_velocity - 1.0).abs() <= f64::EPSILON);
|
||||
|
||||
state.update(5.0);
|
||||
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON);
|
||||
assert!((state.beat_len - 30.0).abs() <= f64::EPSILON);
|
||||
assert!((state.slider_velocity - 45.0).abs() <= f64::EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-15
@@ -3,27 +3,27 @@ use crate::Mods;
|
||||
/// Summary struct for a [`Beatmap`](crate::Beatmap)'s attributes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeatmapAttributes {
|
||||
pub ar: f32,
|
||||
pub od: f32,
|
||||
pub cs: f32,
|
||||
pub hp: f32,
|
||||
pub clock_rate: f32,
|
||||
pub ar: f64,
|
||||
pub od: f64,
|
||||
pub cs: f64,
|
||||
pub hp: f64,
|
||||
pub clock_rate: f64,
|
||||
}
|
||||
|
||||
impl BeatmapAttributes {
|
||||
const AR0_MS: f32 = 1800.0;
|
||||
const AR5_MS: f32 = 1200.0;
|
||||
const AR10_MS: f32 = 450.0;
|
||||
const AR_MS_STEP_1: f32 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
||||
const AR_MS_STEP_2: f32 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
||||
const AR0_MS: f64 = 1800.0;
|
||||
const AR5_MS: f64 = 1200.0;
|
||||
const AR10_MS: f64 = 450.0;
|
||||
const AR_MS_STEP_1: f64 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
||||
const AR_MS_STEP_2: f64 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
|
||||
Self {
|
||||
ar,
|
||||
od,
|
||||
cs,
|
||||
hp,
|
||||
ar: ar as f64,
|
||||
od: od as f64,
|
||||
cs: cs as f64,
|
||||
hp: hp as f64,
|
||||
clock_rate: 1.0,
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ impl BeatmapAttributes {
|
||||
let multiplier = mods.od_ar_hp_multiplier();
|
||||
|
||||
// AR
|
||||
let mut ar = self.ar * multiplier;
|
||||
let mut ar = (self.ar * multiplier) as f64;
|
||||
let mut ar_ms = if ar <= 5.0 {
|
||||
Self::AR0_MS - Self::AR_MS_STEP_1 * ar
|
||||
} else {
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::cmp::Ordering;
|
||||
/// New rhythm speed change.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct TimingPoint {
|
||||
pub beat_len: f32,
|
||||
pub time: f32,
|
||||
pub beat_len: f64,
|
||||
pub time: f64,
|
||||
}
|
||||
|
||||
impl PartialOrd for TimingPoint {
|
||||
@@ -16,8 +16,8 @@ impl PartialOrd for TimingPoint {
|
||||
/// [`TimingPoint`](crate::parse::TimingPoint) that depends on a previous one.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct DifficultyPoint {
|
||||
pub time: f32,
|
||||
pub speed_multiplier: f32,
|
||||
pub time: f64,
|
||||
pub speed_multiplier: f64,
|
||||
}
|
||||
|
||||
impl PartialOrd for DifficultyPoint {
|
||||
|
||||
@@ -7,14 +7,14 @@ use std::cmp::Ordering;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HitObject {
|
||||
pub pos: Pos2,
|
||||
pub start_time: f32,
|
||||
pub start_time: f64,
|
||||
pub kind: HitObjectKind,
|
||||
pub sound: u8,
|
||||
}
|
||||
|
||||
impl HitObject {
|
||||
#[inline]
|
||||
pub fn end_time(&self) -> f32 {
|
||||
pub fn end_time(&self) -> f64 {
|
||||
match &self.kind {
|
||||
HitObjectKind::Circle { .. } => self.start_time,
|
||||
// incorrect, only called in mania which has no sliders though
|
||||
@@ -52,20 +52,20 @@ pub enum HitObjectKind {
|
||||
Circle,
|
||||
#[cfg(feature = "sliders")]
|
||||
Slider {
|
||||
pixel_len: f32,
|
||||
pixel_len: f64,
|
||||
repeats: usize,
|
||||
control_points: Vec<super::PathControlPoint>,
|
||||
},
|
||||
#[cfg(not(feature = "sliders"))]
|
||||
Slider {
|
||||
pixel_len: f32,
|
||||
pixel_len: f64,
|
||||
span_count: usize,
|
||||
last_control_point: Pos2,
|
||||
},
|
||||
Spinner {
|
||||
end_time: f32,
|
||||
end_time: f64,
|
||||
},
|
||||
Hold {
|
||||
end_time: f32,
|
||||
end_time: f64,
|
||||
},
|
||||
}
|
||||
|
||||
+9
-9
@@ -42,11 +42,11 @@ impl<T> OptionExt<T> for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
trait F32Ext: Sized {
|
||||
trait FloatExt: Sized {
|
||||
fn validate(self) -> Result<Self, ParseError>;
|
||||
}
|
||||
|
||||
impl F32Ext for f32 {
|
||||
impl FloatExt for f64 {
|
||||
fn validate(self) -> Result<Self, ParseError> {
|
||||
self.is_finite()
|
||||
.then(|| self)
|
||||
@@ -312,10 +312,10 @@ macro_rules! parse_timingpoints_body {
|
||||
.next()
|
||||
.next_field("timing point time")?
|
||||
.trim()
|
||||
.parse::<f32>()?
|
||||
.parse::<f64>()?
|
||||
.validate()?;
|
||||
|
||||
let beat_len: f32 = split.next().next_field("beat len")?.trim().parse()?;
|
||||
let beat_len: f64 = split.next().next_field("beat len")?.trim().parse()?;
|
||||
|
||||
if beat_len < 0.0 {
|
||||
let point = DifficultyPoint {
|
||||
@@ -429,7 +429,7 @@ macro_rules! parse_hitobjects_body {
|
||||
.next()
|
||||
.next_field("hitobject time")?
|
||||
.trim()
|
||||
.parse::<f32>()?
|
||||
.parse::<f64>()?
|
||||
.validate()?;
|
||||
|
||||
if !$self.hit_objects.is_empty() && time < prev_time {
|
||||
@@ -519,7 +519,7 @@ macro_rules! parse_hitobjects_body {
|
||||
let pixel_len = split
|
||||
.next()
|
||||
.next_field("pixel len")?
|
||||
.parse::<f32>()?
|
||||
.parse::<f64>()?
|
||||
.max(0.0)
|
||||
.min(MAX_COORDINATE_VALUE);
|
||||
|
||||
@@ -733,8 +733,8 @@ pub struct Beatmap {
|
||||
pub od: f32,
|
||||
pub cs: f32,
|
||||
pub hp: f32,
|
||||
pub slider_mult: f32,
|
||||
pub tick_rate: f32,
|
||||
pub slider_mult: f64,
|
||||
pub tick_rate: f64,
|
||||
pub hit_objects: Vec<HitObject>,
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
@@ -769,7 +769,7 @@ mod osu_fruits {
|
||||
|
||||
use super::Pos2;
|
||||
|
||||
pub(super) const MAX_COORDINATE_VALUE: f32 = 131_072.0;
|
||||
pub(super) const MAX_COORDINATE_VALUE: f64 = 131_072.0;
|
||||
|
||||
pub(super) fn convert_points(
|
||||
points: &[&str],
|
||||
|
||||
@@ -115,7 +115,7 @@ impl<'m> AnyPP<'m> {
|
||||
/// Irrelevant for osu!mania.
|
||||
#[allow(unused_variables)]
|
||||
#[inline]
|
||||
pub fn accuracy(self, acc: f32) -> Self {
|
||||
pub fn accuracy(self, acc: f64) -> Self {
|
||||
match self {
|
||||
#[cfg(feature = "fruits")]
|
||||
Self::Fruits(f) => Self::Fruits(f.accuracy(acc)),
|
||||
|
||||
@@ -6,9 +6,9 @@ pub(crate) struct DifficultyObject<'o> {
|
||||
pub(crate) idx: usize,
|
||||
pub(crate) base: &'o HitObject,
|
||||
pub(crate) prev: &'o HitObject,
|
||||
pub(crate) delta: f32,
|
||||
pub(crate) delta: f64,
|
||||
pub(crate) rhythm: &'static HitObjectRhythm,
|
||||
pub(crate) start_time: f32,
|
||||
pub(crate) start_time: f64,
|
||||
}
|
||||
|
||||
impl<'o> DifficultyObject<'o> {
|
||||
@@ -18,7 +18,7 @@ impl<'o> DifficultyObject<'o> {
|
||||
base: &'o HitObject,
|
||||
prev: &'o HitObject,
|
||||
prev_prev: &HitObject,
|
||||
clock_rate: f32,
|
||||
clock_rate: f64,
|
||||
) -> Self {
|
||||
let delta = (base.start_time - prev.start_time) / clock_rate;
|
||||
let rhythm = closest_rhythm(delta, prev, prev_prev, clock_rate);
|
||||
|
||||
@@ -53,8 +53,8 @@ static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct HitObjectRhythm {
|
||||
id: u8,
|
||||
ratio: f32,
|
||||
pub(crate) difficulty: f32,
|
||||
ratio: f64,
|
||||
pub(crate) difficulty: f64,
|
||||
}
|
||||
|
||||
impl PartialEq for HitObjectRhythm {
|
||||
@@ -68,10 +68,10 @@ impl Eq for HitObjectRhythm {}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn closest_rhythm(
|
||||
delta_time: f32,
|
||||
delta_time: f64,
|
||||
last: &HitObject,
|
||||
last_last: &HitObject,
|
||||
clock_rate: f32,
|
||||
clock_rate: f64,
|
||||
) -> &'static HitObjectRhythm {
|
||||
let prev_len = (last.start_time - last_last.start_time) / clock_rate;
|
||||
let ratio = delta_time / prev_len;
|
||||
|
||||
+15
-15
@@ -21,13 +21,13 @@ use stamina_cheese::StaminaCheeseDetector;
|
||||
use crate::{Beatmap, Mods, Strains};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::f32::consts::PI;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const SECTION_LEN: f64 = 400.0;
|
||||
|
||||
const COLOR_SKILL_MULTIPLIER: f32 = 0.01;
|
||||
const RHYTHM_SKILL_MULTIPLIER: f32 = 0.014;
|
||||
const STAMINA_SKILL_MULTIPLIER: f32 = 0.02;
|
||||
const COLOR_SKILL_MULTIPLIER: f64 = 0.01;
|
||||
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
|
||||
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
||||
|
||||
/// Star calculation for osu!taiko maps.
|
||||
///
|
||||
@@ -209,7 +209,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rescale(stars: f32) -> f32 {
|
||||
fn rescale(stars: f64) -> f64 {
|
||||
if stars < 0.0 {
|
||||
stars
|
||||
} else {
|
||||
@@ -218,7 +218,7 @@ fn rescale(stars: f32) -> f32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn simple_color_penalty(stamina: f32, color: f32) -> f32 {
|
||||
fn simple_color_penalty(stamina: f64, color: f64) -> f64 {
|
||||
if color <= 0.0 {
|
||||
0.79 - 0.25
|
||||
} else {
|
||||
@@ -226,7 +226,7 @@ fn simple_color_penalty(stamina: f32, color: f32) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f32) -> f32 {
|
||||
fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f64) -> f64 {
|
||||
let mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
|
||||
|
||||
let iter = skills[0]
|
||||
@@ -259,7 +259,7 @@ fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f32) -> f32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
|
||||
fn norm(p: f64, a: f64, b: f64, c: f64) -> f64 {
|
||||
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
|
||||
}
|
||||
|
||||
@@ -267,28 +267,28 @@ fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
|
||||
/// This data is necessary to calculate PP.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct DifficultyAttributes {
|
||||
pub stars: f32,
|
||||
pub stars: f64,
|
||||
}
|
||||
|
||||
/// Various data created through the pp calculation.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct PerformanceAttributes {
|
||||
pub attributes: DifficultyAttributes,
|
||||
pub pp: f32,
|
||||
pub pp_acc: f32,
|
||||
pub pp_strain: f32,
|
||||
pub pp: f64,
|
||||
pub pp_acc: f64,
|
||||
pub pp_strain: f64,
|
||||
}
|
||||
|
||||
impl PerformanceAttributes {
|
||||
/// Return the star value.
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f32 {
|
||||
pub fn stars(&self) -> f64 {
|
||||
self.attributes.stars
|
||||
}
|
||||
|
||||
/// Return the performance point value.
|
||||
#[inline]
|
||||
pub fn pp(&self) -> f32 {
|
||||
pub fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
}
|
||||
|
||||
+22
-26
@@ -32,11 +32,11 @@ use crate::{Beatmap, Mods, PpResult, StarResult};
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct TaikoPP<'m> {
|
||||
map: &'m Beatmap,
|
||||
stars: Option<f32>,
|
||||
stars: Option<f64>,
|
||||
mods: u32,
|
||||
max_combo: usize,
|
||||
combo: Option<usize>,
|
||||
acc: f32,
|
||||
acc: f64,
|
||||
n_misses: usize,
|
||||
passed_objects: Option<usize>,
|
||||
|
||||
@@ -69,7 +69,7 @@ impl<'m> TaikoPP<'m> {
|
||||
#[inline]
|
||||
pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self {
|
||||
if let Some(stars) = attributes.attributes() {
|
||||
self.stars.replace(stars);
|
||||
self.stars = Some(stars);
|
||||
}
|
||||
|
||||
self
|
||||
@@ -119,7 +119,7 @@ impl<'m> TaikoPP<'m> {
|
||||
|
||||
/// Set the accuracy between 0.0 and 100.0.
|
||||
#[inline]
|
||||
pub fn accuracy(mut self, acc: f32) -> Self {
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
self.acc = acc / 100.0;
|
||||
self.n300.take();
|
||||
self.n100.take();
|
||||
@@ -158,7 +158,7 @@ impl<'m> TaikoPP<'m> {
|
||||
(None, None) => unreachable!(),
|
||||
};
|
||||
|
||||
self.acc = (2 * n300 + n100) as f32 / (2 * (n300 + n100 + misses)) as f32;
|
||||
self.acc = (2 * n300 + n100) as f64 / (2 * (n300 + n100 + misses)) as f64;
|
||||
}
|
||||
|
||||
let inner = TaikoPPInner {
|
||||
@@ -176,10 +176,10 @@ impl<'m> TaikoPP<'m> {
|
||||
|
||||
struct TaikoPPInner<'m> {
|
||||
map: &'m Beatmap,
|
||||
stars: f32,
|
||||
stars: f64,
|
||||
mods: u32,
|
||||
max_combo: usize,
|
||||
acc: f32,
|
||||
acc: f64,
|
||||
n_misses: usize,
|
||||
}
|
||||
|
||||
@@ -208,16 +208,16 @@ impl<'m> TaikoPPInner<'m> {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_strain_value(&self, stars: f32) -> f32 {
|
||||
fn compute_strain_value(&self, stars: f64) -> f64 {
|
||||
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
|
||||
let mut strain = exp_base * exp_base / 100_000.0;
|
||||
|
||||
// Longer maps are worth more
|
||||
let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
|
||||
let len_bonus = 1.0 + 0.1 * (self.max_combo as f64 / 1500.0).min(1.0);
|
||||
strain *= len_bonus;
|
||||
|
||||
// Penalize misses exponentially
|
||||
strain *= 0.985_f32.powi(self.n_misses as i32);
|
||||
strain *= 0.985_f64.powi(self.n_misses as i32);
|
||||
|
||||
// HD bonus
|
||||
if self.mods.hd() {
|
||||
@@ -234,8 +234,8 @@ impl<'m> TaikoPPInner<'m> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn compute_accuracy_value(&self) -> f32 {
|
||||
let mut od = self.map.od;
|
||||
fn compute_accuracy_value(&self) -> f64 {
|
||||
let mut od = self.map.od as f64;
|
||||
|
||||
if self.mods.hr() {
|
||||
od *= 1.4;
|
||||
@@ -248,47 +248,43 @@ impl<'m> TaikoPPInner<'m> {
|
||||
(150.0 / hit_window).powf(1.1)
|
||||
* self.acc.powi(15)
|
||||
* 22.0
|
||||
* (self.max_combo as f32 / 1500.0).powf(0.3).min(1.15)
|
||||
* (self.max_combo as f64 / 1500.0).powf(0.3).min(1.15)
|
||||
}
|
||||
}
|
||||
|
||||
const HITWINDOW_MIN: f32 = 50.0;
|
||||
const HITWINDOW_AVG: f32 = 35.0;
|
||||
const HITWINDOW_MAX: f32 = 20.0;
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
crate::difficulty_range(od, HITWINDOW_MAX, HITWINDOW_AVG, HITWINDOW_MIN)
|
||||
fn difficulty_range_od(od: f64) -> f64 {
|
||||
crate::difficulty_range(od, 20.0, 35.0, 50.0)
|
||||
}
|
||||
|
||||
pub trait TaikoAttributeProvider {
|
||||
fn attributes(self) -> Option<f32>;
|
||||
fn attributes(self) -> Option<f64>;
|
||||
}
|
||||
|
||||
impl TaikoAttributeProvider for f32 {
|
||||
impl TaikoAttributeProvider for f64 {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl TaikoAttributeProvider for DifficultyAttributes {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
Some(self.stars)
|
||||
}
|
||||
}
|
||||
|
||||
impl TaikoAttributeProvider for PerformanceAttributes {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
Some(self.attributes.stars)
|
||||
}
|
||||
}
|
||||
|
||||
impl TaikoAttributeProvider for StarResult {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Taiko(attributes) = self {
|
||||
Some(attributes.stars)
|
||||
@@ -300,7 +296,7 @@ impl TaikoAttributeProvider for StarResult {
|
||||
|
||||
impl TaikoAttributeProvider for PpResult {
|
||||
#[inline]
|
||||
fn attributes(self) -> Option<f32> {
|
||||
fn attributes(self) -> Option<f64> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Taiko(attributes) = self {
|
||||
Some(attributes.attributes.stars)
|
||||
|
||||
+17
-17
@@ -2,25 +2,25 @@ use super::{DifficultyObject, SkillKind};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const DECAY_WEIGHT: f32 = 0.9;
|
||||
const DECAY_WEIGHT: f64 = 0.9;
|
||||
|
||||
const COLOR_SKILL_MULTIPLIER: f32 = 1.0;
|
||||
const COLOR_STRAIN_DECAY_BASE: f32 = 0.4;
|
||||
const COLOR_SKILL_MULTIPLIER: f64 = 1.0;
|
||||
const COLOR_STRAIN_DECAY_BASE: f64 = 0.4;
|
||||
|
||||
const RHYTHM_SKILL_MULTIPLIER: f32 = 10.0;
|
||||
const RHYTHM_STRAIN_DECAY_BASE: f32 = 0.0;
|
||||
const RHYTHM_SKILL_MULTIPLIER: f64 = 10.0;
|
||||
const RHYTHM_STRAIN_DECAY_BASE: f64 = 0.0;
|
||||
|
||||
const STAMINA_SKILL_MULTIPLIER: f32 = 1.0;
|
||||
const STAMINA_STRAIN_DECAY_BASE: f32 = 0.4;
|
||||
const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
|
||||
const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
pub current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
pub current_strain: f64,
|
||||
current_section_peak: f64,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
prev_time: Option<f64>,
|
||||
}
|
||||
|
||||
impl Skill {
|
||||
@@ -43,7 +43,7 @@ impl Skill {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl Skill {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn difficulty_value(&self, buf: &mut [f32]) -> f32 {
|
||||
pub(crate) fn difficulty_value(&self, buf: &mut [f64]) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Skill {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn skill_multiplier(&self) -> f32 {
|
||||
fn skill_multiplier(&self) -> f64 {
|
||||
match self.kind {
|
||||
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
|
||||
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
|
||||
@@ -81,7 +81,7 @@ impl Skill {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay_base(&self) -> f32 {
|
||||
fn strain_decay_base(&self) -> f64 {
|
||||
match self.kind {
|
||||
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
|
||||
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
|
||||
@@ -90,12 +90,12 @@ impl Skill {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||
fn peak_strain(&self, delta_time: f64) -> f64 {
|
||||
self.current_strain * self.strain_decay(delta_time)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay(&self, ms: f32) -> f32 {
|
||||
fn strain_decay(&self, ms: f64) -> f64 {
|
||||
self.strain_decay_base().powf(ms / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-14
@@ -2,7 +2,7 @@ use super::{DifficultyObject, HitObjectRhythm, LimitedQueue, Rim};
|
||||
|
||||
use std::ops::Index;
|
||||
|
||||
const RHYTHM_STRAIN_DECAY: f32 = 0.96;
|
||||
const RHYTHM_STRAIN_DECAY: f64 = 0.96;
|
||||
const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
|
||||
|
||||
const MONO_HISTORY_MAX_LEN: usize = 5;
|
||||
@@ -18,12 +18,12 @@ pub(crate) enum SkillKind {
|
||||
Rhythm {
|
||||
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
|
||||
notes_since_rhythm_change: usize,
|
||||
current_strain: f32,
|
||||
current_strain: f64,
|
||||
},
|
||||
Stamina {
|
||||
note_pair_duration_history: LimitedQueue<f32>,
|
||||
note_pair_duration_history: LimitedQueue<f64>,
|
||||
hand: u8,
|
||||
off_hand_object_duration: f32,
|
||||
off_hand_object_duration: f64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl SkillKind {
|
||||
Self::Stamina {
|
||||
note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN),
|
||||
hand: right_hand as u8,
|
||||
off_hand_object_duration: f32::MAX,
|
||||
off_hand_object_duration: f64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ impl SkillKind {
|
||||
&mut self,
|
||||
current: &DifficultyObject<'_>,
|
||||
cheese: &[bool],
|
||||
) -> f32 {
|
||||
) -> f64 {
|
||||
match self {
|
||||
Self::Color {
|
||||
mono_history,
|
||||
@@ -157,7 +157,7 @@ impl SkillKind {
|
||||
*current_strain *= RHYTHM_STRAIN_DECAY;
|
||||
*notes_since_rhythm_change += 1;
|
||||
|
||||
if current.rhythm.difficulty.abs() <= f32::EPSILON {
|
||||
if current.rhythm.difficulty.abs() <= f64::EPSILON {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@@ -250,15 +250,16 @@ impl SkillKind {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn pattern_len_penalty(pattern_len: usize) -> f32 {
|
||||
let short_pattern_penalty = (0.15 * pattern_len as f32).min(1.0);
|
||||
let long_pattern_penalty = (2.5 - 0.15 * pattern_len as f32).max(0.0).min(1.0);
|
||||
fn pattern_len_penalty(pattern_len: usize) -> f64 {
|
||||
let pattern_len = pattern_len as f64;
|
||||
let short_pattern_penalty = (0.15 * pattern_len).min(1.0);
|
||||
let long_pattern_penalty = (2.5 - 0.15 * pattern_len).max(0.0).min(1.0);
|
||||
|
||||
short_pattern_penalty.min(long_pattern_penalty)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn cheese_penalty(note_pair_duration: f32) -> f32 {
|
||||
fn cheese_penalty(note_pair_duration: f64) -> f64 {
|
||||
if note_pair_duration > 125.0 {
|
||||
1.0
|
||||
} else if note_pair_duration < 100.0 {
|
||||
@@ -269,7 +270,7 @@ fn cheese_penalty(note_pair_duration: f32) -> f32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn speed_bonus(note_pair_duration: f32) -> f32 {
|
||||
fn speed_bonus(note_pair_duration: f64) -> f64 {
|
||||
if note_pair_duration > 200.0 {
|
||||
return 0.0;
|
||||
}
|
||||
@@ -281,6 +282,6 @@ fn speed_bonus(note_pair_duration: f32) -> f32 {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn repetition_penalty(notes_since: usize) -> f32 {
|
||||
(0.032 * notes_since as f32).min(1.0)
|
||||
fn repetition_penalty(notes_since: usize) -> f64 {
|
||||
(0.032 * notes_since as f64).min(1.0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user