f32 -> f64 & osu_precise fixes + update

This commit is contained in:
MaxOhn
2021-11-12 02:44:40 +01:00
parent db474149d8
commit d703f55b3c
36 changed files with 1110 additions and 696 deletions
+2 -1
View File
@@ -7,8 +7,9 @@
Additionally, instead of importing through `rosu_pp::osu::{version}`, you now have to import through `rosu_pp::osu` 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 `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] 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` - 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 # v0.2.3
+9 -9
View File
@@ -229,22 +229,22 @@ impl Data {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct GenericData { struct GenericData {
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")] #[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")] #[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")] #[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")] #[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")] #[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")] #[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")] #[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")]
ar: Option<f32>, ar: Option<f64>,
#[serde(alias = "Mods")] #[serde(alias = "Mods")]
mods: String, mods: String,
#[serde(alias = "Stars")] #[serde(alias = "Stars")]
stars: f32, stars: f64,
pp: f32, pp: f64,
} }
+9 -9
View File
@@ -17,8 +17,8 @@ pub(crate) struct ControlPointIter<'p> {
timing_points: Iter<'p, TimingPoint>, timing_points: Iter<'p, TimingPoint>,
difficulty_points: Iter<'p, DifficultyPoint>, difficulty_points: Iter<'p, DifficultyPoint>,
next_timing: Option<(f32, f32)>, next_timing: Option<(f64, f64)>,
next_difficulty: Option<(f32, f32)>, next_difficulty: Option<(f64, f64)>,
} }
impl<'p> ControlPointIter<'p> { impl<'p> ControlPointIter<'p> {
@@ -39,20 +39,20 @@ impl<'p> ControlPointIter<'p> {
pub(crate) enum ControlPoint { pub(crate) enum ControlPoint {
Timing { Timing {
time: f32, time: f64,
#[allow(dead_code)] // not used in `osu_fast` feature #[allow(dead_code)] // not used in `osu_fast` feature
beat_len: f32, beat_len: f64,
}, },
Difficulty { Difficulty {
time: f32, time: f64,
slider_velocity: f32, slider_velocity: f64,
}, },
} }
#[cfg(any(feature = "osu_precise", feature = "fruits"))] #[cfg(any(feature = "osu_precise", feature = "fruits"))]
impl ControlPoint { impl ControlPoint {
#[inline] #[inline]
pub(crate) fn time(&self) -> f32 { pub(crate) fn time(&self) -> f64 {
match self { match self {
Self::Timing { time, .. } => *time, Self::Timing { time, .. } => *time,
Self::Difficulty { time, .. } => *time, Self::Difficulty { time, .. } => *time,
@@ -70,13 +70,13 @@ impl<'p> Iterator for ControlPointIter<'p> {
Some(ControlPoint::Timing { time, beat_len }) Some(ControlPoint::Timing { time, beat_len })
} }
(_, Some((time, speed_mult))) => { (_, Some((time, slider_velocity))) => {
self.next_difficulty = self.next_difficulty =
next_tuple!(self.difficulty_points, (time, speed_multiplier)); next_tuple!(self.difficulty_points, (time, speed_multiplier));
Some(ControlPoint::Difficulty { Some(ControlPoint::Difficulty {
time, time,
slider_velocity: speed_mult, slider_velocity,
}) })
} }
(Some((time, beat_len)), None) => { (Some((time, beat_len)), None) => {
+41 -41
View File
@@ -19,6 +19,7 @@ struct BezierBuffers {
buf1: Vec<Pos2>, buf1: Vec<Pos2>,
buf2: Vec<Pos2>, buf2: Vec<Pos2>,
buf3: Vec<Pos2>, buf3: Vec<Pos2>,
buf4: Vec<Pos2>,
} }
impl BezierBuffers { impl BezierBuffers {
@@ -38,26 +39,28 @@ impl BezierBuffers {
.extend(iter::repeat(Pos2::zero()).take(additional)); .extend(iter::repeat(Pos2::zero()).take(additional));
self.buf3 self.buf3
.extend(iter::repeat(Pos2::zero()).take(additional)); .extend(iter::repeat(Pos2::zero()).take(additional));
self.buf4
.extend(iter::repeat(Pos2::zero()).take(additional));
} }
} }
struct CircularArcProperties { struct CircularArcProperties {
theta_start: f32, theta_start: f64,
theta_range: f32, theta_range: f64,
direction: f32, direction: f64,
radius: f32, radius: f32,
centre: Pos2, centre: Pos2,
} }
pub(crate) struct Curve { pub(crate) struct Curve {
path: Vec<Pos2>, path: Vec<Pos2>,
lengths: Vec<f32>, lengths: Vec<f64>,
} }
impl Curve { impl Curve {
pub(crate) fn new( pub(crate) fn new(
points: &[PathControlPoint], points: &[PathControlPoint],
expected_len: f32, expected_len: f64,
bufs: &mut CurveBuffers, bufs: &mut CurveBuffers,
) -> Self { ) -> Self {
let mut path = Self::calculate_path(points, bufs); let mut path = Self::calculate_path(points, bufs);
@@ -66,28 +69,28 @@ impl Curve {
Self { path, lengths } 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 d = self.progress_to_dist(progress);
let i = self.idx_of_dist(d); let i = self.idx_of_dist(d);
self.interpolate_vertices(i, 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() 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) 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 self.lengths
.binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal)) .binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal))
.map_or_else(identity, identity) .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() { if self.path.is_empty() {
return Pos2::zero(); return Pos2::zero();
} }
@@ -107,13 +110,13 @@ impl Curve {
// * Avoid division by an almost-zero number in case // * Avoid division by an almost-zero number in case
// * two points are extremely close to each other // * two points are extremely close to each other
if (d0 - d1).abs() <= f32::EPSILON { if (d0 - d1).abs() <= f64::EPSILON {
return p0; return p0;
} }
let w = (d - d0) / (d1 - d0); 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> { fn calculate_path(points: &[PathControlPoint], bufs: &mut CurveBuffers) -> Vec<Pos2> {
@@ -152,18 +155,18 @@ impl Curve {
fn calculate_length( fn calculate_length(
points: &[PathControlPoint], points: &[PathControlPoint],
path: &mut Vec<Pos2>, path: &mut Vec<Pos2>,
expected_len: f32, expected_len: f64,
) -> Vec<f32> { ) -> Vec<f64> {
let mut calculated_len = 0.0; let mut calculated_len = 0.0;
let mut cumulative_len = vec![0.0]; let mut cumulative_len = vec![0.0];
for (&curr, &next) in path.iter().zip(path.iter().skip(1)) { for (&curr, &next) in path.iter().zip(path.iter().skip(1)) {
let diff = next - curr; let diff = next - curr;
calculated_len += diff.length(); calculated_len += diff.length() as f64;
cumulative_len.push(calculated_len); 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 // * In osu-stable, if the last two control points of a slider are equal, extension is not performed
let condition_opt = points let condition_opt = points
.len() .len()
@@ -207,7 +210,7 @@ impl Curve {
// * The direction of the segment to shorten or lengthen // * The direction of the segment to shorten or lengthen
let dir = (path[end_idx] - path[prev_idx]).normalize(); 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); cumulative_len.push(expected_len);
} }
@@ -286,18 +289,22 @@ impl Curve {
} else { } else {
let divisor = 2.0 * (1.0 - CIRCULAR_ARC_TOLERANCE / pr.radius).acos(); 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); 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 directed_range = pr.direction * pr.theta_range;
let subpath = (0..amount_points).map(|i| { 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 theta = pr.theta_start + fract * directed_range;
let (sin, cos) = theta.sin_cos(); 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 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> // * <a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a>
// * over the tree resulting from the subdivisions we make.) // * 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() { while let Some(mut parent) = to_flatten.pop() {
if Self::bezier_is_flat_enough(&parent) { if Self::bezier_is_flat_enough(&parent) {
@@ -345,13 +352,13 @@ impl Curve {
Self::bezier_subdivide( Self::bezier_subdivide(
&parent, &parent,
&mut left_child, &mut bufs.buf4,
right_child.to_mut(), right_child.to_mut(),
&mut bufs.buf1, &mut bufs.buf1,
); );
// * We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration. // * 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(right_child);
to_flatten.push(parent); to_flatten.push(parent);
@@ -395,6 +402,7 @@ impl Curve {
buf1: l, buf1: l,
buf2: r, buf2: r,
buf3: midpoints, buf3: midpoints,
..
} = bufs; } = bufs;
Self::bezier_subdivide(points, l, r, midpoints); Self::bezier_subdivide(points, l, r, midpoints);
@@ -403,22 +411,13 @@ impl Curve {
let l = &l[..count]; let l = &l[..count];
let r = &r[1..count]; let r = &r[1..count];
let left_iter = l let subpath = l
.iter() .iter()
.chain(r)
.skip(1) .skip(1)
.zip(l.iter().skip(2)) .zip(l.iter().chain(r).skip(2))
.zip(l.iter().skip(3)) .zip(l.iter().chain(r).skip(3))
.step_by(2); .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)
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25); .map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
path.extend(subpath); path.extend(subpath);
@@ -472,6 +471,7 @@ impl Curve {
return None; 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 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 a_sq = a.length_squared();
let b_sq = b.length_squared(); let b_sq = b.length_squared();
@@ -479,7 +479,7 @@ impl Curve {
let centre = Pos2 { let centre = Pos2 {
x: (a_sq * (b - c).y + b_sq * (c - a).y + c_sq * (a - b).y) / d, 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; let d_a = a - centre;
@@ -512,8 +512,8 @@ impl Curve {
} }
Some(CircularArcProperties { Some(CircularArcProperties {
theta_start, theta_start: theta_start as f64,
theta_range, theta_range: theta_range as f64,
direction, direction,
radius, radius,
centre, centre,
+10 -10
View File
@@ -1,12 +1,12 @@
use crate::parse::Pos2; use crate::parse::Pos2;
const PLAYFIELD_WIDTH: f32 = 512.0; const PLAYFIELD_WIDTH: f32 = 512.0;
const BASE_SPEED: f32 = 1.0; const BASE_SPEED: f64 = 1.0;
#[derive(Clone)] #[derive(Clone)]
pub struct CatchObject { pub struct CatchObject {
pub(crate) pos: f32, pub(crate) pos: f32,
pub(crate) time: f32, pub(crate) time: f64,
pub(crate) hyper_dash: bool, pub(crate) hyper_dash: bool,
pub(crate) hyper_dist: f32, pub(crate) hyper_dist: f32,
@@ -14,7 +14,7 @@ pub struct CatchObject {
impl CatchObject { impl CatchObject {
#[inline] #[inline]
pub(crate) fn new((pos, time): (Pos2, f32)) -> Self { pub(crate) fn new((pos, time): (Pos2, f64)) -> Self {
Self { Self {
pos: pos.x, pos: pos.x,
time, 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 mut offset_pos = self.pos;
let time_diff = self.time - *last_time; let time_diff = self.time - *last_time;
@@ -31,7 +31,7 @@ impl CatchObject {
let pos_diff = offset_pos - last_pos_ref; let pos_diff = offset_pos - last_pos_ref;
if pos_diff.abs() > f32::EPSILON { 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 pos_diff > 0.0 {
if offset_pos + pos_diff < PLAYFIELD_WIDTH { if offset_pos + pos_diff < PLAYFIELD_WIDTH {
offset_pos += pos_diff; offset_pos += pos_diff;
@@ -56,10 +56,10 @@ impl CatchObject {
pub(crate) fn init_hyper_dash( pub(crate) fn init_hyper_dash(
&mut self, &mut self,
half_catcher_width: f32, half_catcher_width: f64,
next: &CatchObject, next: &CatchObject,
last_direction: &mut i8, last_direction: &mut i8,
last_excess: &mut f32, last_excess: &mut f64,
) { ) {
let next_x = next.pos; let next_x = next.pos;
let curr_x = self.pos; let curr_x = self.pos;
@@ -73,15 +73,15 @@ impl CatchObject {
half_catcher_width half_catcher_width
}; };
let dist_to_next = (next_x - curr_x).abs() - sub; let dist_to_next = (next_x - curr_x).abs() as f64 - sub;
let hyper_dist = time_to_next * BASE_SPEED - dist_to_next; let hyper_dist = (time_to_next * BASE_SPEED - dist_to_next) as f32;
if hyper_dist < 0.0 { if hyper_dist < 0.0 {
self.hyper_dash = true; self.hyper_dash = true;
*last_excess = half_catcher_width; *last_excess = half_catcher_width;
} else { } else {
self.hyper_dist = hyper_dist; 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; *last_direction = this_direction;
+5 -5
View File
@@ -6,14 +6,14 @@ pub(crate) struct DifficultyObject<'o> {
pub(crate) base: &'o CatchObject, pub(crate) base: &'o CatchObject,
pub(crate) last: &'o CatchObject, pub(crate) last: &'o CatchObject,
pub(crate) delta: f32, pub(crate) delta: f64,
pub(crate) start_time: f32, pub(crate) start_time: f64,
pub(crate) normalized_pos: f32, pub(crate) normalized_pos: f32,
pub(crate) last_normalized_pos: f32, pub(crate) last_normalized_pos: f32,
pub(crate) strain_time: f32, pub(crate) strain_time: f64,
pub(crate) clock_rate: f32, pub(crate) clock_rate: f64,
} }
impl<'o> DifficultyObject<'o> { impl<'o> DifficultyObject<'o> {
@@ -22,7 +22,7 @@ impl<'o> DifficultyObject<'o> {
base: &'o CatchObject, base: &'o CatchObject,
last: &'o CatchObject, last: &'o CatchObject,
half_catcher_width: f32, half_catcher_width: f32,
clock_rate: f32, clock_rate: f64,
) -> Self { ) -> Self {
let delta = (base.time - last.time) / clock_rate; let delta = (base.time - last.time) / clock_rate;
let start_time = base.time / clock_rate; let start_time = base.time / clock_rate;
+33 -31
View File
@@ -18,14 +18,14 @@ use crate::{
Beatmap, Mods, Strains, Beatmap, Mods, Strains,
}; };
const SECTION_LENGTH: f32 = 750.0; const SECTION_LENGTH: f64 = 750.0;
const STAR_SCALING_FACTOR: f32 = 0.153; const STAR_SCALING_FACTOR: f64 = 0.153;
const ALLOWED_CATCH_RANGE: f32 = 0.8; const ALLOWED_CATCH_RANGE: f32 = 0.8;
const CATCHER_SIZE: f32 = 106.75; const CATCHER_SIZE: f32 = 106.75;
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0; const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
const BASE_SCORING_DISTANCE: f32 = 100.0; const BASE_SCORING_DISTANCE: f64 = 100.0;
/// Star calculation for osu!ctb maps /// Star calculation for osu!ctb maps
/// ///
@@ -79,7 +79,7 @@ pub fn stars(
// Responsible for timing point values // Responsible for timing point values
slider_state.update(h.start_time); 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; let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
@@ -143,15 +143,15 @@ pub fn stars(
slider_objects.extend(&ticks); slider_objects.extend(&ticks);
for span_idx in 1..=*repeats { 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 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 // Reverse tick
slider_objects.push((pos, h.start_time + time_offset)); slider_objects.push((pos, h.start_time + time_offset));
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| { 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 // Actual ticks
@@ -166,7 +166,7 @@ pub fn stars(
} }
// Slider tail // 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); let pos = h.pos + curve.position_at(progress);
slider_objects.push((pos, h.start_time + duration)); slider_objects.push((pos, h.start_time + duration));
@@ -185,12 +185,13 @@ pub fn stars(
.take(take); .take(take);
// Hyper dash business // 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_direction = 0;
let mut last_excess = half_catcher_width; let mut last_excess = half_catcher_width;
// Strain business // 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 section_len = SECTION_LENGTH * attributes.clock_rate;
let mut current_section_end = let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len; (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 // Responsible for timing point values
slider_state.update(h.start_time); 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; 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); slider_objects.extend(&ticks);
for span_idx in 1..=*repeats { 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 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 // Reverse tick
slider_objects.push((pos, h.start_time + time_offset)); slider_objects.push((pos, h.start_time + time_offset));
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| { 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 // Actual ticks
@@ -408,7 +409,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
} }
// Slider tail // 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); let pos = h.pos + curve.position_at(progress);
slider_objects.push((pos, h.start_time + duration)); slider_objects.push((pos, h.start_time + duration));
@@ -422,12 +423,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
.flatten(); .flatten();
// Hyper dash business // 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_direction = 0;
let mut last_excess = half_catcher_width; let mut last_excess = half_catcher_width;
// Strain business // 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 section_len = SECTION_LENGTH * attributes.clock_rate;
let mut current_section_end = let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len; (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, // BUG: Sometimes there are off-by-one errors,
// presumably caused by floating point inaccuracies // presumably caused by floating point inaccuracies
fn tiny_droplet_count( fn tiny_droplet_count(
start_time: f32, start_time: f64,
time_between_ticks: f32, time_between_ticks: f64,
duration: f32, duration: f64,
span_count: usize, span_count: usize,
ticks: &[(Pos2, f32)], ticks: &[(Pos2, f64)],
) -> usize { ) -> usize {
// tiny droplets preceeding a _tick_ // tiny droplets preceeding a _tick_
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 { 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_ // tiny droplets preceeding a _reverse_
let last = ticks.last().map_or(start_time, |(_, last)| *last); 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 since_last_tick = repeat_time - last;
let span_last_section = if since_last_tick > 80.0 { let span_last_section = if since_last_tick > 80.0 {
@@ -556,7 +558,7 @@ fn tiny_droplet_count(
// tiny droplets preceeding the slider tail // tiny droplets preceeding the slider tail
// necessary to handle distinctly because of the legacy last tick // necessary to handle distinctly because of the legacy last tick
let last = ticks.last().map_or(start_time, |(_, last)| *last); 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 since_last_tick = end_time - last;
let last_section = if since_last_tick > 80.0 { let last_section = if since_last_tick > 80.0 {
@@ -574,7 +576,7 @@ fn tiny_droplet_count(
} }
#[inline] #[inline]
fn shrink_down(mut val: f32) -> f32 { fn shrink_down(mut val: f64) -> f64 {
while val > 100.0 { while val > 100.0 {
val /= 2.0; val /= 2.0;
} }
@@ -583,7 +585,7 @@ fn shrink_down(mut val: f32) -> f32 {
} }
#[inline] #[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; let mut count = 0;
while start < end { while start < end {
@@ -631,9 +633,9 @@ impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
/// This data is necessary to calculate PP. /// This data is necessary to calculate PP.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct DifficultyAttributes { pub struct DifficultyAttributes {
pub stars: f32, pub stars: f64,
pub max_combo: usize, pub max_combo: usize,
pub ar: f32, pub ar: f64,
pub n_fruits: usize, pub n_fruits: usize,
pub n_droplets: usize, pub n_droplets: usize,
pub n_tiny_droplets: usize, pub n_tiny_droplets: usize,
@@ -643,19 +645,19 @@ pub struct DifficultyAttributes {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct PerformanceAttributes { pub struct PerformanceAttributes {
pub attributes: DifficultyAttributes, pub attributes: DifficultyAttributes,
pub pp: f32, pub pp: f64,
} }
impl PerformanceAttributes { impl PerformanceAttributes {
/// Return the star value. /// Return the star value.
#[inline] #[inline]
pub fn stars(&self) -> f32 { pub fn stars(&self) -> f64 {
self.attributes.stars self.attributes.stars
} }
/// Return the performance point value. /// Return the performance point value.
#[inline] #[inline]
pub fn pp(&self) -> f32 { pub fn pp(&self) -> f64 {
self.pp self.pp
} }
} }
+21 -20
View File
@@ -5,23 +5,23 @@ use std::cmp::Ordering;
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0; const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0; const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR; const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR;
const DIRECTION_CHANGE_BONUS: f32 = 21.0; const DIRECTION_CHANGE_BONUS: f64 = 21.0;
const SKILL_MULTIPLIER: f32 = 900.0; const SKILL_MULTIPLIER: f64 = 900.0;
const STRAIN_DECAY_BASE: f32 = 0.2; const STRAIN_DECAY_BASE: f64 = 0.2;
const DECAY_WEIGHT: f32 = 0.94; const DECAY_WEIGHT: f64 = 0.94;
pub(crate) struct Movement { pub(crate) struct Movement {
pub(crate) half_catcher_width: f32, pub(crate) half_catcher_width: f32,
last_player_position: Option<f32>, last_player_position: Option<f32>,
last_distance_moved: f32, last_distance_moved: f32,
last_strain_time: f32, last_strain_time: f64,
current_strain: f32, current_strain: f64,
current_section_peak: f32, current_section_peak: f64,
pub(crate) strain_peaks: Vec<f32>, pub(crate) strain_peaks: Vec<f64>,
prev_time: Option<f32>, prev_time: Option<f64>,
} }
impl Movement { impl Movement {
@@ -51,7 +51,7 @@ impl Movement {
} }
#[inline] #[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()); 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); 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 difficulty = 0.0;
let mut weight = 1.0; let mut weight = 1.0;
@@ -77,7 +77,7 @@ impl Movement {
difficulty difficulty
} }
fn strain_value_of(&mut self, current: &DifficultyObject<'_>) -> f32 { fn strain_value_of(&mut self, current: &DifficultyObject<'_>) -> f64 {
let last_player_pos = self let last_player_pos = self
.last_player_position .last_player_position
.unwrap_or(current.last_normalized_pos); .unwrap_or(current.last_normalized_pos);
@@ -89,14 +89,15 @@ impl Movement {
let dist_moved = pos - last_player_pos; let dist_moved = pos - last_player_pos;
let weighted_strain_time = current.strain_time + 13.0 + (3.0 / current.clock_rate); 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 dist_moved.abs() > 0.1 {
if self.last_distance_moved.abs() > 0.1 if self.last_distance_moved.abs() > 0.1
&& dist_moved.signum() != self.last_distance_moved.signum() && dist_moved.signum() != self.last_distance_moved.signum()
{ {
let bonus_factor = dist_moved.abs().min(50.0) / 50.0; 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); 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() dist_addition += DIRECTION_CHANGE_BONUS / (self.last_strain_time + 16.0).sqrt()
* bonus_factor * bonus_factor
@@ -104,8 +105,8 @@ impl Movement {
* (1.0 - (weighted_strain_time / 1000.0).powi(3)).max(0.0); * (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) dist_addition += (12.5 * dist_moved.abs().min(NORMALIZED_HITOBJECT_RADIUS * 2.0)
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0) / (NORMALIZED_HITOBJECT_RADIUS * 6.0)) as f64
/ weighted_strain_time.sqrt(); / weighted_strain_time.sqrt();
} }
@@ -120,7 +121,7 @@ impl Movement {
dist_addition *= 1.0 dist_addition *= 1.0
+ edge_dash_bonus + 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); * ((current.strain_time * current.clock_rate).min(265.0) / 265.0).powf(1.5);
} }
@@ -132,12 +133,12 @@ impl Movement {
} }
#[inline] #[inline]
fn peak_strain(&self, delta_time: f32) -> f32 { fn peak_strain(&self, delta_time: f64) -> f64 {
self.current_strain * strain_decay(delta_time) self.current_strain * strain_decay(delta_time)
} }
} }
#[inline] #[inline]
fn strain_decay(ms: f32) -> f32 { fn strain_decay(ms: f64) -> f64 {
STRAIN_DECAY_BASE.powf(ms / 1000.0) STRAIN_DECAY_BASE.powf(ms / 1000.0)
} }
+11 -11
View File
@@ -145,7 +145,7 @@ impl<'m> FruitsPP<'m> {
/// Generate the hit results with respect to the given accuracy between `0` and `100`. /// 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. /// 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() { if self.attributes.is_none() {
self.attributes = Some(stars(self.map, self.mods, self.passed_objects)); self.attributes = Some(stars(self.map, self.mods, self.passed_objects));
} }
@@ -167,7 +167,7 @@ impl<'m> FruitsPP<'m> {
acc /= 100.0; acc /= 100.0;
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| { 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_fruits)
.saturating_sub(n_droplets) .saturating_sub(n_droplets)
}); });
@@ -290,17 +290,17 @@ impl FruitsPPInner {
// Longer maps are worth more // Longer maps are worth more
let len_bonus = 0.95 let len_bonus = 0.95
+ 0.3 * (combo_hits as f32 / 2500.0).min(1.0) + 0.3 * (combo_hits as f64 / 2500.0).min(1.0)
+ (combo_hits > 2500) as u8 as f32 * (combo_hits as f32 / 2500.0).log10() * 0.475; + (combo_hits > 2500) as u8 as f64 * (combo_hits as f64 / 2500.0).log10() * 0.475;
pp *= len_bonus; pp *= len_bonus;
// Penalize misses exponentially // 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 // Combo scaling
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) { 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) .powf(0.8)
.min(1.0); .min(1.0);
} }
@@ -309,7 +309,7 @@ impl FruitsPPInner {
let ar = attributes.ar; let ar = attributes.ar;
let mut ar_factor = 1.0; let mut ar_factor = 1.0;
if ar > 9.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 { } else if ar < 8.0 {
ar_factor += 0.025 * (8.0 - ar); ar_factor += 0.025 * (8.0 - ar);
} }
@@ -359,13 +359,13 @@ impl FruitsPPInner {
} }
#[inline] #[inline]
fn acc(&self) -> f32 { fn acc(&self) -> f64 {
let total_hits = self.total_hits(); let total_hits = self.total_hits();
if total_hits == 0 { if total_hits == 0 {
1.0 1.0
} else { } else {
(self.successful_hits() as f32 / total_hits as f32) (self.successful_hits() as f64 / total_hits as f64)
.max(0.0) .max(0.0)
.min(1.0) .min(1.0)
} }
@@ -447,7 +447,7 @@ mod test {
+ calculator.n_tiny_droplets.unwrap_or(0); + calculator.n_tiny_droplets.unwrap_or(0);
let denominator = let denominator =
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses; 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!( assert!(
(target_acc - acc).abs() < 1.0, (target_acc - acc).abs() < 1.0,
@@ -487,7 +487,7 @@ mod test {
+ calculator.n_tiny_droplets.unwrap_or(0); + calculator.n_tiny_droplets.unwrap_or(0);
let denominator = let denominator =
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses; 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!( assert!(
(target_acc - acc).abs() < 1.0, (target_acc - acc).abs() < 1.0,
+8 -8
View File
@@ -3,8 +3,8 @@ use crate::{Beatmap, ControlPoint, ControlPointIter};
pub(crate) struct SliderState<'p> { pub(crate) struct SliderState<'p> {
control_points: ControlPointIter<'p>, control_points: ControlPointIter<'p>,
next: Option<ControlPoint>, next: Option<ControlPoint>,
pub(crate) beat_len: f32, pub(crate) beat_len: f64,
pub(crate) slider_velocity: f32, pub(crate) slider_velocity: f64,
} }
impl<'p> SliderState<'p> { impl<'p> SliderState<'p> {
@@ -29,7 +29,7 @@ impl<'p> SliderState<'p> {
} }
#[inline] #[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()) { while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
match next { match next {
ControlPoint::Timing { beat_len, .. } => { ControlPoint::Timing { beat_len, .. } => {
@@ -88,14 +88,14 @@ mod test {
let mut state = SliderState::new(&map); let mut state = SliderState::new(&map);
state.update(2.0); 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); state.update(3.0);
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON); assert!((state.beat_len - 20.0).abs() <= f64::EPSILON);
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON); assert!((state.slider_velocity - 1.0).abs() <= f64::EPSILON);
state.update(5.0); state.update(5.0);
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON); assert!((state.beat_len - 30.0).abs() <= f64::EPSILON);
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON); assert!((state.slider_velocity - 45.0).abs() <= f64::EPSILON);
} }
} }
+6 -6
View File
@@ -298,8 +298,8 @@ impl BeatmapExt for Beatmap {
/// `section_length` is the time in ms inbetween two strains. /// `section_length` is the time in ms inbetween two strains.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Strains { pub struct Strains {
pub section_length: f32, pub section_length: f64,
pub strains: Vec<f32>, pub strains: Vec<f64>,
} }
/// Basic enum containing the result of a star calculation based on the mode. /// Basic enum containing the result of a star calculation based on the mode.
@@ -318,7 +318,7 @@ pub enum StarResult {
impl StarResult { impl StarResult {
/// The final star value. /// The final star value.
#[inline] #[inline]
pub fn stars(&self) -> f32 { pub fn stars(&self) -> f64 {
match self { match self {
#[cfg(feature = "fruits")] #[cfg(feature = "fruits")]
Self::Fruits(attributes) => attributes.stars, Self::Fruits(attributes) => attributes.stars,
@@ -348,7 +348,7 @@ pub enum PpResult {
impl PpResult { impl PpResult {
/// The final pp value. /// The final pp value.
#[inline] #[inline]
pub fn pp(&self) -> f32 { pub fn pp(&self) -> f64 {
match self { match self {
#[cfg(feature = "fruits")] #[cfg(feature = "fruits")]
Self::Fruits(attributes) => attributes.pp, Self::Fruits(attributes) => attributes.pp,
@@ -363,7 +363,7 @@ impl PpResult {
/// The final star value. /// The final star value.
#[inline] #[inline]
pub fn stars(&self) -> f32 { pub fn stars(&self) -> f64 {
match self { match self {
#[cfg(feature = "fruits")] #[cfg(feature = "fruits")]
Self::Fruits(attributes) => attributes.stars(), Self::Fruits(attributes) => attributes.stars(),
@@ -379,7 +379,7 @@ impl PpResult {
#[cfg(any(feature = "osu", feature = "taiko"))] #[cfg(any(feature = "osu", feature = "taiko"))]
#[inline] #[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 { if val > 5.0 {
avg + (max - avg) * (val - 5.0) / 5.0 avg + (max - avg) * (val - 5.0) / 5.0
} else if val < 5.0 { } else if val < 5.0 {
+11 -11
View File
@@ -8,8 +8,8 @@ use strain::Strain;
use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains}; use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
const SECTION_LEN: f32 = 400.0; const SECTION_LEN: f64 = 400.0;
const STAR_SCALING_FACTOR: f32 = 0.018; const STAR_SCALING_FACTOR: f64 = 0.018;
/// Star calculation for osu!mania maps /// Star calculation for osu!mania maps
/// ///
@@ -150,13 +150,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
pub(crate) struct DifficultyHitObject<'o> { pub(crate) struct DifficultyHitObject<'o> {
base: &'o HitObject, base: &'o HitObject,
column: usize, column: usize,
delta: f32, delta: f64,
start_time: f32, start_time: f64,
} }
impl<'o> DifficultyHitObject<'o> { impl<'o> DifficultyHitObject<'o> {
#[inline] #[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 x_divisor = 512.0 / columns;
let column = (base.pos.x / x_divisor).floor().min(columns - 1.0) as usize; 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. /// This data is necessary to calculate PP.
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default)]
pub struct DifficultyAttributes { pub struct DifficultyAttributes {
pub stars: f32, pub stars: f64,
} }
/// Various data created through the pp calculation. /// Various data created through the pp calculation.
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default)]
pub struct PerformanceAttributes { pub struct PerformanceAttributes {
pub attributes: DifficultyAttributes, pub attributes: DifficultyAttributes,
pub pp_acc: f32, pub pp_acc: f64,
pub pp_strain: f32, pub pp_strain: f64,
pub pp: f32, pub pp: f64,
} }
impl PerformanceAttributes { impl PerformanceAttributes {
/// Return the star value. /// Return the star value.
#[inline] #[inline]
pub fn stars(&self) -> f32 { pub fn stars(&self) -> f64 {
self.attributes.stars self.attributes.stars
} }
/// Return the performance point value. /// Return the performance point value.
#[inline] #[inline]
pub fn pp(&self) -> f32 { pub fn pp(&self) -> f64 {
self.pp self.pp
} }
} }
+17 -17
View File
@@ -30,9 +30,9 @@ use crate::{Beatmap, Mods, PpResult, StarResult};
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub struct ManiaPP<'m> { pub struct ManiaPP<'m> {
map: &'m Beatmap, map: &'m Beatmap,
stars: Option<f32>, stars: Option<f64>,
mods: u32, mods: u32,
score: Option<f32>, score: Option<f64>,
passed_objects: Option<usize>, passed_objects: Option<usize>,
} }
@@ -56,7 +56,7 @@ impl<'m> ManiaPP<'m> {
#[inline] #[inline]
pub fn attributes(mut self, attributes: impl ManiaAttributeProvider) -> Self { pub fn attributes(mut self, attributes: impl ManiaAttributeProvider) -> Self {
if let Some(stars) = attributes.attributes() { if let Some(stars) = attributes.attributes() {
self.stars.replace(stars); self.stars = Some(stars);
} }
self 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. /// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
#[inline] #[inline]
pub fn score(mut self, score: u32) -> Self { pub fn score(mut self, score: u32) -> Self {
self.score.replace(score as f32); self.score = Some(score as f64);
self self
} }
@@ -100,17 +100,17 @@ impl<'m> ManiaPP<'m> {
let ht = self.mods.ht(); let ht = self.mods.ht();
let mut scaled_score = self.score.map_or(1_000_000.0, |score| { 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 { if let Some(passed_objects) = self.passed_objects {
let percent_passed = 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; 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 clock_rate = self.mods.speed();
let mut multiplier = 0.8; 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; 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 { if score <= 500_000.0 {
strain_value = 0.0; strain_value = 0.0;
@@ -162,7 +162,7 @@ impl<'m> ManiaPP<'m> {
} }
#[inline] #[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) (0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
* strain * strain
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1) * ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
@@ -170,33 +170,33 @@ impl<'m> ManiaPP<'m> {
} }
pub trait ManiaAttributeProvider { pub trait ManiaAttributeProvider {
fn attributes(self) -> Option<f32>; fn attributes(self) -> Option<f64>;
} }
impl ManiaAttributeProvider for f32 { impl ManiaAttributeProvider for f64 {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
Some(self) Some(self)
} }
} }
impl ManiaAttributeProvider for DifficultyAttributes { impl ManiaAttributeProvider for DifficultyAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
Some(self.stars) Some(self.stars)
} }
} }
impl ManiaAttributeProvider for PerformanceAttributes { impl ManiaAttributeProvider for PerformanceAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
Some(self.attributes.stars) Some(self.attributes.stars)
} }
} }
impl ManiaAttributeProvider for StarResult { impl ManiaAttributeProvider for StarResult {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
#[allow(irrefutable_let_patterns)] #[allow(irrefutable_let_patterns)]
if let Self::Mania(attributes) = self { if let Self::Mania(attributes) = self {
Some(attributes.stars) Some(attributes.stars)
@@ -208,7 +208,7 @@ impl ManiaAttributeProvider for StarResult {
impl ManiaAttributeProvider for PpResult { impl ManiaAttributeProvider for PpResult {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
#[allow(irrefutable_let_patterns)] #[allow(irrefutable_let_patterns)]
if let Self::Mania(attributes) = self { if let Self::Mania(attributes) = self {
Some(attributes.attributes.stars) Some(attributes.attributes.stars)
+19 -19
View File
@@ -3,25 +3,25 @@ use super::DifficultyHitObject;
use std::cmp::Ordering; use std::cmp::Ordering;
pub(crate) struct Strain { pub(crate) struct Strain {
current_strain: f32, current_strain: f64,
current_section_peak: f32, current_section_peak: f64,
individual_strain: f32, individual_strain: f64,
overall_strain: f32, overall_strain: f64,
hold_end_times: Vec<f32>, hold_end_times: Vec<f64>,
individual_strains: Vec<f32>, individual_strains: Vec<f64>,
pub(crate) strain_peaks: Vec<f32>, pub(crate) strain_peaks: Vec<f64>,
prev_time: Option<f32>, prev_time: Option<f64>,
} }
const INDIVISUAL_DECAY_BASE: f32 = 0.125; const INDIVISUAL_DECAY_BASE: f64 = 0.125;
const OVERALL_DECAY_BASE: f32 = 0.3; const OVERALL_DECAY_BASE: f64 = 0.3;
const STRAIN_DECAY_BASE: f32 = 1.0; const STRAIN_DECAY_BASE: f64 = 1.0;
const SKILL_MULTIPLIER: f32 = 1.0; const SKILL_MULTIPLIER: f64 = 1.0;
const DECAY_WEIGHT: f32 = 0.9; const DECAY_WEIGHT: f64 = 0.9;
impl Strain { impl Strain {
#[inline] #[inline]
@@ -47,18 +47,18 @@ impl Strain {
} }
#[inline] #[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()); self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
} }
#[inline] #[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.individual_strain, delta_time, INDIVISUAL_DECAY_BASE)
+ apply_decay(self.overall_strain, delta_time, OVERALL_DECAY_BASE) + apply_decay(self.overall_strain, delta_time, OVERALL_DECAY_BASE)
} }
#[inline] #[inline]
fn strain_decay(&self, ms: f32) -> f32 { fn strain_decay(&self, ms: f64) -> f64 {
STRAIN_DECAY_BASE.powf(ms / 1000.0) STRAIN_DECAY_BASE.powf(ms / 1000.0)
} }
@@ -70,7 +70,7 @@ impl Strain {
self.prev_time.replace(current.start_time); 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 end_time = current.base.end_time();
let mut hold_factor = 1.0; let mut hold_factor = 1.0;
@@ -107,7 +107,7 @@ impl Strain {
} }
#[inline] #[inline]
pub(crate) fn difficulty_value(&mut self) -> f32 { pub(crate) fn difficulty_value(&mut self) -> f64 {
let mut difficulty = 0.0; let mut difficulty = 0.0;
let mut weight = 1.0; let mut weight = 1.0;
@@ -124,6 +124,6 @@ impl Strain {
} }
#[inline] #[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) value * decay_base.powf(delta_time / 1000.0)
} }
+1 -1
View File
@@ -10,6 +10,6 @@ pub(crate) fn is_linear(
#[cfg(feature = "osu")] #[cfg(feature = "osu")]
#[inline] #[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 start + (end - start) * percent
} }
+4 -4
View File
@@ -21,8 +21,8 @@ pub trait Mods: Copy {
fn change_speed(self) -> bool; fn change_speed(self) -> bool;
fn change_map(self) -> bool; fn change_map(self) -> bool;
fn speed(self) -> f32; fn speed(self) -> f64;
fn od_ar_hp_multiplier(self) -> f32; fn od_ar_hp_multiplier(self) -> f64;
fn nf(self) -> bool; fn nf(self) -> bool;
fn ez(self) -> bool; fn ez(self) -> bool;
fn td(self) -> bool; fn td(self) -> bool;
@@ -47,7 +47,7 @@ impl Mods for u32 {
} }
#[inline] #[inline]
fn speed(self) -> f32 { fn speed(self) -> f64 {
if self & Self::DT > 0 { if self & Self::DT > 0 {
1.5 1.5
} else if self & Self::HT > 0 { } else if self & Self::HT > 0 {
@@ -58,7 +58,7 @@ impl Mods for u32 {
} }
#[inline] #[inline]
fn od_ar_hp_multiplier(self) -> f32 { fn od_ar_hp_multiplier(self) -> f64 {
if self & Self::HR > 0 { if self & Self::HR > 0 {
1.4 1.4
} else if self & Self::EZ > 0 { } else if self & Self::EZ > 0 {
+17 -17
View File
@@ -22,16 +22,16 @@ pub use fast::*;
/// This data is necessary to calculate PP. /// This data is necessary to calculate PP.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct DifficultyAttributes { pub struct DifficultyAttributes {
pub aim_strain: f32, pub aim_strain: f64,
pub speed_strain: f32, pub speed_strain: f64,
pub flashlight_rating: f32, pub flashlight_rating: f64,
pub ar: f32, pub ar: f64,
pub od: f32, pub od: f64,
pub hp: f32, pub hp: f64,
pub n_circles: usize, pub n_circles: usize,
pub n_sliders: usize, pub n_sliders: usize,
pub n_spinners: usize, pub n_spinners: usize,
pub stars: f32, pub stars: f64,
pub max_combo: usize, pub max_combo: usize,
} }
@@ -39,29 +39,29 @@ pub struct DifficultyAttributes {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct PerformanceAttributes { pub struct PerformanceAttributes {
pub attributes: DifficultyAttributes, pub attributes: DifficultyAttributes,
pub pp_acc: f32, pub pp_acc: f64,
pub pp_aim: f32, pub pp_aim: f64,
pub pp_flashlight: f32, pub pp_flashlight: f64,
pub pp_speed: f32, pub pp_speed: f64,
pub pp: f32, pub pp: f64,
} }
impl PerformanceAttributes { impl PerformanceAttributes {
/// Return the star value. /// Return the star value.
#[inline] #[inline]
pub fn stars(&self) -> f32 { pub fn stars(&self) -> f64 {
self.attributes.stars self.attributes.stars
} }
/// Return the performance point value. /// Return the performance point value.
#[inline] #[inline]
pub fn pp(&self) -> f32 { pub fn pp(&self) -> f64 {
self.pp self.pp
} }
} }
#[inline] #[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) super::difficulty_range(od, 20.0, 50.0, 80.0)
} }
@@ -72,7 +72,7 @@ fn custom_osu() {
use crate::{Beatmap, OsuPP}; 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 file = File::open(path).unwrap();
let start = Instant::now(); let start = Instant::now();
@@ -94,7 +94,7 @@ fn custom_osu() {
println!("Parsing average: {:?}", accum / iters); println!("Parsing average: {:?}", accum / iters);
let start = Instant::now(); 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 iters = 100;
let accum = start.elapsed(); let accum = start.elapsed();
+58 -59
View File
@@ -35,7 +35,7 @@ pub struct OsuPP<'m> {
attributes: Option<DifficultyAttributes>, attributes: Option<DifficultyAttributes>,
mods: u32, mods: u32,
combo: Option<usize>, combo: Option<usize>,
acc: Option<f32>, acc: Option<f64>,
n300: Option<usize>, n300: Option<usize>,
n100: Option<usize>, n100: Option<usize>,
@@ -138,7 +138,7 @@ impl<'m> OsuPP<'m> {
/// ///
/// Be sure to set `misses` beforehand! /// Be sure to set `misses` beforehand!
/// In case of a partial play, be also sure to set `passed_objects` 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 let n_objects = self
.passed_objects .passed_objects
.unwrap_or_else(|| self.map.hit_objects.len()); .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 placed_points = 2 * n100 + n50 + self.n_misses;
let missing_objects = n_objects - n100 - n50 - self.n_misses; let missing_objects = n_objects - n100 - n50 - self.n_misses;
let missing_points = 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); let mut n300 = missing_objects.min(missing_points / 6);
n50 += missing_objects - n300; n50 += missing_objects - n300;
@@ -171,10 +171,10 @@ impl<'m> OsuPP<'m> {
self.n100 = Some(n100); self.n100 = Some(n100);
self.n50 = Some(n50); 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 { } else {
let misses = self.n_misses.min(n_objects); 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 delta = target_total - (n_objects - misses);
let mut n300 = delta / 5; let mut n300 = delta / 5;
@@ -191,7 +191,7 @@ impl<'m> OsuPP<'m> {
self.n100 = Some(n100); self.n100 = Some(n100);
self.n50 = Some(n50); 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); self.acc = Some(acc);
@@ -213,7 +213,7 @@ impl<'m> OsuPP<'m> {
let n100 = n100.unwrap_or(0); let n100 = n100.unwrap_or(0);
let n50 = n50.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 = let effective_misses =
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits); 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 n50 = n50.unwrap_or(0);
let numerator = n300 * 6 + n100 * 2 + n50; 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 = let effective_misses =
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits); calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
@@ -295,13 +295,13 @@ struct OsuPPInner {
attributes: DifficultyAttributes, attributes: DifficultyAttributes,
mods: u32, mods: u32,
combo: Option<usize>, combo: Option<usize>,
acc: f32, acc: f64,
n300: usize, n300: usize,
n100: usize, n100: usize,
n50: usize, n50: usize,
total_hits: f32, total_hits: f64,
effective_misses: usize, effective_misses: usize,
} }
@@ -311,18 +311,23 @@ impl OsuPPInner {
// NF penalty // NF penalty
if self.mods.nf() { 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 // SO penalty
if self.mods.so() { if self.mods.so() {
let n_spinners = self.attributes.n_spinners; 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 // Relax penalty
if self.mods.rx() { 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; 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 attributes = &self.attributes;
let total_hits = self.total_hits; let total_hits = self.total_hits;
@@ -364,48 +369,44 @@ impl OsuPPInner {
// Longer maps are worth more // Longer maps are worth more
let len_bonus = 0.95 let len_bonus = 0.95
+ 0.4 * (total_hits / 2000.0).min(1.0) + 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; aim_value *= len_bonus;
// Penalize misses // Penalize misses
let effective_misses = self.effective_misses as i32; let effective_misses = self.effective_misses as i32;
if effective_misses > 0 { if effective_misses > 0 {
aim_value *= 0.97 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 // Combo scaling
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) { 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 // AR bonus
let ar_factor = if attributes.ar > 10.33 { let ar_factor = if attributes.ar > 10.33 {
attributes.ar - 10.33 0.3 * (attributes.ar - 10.33)
} else if attributes.ar < 8.0 { } else if attributes.ar < 8.0 {
0.025 * (8.0 - attributes.ar) 0.1 * (8.0 - attributes.ar)
} else { } else {
0.0 0.0
}; };
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip(); aim_value *= 1.0 + ar_factor * len_bonus; // * Buff for longer maps with high AR.
let ar_bonus = 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
// HD bonus (this would include the Blinds mod but it's currently not representable) // HD bonus (this would include the Blinds mod but it's currently not representable)
if self.mods.hd() { if self.mods.hd() {
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar); aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
} }
aim_value *= ar_bonus; aim_value *= self.acc;
// Scale with accuracy
aim_value *= 0.5 + self.acc / 2.0;
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0; aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
aim_value aim_value
} }
fn compute_speed_value(&self) -> f32 { fn compute_speed_value(&self) -> f64 {
let attributes = &self.attributes; let attributes = &self.attributes;
let total_hits = self.total_hits; let total_hits = self.total_hits;
@@ -415,11 +416,11 @@ impl OsuPPInner {
// Longer maps are worth more // Longer maps are worth more
let len_bonus = 0.95 let len_bonus = 0.95
+ 0.4 * (total_hits / 2000.0).min(1.0) + 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; speed_value *= len_bonus;
// Penalize misses // Penalize misses
let effective_misses = self.effective_misses as f32; let effective_misses = self.effective_misses as f64;
if effective_misses > 0.0 { if effective_misses > 0.0 {
speed_value *= 0.97 speed_value *= 0.97
* (1.0 - (effective_misses / total_hits).powf(0.775)) * (1.0 - (effective_misses / total_hits).powf(0.775))
@@ -428,19 +429,17 @@ impl OsuPPInner {
// Combo scaling // Combo scaling
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) { 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 // AR bonus
let ar_factor = if attributes.ar > 10.33 { let ar_factor = if attributes.ar > 10.33 {
attributes.ar - 10.33 0.3 * (attributes.ar - 10.33)
} else { } else {
0.0 0.0
}; };
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip(); speed_value *= 1.0 + ar_factor * len_bonus; // * Buff for longer maps with high AR.
speed_value *= 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
// HD bonus (this would include the Blinds mod but it's currently not representable) // HD bonus (this would include the Blinds mod but it's currently not representable)
if self.mods.hd() { if self.mods.hd() {
@@ -453,34 +452,34 @@ impl OsuPPInner {
speed_value *= od_factor * acc_factor; speed_value *= od_factor * acc_factor;
// Penalize n50s // Penalize n50s
speed_value *= 0.98_f32.powf( speed_value *= 0.98_f64.powf(
(self.n50 as f32 >= total_hits / 500.0) as u8 as f32 (self.n50 as f64 >= total_hits / 500.0) as u8 as f64
* (self.n50 as f32 - total_hits / 500.0), * (self.n50 as f64 - total_hits / 500.0),
); );
speed_value speed_value
} }
fn compute_accuracy_value(&self) -> f32 { fn compute_accuracy_value(&self) -> f64 {
if self.mods.rx() { if self.mods.rx() {
return 0.0; return 0.0;
} }
let attributes = &self.attributes; let attributes = &self.attributes;
let total_hits = self.total_hits; let total_hits = self.total_hits;
let n_circles = attributes.n_circles as f32; let n_circles = attributes.n_circles as f64;
let n300 = self.n300 as f32; let n300 = self.n300 as f64;
let n100 = self.n100 as f32; let n100 = self.n100 as f64;
let n50 = self.n50 as f32; 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)) * (((n300 - (total_hits - n_circles)) * 6.0 + n100 * 2.0 + n50) / (n_circles * 6.0))
.max(0.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 // 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) // HD bonus (this would include the Blinds mod but it's currently not representable)
if self.mods.hd() { if self.mods.hd() {
@@ -495,7 +494,7 @@ impl OsuPPInner {
acc_value acc_value
} }
fn compute_flashlight_value(&self) -> f32 { fn compute_flashlight_value(&self) -> f64 {
if !self.mods.fl() { if !self.mods.fl() {
return 0.0; return 0.0;
} }
@@ -519,7 +518,7 @@ impl OsuPPInner {
// Penalize misses by assessing # of misses relative to the total # of objects. // Penalize misses by assessing # of misses relative to the total # of objects.
// Default a 3% reduction for any # of misses // 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 { if effective_misses > 0.0 {
flashlight_value *= 0.97 flashlight_value *= 0.97
* (1.0 - (effective_misses / total_hits).powf(0.775)) * (1.0 - (effective_misses / total_hits).powf(0.775))
@@ -528,13 +527,13 @@ impl OsuPPInner {
// Combo scaling // Combo scaling
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) { 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 // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
flashlight_value *= 0.7 flashlight_value *= 0.7
+ 0.1 * (total_hits / 200.0).min(1.0) + 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_ // Scale the aim value with accuracy _slightly_
flashlight_value *= 0.5 + self.acc / 2.0; flashlight_value *= 0.5 + self.acc / 2.0;
@@ -550,23 +549,23 @@ fn calculate_effective_misses(
attributes: &DifficultyAttributes, attributes: &DifficultyAttributes,
combo: Option<usize>, combo: Option<usize>,
n_misses: usize, n_misses: usize,
total_hits: f32, total_hits: f64,
) -> usize { ) -> usize {
// Guess the number of misses + slider breaks from combo // * Guess the number of misses + slider breaks from combo
let mut combo_based_misses: f32 = 0.0; let mut combo_based_misses: f64 = 0.0;
if attributes.n_sliders > 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); combo_based_misses = full_combo_threshold / combo.max(1.0);
} }
} }
// We're clamping misses because since it's derived from combo it // * Clamp misscount since it's derived from combo and can be
// can be higher than total hits and that breaks some calculations // * higher than total hits and that breaks some calculations
combo_based_misses = combo_based_misses.min(total_hits); combo_based_misses = combo_based_misses.min(total_hits);
n_misses.max(combo_based_misses.floor() as usize) n_misses.max(combo_based_misses.floor() as usize)
@@ -634,7 +633,7 @@ mod test {
+ 2 * calculator.n100.unwrap_or(0) + 2 * calculator.n100.unwrap_or(0)
+ calculator.n50.unwrap_or(0); + calculator.n50.unwrap_or(0);
let denominator = 6 * total_objects; 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!( assert!(
(target_acc - acc).abs() < 1.0, (target_acc - acc).abs() < 1.0,
@@ -668,7 +667,7 @@ mod test {
+ 2 * calculator.n100.unwrap_or(0) + 2 * calculator.n100.unwrap_or(0)
+ calculator.n50.unwrap_or(0); + calculator.n50.unwrap_or(0);
let denominator = 6 * total_objects; 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!( assert!(
(target_acc - acc).abs() < 1.0, (target_acc - acc).abs() < 1.0,
+207 -36
View File
@@ -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) struct DifficultyObject<'h> {
pub(crate) base: &'h OsuObject, pub(crate) base: &'h OsuObject,
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
pub(crate) jump_dist: f32, pub(crate) delta: f64,
pub(crate) travel_dist: f32, pub(crate) strain_time: f64,
pub(crate) angle: Option<f32>,
pub(crate) delta: f32, pub(crate) angle: Option<f64>,
pub(crate) strain_time: f32, 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> { impl<'h> DifficultyObject<'h> {
pub(crate) fn new( pub(super) fn new(
base: &'h OsuObject, base: &'h OsuObject,
prev: &OsuObject, prev: &mut OsuObject,
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time) prev_prev: Option<&OsuObject>,
prev_prev: Option<OsuObject>, scaling_factor: &ScalingFactor,
scale_factor: f32, clock_rate: f64,
scaling_factor: f32,
) -> Self { ) -> Self {
let delta = base.time - prev.time; let delta = base.time - prev.time;
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects // * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
let strain_time = delta.max(25.0); let strain_time = delta.max(MIN_DELTA_TIME);
let pos = base.pos; // stacked position // * We don't need to calculate either angle or distances
let travel_dist = prev.travel_dist(); // * when one of the last->curr objects is a spinner
let prev_cursor_pos = prev.lazy_end_pos(scale_factor); let (travel_dist, travel_time, movement_dist, movement_time, jump_dist, angle) =
if base.is_spinner() || prev.is_spinner() {
// We don't need to calculate either angle or distance (0.0, 0.0, 0.0, 0.0, 0.0, None)
// 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 { } else {
let jump_dist = ((pos - prev_cursor_pos) * scaling_factor).length(); let prev_stack_offset = scaling_factor.stack_offset(prev.stack_height);
let angle = prev_prev.map(|prev_prev| { // Important to call `Self::compute_slider_cursor_pos` before using `prev.lazy_end_pos`
let prev_prev_cursor_pos = prev_prev.lazy_end_pos(scale_factor); // 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 prev_cursor_pos = prev.lazy_end_pos(prev_stack_offset);
let jump_dist =
((base.pos - prev_cursor_pos) * scaling_factor.adjusted()).length() as f64;
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));
let v1 = prev_prev_cursor_pos - prev.pos; let v1 = prev_prev_cursor_pos - prev.pos;
let v2 = pos - prev_cursor_pos; let v2 = base.pos - prev_cursor_pos;
let dot = v1.dot(v2); let dot = (v1.dot(v2)) as f64;
let det = v1.x * v2.y - v1.y * v2.x; let det = (v1.x * v2.y - v1.y * v2.x) as f64;
det.atan2(dot).abs() det.atan2(dot).abs()
}); });
(jump_dist, angle) 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 { Self {
base, base,
prev: prev_vals,
jump_dist,
travel_dist,
angle,
delta, delta,
strain_time, 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
View File
@@ -8,24 +8,25 @@ use std::mem;
mod difficulty_object; mod difficulty_object;
mod osu_object; mod osu_object;
mod scaling_factor;
mod skill; mod skill;
mod skill_kind; mod skill_kind;
mod slider_state; mod slider_state;
use difficulty_object::DifficultyObject; use difficulty_object::DifficultyObject;
use osu_object::{ObjectParameters, OsuObject}; use osu_object::{ObjectParameters, OsuObject};
use scaling_factor::ScalingFactor;
use skill::Skill; use skill::Skill;
use skill_kind::SkillKind; use skill_kind::SkillKind;
use slider_state::SliderState; use slider_state::SliderState;
use crate::{curve::CurveBuffers, parse::Pos2, Beatmap, Mods, Strains}; use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
use super::DifficultyAttributes; use super::DifficultyAttributes;
const OBJECT_RADIUS: f32 = 64.0; const SECTION_LEN: f64 = 400.0;
const SECTION_LEN: f32 = 400.0; const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675; const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
const NORMALIZED_RADIUS: f32 = 52.0;
const STACK_DISTANCE: f32 = 3.0; const STACK_DISTANCE: f32 = 3.0;
/// Star calculation for osu!standard maps. /// 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(); let hr = mods.hr();
if hr { if hr {
@@ -64,19 +65,10 @@ pub fn stars(
} }
let time_preempt = difficulty_range_ar(raw_ar); let time_preempt = difficulty_range_ar(raw_ar);
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0; let scaling_factor = ScalingFactor::new(map_attributes.cs);
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 mut params = ObjectParameters { let mut params = ObjectParameters {
map, map,
radius,
scaling_factor,
max_combo: 0, max_combo: 0,
slider_state: SliderState::new(map), slider_state: SliderState::new(map),
ticks: Vec::new(), ticks: Vec::new(),
@@ -92,7 +84,7 @@ pub fn stars(
let mut hit_objects = Vec::with_capacity(take); let mut hit_objects = Vec::with_capacity(take);
hit_objects.extend(hit_objects_iter); 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 { if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold); stacking(&mut hit_objects, stack_threshold);
@@ -100,13 +92,13 @@ pub fn stars(
old_stacking(&mut hit_objects, stack_threshold); 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 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.time /= map_attributes.clock_rate;
h.pos += Pos2::new(stack_offset);
h h
}); });
@@ -114,16 +106,16 @@ pub fn stars(
let fl = mods.fl(); let fl = mods.fl();
let mut skills = Vec::with_capacity(2 + fl as usize); let mut skills = Vec::with_capacity(2 + fl as usize);
skills.push(Skill::new(SkillKind::Aim)); skills.push(Skill::aim());
skills.push(Skill::new(SkillKind::speed(hit_window))); skills.push(Skill::speed(hit_window));
if fl { 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_prev = None;
let mut prev = hit_objects.next().unwrap(); let mut prev = hit_objects.next().unwrap();
let mut prev_vals = None;
// First object has no predecessor and thus no strain, handle distinctly // First object has no predecessor and thus no strain, handle distinctly
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN; let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
@@ -132,11 +124,10 @@ pub fn stars(
let curr = hit_objects.next().unwrap(); let curr = hit_objects.next().unwrap();
let h = DifficultyObject::new( let h = DifficultyObject::new(
&curr, &curr,
&prev, &mut prev,
prev_vals, prev_prev.as_ref(),
prev_prev, &scaling_factor,
scale_factor, map_attributes.clock_rate,
scaling_factor,
); );
while h.base.time > current_section_end { while h.base.time > current_section_end {
@@ -152,18 +143,16 @@ pub fn stars(
} }
prev_prev = Some(prev); prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr; prev = curr;
// Handle all other objects // Handle all other objects
for curr in hit_objects { for curr in hit_objects {
let h = DifficultyObject::new( let h = DifficultyObject::new(
&curr, &curr,
&prev, &mut prev,
prev_vals, prev_prev.as_ref(),
prev_prev, &scaling_factor,
scale_factor, map_attributes.clock_rate,
scaling_factor,
); );
while h.base.time > current_section_end { while h.base.time > current_section_end {
@@ -180,7 +169,6 @@ pub fn stars(
} }
prev_prev = Some(prev); prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr; prev = curr;
} }
@@ -224,9 +212,9 @@ pub fn stars(
.powf(1.0 / 1.1); .powf(1.0 / 1.1);
let star_rating = if base_performance > 0.00001 { let star_rating = if base_performance > 0.00001 {
1.12_f32.cbrt() 1.12_f64.cbrt()
* 0.027 * 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 { } else {
0.0 0.0
}; };
@@ -258,7 +246,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
return Strains::default(); return Strains::default();
} }
let mut raw_ar = map.ar; let mut raw_ar = map.ar as f64;
let hr = mods.hr(); let hr = mods.hr();
if 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 time_preempt = difficulty_range_ar(raw_ar);
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0; let scaling_factor = ScalingFactor::new(map_attributes.cs);
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 mut params = ObjectParameters { let mut params = ObjectParameters {
map, map,
radius,
scaling_factor,
max_combo: 0, max_combo: 0,
slider_state: SliderState::new(map), slider_state: SliderState::new(map),
ticks: Vec::new(), 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()); let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
hit_objects.extend(hit_objects_iter); 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 { if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold); 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); 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 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.time /= map_attributes.clock_rate;
h.pos += Pos2::new(stack_offset);
h h
}); });
@@ -317,16 +296,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let fl = mods.fl(); let fl = mods.fl();
let mut skills = Vec::with_capacity(2 + fl as usize); let mut skills = Vec::with_capacity(2 + fl as usize);
skills.push(Skill::new(SkillKind::Aim)); skills.push(Skill::aim());
skills.push(Skill::new(SkillKind::speed(hit_window))); skills.push(Skill::speed(hit_window));
if fl { 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_prev = None;
let mut prev = hit_objects.next().unwrap(); let mut prev = hit_objects.next().unwrap();
let mut prev_vals = None;
// First object has no predecessor and thus no strain, handle distinctly // First object has no predecessor and thus no strain, handle distinctly
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN; let mut current_section_end = (prev.time / 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 curr = hit_objects.next().unwrap();
let h = DifficultyObject::new( let h = DifficultyObject::new(
&curr, &curr,
&prev, &mut prev,
prev_vals, prev_prev.as_ref(),
prev_prev, &scaling_factor,
scale_factor, map_attributes.clock_rate,
scaling_factor,
); );
while h.base.time > current_section_end { 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_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr; prev = curr;
// Handle all other objects // Handle all other objects
for curr in hit_objects { for curr in hit_objects {
let h = DifficultyObject::new( let h = DifficultyObject::new(
&curr, &curr,
&prev, &mut prev,
prev_vals, prev_prev.as_ref(),
prev_prev, &scaling_factor,
scale_factor, map_attributes.clock_rate,
scaling_factor,
); );
while h.base.time > current_section_end { 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_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr; 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 mut extended_start_idx = 0;
let extended_end_idx = hit_objects.len() - 1; 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() { for i in 0..hit_objects.len() {
if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() { if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() {
continue; 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] #[inline]
fn difficulty_range_ar(ar: f32) -> f32 { fn difficulty_range_ar(ar: f64) -> f64 {
crate::difficulty_range(ar, OSU_AR_MAX, OSU_AR_AVG, OSU_AR_MIN) crate::difficulty_range(ar, 450.0, 1200.0, 1800.0)
} }
+130 -86
View File
@@ -6,35 +6,47 @@ use crate::{
Beatmap, Beatmap,
}; };
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0; const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
const BASE_SCORING_DISTANCE: f32 = 100.0; const BASE_SCORING_DISTANCE: f64 = 100.0;
pub(crate) struct OsuObject { pub(crate) struct OsuObject {
pub(crate) time: f32, pub(crate) time: f64,
pub(crate) pos: Pos2, pub(crate) pos: Pos2,
pub(crate) stack_height: f32, pub(crate) stack_height: f32,
kind: OsuObjectKind, pub(crate) kind: OsuObjectKind,
} }
enum OsuObjectKind { pub(crate) enum OsuObjectKind {
Circle, Circle,
Slider { Slider {
end_time: f32, end_time: f64,
end_pos: Pos2, end_pos: Pos2,
lazy_end_pos: Pos2, lazy_end_pos: Pos2,
travel_dist: f32, nested_objects: Vec<NestedObject>,
}, },
Spinner { 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) struct ObjectParameters<'a> {
pub(crate) map: &'a Beatmap, pub(crate) map: &'a Beatmap,
pub(crate) radius: f32,
pub(crate) scaling_factor: f32,
pub(crate) max_combo: usize, pub(crate) max_combo: usize,
pub(crate) ticks: Vec<f32>, pub(crate) ticks: Vec<(Pos2, f64)>,
pub(crate) slider_state: SliderState<'a>, pub(crate) slider_state: SliderState<'a>,
pub(crate) curve_bufs: CurveBuffers, pub(crate) curve_bufs: CurveBuffers,
} }
@@ -44,8 +56,6 @@ impl OsuObject {
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> { pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
let ObjectParameters { let ObjectParameters {
map, map,
radius,
scaling_factor,
max_combo, max_combo,
ticks, ticks,
slider_state, slider_state,
@@ -71,18 +81,15 @@ impl OsuObject {
repeats, repeats,
control_points, 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 // Responsible for timing point values
slider_state.update(h.start_time); 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; 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 { if map.version >= 8 {
tick_dist /= tick_dist /=
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0; (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 duration = end_time - h.start_time;
let span_duration = duration / span_count; 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. // * 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 // * 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. // * been edited by the user, and should never be reached in normal usage.
@@ -141,54 +119,132 @@ impl OsuObject {
ticks.clear(); ticks.clear();
ticks.reserve((len / tick_dist) as usize); 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 { while curr_dist < len - min_dist_from_end {
let progress = curr_dist / len; let progress = curr_dist / len;
compute_vertex(progress); let curr_time = h.start_time + progress * span_duration;
ticks.push(progress); 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; curr_dist += tick_dist;
} }
// Other spans // Other spans
for span_idx in 1..=*repeats { 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 // Repeat point
compute_vertex(progress); 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 { if span_idx & 1 == 1 {
ticks // S-------->R | Span 0
.iter() // 2 4 6 8 | => span_duration = 8
.rev() // R<--------- | Span 1
.for_each(|&tick_progress| compute_vertex(tick_progress + progress)); // 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 { } else {
ticks let tick_iter = ticks.iter().map(|(pos, time)| NestedObject {
.iter() pos: *pos,
.for_each(|&tick_progress| compute_vertex(tick_progress + progress)); time: time + span_duration * span_idx_f64,
kind: NestedObjectKind::Tick,
});
nested_objects.extend(tick_iter);
} }
} }
// Slider tail // 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) let final_span_end_time = (h.start_time + duration / 2.0)
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET); .max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
let progress = (*repeats % 2 == 1) as u8 as f32;
let final_progress =
(final_span_end_time - final_span_start_time) / span_duration + progress;
compute_vertex(final_progress); let progress = (*repeats % 2 == 0) as u8 as f64;
let mut end_pos = h.pos + curve.position_at(progress);
let mut end_pos = h.pos + curve.position_at(1.0 - progress);
travel_dist *= *scaling_factor;
if hr { if hr {
end_pos.y = 384.0 - end_pos.y; 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 { Self {
time: h.start_time, time: h.start_time,
pos, pos,
@@ -197,7 +253,7 @@ impl OsuObject {
end_time: final_span_end_time, end_time: final_span_end_time,
end_pos, end_pos,
lazy_end_pos, lazy_end_pos,
travel_dist, nested_objects,
}, },
} }
} }
@@ -216,15 +272,7 @@ impl OsuObject {
} }
#[inline] #[inline]
pub(crate) fn travel_dist(&self) -> f32 { pub(crate) fn end_time(&self) -> f64 {
match &self.kind {
OsuObjectKind::Slider { travel_dist, .. } => *travel_dist,
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
}
}
#[inline]
pub(crate) fn end_time(&self) -> f32 {
match &self.kind { match &self.kind {
OsuObjectKind::Circle => self.time, OsuObjectKind::Circle => self.time,
OsuObjectKind::Slider { end_time, .. } => *end_time, OsuObjectKind::Slider { end_time, .. } => *end_time,
@@ -241,14 +289,10 @@ impl OsuObject {
} }
#[inline] #[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 { match &self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos, OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
OsuObjectKind::Slider { lazy_end_pos, .. } => { OsuObjectKind::Slider { lazy_end_pos, .. } => *lazy_end_pos + stack_offset,
let stack_offset = scale_factor * self.stack_height;
*lazy_end_pos + Pos2::new(stack_offset)
}
} }
} }
+50
View File
@@ -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
View File
@@ -4,23 +4,38 @@ use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKin
use std::cmp::Ordering; use std::cmp::Ordering;
const REDUCED_STRAIN_BASELINE: f32 = 0.75; const REDUCED_STRAIN_BASELINE: f64 = 0.75;
pub(crate) struct Skill { pub(crate) struct Skill {
curr_strain: f32, curr_strain: f64,
curr_section_peak: f32, curr_section_peak: f64,
kind: SkillKind, kind: SkillKind,
pub(crate) strain_peaks: Vec<f32>, pub(crate) strain_peaks: Vec<f64>,
prev_time: Option<f32>, prev_time: Option<f64>,
} }
impl Skill { impl Skill {
#[inline] #[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 { Self {
curr_strain: 1.0, curr_strain: 0.0,
curr_section_peak: 0.0, curr_section_peak: 0.0,
kind, kind,
@@ -44,18 +59,18 @@ impl Skill {
} }
#[inline] #[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 // The maximum strain of the new section is not zero by default
self.curr_section_peak = self.calculate_initial_strain(time); 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 difficulty = 0.0;
let mut weight = 1.0; let mut weight = 1.0;
let decay_weight = self.kind.decay_weight(); let decay_weight = self.kind.decay_weight();
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values(); 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 self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal)); .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(); let peaks = self.strain_peaks.iter_mut();
for (i, strain) in peaks.take(reduced_section_count).enumerate() { 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(); let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale); *strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
} }
@@ -79,22 +94,22 @@ impl Skill {
difficulty * difficulty_multiplier 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 prev_time = self.prev_time.unwrap_or(0.0);
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time); let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
match &self.kind { match &self.kind {
SkillKind::Aim | SkillKind::Flashlight { .. } => decayed_strain, SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => decayed_strain,
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * 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_decay(curr.delta);
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier(); self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
match &mut self.kind { match &mut self.kind {
SkillKind::Aim | SkillKind::Flashlight { .. } => self.curr_strain, SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => self.curr_strain,
SkillKind::Speed { SkillKind::Speed {
curr_rhythm, curr_rhythm,
history, history,
+238 -75
View File
@@ -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 crate::{math_util, parse::Pos2};
use super::DifficultyObject; 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: f64 = 40.0;
const SPEED_BALANCING_FACTOR: f32 = 40.0;
const TIMING_THRESHOLD: f32 = 107.0; const AIM_SKILL_MULTIPLIER: f64 = 23.25;
const AIM_STRAIN_DECAY_BASE: f64 = 0.15;
const AIM_SKILL_MULTIPLIER: f32 = 26.25; const AIM_DECAY_WEIGHT: f64 = 0.9;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15; const AIM_DIFFICULTY_MULTIPLIER: f64 = 1.06;
const AIM_DECAY_WEIGHT: f32 = 0.9;
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
const AIM_REDUCED_SECTION_COUNT: usize = 10; 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_SKILL_MULTIPLIER: f64 = 1375.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3; const SPEED_STRAIN_DECAY_BASE: f64 = 0.3;
const SPEED_DECAY_WEIGHT: f32 = 0.9; const SPEED_DECAY_WEIGHT: f64 = 0.9;
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04; const SPEED_DIFFICULTY_MULTIPLIER: f64 = 1.04;
const SPEED_REDUCED_SECTION_COUNT: usize = 5; const SPEED_REDUCED_SECTION_COUNT: usize = 5;
const SPEED_HISTORY_LENGTH: usize = 32; const SPEED_HISTORY_LENGTH: usize = 32;
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0; const SPEED_RHYTHM_MULTIPLIER: f64 = 0.75;
const SPEED_RHYTHM_MULTIPLIER: f32 = 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_SKILL_MULTIPLIER: f64 = 0.15;
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15; const FLASHLIGHT_STRAIN_DECAY_BASE: f64 = 0.15;
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0; const FLASHLIGHT_DECAY_WEIGHT: f64 = 1.0;
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06; const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f64 = 1.06;
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10; const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
const FLASHLIGHT_HISTORY_LENGTH: 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 { pub(crate) struct FlashlightHistoryEntry {
end_pos: Pos2, end_pos: Pos2,
is_spinner: bool, is_spinner: bool,
jump_dist: f32, jump_dist: f64,
strain_time: f32, strain_time: f64,
} }
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry { impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
@@ -57,8 +91,8 @@ impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
pub(crate) struct SpeedHistoryEntry { pub(crate) struct SpeedHistoryEntry {
is_slider: bool, is_slider: bool,
start_time: f32, start_time: f64,
strain_time: f32, strain_time: f64,
} }
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry { impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
@@ -72,37 +106,45 @@ impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
} }
pub(crate) enum SkillKind { pub(crate) enum SkillKind {
Aim, Aim {
history: VecDeque<AimHistoryEntry>,
},
Flashlight { Flashlight {
history: VecDeque<FlashlightHistoryEntry>, history: VecDeque<FlashlightHistoryEntry>,
scaling_factor: f32, scaling_factor: f64,
}, },
Speed { Speed {
curr_rhythm: f32, curr_rhythm: f64,
history: VecDeque<SpeedHistoryEntry>, history: VecDeque<SpeedHistoryEntry>,
hit_window: f32, hit_window: f64,
}, },
} }
impl SkillKind { 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 { Self::Flashlight {
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH), history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH + 1),
scaling_factor, scaling_factor,
} }
} }
pub(crate) fn speed(hit_window: f32) -> Self { pub(crate) fn speed(hit_window: f64) -> Self {
Self::Speed { Self::Speed {
curr_rhythm: 1.0, curr_rhythm: 1.0,
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH), history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH + 1),
hit_window, hit_window,
} }
} }
pub(crate) fn pre_process(&mut self) { pub(crate) fn pre_process(&mut self) {
match self { match self {
Self::Aim => {} Self::Aim { history } => history.truncate(AIM_HISTORY_LENGTH),
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH), Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
Self::Speed { history, .. } => history.truncate(SPEED_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<'_>) { pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
match self { match self {
Self::Aim => {} Self::Aim { history } => history.push_front(current.into()),
Self::Flashlight { history, .. } => history.push_front(current.into()), Self::Flashlight { history, .. } => history.push_front(current.into()),
Self::Speed { 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 { match self {
Self::Aim => { Self::Aim { history } => {
if curr.base.is_spinner() { if curr.base.is_spinner() || history.len() < 2 || history[0].is_spinner {
return 0.0; 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 { // * Calculate the velocity to the current hitobject,
if let Some(angle) = curr.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) { // * which starts with a base distance / time assuming the last object is a hitcircle.
let scale = 90.0; let mut curr_velocity = curr.jump_dist / curr.strain_time;
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2) // * But if the last object is a slider, then we extend the
* (prev_jump_dist - scale).max(0.0) // * travel velocity through the slider into the current object.
* (curr.jump_dist - scale).max(0.0)) if prev.is_slider {
.sqrt(); // * 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)) // * calculate the slider velocity from slider head to slider end.
/ (TIMING_THRESHOLD).max(prev_strain_time) 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); if prev_velocity.max(curr_velocity).abs() > f64::EPSILON {
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist); // * 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 = let velocity_diff = (prev_velocity - curr_velocity).abs();
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD)) // * Scale with ratio of difference compared to 0.5 * max dist.
.max(dist_exp / curr.strain_time) 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 { Self::Flashlight {
history, history,
@@ -164,7 +322,7 @@ impl SkillKind {
if let Some(prev) = history.next() { if let Some(prev) = history.next() {
// Handle first entry distinctly for slight optimization // Handle first entry distinctly for slight optimization
if !prev.is_spinner { 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; cumulative_strain_time += prev.strain_time;
// * We want to nerf objects that can be easily seen within the Flashlight circle radius // * We want to nerf objects that can be easily seen within the Flashlight circle radius
@@ -180,7 +338,7 @@ impl SkillKind {
for (factor, prev) in factors.zip(history) { for (factor, prev) in factors.zip(history) {
if !prev.is_spinner { 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; cumulative_strain_time += prev.strain_time;
// * We also want to nerf stacks so that only the first object of the stack is accounted for // * 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] #[inline]
pub(crate) fn difficulty_values(&self) -> (usize, f32) { pub(crate) fn difficulty_values(&self) -> (usize, f64) {
match self { match self {
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER), Self::Aim { .. } => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
Self::Flashlight { .. } => ( Self::Flashlight { .. } => (
FLASHLIGHT_REDUCED_SECTION_COUNT, FLASHLIGHT_REDUCED_SECTION_COUNT,
FLASHLIGHT_DIFFICULTY_MULTIPLIER, FLASHLIGHT_DIFFICULTY_MULTIPLIER,
@@ -252,34 +410,34 @@ impl SkillKind {
} }
#[inline] #[inline]
pub(crate) fn skill_multiplier(&self) -> f32 { pub(crate) fn skill_multiplier(&self) -> f64 {
match self { match self {
SkillKind::Aim => AIM_SKILL_MULTIPLIER, SkillKind::Aim { .. } => AIM_SKILL_MULTIPLIER,
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER, SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER, SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
} }
} }
#[inline] #[inline]
pub(crate) fn strain_decay_base(&self) -> f32 { pub(crate) fn strain_decay_base(&self) -> f64 {
match self { match self {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE, SkillKind::Aim { .. } => AIM_STRAIN_DECAY_BASE,
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE, SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE, SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
} }
} }
#[inline] #[inline]
pub(crate) fn decay_weight(&self) -> f32 { pub(crate) fn decay_weight(&self) -> f64 {
match self { match self {
SkillKind::Aim => AIM_DECAY_WEIGHT, SkillKind::Aim { .. } => AIM_DECAY_WEIGHT,
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT, SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT, SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
} }
} }
#[inline] #[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) self.strain_decay_base().powf(ms / 1000.0)
} }
} }
@@ -287,8 +445,8 @@ impl SkillKind {
pub(crate) fn calculate_speed_rhythm_bonus( pub(crate) fn calculate_speed_rhythm_bonus(
current: &DifficultyObject<'_>, current: &DifficultyObject<'_>,
history: &VecDeque<SpeedHistoryEntry>, history: &VecDeque<SpeedHistoryEntry>,
hit_window: f32, hit_window: f64,
) -> f32 { ) -> f64 {
if current.base.is_spinner() { if current.base.is_spinner() {
return 0.0; return 0.0;
} }
@@ -298,7 +456,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
let mut island_size = 1; let mut island_size = 1;
let mut first_delta_switch = false; let mut first_delta_switch = false;
let adjusted_hit_window = hit_window * 0.6; 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 // * Store the ratio of the current start of an island to buff for tighter rhythms
let mut start_ratio = 0.0; 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 - (current.base.time - curr.start_time)).max(0.0)
/ SPEED_HISTORY_TIME_MAX; / 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 // * 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 curr_delta = curr.strain_time;
let prev_delta = prev.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() rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
* curr_historical_decay * curr_historical_decay
* ((4 + island_size) as f32).sqrt() * ((4 + island_size) as f64).sqrt()
* ((4 + prev_island_size) as f32).sqrt() * ((4 + prev_island_size) as f64).sqrt()
/ 4.0; / 4.0;
start_ratio = effective_ratio; 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 (4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
} }
#[inline] fn calculate_wide_angle_bonus(angle: f64) -> f64 {
fn apply_diminishing_exp(val: f32) -> f32 { let base = (3.0 / 4.0 * ((PI / 6.0).max(angle).min(5.0 / 6.0 * PI) - PI / 6.0)).sin();
val.powf(0.99)
base * base
}
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
1.0 - calculate_wide_angle_bonus(angle)
} }
+8 -8
View File
@@ -3,8 +3,8 @@ use crate::{Beatmap, ControlPoint, ControlPointIter};
pub(crate) struct SliderState<'p> { pub(crate) struct SliderState<'p> {
control_points: ControlPointIter<'p>, control_points: ControlPointIter<'p>,
next: Option<ControlPoint>, next: Option<ControlPoint>,
pub(crate) beat_len: f32, pub(crate) beat_len: f64,
pub(crate) slider_velocity: f32, pub(crate) slider_velocity: f64,
} }
impl<'p> SliderState<'p> { impl<'p> SliderState<'p> {
@@ -29,7 +29,7 @@ impl<'p> SliderState<'p> {
} }
#[inline] #[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()) { while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
match next { match next {
ControlPoint::Timing { beat_len, .. } => { ControlPoint::Timing { beat_len, .. } => {
@@ -88,14 +88,14 @@ mod test {
let mut state = SliderState::new(&map); let mut state = SliderState::new(&map);
state.update(2.0); 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); state.update(3.0);
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON); assert!((state.beat_len - 20.0).abs() <= f64::EPSILON);
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON); assert!((state.slider_velocity - 1.0).abs() <= f64::EPSILON);
state.update(5.0); state.update(5.0);
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON); assert!((state.beat_len - 30.0).abs() <= f64::EPSILON);
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON); assert!((state.slider_velocity - 45.0).abs() <= f64::EPSILON);
} }
} }
+15 -15
View File
@@ -3,27 +3,27 @@ use crate::Mods;
/// Summary struct for a [`Beatmap`](crate::Beatmap)'s attributes. /// Summary struct for a [`Beatmap`](crate::Beatmap)'s attributes.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct BeatmapAttributes { pub struct BeatmapAttributes {
pub ar: f32, pub ar: f64,
pub od: f32, pub od: f64,
pub cs: f32, pub cs: f64,
pub hp: f32, pub hp: f64,
pub clock_rate: f32, pub clock_rate: f64,
} }
impl BeatmapAttributes { impl BeatmapAttributes {
const AR0_MS: f32 = 1800.0; const AR0_MS: f64 = 1800.0;
const AR5_MS: f32 = 1200.0; const AR5_MS: f64 = 1200.0;
const AR10_MS: f32 = 450.0; const AR10_MS: f64 = 450.0;
const AR_MS_STEP_1: f32 = (Self::AR0_MS - Self::AR5_MS) / 5.0; const AR_MS_STEP_1: f64 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
const AR_MS_STEP_2: f32 = (Self::AR5_MS - Self::AR10_MS) / 5.0; const AR_MS_STEP_2: f64 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
#[inline] #[inline]
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self { pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
Self { Self {
ar, ar: ar as f64,
od, od: od as f64,
cs, cs: cs as f64,
hp, hp: hp as f64,
clock_rate: 1.0, clock_rate: 1.0,
} }
} }
@@ -40,7 +40,7 @@ impl BeatmapAttributes {
let multiplier = mods.od_ar_hp_multiplier(); let multiplier = mods.od_ar_hp_multiplier();
// AR // AR
let mut ar = self.ar * multiplier; let mut ar = (self.ar * multiplier) as f64;
let mut ar_ms = if ar <= 5.0 { let mut ar_ms = if ar <= 5.0 {
Self::AR0_MS - Self::AR_MS_STEP_1 * ar Self::AR0_MS - Self::AR_MS_STEP_1 * ar
} else { } else {
+4 -4
View File
@@ -3,8 +3,8 @@ use std::cmp::Ordering;
/// New rhythm speed change. /// New rhythm speed change.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub struct TimingPoint { pub struct TimingPoint {
pub beat_len: f32, pub beat_len: f64,
pub time: f32, pub time: f64,
} }
impl PartialOrd for TimingPoint { impl PartialOrd for TimingPoint {
@@ -16,8 +16,8 @@ impl PartialOrd for TimingPoint {
/// [`TimingPoint`](crate::parse::TimingPoint) that depends on a previous one. /// [`TimingPoint`](crate::parse::TimingPoint) that depends on a previous one.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub struct DifficultyPoint { pub struct DifficultyPoint {
pub time: f32, pub time: f64,
pub speed_multiplier: f32, pub speed_multiplier: f64,
} }
impl PartialOrd for DifficultyPoint { impl PartialOrd for DifficultyPoint {
+6 -6
View File
@@ -7,14 +7,14 @@ use std::cmp::Ordering;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct HitObject { pub struct HitObject {
pub pos: Pos2, pub pos: Pos2,
pub start_time: f32, pub start_time: f64,
pub kind: HitObjectKind, pub kind: HitObjectKind,
pub sound: u8, pub sound: u8,
} }
impl HitObject { impl HitObject {
#[inline] #[inline]
pub fn end_time(&self) -> f32 { pub fn end_time(&self) -> f64 {
match &self.kind { match &self.kind {
HitObjectKind::Circle { .. } => self.start_time, HitObjectKind::Circle { .. } => self.start_time,
// incorrect, only called in mania which has no sliders though // incorrect, only called in mania which has no sliders though
@@ -52,20 +52,20 @@ pub enum HitObjectKind {
Circle, Circle,
#[cfg(feature = "sliders")] #[cfg(feature = "sliders")]
Slider { Slider {
pixel_len: f32, pixel_len: f64,
repeats: usize, repeats: usize,
control_points: Vec<super::PathControlPoint>, control_points: Vec<super::PathControlPoint>,
}, },
#[cfg(not(feature = "sliders"))] #[cfg(not(feature = "sliders"))]
Slider { Slider {
pixel_len: f32, pixel_len: f64,
span_count: usize, span_count: usize,
last_control_point: Pos2, last_control_point: Pos2,
}, },
Spinner { Spinner {
end_time: f32, end_time: f64,
}, },
Hold { Hold {
end_time: f32, end_time: f64,
}, },
} }
+9 -9
View File
@@ -42,11 +42,11 @@ impl<T> OptionExt<T> for Option<T> {
} }
} }
trait F32Ext: Sized { trait FloatExt: Sized {
fn validate(self) -> Result<Self, ParseError>; fn validate(self) -> Result<Self, ParseError>;
} }
impl F32Ext for f32 { impl FloatExt for f64 {
fn validate(self) -> Result<Self, ParseError> { fn validate(self) -> Result<Self, ParseError> {
self.is_finite() self.is_finite()
.then(|| self) .then(|| self)
@@ -312,10 +312,10 @@ macro_rules! parse_timingpoints_body {
.next() .next()
.next_field("timing point time")? .next_field("timing point time")?
.trim() .trim()
.parse::<f32>()? .parse::<f64>()?
.validate()?; .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 { if beat_len < 0.0 {
let point = DifficultyPoint { let point = DifficultyPoint {
@@ -429,7 +429,7 @@ macro_rules! parse_hitobjects_body {
.next() .next()
.next_field("hitobject time")? .next_field("hitobject time")?
.trim() .trim()
.parse::<f32>()? .parse::<f64>()?
.validate()?; .validate()?;
if !$self.hit_objects.is_empty() && time < prev_time { if !$self.hit_objects.is_empty() && time < prev_time {
@@ -519,7 +519,7 @@ macro_rules! parse_hitobjects_body {
let pixel_len = split let pixel_len = split
.next() .next()
.next_field("pixel len")? .next_field("pixel len")?
.parse::<f32>()? .parse::<f64>()?
.max(0.0) .max(0.0)
.min(MAX_COORDINATE_VALUE); .min(MAX_COORDINATE_VALUE);
@@ -733,8 +733,8 @@ pub struct Beatmap {
pub od: f32, pub od: f32,
pub cs: f32, pub cs: f32,
pub hp: f32, pub hp: f32,
pub slider_mult: f32, pub slider_mult: f64,
pub tick_rate: f32, pub tick_rate: f64,
pub hit_objects: Vec<HitObject>, pub hit_objects: Vec<HitObject>,
#[cfg(any(feature = "osu", feature = "fruits"))] #[cfg(any(feature = "osu", feature = "fruits"))]
@@ -769,7 +769,7 @@ mod osu_fruits {
use super::Pos2; 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( pub(super) fn convert_points(
points: &[&str], points: &[&str],
+1 -1
View File
@@ -115,7 +115,7 @@ impl<'m> AnyPP<'m> {
/// Irrelevant for osu!mania. /// Irrelevant for osu!mania.
#[allow(unused_variables)] #[allow(unused_variables)]
#[inline] #[inline]
pub fn accuracy(self, acc: f32) -> Self { pub fn accuracy(self, acc: f64) -> Self {
match self { match self {
#[cfg(feature = "fruits")] #[cfg(feature = "fruits")]
Self::Fruits(f) => Self::Fruits(f.accuracy(acc)), Self::Fruits(f) => Self::Fruits(f.accuracy(acc)),
+3 -3
View File
@@ -6,9 +6,9 @@ pub(crate) struct DifficultyObject<'o> {
pub(crate) idx: usize, pub(crate) idx: usize,
pub(crate) base: &'o HitObject, pub(crate) base: &'o HitObject,
pub(crate) prev: &'o HitObject, pub(crate) prev: &'o HitObject,
pub(crate) delta: f32, pub(crate) delta: f64,
pub(crate) rhythm: &'static HitObjectRhythm, pub(crate) rhythm: &'static HitObjectRhythm,
pub(crate) start_time: f32, pub(crate) start_time: f64,
} }
impl<'o> DifficultyObject<'o> { impl<'o> DifficultyObject<'o> {
@@ -18,7 +18,7 @@ impl<'o> DifficultyObject<'o> {
base: &'o HitObject, base: &'o HitObject,
prev: &'o HitObject, prev: &'o HitObject,
prev_prev: &HitObject, prev_prev: &HitObject,
clock_rate: f32, clock_rate: f64,
) -> Self { ) -> Self {
let delta = (base.start_time - prev.start_time) / clock_rate; let delta = (base.start_time - prev.start_time) / clock_rate;
let rhythm = closest_rhythm(delta, prev, prev_prev, clock_rate); let rhythm = closest_rhythm(delta, prev, prev_prev, clock_rate);
+4 -4
View File
@@ -53,8 +53,8 @@ static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub(crate) struct HitObjectRhythm { pub(crate) struct HitObjectRhythm {
id: u8, id: u8,
ratio: f32, ratio: f64,
pub(crate) difficulty: f32, pub(crate) difficulty: f64,
} }
impl PartialEq for HitObjectRhythm { impl PartialEq for HitObjectRhythm {
@@ -68,10 +68,10 @@ impl Eq for HitObjectRhythm {}
#[inline] #[inline]
pub(crate) fn closest_rhythm( pub(crate) fn closest_rhythm(
delta_time: f32, delta_time: f64,
last: &HitObject, last: &HitObject,
last_last: &HitObject, last_last: &HitObject,
clock_rate: f32, clock_rate: f64,
) -> &'static HitObjectRhythm { ) -> &'static HitObjectRhythm {
let prev_len = (last.start_time - last_last.start_time) / clock_rate; let prev_len = (last.start_time - last_last.start_time) / clock_rate;
let ratio = delta_time / prev_len; let ratio = delta_time / prev_len;
+15 -15
View File
@@ -21,13 +21,13 @@ use stamina_cheese::StaminaCheeseDetector;
use crate::{Beatmap, Mods, Strains}; use crate::{Beatmap, Mods, Strains};
use std::cmp::Ordering; 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 COLOR_SKILL_MULTIPLIER: f64 = 0.01;
const RHYTHM_SKILL_MULTIPLIER: f32 = 0.014; const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
const STAMINA_SKILL_MULTIPLIER: f32 = 0.02; const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
/// Star calculation for osu!taiko maps. /// Star calculation for osu!taiko maps.
/// ///
@@ -209,7 +209,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
} }
#[inline] #[inline]
fn rescale(stars: f32) -> f32 { fn rescale(stars: f64) -> f64 {
if stars < 0.0 { if stars < 0.0 {
stars stars
} else { } else {
@@ -218,7 +218,7 @@ fn rescale(stars: f32) -> f32 {
} }
#[inline] #[inline]
fn simple_color_penalty(stamina: f32, color: f32) -> f32 { fn simple_color_penalty(stamina: f64, color: f64) -> f64 {
if color <= 0.0 { if color <= 0.0 {
0.79 - 0.25 0.79 - 0.25
} else { } 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 mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
let iter = skills[0] let iter = skills[0]
@@ -259,7 +259,7 @@ fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f32) -> f32 {
} }
#[inline] #[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()) (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. /// This data is necessary to calculate PP.
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default)]
pub struct DifficultyAttributes { pub struct DifficultyAttributes {
pub stars: f32, pub stars: f64,
} }
/// Various data created through the pp calculation. /// Various data created through the pp calculation.
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default)]
pub struct PerformanceAttributes { pub struct PerformanceAttributes {
pub attributes: DifficultyAttributes, pub attributes: DifficultyAttributes,
pub pp: f32, pub pp: f64,
pub pp_acc: f32, pub pp_acc: f64,
pub pp_strain: f32, pub pp_strain: f64,
} }
impl PerformanceAttributes { impl PerformanceAttributes {
/// Return the star value. /// Return the star value.
#[inline] #[inline]
pub fn stars(&self) -> f32 { pub fn stars(&self) -> f64 {
self.attributes.stars self.attributes.stars
} }
/// Return the performance point value. /// Return the performance point value.
#[inline] #[inline]
pub fn pp(&self) -> f32 { pub fn pp(&self) -> f64 {
self.pp self.pp
} }
} }
+22 -26
View File
@@ -32,11 +32,11 @@ use crate::{Beatmap, Mods, PpResult, StarResult};
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub struct TaikoPP<'m> { pub struct TaikoPP<'m> {
map: &'m Beatmap, map: &'m Beatmap,
stars: Option<f32>, stars: Option<f64>,
mods: u32, mods: u32,
max_combo: usize, max_combo: usize,
combo: Option<usize>, combo: Option<usize>,
acc: f32, acc: f64,
n_misses: usize, n_misses: usize,
passed_objects: Option<usize>, passed_objects: Option<usize>,
@@ -69,7 +69,7 @@ impl<'m> TaikoPP<'m> {
#[inline] #[inline]
pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self { pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self {
if let Some(stars) = attributes.attributes() { if let Some(stars) = attributes.attributes() {
self.stars.replace(stars); self.stars = Some(stars);
} }
self self
@@ -119,7 +119,7 @@ impl<'m> TaikoPP<'m> {
/// Set the accuracy between 0.0 and 100.0. /// Set the accuracy between 0.0 and 100.0.
#[inline] #[inline]
pub fn accuracy(mut self, acc: f32) -> Self { pub fn accuracy(mut self, acc: f64) -> Self {
self.acc = acc / 100.0; self.acc = acc / 100.0;
self.n300.take(); self.n300.take();
self.n100.take(); self.n100.take();
@@ -158,7 +158,7 @@ impl<'m> TaikoPP<'m> {
(None, None) => unreachable!(), (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 { let inner = TaikoPPInner {
@@ -176,10 +176,10 @@ impl<'m> TaikoPP<'m> {
struct TaikoPPInner<'m> { struct TaikoPPInner<'m> {
map: &'m Beatmap, map: &'m Beatmap,
stars: f32, stars: f64,
mods: u32, mods: u32,
max_combo: usize, max_combo: usize,
acc: f32, acc: f64,
n_misses: usize, 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 exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
let mut strain = exp_base * exp_base / 100_000.0; let mut strain = exp_base * exp_base / 100_000.0;
// Longer maps are worth more // 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; strain *= len_bonus;
// Penalize misses exponentially // 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 // HD bonus
if self.mods.hd() { if self.mods.hd() {
@@ -234,8 +234,8 @@ impl<'m> TaikoPPInner<'m> {
} }
#[inline] #[inline]
fn compute_accuracy_value(&self) -> f32 { fn compute_accuracy_value(&self) -> f64 {
let mut od = self.map.od; let mut od = self.map.od as f64;
if self.mods.hr() { if self.mods.hr() {
od *= 1.4; od *= 1.4;
@@ -248,47 +248,43 @@ impl<'m> TaikoPPInner<'m> {
(150.0 / hit_window).powf(1.1) (150.0 / hit_window).powf(1.1)
* self.acc.powi(15) * self.acc.powi(15)
* 22.0 * 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] #[inline]
fn difficulty_range_od(od: f32) -> f32 { fn difficulty_range_od(od: f64) -> f64 {
crate::difficulty_range(od, HITWINDOW_MAX, HITWINDOW_AVG, HITWINDOW_MIN) crate::difficulty_range(od, 20.0, 35.0, 50.0)
} }
pub trait TaikoAttributeProvider { pub trait TaikoAttributeProvider {
fn attributes(self) -> Option<f32>; fn attributes(self) -> Option<f64>;
} }
impl TaikoAttributeProvider for f32 { impl TaikoAttributeProvider for f64 {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
Some(self) Some(self)
} }
} }
impl TaikoAttributeProvider for DifficultyAttributes { impl TaikoAttributeProvider for DifficultyAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
Some(self.stars) Some(self.stars)
} }
} }
impl TaikoAttributeProvider for PerformanceAttributes { impl TaikoAttributeProvider for PerformanceAttributes {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
Some(self.attributes.stars) Some(self.attributes.stars)
} }
} }
impl TaikoAttributeProvider for StarResult { impl TaikoAttributeProvider for StarResult {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
#[allow(irrefutable_let_patterns)] #[allow(irrefutable_let_patterns)]
if let Self::Taiko(attributes) = self { if let Self::Taiko(attributes) = self {
Some(attributes.stars) Some(attributes.stars)
@@ -300,7 +296,7 @@ impl TaikoAttributeProvider for StarResult {
impl TaikoAttributeProvider for PpResult { impl TaikoAttributeProvider for PpResult {
#[inline] #[inline]
fn attributes(self) -> Option<f32> { fn attributes(self) -> Option<f64> {
#[allow(irrefutable_let_patterns)] #[allow(irrefutable_let_patterns)]
if let Self::Taiko(attributes) = self { if let Self::Taiko(attributes) = self {
Some(attributes.attributes.stars) Some(attributes.attributes.stars)
+17 -17
View File
@@ -2,25 +2,25 @@ use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering; 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_SKILL_MULTIPLIER: f64 = 1.0;
const COLOR_STRAIN_DECAY_BASE: f32 = 0.4; const COLOR_STRAIN_DECAY_BASE: f64 = 0.4;
const RHYTHM_SKILL_MULTIPLIER: f32 = 10.0; const RHYTHM_SKILL_MULTIPLIER: f64 = 10.0;
const RHYTHM_STRAIN_DECAY_BASE: f32 = 0.0; const RHYTHM_STRAIN_DECAY_BASE: f64 = 0.0;
const STAMINA_SKILL_MULTIPLIER: f32 = 1.0; const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
const STAMINA_STRAIN_DECAY_BASE: f32 = 0.4; const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
pub(crate) struct Skill { pub(crate) struct Skill {
pub current_strain: f32, pub current_strain: f64,
current_section_peak: f32, current_section_peak: f64,
kind: SkillKind, kind: SkillKind,
pub(crate) strain_peaks: Vec<f32>, pub(crate) strain_peaks: Vec<f64>,
prev_time: Option<f32>, prev_time: Option<f64>,
} }
impl Skill { impl Skill {
@@ -43,7 +43,7 @@ impl Skill {
} }
#[inline] #[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()); self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
} }
@@ -56,7 +56,7 @@ impl Skill {
} }
#[inline] #[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 difficulty = 0.0;
let mut weight = 1.0; let mut weight = 1.0;
@@ -72,7 +72,7 @@ impl Skill {
} }
#[inline] #[inline]
fn skill_multiplier(&self) -> f32 { fn skill_multiplier(&self) -> f64 {
match self.kind { match self.kind {
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER, SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER, SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
@@ -81,7 +81,7 @@ impl Skill {
} }
#[inline] #[inline]
fn strain_decay_base(&self) -> f32 { fn strain_decay_base(&self) -> f64 {
match self.kind { match self.kind {
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE, SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE, SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
@@ -90,12 +90,12 @@ impl Skill {
} }
#[inline] #[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) self.current_strain * self.strain_decay(delta_time)
} }
#[inline] #[inline]
fn strain_decay(&self, ms: f32) -> f32 { fn strain_decay(&self, ms: f64) -> f64 {
self.strain_decay_base().powf(ms / 1000.0) self.strain_decay_base().powf(ms / 1000.0)
} }
} }
+15 -14
View File
@@ -2,7 +2,7 @@ use super::{DifficultyObject, HitObjectRhythm, LimitedQueue, Rim};
use std::ops::Index; 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 MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
const MONO_HISTORY_MAX_LEN: usize = 5; const MONO_HISTORY_MAX_LEN: usize = 5;
@@ -18,12 +18,12 @@ pub(crate) enum SkillKind {
Rhythm { Rhythm {
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm) rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
notes_since_rhythm_change: usize, notes_since_rhythm_change: usize,
current_strain: f32, current_strain: f64,
}, },
Stamina { Stamina {
note_pair_duration_history: LimitedQueue<f32>, note_pair_duration_history: LimitedQueue<f64>,
hand: u8, hand: u8,
off_hand_object_duration: f32, off_hand_object_duration: f64,
}, },
} }
@@ -51,7 +51,7 @@ impl SkillKind {
Self::Stamina { Self::Stamina {
note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN), note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN),
hand: right_hand as u8, 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, &mut self,
current: &DifficultyObject<'_>, current: &DifficultyObject<'_>,
cheese: &[bool], cheese: &[bool],
) -> f32 { ) -> f64 {
match self { match self {
Self::Color { Self::Color {
mono_history, mono_history,
@@ -157,7 +157,7 @@ impl SkillKind {
*current_strain *= RHYTHM_STRAIN_DECAY; *current_strain *= RHYTHM_STRAIN_DECAY;
*notes_since_rhythm_change += 1; *notes_since_rhythm_change += 1;
if current.rhythm.difficulty.abs() <= f32::EPSILON { if current.rhythm.difficulty.abs() <= f64::EPSILON {
return 0.0; return 0.0;
} }
@@ -250,15 +250,16 @@ impl SkillKind {
} }
#[inline] #[inline]
fn pattern_len_penalty(pattern_len: usize) -> f32 { fn pattern_len_penalty(pattern_len: usize) -> f64 {
let short_pattern_penalty = (0.15 * pattern_len as f32).min(1.0); let pattern_len = pattern_len as f64;
let long_pattern_penalty = (2.5 - 0.15 * pattern_len as f32).max(0.0).min(1.0); 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) short_pattern_penalty.min(long_pattern_penalty)
} }
#[inline] #[inline]
fn cheese_penalty(note_pair_duration: f32) -> f32 { fn cheese_penalty(note_pair_duration: f64) -> f64 {
if note_pair_duration > 125.0 { if note_pair_duration > 125.0 {
1.0 1.0
} else if note_pair_duration < 100.0 { } else if note_pair_duration < 100.0 {
@@ -269,7 +270,7 @@ fn cheese_penalty(note_pair_duration: f32) -> f32 {
} }
#[inline] #[inline]
fn speed_bonus(note_pair_duration: f32) -> f32 { fn speed_bonus(note_pair_duration: f64) -> f64 {
if note_pair_duration > 200.0 { if note_pair_duration > 200.0 {
return 0.0; return 0.0;
} }
@@ -281,6 +282,6 @@ fn speed_bonus(note_pair_duration: f32) -> f32 {
} }
#[inline] #[inline]
fn repetition_penalty(notes_since: usize) -> f32 { fn repetition_penalty(notes_since: usize) -> f64 {
(0.032 * notes_since as f32).min(1.0) (0.032 * notes_since as f64).min(1.0)
} }