use osu's bezier approximation
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
/maps
|
/maps
|
||||||
|
output
|
||||||
/pp-gen/.env
|
/pp-gen/.env
|
||||||
/pp-gen/target
|
/pp-gen/target
|
||||||
/pp-gen/input/*
|
/pp-gen/input/*
|
||||||
|
|||||||
+246
-22
@@ -3,6 +3,8 @@
|
|||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
))]
|
))]
|
||||||
|
|
||||||
|
use std::{borrow::Cow, cmp::Ordering, convert::identity};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
math_util,
|
math_util,
|
||||||
parse::{PathType, Pos2},
|
parse::{PathType, Pos2},
|
||||||
@@ -27,7 +29,10 @@ impl Points {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum Curve<'p> {
|
pub(crate) enum Curve<'p> {
|
||||||
Bezier(Points),
|
Bezier {
|
||||||
|
path: Vec<Pos2>,
|
||||||
|
lengths: Vec<f32>,
|
||||||
|
},
|
||||||
Catmull(Points),
|
Catmull(Points),
|
||||||
Linear(&'p [Pos2]),
|
Linear(&'p [Pos2]),
|
||||||
Perfect {
|
Perfect {
|
||||||
@@ -37,50 +42,235 @@ pub(crate) enum Curve<'p> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BezierBuffers {
|
||||||
|
buf1: Vec<Pos2>,
|
||||||
|
buf2: Vec<Pos2>,
|
||||||
|
buf3: Vec<Pos2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BezierBuffers {
|
||||||
|
fn new(len: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
buf1: vec![Pos2::zero(); len],
|
||||||
|
buf2: vec![Pos2::zero(); (len - 1) * 2 + 1],
|
||||||
|
buf3: vec![Pos2::zero(); len],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'p> Curve<'p> {
|
impl<'p> Curve<'p> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new(points: &'p [Pos2], kind: PathType) -> Self {
|
pub(crate) fn new(points: &'p [Pos2], kind: PathType, expected_len: f32) -> Self {
|
||||||
match kind {
|
match kind {
|
||||||
PathType::Bezier => Self::bezier(points),
|
PathType::Bezier => Self::bezier(points, expected_len),
|
||||||
PathType::Catmull => Self::catmull(points),
|
PathType::Catmull => Self::catmull(points),
|
||||||
PathType::Linear => Self::Linear(points),
|
PathType::Linear => Self::Linear(points),
|
||||||
PathType::PerfectCurve => Self::perfect(points),
|
PathType::PerfectCurve => Self::perfect(points),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bezier(points: &[Pos2]) -> Self {
|
fn bezier(points: &[Pos2], expected_len: f32) -> Self {
|
||||||
if points.len() == 1 {
|
let points: Vec<_> = points
|
||||||
return Self::Bezier(Points::Single(points[0]));
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|point| point - points[0])
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let len = points.len();
|
||||||
|
|
||||||
|
if len == 1 {
|
||||||
|
return Self::Bezier {
|
||||||
|
path: points.to_owned(),
|
||||||
|
lengths: vec![0.0],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First calculate a path of coordinates
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
let mut result = Vec::new();
|
let mut path = Vec::new();
|
||||||
|
let mut bufs = BezierBuffers::new(len);
|
||||||
|
|
||||||
for (end, (curr, next)) in (1..).zip(points.iter().zip(points.iter().skip(1))) {
|
for (end, (curr, next)) in (1..).zip(points.iter().zip(points.iter().skip(1))) {
|
||||||
if end - start > 1 && curr == next {
|
if end - start > 1 && curr == next {
|
||||||
Self::_bezier(&mut result, &points[start..end]);
|
Self::bezier_subpath(&mut path, &points[start..end], &mut bufs);
|
||||||
start = end;
|
start = end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::_bezier(&mut result, &points[start..]);
|
Self::bezier_subpath(&mut path, &points[start..], &mut bufs);
|
||||||
|
let last_point = &points[len - 1];
|
||||||
|
path.push(*last_point);
|
||||||
|
|
||||||
Self::Bezier(Points::Multi(result))
|
// Then calculated cumulative lenghts
|
||||||
|
let mut calculated_len = 0.0;
|
||||||
|
let mut cumulative_len = Vec::new();
|
||||||
|
cumulative_len.push(0.0);
|
||||||
|
|
||||||
|
for i in 0..path.len() - 1 {
|
||||||
|
let diff = path[i + 1] - path[i];
|
||||||
|
calculated_len += diff.length();
|
||||||
|
cumulative_len.push(calculated_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _bezier(result: &mut Vec<Pos2>, points: &[Pos2]) {
|
if (expected_len - calculated_len).abs() > f32::EPSILON {
|
||||||
let step = (BEZIER_TOLERANCE / points.len() as f32).max(0.01);
|
// * In osu-stable, if the last two control points of a slider are equal, extension is not performed
|
||||||
let mut i = 0.0;
|
if points
|
||||||
let n = points.len() as i32 - 1;
|
.get(len - 2)
|
||||||
|
.filter(|&p| p == last_point && expected_len > calculated_len)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
cumulative_len.push(calculated_len);
|
||||||
|
|
||||||
while i < 1.0 + step {
|
return Self::Bezier {
|
||||||
let point = (0..).zip(points).fold(Pos2::zero(), |point, (p, curr)| {
|
path,
|
||||||
point + *curr * math_util::cpn(p, n) * (1.0 - i).powi(n - p) * i.powi(p)
|
lengths: cumulative_len,
|
||||||
});
|
};
|
||||||
|
|
||||||
result.push(point);
|
|
||||||
i += step;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * The last length is always incorrect
|
||||||
|
cumulative_len.pop();
|
||||||
|
|
||||||
|
let mut path_end_idx = path.len() - 1;
|
||||||
|
|
||||||
|
if calculated_len > expected_len {
|
||||||
|
// * The path will be shortened further, in which case we should trim
|
||||||
|
// * any more unnecessary lengths and their associated path segments
|
||||||
|
while cumulative_len
|
||||||
|
.last()
|
||||||
|
.filter(|&l| *l > expected_len)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
cumulative_len.pop();
|
||||||
|
path.remove(path_end_idx);
|
||||||
|
path_end_idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path_end_idx == 0 {
|
||||||
|
// * The expected distance is negative or zero
|
||||||
|
// * Perhaps negative path lengths should be disallowed altogether
|
||||||
|
cumulative_len.push(0.0);
|
||||||
|
|
||||||
|
return Self::Bezier {
|
||||||
|
path,
|
||||||
|
lengths: cumulative_len,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The direction of the segment to shorten or lengthen
|
||||||
|
let dir = (path[path_end_idx] - path[path_end_idx - 1]).normalize();
|
||||||
|
|
||||||
|
path[path_end_idx] =
|
||||||
|
path[path_end_idx - 1] + dir * (expected_len - cumulative_len.last().unwrap());
|
||||||
|
cumulative_len.push(expected_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Bezier {
|
||||||
|
path,
|
||||||
|
lengths: cumulative_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bezier_subpath(result: &mut Vec<Pos2>, points: &[Pos2], bufs: &mut BezierBuffers) {
|
||||||
|
let p = points.len();
|
||||||
|
|
||||||
|
let mut to_flatten = Vec::new();
|
||||||
|
let mut free_bufs = Vec::with_capacity(1);
|
||||||
|
|
||||||
|
// In osu!lazer's code, `p` is always 0 when approximating bezier
|
||||||
|
// so the first big `if` can be omitted
|
||||||
|
|
||||||
|
to_flatten.push(Cow::Borrowed(points));
|
||||||
|
|
||||||
|
// * "toFlatten" contains all the curves which are not yet approximated well enough.
|
||||||
|
// * We use a stack to emulate recursion without the risk of running into a stack overflow.
|
||||||
|
// * (More specifically, we iteratively and adaptively refine our curve with a
|
||||||
|
// * <a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a>
|
||||||
|
// * over the tree resulting from the subdivisions we make.)
|
||||||
|
|
||||||
|
let mut left_child = bufs.buf2.to_owned();
|
||||||
|
|
||||||
|
while let Some(mut parent) = to_flatten.pop() {
|
||||||
|
if Self::bezier_is_flat_enough(&parent) {
|
||||||
|
// * If the control points we currently operate on are sufficiently "flat", we use
|
||||||
|
// * an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation
|
||||||
|
// * of the bezier curve represented by our control points, consisting of the same amount
|
||||||
|
// * of points as there are control points.
|
||||||
|
Self::bezier_approximate(&parent, result, bufs);
|
||||||
|
free_bufs.push(parent);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep
|
||||||
|
// * subdividing the curve we are currently operating on.
|
||||||
|
let mut right_child = free_bufs
|
||||||
|
.pop()
|
||||||
|
.unwrap_or_else(|| Cow::Owned(vec![Pos2::zero(); p]));
|
||||||
|
|
||||||
|
Self::bezier_subdivide(
|
||||||
|
&parent,
|
||||||
|
&mut left_child,
|
||||||
|
right_child.to_mut(),
|
||||||
|
&mut bufs.buf1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// * We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
|
||||||
|
parent.to_mut().copy_from_slice(&left_child[..p]);
|
||||||
|
|
||||||
|
to_flatten.push(right_child);
|
||||||
|
to_flatten.push(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bezier_is_flat_enough(points: &[Pos2]) -> bool {
|
||||||
|
let limit = BEZIER_TOLERANCE * BEZIER_TOLERANCE * 4.0;
|
||||||
|
|
||||||
|
!points
|
||||||
|
.iter()
|
||||||
|
.zip(points.iter().skip(1))
|
||||||
|
.zip(points.iter().skip(2))
|
||||||
|
.any(|((&prev, &curr), &next)| (prev - curr * 2.0 + next).length_squared() > limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bezier_subdivide(points: &[Pos2], l: &mut [Pos2], r: &mut [Pos2], buf: &mut [Pos2]) {
|
||||||
|
let count = points.len();
|
||||||
|
let midpoints = buf;
|
||||||
|
midpoints[..count].copy_from_slice(&points[..count]);
|
||||||
|
|
||||||
|
for i in (1..count).rev() {
|
||||||
|
l[count - i - 1] = midpoints[0];
|
||||||
|
r[i] = midpoints[i];
|
||||||
|
|
||||||
|
for j in 0..i {
|
||||||
|
midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l[count - 1] = midpoints[0];
|
||||||
|
r[0] = midpoints[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// * https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
|
||||||
|
fn bezier_approximate(points: &[Pos2], output: &mut Vec<Pos2>, bufs: &mut BezierBuffers) {
|
||||||
|
let count = points.len();
|
||||||
|
let r = &mut bufs.buf1;
|
||||||
|
let l = &mut bufs.buf2;
|
||||||
|
|
||||||
|
Self::bezier_subdivide(points, l, r, &mut bufs.buf3);
|
||||||
|
l[count..2 * count - 1].copy_from_slice(&r[1..count]);
|
||||||
|
output.push(points[0]);
|
||||||
|
|
||||||
|
let new_points = l
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.zip(l.iter().skip(2))
|
||||||
|
.zip(l.iter().skip(3))
|
||||||
|
.step_by(2)
|
||||||
|
.take(count.saturating_sub(2))
|
||||||
|
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
|
||||||
|
|
||||||
|
output.extend(new_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn catmull(points: &[Pos2]) -> Self {
|
fn catmull(points: &[Pos2]) -> Self {
|
||||||
@@ -164,9 +354,43 @@ impl<'p> Curve<'p> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn interpolate_vertices(path: &[Pos2], lengths: &[f32], i: usize, d: f32) -> Pos2 {
|
||||||
|
if path.is_empty() {
|
||||||
|
return Pos2::zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
return path[0];
|
||||||
|
} else if i >= path.len() {
|
||||||
|
return path[path.len() - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let p0 = path[i - 1];
|
||||||
|
let p1 = path[i];
|
||||||
|
|
||||||
|
let d0 = lengths[i - 1];
|
||||||
|
let d1 = lengths[i];
|
||||||
|
|
||||||
|
// * Avoid division by an almost-zero number in case
|
||||||
|
// * two points are extremely close to each other
|
||||||
|
if (d0 - d1).abs() <= f32::EPSILON {
|
||||||
|
return p0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = (d - d0) / (d1 - d0);
|
||||||
|
|
||||||
|
p0 + (p1 - p0) * w
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn point_at_distance(&self, dist: f32) -> Pos2 {
|
pub(crate) fn point_at_distance(&self, dist: f32) -> Pos2 {
|
||||||
match self {
|
match self {
|
||||||
Self::Bezier(points) => points.point_at_distance(dist),
|
Self::Bezier { path, lengths } => {
|
||||||
|
let idx = lengths
|
||||||
|
.binary_search_by(|len| len.partial_cmp(&dist).unwrap_or(Ordering::Equal))
|
||||||
|
.map_or_else(identity, identity);
|
||||||
|
|
||||||
|
Self::interpolate_vertices(path, lengths, idx, dist)
|
||||||
|
}
|
||||||
Self::Catmull(points) => points.point_at_distance(dist),
|
Self::Catmull(points) => points.point_at_distance(dist),
|
||||||
Self::Linear(points) => math_util::point_at_distance(points, dist),
|
Self::Linear(points) => math_util::point_at_distance(points, dist),
|
||||||
Self::Perfect {
|
Self::Perfect {
|
||||||
|
|||||||
+2
-2
@@ -92,7 +92,7 @@ pub fn stars(
|
|||||||
/ 100.0;
|
/ 100.0;
|
||||||
|
|
||||||
// Build the curve w.r.t. the curve points
|
// Build the curve w.r.t. the curve points
|
||||||
let curve = Curve::new(curve_points, *path_type);
|
let curve = Curve::new(curve_points, *path_type, *pixel_len);
|
||||||
|
|
||||||
let mut current_distance = tick_distance;
|
let mut current_distance = tick_distance;
|
||||||
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
||||||
@@ -323,7 +323,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
/ 100.0;
|
/ 100.0;
|
||||||
|
|
||||||
// Build the curve w.r.t. the curve points
|
// Build the curve w.r.t. the curve points
|
||||||
let curve = Curve::new(curve_points, *path_type);
|
let curve = Curve::new(curve_points, *path_type, *pixel_len);
|
||||||
|
|
||||||
let mut current_distance = tick_distance;
|
let mut current_distance = tick_distance;
|
||||||
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
||||||
|
|||||||
+1
-18
@@ -11,23 +11,6 @@ pub(crate) use fruits_osu::*;
|
|||||||
mod fruits_osu {
|
mod fruits_osu {
|
||||||
use crate::parse::Pos2;
|
use crate::parse::Pos2;
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn cpn(mut p: i32, n: i32) -> f32 {
|
|
||||||
if p < 0 || p > n {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p = p.min(n - p);
|
|
||||||
let diff = n - p;
|
|
||||||
let mut out = 1.0;
|
|
||||||
|
|
||||||
for i in 1..=p {
|
|
||||||
out *= (diff + i) as f32 / i as f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn point_at_distance(points: &[Pos2], dist: f32) -> Pos2 {
|
pub(crate) fn point_at_distance(points: &[Pos2], dist: f32) -> Pos2 {
|
||||||
if points.len() < 2 {
|
if points.len() < 2 {
|
||||||
return Pos2::zero();
|
return Pos2::zero();
|
||||||
@@ -43,7 +26,7 @@ mod fruits_osu {
|
|||||||
let mut new_dist = 0.0;
|
let mut new_dist = 0.0;
|
||||||
|
|
||||||
for (&curr, &next) in points.iter().zip(points.iter().skip(1)) {
|
for (&curr, &next) in points.iter().zip(points.iter().skip(1)) {
|
||||||
new_dist = (curr - next).length();
|
new_dist = (curr - next).length().max(f32::EPSILON);
|
||||||
curr_dist += new_dist;
|
curr_dist += new_dist;
|
||||||
|
|
||||||
if dist <= curr_dist {
|
if dist <= curr_dist {
|
||||||
|
|||||||
+1
-1
@@ -346,7 +346,7 @@ impl OsuPPInner {
|
|||||||
|
|
||||||
PerformanceAttributes {
|
PerformanceAttributes {
|
||||||
attributes: self.attributes,
|
attributes: self.attributes,
|
||||||
pp_acc: aim_value,
|
pp_acc: acc_value,
|
||||||
pp_aim: aim_value,
|
pp_aim: aim_value,
|
||||||
pp_flashlight: flashlight_value,
|
pp_flashlight: flashlight_value,
|
||||||
pp_speed: speed_value,
|
pp_speed: speed_value,
|
||||||
|
|||||||
@@ -337,3 +337,27 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
strains,
|
strains,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_osu() {
|
||||||
|
let file = std::fs::File::open("E:Games/osu!/beatmaps/2753127_.osu").unwrap();
|
||||||
|
// let file = std::fs::File::open("E:Games/osu!/beatmaps/2571051.osu").unwrap();
|
||||||
|
let map = Beatmap::parse(file).unwrap();
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let result = crate::OsuPP::new(&map).mods(0).calculate();
|
||||||
|
|
||||||
|
let iters = 500;
|
||||||
|
let accum = start.elapsed();
|
||||||
|
|
||||||
|
// let mut accum = accum;
|
||||||
|
|
||||||
|
// for _ in 0..iters {
|
||||||
|
// let start = std::time::Instant::now();
|
||||||
|
// let _result = crate::OsuPP::new(&map).mods(0).calculate();
|
||||||
|
// accum += start.elapsed();
|
||||||
|
// }
|
||||||
|
|
||||||
|
println!("{:#?}", result);
|
||||||
|
println!("Average: {:?}", accum / iters);
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ impl OsuObject {
|
|||||||
let span_duration = duration / *repeats as f32;
|
let span_duration = duration / *repeats as f32;
|
||||||
|
|
||||||
// Build the curve w.r.t. the curve points
|
// Build the curve w.r.t. the curve points
|
||||||
let curve = Curve::new(curve_points, *path_type);
|
let curve = Curve::new(curve_points, *path_type, *pixel_len);
|
||||||
|
|
||||||
// Called on each slider object except for the head.
|
// Called on each slider object except for the head.
|
||||||
// Increases combo and adjusts `end_pos` and `travel_dist`
|
// Increases combo and adjusts `end_pos` and `travel_dist`
|
||||||
@@ -95,6 +95,7 @@ impl OsuObject {
|
|||||||
if dist > approx_follow_circle_radius {
|
if dist > approx_follow_circle_radius {
|
||||||
dist -= approx_follow_circle_radius;
|
dist -= approx_follow_circle_radius;
|
||||||
lazy_end_pos += diff.normalize() * dist;
|
lazy_end_pos += diff.normalize() * dist;
|
||||||
|
|
||||||
travel_dist += dist;
|
travel_dist += dist;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ impl Skill {
|
|||||||
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));
|
||||||
|
|
||||||
for &strain in &self.strain_peaks {
|
for &strain in self.strain_peaks.iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= decay_weight;
|
weight *= decay_weight;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,9 +159,9 @@ impl SkillKind {
|
|||||||
let mut small_dist_nerf = 1.0;
|
let mut small_dist_nerf = 1.0;
|
||||||
let mut result = 0.0;
|
let mut result = 0.0;
|
||||||
let mut cumulative_strain_time = 0.0;
|
let mut cumulative_strain_time = 0.0;
|
||||||
let mut history = history.iter();
|
let mut history_iter = history.iter();
|
||||||
|
|
||||||
if let Some(prev) = history.next() {
|
if let Some(prev) = history_iter.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();
|
||||||
@@ -180,7 +180,7 @@ impl SkillKind {
|
|||||||
|
|
||||||
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
|
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
|
||||||
|
|
||||||
for (factor, prev) in factors.zip(history) {
|
for (factor, prev) in factors.zip(history_iter) {
|
||||||
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();
|
||||||
cumulative_strain_time += prev.strain_time;
|
cumulative_strain_time += prev.strain_time;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub enum ParseError {
|
|||||||
InvalidPathType,
|
InvalidPathType,
|
||||||
InvalidTimingSignature,
|
InvalidTimingSignature,
|
||||||
MissingField(&'static str),
|
MissingField(&'static str),
|
||||||
|
TooManyRepeats,
|
||||||
UnknownHitObjectKind,
|
UnknownHitObjectKind,
|
||||||
|
|
||||||
#[cfg(not(all(
|
#[cfg(not(all(
|
||||||
@@ -54,6 +55,7 @@ impl fmt::Display for ParseError {
|
|||||||
Self::InvalidPathType => f.write_str("invalid path type"),
|
Self::InvalidPathType => f.write_str("invalid path type"),
|
||||||
Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
|
Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
|
||||||
Self::MissingField(field) => write!(f, "missing field `{}`", field),
|
Self::MissingField(field) => write!(f, "missing field `{}`", field),
|
||||||
|
Self::TooManyRepeats => f.write_str("repeat count is way too high"),
|
||||||
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
|
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
|
||||||
|
|
||||||
#[cfg(not(all(
|
#[cfg(not(all(
|
||||||
@@ -84,6 +86,7 @@ impl StdError for ParseError {
|
|||||||
Self::InvalidPathType => None,
|
Self::InvalidPathType => None,
|
||||||
Self::InvalidTimingSignature => None,
|
Self::InvalidTimingSignature => None,
|
||||||
Self::MissingField(_) => None,
|
Self::MissingField(_) => None,
|
||||||
|
Self::TooManyRepeats => None,
|
||||||
Self::UnknownHitObjectKind => None,
|
Self::UnknownHitObjectKind => None,
|
||||||
|
|
||||||
#[cfg(not(all(
|
#[cfg(not(all(
|
||||||
|
|||||||
+15
-7
@@ -419,9 +419,9 @@ macro_rules! parse_hitobjects_body {
|
|||||||
y: next_field!(split.next(), "y position").parse()?,
|
y: next_field!(split.next(), "y position").parse()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let time = next_field!(split.next(), "hitobject time")
|
let time: f32 = next_field!(split.next(), "hitobject time")
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<f32>()?;
|
.parse()?;
|
||||||
|
|
||||||
validate_float!(time);
|
validate_float!(time);
|
||||||
|
|
||||||
@@ -449,6 +449,17 @@ macro_rules! parse_hitobjects_body {
|
|||||||
|
|
||||||
let mut curve_point_iter = next_field!(split.next(), "curve points").split('|');
|
let mut curve_point_iter = next_field!(split.next(), "curve points").split('|');
|
||||||
|
|
||||||
|
let mut repeats: usize = next_field!(split.next(), "repeats")
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
if repeats > 9000 {
|
||||||
|
return Err(ParseError::TooManyRepeats);
|
||||||
|
}
|
||||||
|
|
||||||
|
// * osu-stable treated the first span of the slider
|
||||||
|
// * as a repeat, but no repeats are happening
|
||||||
|
repeats = repeats.saturating_sub(1);
|
||||||
|
|
||||||
let mut path_type: PathType =
|
let mut path_type: PathType =
|
||||||
next_field!(curve_point_iter.next(), "path kind").parse()?;
|
next_field!(curve_point_iter.next(), "path kind").parse()?;
|
||||||
|
|
||||||
@@ -499,10 +510,7 @@ macro_rules! parse_hitobjects_body {
|
|||||||
if curve_points.is_empty() {
|
if curve_points.is_empty() {
|
||||||
HitObjectKind::Circle
|
HitObjectKind::Circle
|
||||||
} else {
|
} else {
|
||||||
let repeats = next_field!(split.next(), "repeats")
|
// TODO: Should be Option<f32>?
|
||||||
.parse::<usize>()?
|
|
||||||
.min(9000);
|
|
||||||
|
|
||||||
let pixel_len = next_field!(split.next(), "pixel len")
|
let pixel_len = next_field!(split.next(), "pixel len")
|
||||||
.parse::<f32>()?
|
.parse::<f32>()?
|
||||||
.max(0.0)
|
.max(0.0)
|
||||||
@@ -552,7 +560,7 @@ macro_rules! parse_hitobjects_body {
|
|||||||
pos,
|
pos,
|
||||||
start_time: time,
|
start_time: time,
|
||||||
kind,
|
kind,
|
||||||
sound,
|
sound, // TODO: omit if not taiko?
|
||||||
});
|
});
|
||||||
|
|
||||||
prev_time = time;
|
prev_time = time;
|
||||||
|
|||||||
Reference in New Issue
Block a user