mirrored slider parsing from osu!lazer
This commit is contained in:
@@ -45,7 +45,7 @@ pub(crate) enum ControlPoint {
|
|||||||
},
|
},
|
||||||
Difficulty {
|
Difficulty {
|
||||||
time: f32,
|
time: f32,
|
||||||
speed_mult: f32,
|
slider_velocity: f32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,10 @@ impl<'p> Iterator for ControlPointIter<'p> {
|
|||||||
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 { time, speed_mult })
|
Some(ControlPoint::Difficulty {
|
||||||
|
time,
|
||||||
|
slider_velocity: speed_mult,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
(Some((time, beat_len)), None) => {
|
(Some((time, beat_len)), None) => {
|
||||||
self.next_timing = next_tuple!(self.timing_points, (time, beat_len));
|
self.next_timing = next_tuple!(self.timing_points, (time, beat_len));
|
||||||
|
|||||||
+491
-40
@@ -3,44 +3,16 @@
|
|||||||
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 std::{borrow::Cow, cmp::Ordering, convert::identity, f32::consts::PI};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
math_util,
|
math_util,
|
||||||
parse::{PathType, Pos2},
|
parse::{PathControlPoint, PathType, Pos2},
|
||||||
};
|
};
|
||||||
|
|
||||||
const BEZIER_TOLERANCE: f32 = 0.25;
|
const BEZIER_TOLERANCE: f32 = 0.25;
|
||||||
const CATMULL_DETAIL: f32 = 50.0;
|
const CATMULL_DETAIL: usize = 50;
|
||||||
|
const CIRCULAR_ARC_TOLERANCE: f32 = 0.1;
|
||||||
pub(crate) enum Points {
|
|
||||||
Single(Pos2),
|
|
||||||
Multi(Vec<Pos2>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Points {
|
|
||||||
#[inline]
|
|
||||||
fn point_at_distance(&self, dist: f32) -> Pos2 {
|
|
||||||
match self {
|
|
||||||
Points::Multi(points) => math_util::point_at_distance(points, dist),
|
|
||||||
Points::Single(point) => *point,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) enum Curve<'p> {
|
|
||||||
Bezier {
|
|
||||||
path: Vec<Pos2>,
|
|
||||||
lengths: Vec<f32>,
|
|
||||||
},
|
|
||||||
Catmull(Points),
|
|
||||||
Linear(&'p [Pos2]),
|
|
||||||
Perfect {
|
|
||||||
origin: Pos2,
|
|
||||||
center: Pos2,
|
|
||||||
radius: f32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BezierBuffers {
|
struct BezierBuffers {
|
||||||
buf1: Vec<Pos2>,
|
buf1: Vec<Pos2>,
|
||||||
@@ -58,7 +30,485 @@ impl BezierBuffers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> Curve<'p> {
|
// TODO: Remove Default when cleaned up
|
||||||
|
#[derive(Default)]
|
||||||
|
struct CircularArcProperties {
|
||||||
|
is_valid: bool,
|
||||||
|
theta_start: f32,
|
||||||
|
theta_range: f32,
|
||||||
|
direction: f32,
|
||||||
|
radius: f32,
|
||||||
|
centre: Pos2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Curve {
|
||||||
|
path: Vec<Pos2>,
|
||||||
|
lengths: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Curve {
|
||||||
|
pub(crate) fn new(points: &[PathControlPoint], expected_len: f32) -> Self {
|
||||||
|
let mut path = Self::calculate_path(points);
|
||||||
|
let lengths = Self::calculate_length(points, &mut path, expected_len);
|
||||||
|
|
||||||
|
Self { path, lengths }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn position_at(&self, progress: f32) -> Pos2 {
|
||||||
|
let d = self.progress_to_dist(progress);
|
||||||
|
let i = self.idx_of_dist(d);
|
||||||
|
|
||||||
|
self.interpolate_vertices(i, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress_to_dist(&self, progress: f32) -> f32 {
|
||||||
|
progress.clamp(0.0, 1.0) * self.dist()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dist(&self) -> f32 {
|
||||||
|
self.lengths.last().copied().unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn idx_of_dist(&self, d: f32) -> usize {
|
||||||
|
self.lengths
|
||||||
|
.binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal))
|
||||||
|
.map_or_else(identity, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate_vertices(&self, i: usize, d: f32) -> Pos2 {
|
||||||
|
if self.path.is_empty() {
|
||||||
|
return Pos2::zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
return self.path[0];
|
||||||
|
} else if i >= self.path.len() {
|
||||||
|
return self.path[self.path.len() - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let p0 = self.path[i - 1];
|
||||||
|
let p1 = self.path[i];
|
||||||
|
|
||||||
|
let d0 = self.lengths[i - 1];
|
||||||
|
let d1 = self.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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_path(points: &[PathControlPoint]) -> Vec<Pos2> {
|
||||||
|
if points.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = Vec::new();
|
||||||
|
let vertices: Vec<_> = points.iter().map(|p| p.pos).collect();
|
||||||
|
let mut start = 0;
|
||||||
|
|
||||||
|
for i in 0..points.len() {
|
||||||
|
if points[i].kind.is_none() && i < points.len() - 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The current vertex ends the segment
|
||||||
|
let segment_vertices = &vertices[start..i + 1];
|
||||||
|
let segment_kind = points[start].kind.unwrap_or(PathType::Linear);
|
||||||
|
|
||||||
|
// TODO: push onto `path` directly?
|
||||||
|
let sub_path = Self::calculate_subpath(segment_vertices, segment_kind);
|
||||||
|
|
||||||
|
for t in sub_path {
|
||||||
|
if path.last().filter(|&l| l == &t).is_none() {
|
||||||
|
path.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Start the new segment at the current vertex
|
||||||
|
start = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_length(
|
||||||
|
points: &[PathControlPoint],
|
||||||
|
path: &mut Vec<Pos2>,
|
||||||
|
expected_len: f32,
|
||||||
|
) -> Vec<f32> {
|
||||||
|
let mut calculated_len = 0.0;
|
||||||
|
let mut cumulative_len = vec![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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expected_len - calculated_len).abs() > f32::EPSILON {
|
||||||
|
// * In osu-stable, if the last two control points of a slider are equal, extension is not performed
|
||||||
|
let condition_opt = points
|
||||||
|
.len()
|
||||||
|
.checked_sub(2)
|
||||||
|
.and_then(|i| points.get(i..))
|
||||||
|
.filter(|suffix| suffix[0].pos == suffix[1].pos && expected_len > calculated_len);
|
||||||
|
|
||||||
|
if condition_opt.is_some() {
|
||||||
|
cumulative_len.push(calculated_len);
|
||||||
|
|
||||||
|
return cumulative_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * 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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
cumulative_len
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_subpath(sub_points: &[Pos2], kind: PathType) -> Vec<Pos2> {
|
||||||
|
match kind {
|
||||||
|
PathType::Bezier => Self::approximate_bezier(sub_points),
|
||||||
|
PathType::Catmull => Self::approximate_catmull(sub_points),
|
||||||
|
PathType::Linear => Self::approximate_linear(sub_points),
|
||||||
|
PathType::PerfectCurve => {
|
||||||
|
if let [a, b, c] = sub_points {
|
||||||
|
let sub_path = Self::approximate_circular_arc(*a, *b, *c);
|
||||||
|
|
||||||
|
if !sub_path.is_empty() {
|
||||||
|
return sub_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::approximate_bezier(sub_points)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approximate_bezier(points: &[Pos2]) -> Vec<Pos2> {
|
||||||
|
let mut path = Vec::new(); // TODO: argument?
|
||||||
|
let mut bufs = BezierBuffers::new(points.len()); // TODO: argument?
|
||||||
|
|
||||||
|
Self::approximate_bspline(&mut path, points, &mut bufs);
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approximate_catmull(points: &[Pos2]) -> Vec<Pos2> {
|
||||||
|
// TODO: argument?
|
||||||
|
let mut result = Vec::with_capacity((points.len() - 1) * CATMULL_DETAIL * 2);
|
||||||
|
|
||||||
|
let catmull_detail = CATMULL_DETAIL as f32;
|
||||||
|
|
||||||
|
for i in 0..points.len() - 1 {
|
||||||
|
let v2 = points[i];
|
||||||
|
|
||||||
|
let v1 = i
|
||||||
|
.checked_sub(1)
|
||||||
|
.and_then(|i| points.get(i).copied())
|
||||||
|
.unwrap_or(v2);
|
||||||
|
|
||||||
|
let v3 = points.get(i + 1).copied().unwrap_or_else(|| v2 * 2.0 - v1);
|
||||||
|
let v4 = points.get(i + 2).copied().unwrap_or_else(|| v3 * 2.0 - v2);
|
||||||
|
|
||||||
|
for c in 0..CATMULL_DETAIL {
|
||||||
|
let p1 = Self::catmull_find_point(v1, v2, v3, v4, c as f32 / catmull_detail);
|
||||||
|
let p2 = Self::catmull_find_point(v1, v2, v3, v4, (c + 1) as f32 / catmull_detail);
|
||||||
|
|
||||||
|
result.push(p1);
|
||||||
|
result.push(p2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approximate_linear(points: &[Pos2]) -> Vec<Pos2> {
|
||||||
|
points.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approximate_circular_arc(a: Pos2, b: Pos2, c: Pos2) -> Vec<Pos2> {
|
||||||
|
let pr = Self::circular_arc_properties(a, b, c);
|
||||||
|
|
||||||
|
if !pr.is_valid {
|
||||||
|
return Self::approximate_bezier(&[a, b, c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// * We select the amount of points for the approximation by requiring the discrete curvature
|
||||||
|
// * to be smaller than the provided tolerance. The exact angle required to meet the tolerance
|
||||||
|
// * is: 2 * Math.Acos(1 - TOLERANCE / r)
|
||||||
|
// * The special case is required for extremely short sliders where the radius is smaller than
|
||||||
|
// * the tolerance. This is a pathological rather than a realistic case.
|
||||||
|
let amount_points = if 2.0 * pr.radius <= CIRCULAR_ARC_TOLERANCE {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
let divisor = 2.0 * (1.0 - CIRCULAR_ARC_TOLERANCE / pr.radius).acos();
|
||||||
|
|
||||||
|
((pr.theta_range / divisor).ceil() as usize).max(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: argument?
|
||||||
|
let mut output = Vec::with_capacity(amount_points);
|
||||||
|
|
||||||
|
for i in 0..amount_points {
|
||||||
|
let fract = i as f32 / (amount_points - 1) as f32;
|
||||||
|
let theta = pr.theta_start + pr.direction * fract * pr.theta_range;
|
||||||
|
let (sin, cos) = theta.sin_cos();
|
||||||
|
let origin = Pos2 { x: cos, y: sin };
|
||||||
|
|
||||||
|
output.push(pr.centre + origin * pr.radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approximate_bspline(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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(points[p - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_find_point(v1: Pos2, v2: Pos2, v3: Pos2, v4: Pos2, t: f32) -> Pos2 {
|
||||||
|
let t2 = t * t;
|
||||||
|
let t3 = t * t * t;
|
||||||
|
|
||||||
|
let x = 0.5
|
||||||
|
* (2.0 * v2.x
|
||||||
|
+ (-v1.x + v3.x) * t
|
||||||
|
+ (2.0 * v1.x - 5.0 * v2.x + 4.0 * v3.x - v4.x) * t2
|
||||||
|
+ (-v1.x + 3.0 * v2.x - 3.0 * v3.x + v4.x) * t3);
|
||||||
|
|
||||||
|
let y = 0.5
|
||||||
|
* (2.0 * v2.y
|
||||||
|
+ (-v1.y + v3.y) * t
|
||||||
|
+ (2.0 * v1.y - 5.0 * v2.y + 4.0 * v3.y - v4.y) * t2
|
||||||
|
+ (-v1.y + 3.0 * v2.y - 3.0 * v3.y + v4.y) * t3);
|
||||||
|
|
||||||
|
Pos2 { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn circular_arc_properties(a: Pos2, b: Pos2, c: Pos2) -> CircularArcProperties {
|
||||||
|
// * If we have a degenerate triangle where a side-length is almost zero,
|
||||||
|
// * then give up and fallback to a more numerically stable method.
|
||||||
|
if ((b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y)).abs() <= f32::EPSILON {
|
||||||
|
// * Implicitly sets `is_valid` to false
|
||||||
|
return CircularArcProperties::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
|
||||||
|
let a_s_q = a.length_squared();
|
||||||
|
let b_s_q = b.length_squared();
|
||||||
|
let c_s_q = c.length_squared();
|
||||||
|
|
||||||
|
let centre = Pos2 {
|
||||||
|
x: (a_s_q * (b - c).y + b_s_q * (c - a).y + c_s_q * (a - b).y) / d,
|
||||||
|
y: ((c - b).x + b_s_q * (a - c).x + c_s_q * (b - a).x) / d,
|
||||||
|
};
|
||||||
|
|
||||||
|
let d_a = a - centre;
|
||||||
|
let d_c = c - centre;
|
||||||
|
|
||||||
|
let radius = d_a.length();
|
||||||
|
|
||||||
|
let theta_start = d_a.y.atan2(d_a.x);
|
||||||
|
let mut theta_end = d_c.y.atan2(d_c.x);
|
||||||
|
|
||||||
|
while theta_end < theta_start {
|
||||||
|
theta_end += 2.0 * PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut direction = 1.0;
|
||||||
|
let mut theta_range = theta_end - theta_start;
|
||||||
|
|
||||||
|
// * Decide in which direction to draw the circle,
|
||||||
|
// * depending on which side of AC B lies.
|
||||||
|
let mut ortho_a_to_c = c - a;
|
||||||
|
|
||||||
|
ortho_a_to_c = Pos2 {
|
||||||
|
x: ortho_a_to_c.y,
|
||||||
|
y: -ortho_a_to_c.x,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ortho_a_to_c.dot(b - a) < 0.0 {
|
||||||
|
direction = -direction;
|
||||||
|
theta_range = 2.0 * PI - theta_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
CircularArcProperties {
|
||||||
|
is_valid: true,
|
||||||
|
theta_start,
|
||||||
|
theta_range,
|
||||||
|
direction,
|
||||||
|
radius,
|
||||||
|
centre,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum Points {
|
||||||
|
Single(Pos2),
|
||||||
|
Multi(Vec<Pos2>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Points {
|
||||||
|
#[inline]
|
||||||
|
fn point_at_distance(&self, dist: f32) -> Pos2 {
|
||||||
|
match self {
|
||||||
|
Points::Multi(points) => math_util::point_at_distance(points, dist),
|
||||||
|
Points::Single(point) => *point,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum Curve_<'p> {
|
||||||
|
Bezier {
|
||||||
|
path: Vec<Pos2>,
|
||||||
|
lengths: Vec<f32>,
|
||||||
|
},
|
||||||
|
Catmull(Points),
|
||||||
|
Linear(&'p [Pos2]),
|
||||||
|
Perfect {
|
||||||
|
origin: Pos2,
|
||||||
|
center: Pos2,
|
||||||
|
radius: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'p> Curve_<'p> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new(points: &'p [Pos2], kind: PathType, expected_len: f32) -> Self {
|
pub(crate) fn new(points: &'p [Pos2], kind: PathType, expected_len: f32) -> Self {
|
||||||
match kind {
|
match kind {
|
||||||
@@ -80,7 +530,7 @@ impl<'p> Curve<'p> {
|
|||||||
|
|
||||||
if len == 1 {
|
if len == 1 {
|
||||||
return Self::Bezier {
|
return Self::Bezier {
|
||||||
path: points.to_owned(),
|
path: points,
|
||||||
lengths: vec![0.0],
|
lengths: vec![0.0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,8 +553,7 @@ impl<'p> Curve<'p> {
|
|||||||
|
|
||||||
// Then calculated cumulative lenghts
|
// Then calculated cumulative lenghts
|
||||||
let mut calculated_len = 0.0;
|
let mut calculated_len = 0.0;
|
||||||
let mut cumulative_len = Vec::new();
|
let mut cumulative_len = vec![0.0];
|
||||||
cumulative_len.push(0.0);
|
|
||||||
|
|
||||||
for i in 0..path.len() - 1 {
|
for i in 0..path.len() - 1 {
|
||||||
let diff = path[i + 1] - path[i];
|
let diff = path[i + 1] - path[i];
|
||||||
@@ -280,7 +729,7 @@ impl<'p> Curve<'p> {
|
|||||||
return Self::Catmull(Points::Single(points[0]));
|
return Self::Catmull(Points::Single(points[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result = Vec::with_capacity((len as f32 * CATMULL_DETAIL * 2.0) as usize);
|
let mut result = Vec::with_capacity((len * CATMULL_DETAIL * 2) as usize);
|
||||||
|
|
||||||
// Handle first iteration distinctly because of v1
|
// Handle first iteration distinctly because of v1
|
||||||
let v1 = points[0];
|
let v1 = points[0];
|
||||||
@@ -315,8 +764,10 @@ impl<'p> Curve<'p> {
|
|||||||
let y3 = 2.0 * v1.y - 5.0 * v2.y + 4.0 * v3.y - v4.y;
|
let y3 = 2.0 * v1.y - 5.0 * v2.y + 4.0 * v3.y - v4.y;
|
||||||
let y4 = -v1.y + 3.0 * (v2.y - v3.y) + v4.y;
|
let y4 = -v1.y + 3.0 * (v2.y - v3.y) + v4.y;
|
||||||
|
|
||||||
|
let catmull_detail = CATMULL_DETAIL as f32;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let t1 = c / CATMULL_DETAIL;
|
let t1 = c / catmull_detail;
|
||||||
let t2 = t1 * t1;
|
let t2 = t1 * t1;
|
||||||
let t3 = t2 * t1;
|
let t3 = t2 * t1;
|
||||||
|
|
||||||
@@ -325,7 +776,7 @@ impl<'p> Curve<'p> {
|
|||||||
y: 0.5 * (y1 + y2 * t1 + y3 * t2 + y4 * t3),
|
y: 0.5 * (y1 + y2 * t1 + y3 * t2 + y4 * t3),
|
||||||
});
|
});
|
||||||
|
|
||||||
let t1 = (c + 1.0) / CATMULL_DETAIL;
|
let t1 = (c + 1.0) / catmull_detail;
|
||||||
let t2 = t1 * t1;
|
let t2 = t1 * t1;
|
||||||
let t3 = t2 * t1;
|
let t3 = t2 * t1;
|
||||||
|
|
||||||
@@ -336,7 +787,7 @@ impl<'p> Curve<'p> {
|
|||||||
|
|
||||||
c += 1.0;
|
c += 1.0;
|
||||||
|
|
||||||
if c >= CATMULL_DETAIL {
|
if c >= catmull_detail {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-20
@@ -70,17 +70,17 @@ pub fn stars(
|
|||||||
pixel_len,
|
pixel_len,
|
||||||
repeats,
|
repeats,
|
||||||
curve_points,
|
curve_points,
|
||||||
path_type,
|
|
||||||
} => {
|
} => {
|
||||||
// HR business
|
// HR business
|
||||||
last_pos
|
last_pos.replace(
|
||||||
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
|
h.pos.x + curve_points[curve_points.len() - 1].pos.x - curve_points[0].pos.x,
|
||||||
|
);
|
||||||
*last_time = h.start_time;
|
*last_time = h.start_time;
|
||||||
|
|
||||||
// Responsible for timing point values
|
// Responsible for timing point values
|
||||||
slider_state.update(h.start_time);
|
slider_state.update(h.start_time);
|
||||||
|
|
||||||
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
let mut tick_distance = 100.0 * map.slider_mult / map.tick_rate;
|
||||||
|
|
||||||
if map.version >= 8 {
|
if map.version >= 8 {
|
||||||
tick_distance /=
|
tick_distance /=
|
||||||
@@ -88,11 +88,11 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
||||||
/ (map.sv * slider_state.speed_mult)
|
/ (map.slider_mult * slider_state.speed_mult)
|
||||||
/ 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, *pixel_len);
|
let curve = Curve::new(curve_points, *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));
|
||||||
@@ -103,7 +103,8 @@ pub fn stars(
|
|||||||
// Tick of the first span
|
// Tick of the first span
|
||||||
if current_distance < target {
|
if current_distance < target {
|
||||||
for tick_idx in 1.. {
|
for tick_idx in 1.. {
|
||||||
let pos = curve.point_at_distance(current_distance);
|
let progress = current_distance / *pixel_len;
|
||||||
|
let pos = curve.position_at(progress);
|
||||||
let time = h.start_time + time_add * tick_idx as f32;
|
let time = h.start_time + time_add * tick_idx as f32;
|
||||||
ticks.push((pos, time));
|
ticks.push((pos, time));
|
||||||
current_distance += tick_distance;
|
current_distance += tick_distance;
|
||||||
@@ -129,7 +130,8 @@ pub fn stars(
|
|||||||
for repeat_id in 1..*repeats {
|
for repeat_id in 1..*repeats {
|
||||||
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
||||||
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||||
let pos = curve.point_at_distance(dist);
|
let progress = dist / *pixel_len;
|
||||||
|
let pos = curve.position_at(progress);
|
||||||
|
|
||||||
// Reverse tick
|
// Reverse tick
|
||||||
slider_objects.push((pos, h.start_time + time_offset));
|
slider_objects.push((pos, h.start_time + time_offset));
|
||||||
@@ -148,8 +150,8 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Slider tail
|
// Slider tail
|
||||||
let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
// let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||||
let pos = curve.point_at_distance(dist_end);
|
let pos = curve.position_at(1.0); // TODO: what if reversing odd amount?
|
||||||
slider_objects.push((pos, h.start_time + duration));
|
slider_objects.push((pos, h.start_time + duration));
|
||||||
|
|
||||||
fruits += 1 + *repeats;
|
fruits += 1 + *repeats;
|
||||||
@@ -301,17 +303,17 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
pixel_len,
|
pixel_len,
|
||||||
repeats,
|
repeats,
|
||||||
curve_points,
|
curve_points,
|
||||||
path_type,
|
|
||||||
} => {
|
} => {
|
||||||
// HR business
|
// HR business
|
||||||
last_pos
|
last_pos.replace(
|
||||||
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
|
h.pos.x + curve_points[curve_points.len() - 1].pos.x - curve_points[0].pos.x,
|
||||||
|
);
|
||||||
*last_time = h.start_time;
|
*last_time = h.start_time;
|
||||||
|
|
||||||
// Responsible for timing point values
|
// Responsible for timing point values
|
||||||
slider_state.update(h.start_time);
|
slider_state.update(h.start_time);
|
||||||
|
|
||||||
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
let mut tick_distance = 100.0 * map.slider_mult / map.tick_rate;
|
||||||
|
|
||||||
if map.version >= 8 {
|
if map.version >= 8 {
|
||||||
tick_distance /=
|
tick_distance /=
|
||||||
@@ -319,11 +321,11 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
||||||
/ (map.sv * slider_state.speed_mult)
|
/ (map.slider_mult * slider_state.speed_mult)
|
||||||
/ 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, *pixel_len);
|
let curve = Curve::new(curve_points, *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));
|
||||||
@@ -334,7 +336,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
// Tick of the first span
|
// Tick of the first span
|
||||||
if current_distance < target {
|
if current_distance < target {
|
||||||
for tick_idx in 1.. {
|
for tick_idx in 1.. {
|
||||||
let pos = curve.point_at_distance(current_distance);
|
let progress = current_distance / *pixel_len;
|
||||||
|
let pos = curve.position_at(progress);
|
||||||
let time = h.start_time + time_add * tick_idx as f32;
|
let time = h.start_time + time_add * tick_idx as f32;
|
||||||
ticks.push((pos, time));
|
ticks.push((pos, time));
|
||||||
current_distance += tick_distance;
|
current_distance += tick_distance;
|
||||||
@@ -357,7 +360,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
for repeat_id in 1..*repeats {
|
for repeat_id in 1..*repeats {
|
||||||
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
||||||
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||||
let pos = curve.point_at_distance(dist);
|
let progress = dist / *pixel_len;
|
||||||
|
let pos = curve.position_at(progress);
|
||||||
|
|
||||||
// Reverse tick
|
// Reverse tick
|
||||||
slider_objects.push((pos, h.start_time + time_offset));
|
slider_objects.push((pos, h.start_time + time_offset));
|
||||||
@@ -374,8 +378,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Slider tail
|
// Slider tail
|
||||||
let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
// let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||||
let pos = curve.point_at_distance(dist_end);
|
let pos = curve.position_at(1.0); // TODO: what if reversing odd amount?
|
||||||
slider_objects.push((pos, h.start_time + duration));
|
slider_objects.push((pos, h.start_time + duration));
|
||||||
|
|
||||||
let iter = slider_objects.into_iter().map(CatchObject::new);
|
let iter = slider_objects.into_iter().map(CatchObject::new);
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ impl<'p> SliderState<'p> {
|
|||||||
|
|
||||||
let (beat_len, speed_mult) = match control_points.next() {
|
let (beat_len, speed_mult) = match control_points.next() {
|
||||||
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
||||||
Some(ControlPoint::Difficulty { speed_mult, .. }) => (1000.0, speed_mult),
|
Some(ControlPoint::Difficulty {
|
||||||
|
slider_velocity: speed_mult,
|
||||||
|
..
|
||||||
|
}) => (1000.0, speed_mult),
|
||||||
None => (1000.0, 1.0),
|
None => (1000.0, 1.0),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,7 +37,10 @@ impl<'p> SliderState<'p> {
|
|||||||
self.beat_len = *beat_len;
|
self.beat_len = *beat_len;
|
||||||
self.speed_mult = 1.0;
|
self.speed_mult = 1.0;
|
||||||
}
|
}
|
||||||
ControlPoint::Difficulty { speed_mult, .. } => self.speed_mult = *speed_mult,
|
ControlPoint::Difficulty {
|
||||||
|
slider_velocity: speed_mult,
|
||||||
|
..
|
||||||
|
} => self.speed_mult = *speed_mult,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.next = self.control_points.next();
|
self.next = self.control_points.next();
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_osu() {
|
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/2753127.osu").unwrap();
|
||||||
// let file = std::fs::File::open("E:Games/osu!/beatmaps/2571051.osu").unwrap();
|
// let file = std::fs::File::open("E:Games/osu!/beatmaps/2571051.osu").unwrap();
|
||||||
let map = Beatmap::parse(file).unwrap();
|
let map = Beatmap::parse(file).unwrap();
|
||||||
|
|
||||||
@@ -350,6 +350,7 @@ fn custom_osu() {
|
|||||||
let iters = 500;
|
let iters = 500;
|
||||||
let accum = start.elapsed();
|
let accum = start.elapsed();
|
||||||
|
|
||||||
|
// * Tiny benchmark
|
||||||
// let mut accum = accum;
|
// let mut accum = accum;
|
||||||
|
|
||||||
// for _ in 0..iters {
|
// for _ in 0..iters {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||||
|
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
||||||
|
|
||||||
pub(crate) struct OsuObject {
|
pub(crate) struct OsuObject {
|
||||||
pub(crate) time: f32,
|
pub(crate) time: f32,
|
||||||
@@ -47,7 +48,6 @@ impl OsuObject {
|
|||||||
pixel_len,
|
pixel_len,
|
||||||
repeats,
|
repeats,
|
||||||
curve_points,
|
curve_points,
|
||||||
path_type,
|
|
||||||
} => {
|
} => {
|
||||||
// Key values which are computed here
|
// Key values which are computed here
|
||||||
let mut lazy_end_pos = h.pos;
|
let mut lazy_end_pos = h.pos;
|
||||||
@@ -56,21 +56,26 @@ impl OsuObject {
|
|||||||
// 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 approx_follow_circle_radius = radius * 3.0;
|
let approx_follow_circle_radius = radius * 3.0;
|
||||||
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||||
|
|
||||||
if map.version >= 8 {
|
if map.version >= 8 {
|
||||||
tick_distance /=
|
tick_dist /=
|
||||||
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
|
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
|
||||||
/ (map.sv * slider_state.speed_mult)
|
|
||||||
/ 100.0;
|
|
||||||
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, *pixel_len);
|
let curve = Curve::new(curve_points, *pixel_len);
|
||||||
|
|
||||||
|
let velocity =
|
||||||
|
(BASE_SCORING_DISTANCE * map.slider_mult * slider_state.slider_velocity)
|
||||||
|
/ slider_state.beat_len;
|
||||||
|
|
||||||
|
let end_time = h.start_time + span_count * curve.dist() / velocity;
|
||||||
|
let duration = end_time - h.start_time;
|
||||||
|
let span_duration = duration / span_count;
|
||||||
|
|
||||||
// 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`
|
||||||
@@ -86,25 +91,23 @@ impl OsuObject {
|
|||||||
progress %= 1.0;
|
progress %= 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let curr_dist = pixel_len * progress;
|
let curr_pos = curve.position_at(progress);
|
||||||
let curr_pos = curve.point_at_distance(curr_dist);
|
let diff = h.pos + curr_pos - lazy_end_pos;
|
||||||
|
|
||||||
let diff = curr_pos - lazy_end_pos;
|
|
||||||
let mut dist = diff.length();
|
let mut dist = diff.length();
|
||||||
|
|
||||||
if dist > approx_follow_circle_radius {
|
if dist > approx_follow_circle_radius {
|
||||||
|
// * The cursor would be outside the follow circle, we need to move it
|
||||||
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut current_distance = tick_distance;
|
let mut current_distance = tick_dist;
|
||||||
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
|
let time_add = duration * (tick_dist / (pixel_len * span_count));
|
||||||
|
|
||||||
let target = pixel_len - tick_distance / 8.0;
|
let target = pixel_len - tick_dist / 8.0;
|
||||||
ticks.reserve((target / tick_distance) as usize);
|
ticks.reserve((target / tick_dist) as usize);
|
||||||
|
|
||||||
// Tick of the first span
|
// Tick of the first span
|
||||||
if current_distance < target {
|
if current_distance < target {
|
||||||
@@ -112,7 +115,7 @@ impl OsuObject {
|
|||||||
let time = h.start_time + time_add * tick_idx as f32;
|
let time = h.start_time + time_add * tick_idx as f32;
|
||||||
compute_vertex(time);
|
compute_vertex(time);
|
||||||
ticks.push(time);
|
ticks.push(time);
|
||||||
current_distance += tick_distance;
|
current_distance += tick_dist;
|
||||||
|
|
||||||
if current_distance >= target {
|
if current_distance >= target {
|
||||||
break;
|
break;
|
||||||
@@ -146,7 +149,7 @@ impl OsuObject {
|
|||||||
|
|
||||||
ticks.clear();
|
ticks.clear();
|
||||||
|
|
||||||
let end_pos = curve.point_at_distance(*pixel_len);
|
let end_pos = curve.position_at(1.0); // TODO: what if reversing odd amount?
|
||||||
travel_dist *= scaling_factor;
|
travel_dist *= scaling_factor;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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: f32,
|
||||||
pub(crate) speed_mult: f32,
|
pub(crate) slider_velocity: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> SliderState<'p> {
|
impl<'p> SliderState<'p> {
|
||||||
@@ -12,9 +12,12 @@ impl<'p> SliderState<'p> {
|
|||||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||||
let mut control_points = ControlPointIter::new(map);
|
let mut control_points = ControlPointIter::new(map);
|
||||||
|
|
||||||
let (beat_len, speed_mult) = match control_points.next() {
|
let (beat_len, slider_velocity) = match control_points.next() {
|
||||||
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
||||||
Some(ControlPoint::Difficulty { speed_mult, .. }) => (1000.0, speed_mult),
|
Some(ControlPoint::Difficulty {
|
||||||
|
slider_velocity: speed_mult,
|
||||||
|
..
|
||||||
|
}) => (1000.0, speed_mult),
|
||||||
None => (1000.0, 1.0),
|
None => (1000.0, 1.0),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ impl<'p> SliderState<'p> {
|
|||||||
next: control_points.next(),
|
next: control_points.next(),
|
||||||
control_points,
|
control_points,
|
||||||
beat_len,
|
beat_len,
|
||||||
speed_mult,
|
slider_velocity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,9 +35,12 @@ impl<'p> SliderState<'p> {
|
|||||||
match next {
|
match next {
|
||||||
ControlPoint::Timing { beat_len, .. } => {
|
ControlPoint::Timing { beat_len, .. } => {
|
||||||
self.beat_len = *beat_len;
|
self.beat_len = *beat_len;
|
||||||
self.speed_mult = 1.0;
|
self.slider_velocity = 1.0;
|
||||||
}
|
}
|
||||||
ControlPoint::Difficulty { speed_mult, .. } => self.speed_mult = *speed_mult,
|
ControlPoint::Difficulty {
|
||||||
|
slider_velocity: speed_mult,
|
||||||
|
..
|
||||||
|
} => self.slider_velocity = *speed_mult,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.next = self.control_points.next();
|
self.next = self.control_points.next();
|
||||||
@@ -88,10 +94,10 @@ mod test {
|
|||||||
|
|
||||||
state.update(3.0);
|
state.update(3.0);
|
||||||
assert_eq!(state.beat_len, 20.0);
|
assert_eq!(state.beat_len, 20.0);
|
||||||
assert_eq!(state.speed_mult, 1.0);
|
assert_eq!(state.slider_velocity, 1.0);
|
||||||
|
|
||||||
state.update(5.0);
|
state.update(5.0);
|
||||||
assert_eq!(state.beat_len, 30.0);
|
assert_eq!(state.beat_len, 30.0);
|
||||||
assert_eq!(state.speed_mult, 45.0);
|
assert_eq!(state.slider_velocity, 45.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use super::Pos2;
|
use super::{PathControlPoint, Pos2};
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "fruits",
|
feature = "fruits",
|
||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
))]
|
))]
|
||||||
use super::PathType;
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
/// "Intermediate" hitobject created through parsing.
|
/// "Intermediate" hitobject created through parsing.
|
||||||
@@ -63,8 +61,7 @@ pub enum HitObjectKind {
|
|||||||
Slider {
|
Slider {
|
||||||
pixel_len: f32,
|
pixel_len: f32,
|
||||||
repeats: usize,
|
repeats: usize,
|
||||||
curve_points: Vec<Pos2>,
|
curve_points: Vec<PathControlPoint>,
|
||||||
path_type: PathType,
|
|
||||||
},
|
},
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
feature = "fruits",
|
feature = "fruits",
|
||||||
|
|||||||
+530
-231
@@ -1,8 +1,4 @@
|
|||||||
#[cfg(any(
|
use crate::math_util::is_linear;
|
||||||
feature = "fruits",
|
|
||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
|
||||||
))]
|
|
||||||
use crate::math_util;
|
|
||||||
|
|
||||||
mod attributes;
|
mod attributes;
|
||||||
mod control_point;
|
mod control_point;
|
||||||
@@ -21,7 +17,6 @@ pub use pos2::Pos2;
|
|||||||
use sort::legacy_sort;
|
use sort::legacy_sort;
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
use std::io::{BufRead, BufReader, Read};
|
use std::io::{BufRead, BufReader, Read};
|
||||||
@@ -32,30 +27,34 @@ use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
|
|||||||
#[cfg(feature = "async_std")]
|
#[cfg(feature = "async_std")]
|
||||||
use async_std::io::{prelude::BufReadExt, BufReader as AsyncBufReader, Read as AsyncRead};
|
use async_std::io::{prelude::BufReadExt, BufReader as AsyncBufReader, Read as AsyncRead};
|
||||||
|
|
||||||
macro_rules! sort {
|
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
||||||
($slice:expr) => {
|
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal));
|
||||||
$slice.sort_unstable_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal))
|
|
||||||
};
|
|
||||||
|
|
||||||
(stable $slice:expr) => {
|
|
||||||
$slice.sort_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal))
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! next_field {
|
fn sort<T: PartialOrd>(slice: &mut [T]) {
|
||||||
($opt:expr, $err:literal) => {
|
slice.sort_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal));
|
||||||
$opt.ok_or_else(|| ParseError::MissingField($err))?
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! validate_float {
|
trait OptionExt<T> {
|
||||||
($x:expr) => {{
|
fn next_field(self, field: &'static str) -> Result<T, ParseError>;
|
||||||
if $x.is_finite() {
|
}
|
||||||
$x
|
|
||||||
} else {
|
impl<T> OptionExt<T> for Option<T> {
|
||||||
return Err(ParseError::InvalidFloatingPoint);
|
fn next_field(self, field: &'static str) -> Result<T, ParseError> {
|
||||||
}
|
self.ok_or_else(|| ParseError::MissingField(field))
|
||||||
}};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait F32Ext: Sized {
|
||||||
|
fn validate(self) -> Result<Self, ParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl F32Ext for f32 {
|
||||||
|
fn validate(self) -> Result<Self, ParseError> {
|
||||||
|
self.is_finite()
|
||||||
|
.then(|| self)
|
||||||
|
.ok_or(ParseError::InvalidFloatingPoint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! line_prepare {
|
macro_rules! line_prepare {
|
||||||
@@ -236,12 +235,12 @@ macro_rules! parse_difficulty_body {
|
|||||||
$buf.clear();
|
$buf.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
$self.od = next_field!(od, "od");
|
$self.od = od.next_field("od")?;
|
||||||
$self.cs = next_field!(cs, "cs");
|
$self.cs = cs.next_field("cs")?;
|
||||||
$self.hp = next_field!(hp, "hp");
|
$self.hp = hp.next_field("hp")?;
|
||||||
$self.ar = ar.unwrap_or($self.od);
|
$self.ar = ar.unwrap_or($self.od);
|
||||||
$self.sv = next_field!(sv, "sv");
|
$self.slider_mult = sv.next_field("sv")?;
|
||||||
$self.tick_rate = next_field!(tick_rate, "sv");
|
$self.tick_rate = tick_rate.next_field("tick rate")?;
|
||||||
|
|
||||||
Ok(empty)
|
Ok(empty)
|
||||||
}};
|
}};
|
||||||
@@ -312,14 +311,14 @@ macro_rules! parse_timingpoints_body {
|
|||||||
|
|
||||||
let mut split = line.split(',');
|
let mut split = line.split(',');
|
||||||
|
|
||||||
let time = next_field!(split.next(), "timing point time")
|
let time = split
|
||||||
|
.next()
|
||||||
|
.next_field("timing point time")?
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<f32>()?;
|
.parse::<f32>()?
|
||||||
validate_float!(time);
|
.validate()?;
|
||||||
|
|
||||||
let beat_len = next_field!(split.next(), "beat len")
|
let beat_len: f32 = split.next().next_field("beat len")?.trim().parse()?;
|
||||||
.trim()
|
|
||||||
.parse::<f32>()?;
|
|
||||||
|
|
||||||
if beat_len < 0.0 {
|
if beat_len < 0.0 {
|
||||||
let point = DifficultyPoint {
|
let point = DifficultyPoint {
|
||||||
@@ -348,11 +347,11 @@ macro_rules! parse_timingpoints_body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unsorted_timings {
|
if unsorted_timings {
|
||||||
sort!($self.timing_points);
|
sort_unstable(&mut $self.timing_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
if unsorted_difficulties {
|
if unsorted_difficulties {
|
||||||
sort!($self.difficulty_points);
|
sort_unstable(&mut $self.difficulty_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(empty)
|
Ok(empty)
|
||||||
@@ -395,217 +394,217 @@ macro_rules! parse_timingpoints {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_hitobjects_body {
|
// macro_rules! parse_hitobjects_body {
|
||||||
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
// ($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||||
let mut unsorted = false;
|
// let mut unsorted = false;
|
||||||
let mut prev_time = 0.0;
|
// let mut prev_time = 0.0;
|
||||||
|
|
||||||
let mut empty = true;
|
// let mut empty = true;
|
||||||
|
|
||||||
while read_line!($reader, $buf)? != 0 {
|
// while read_line!($reader, $buf)? != 0 {
|
||||||
let line = line_prepare!($buf);
|
// let line = line_prepare!($buf);
|
||||||
|
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
// if line.starts_with('[') && line.ends_with(']') {
|
||||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
// *$section = Section::from_str(&line[1..line.len() - 1]);
|
||||||
empty = false;
|
// empty = false;
|
||||||
$buf.clear();
|
// $buf.clear();
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
|
|
||||||
let mut split = line.split(',');
|
// let mut split = line.split(',');
|
||||||
|
|
||||||
let pos = Pos2 {
|
// let pos = Pos2 {
|
||||||
x: next_field!(split.next(), "x position").parse()?,
|
// x: next_field!(split.next(), "x position").parse()?,
|
||||||
y: next_field!(split.next(), "y position").parse()?,
|
// y: next_field!(split.next(), "y position").parse()?,
|
||||||
};
|
// };
|
||||||
|
|
||||||
let time: f32 = next_field!(split.next(), "hitobject time")
|
// let time: f32 = next_field!(split.next(), "hitobject time")
|
||||||
.trim()
|
// .trim()
|
||||||
.parse()?;
|
// .parse()?;
|
||||||
|
|
||||||
validate_float!(time);
|
// validate_float!(time);
|
||||||
|
|
||||||
if !$self.hit_objects.is_empty() && time < prev_time {
|
// if !$self.hit_objects.is_empty() && time < prev_time {
|
||||||
unsorted = true;
|
// unsorted = true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
let kind: u8 = next_field!(split.next(), "hitobject kind").parse()?;
|
// let kind: u8 = next_field!(split.next(), "hitobject kind").parse()?;
|
||||||
let sound = split.next().map(str::parse).transpose()?.unwrap_or(0);
|
// let sound = split.next().map(str::parse).transpose()?.unwrap_or(0);
|
||||||
|
|
||||||
let kind = if kind & Self::CIRCLE_FLAG > 0 {
|
// let kind = if kind & Self::CIRCLE_FLAG > 0 {
|
||||||
$self.n_circles += 1;
|
// $self.n_circles += 1;
|
||||||
|
|
||||||
HitObjectKind::Circle
|
// HitObjectKind::Circle
|
||||||
} else if kind & Self::SLIDER_FLAG > 0 {
|
// } else if kind & Self::SLIDER_FLAG > 0 {
|
||||||
$self.n_sliders += 1;
|
// $self.n_sliders += 1;
|
||||||
|
|
||||||
#[cfg(any(
|
// #[cfg(any(
|
||||||
feature = "fruits",
|
// feature = "fruits",
|
||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
// all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
))]
|
// ))]
|
||||||
{
|
// {
|
||||||
let mut curve_points = Vec::with_capacity(4);
|
// let mut curve_points = Vec::with_capacity(4);
|
||||||
curve_points.push(pos);
|
// curve_points.push(pos);
|
||||||
|
|
||||||
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")
|
// let mut repeats: usize = next_field!(split.next(), "repeats")
|
||||||
.parse()?;
|
// .parse()?;
|
||||||
|
|
||||||
if repeats > 9000 {
|
// if repeats > 9000 {
|
||||||
return Err(ParseError::TooManyRepeats);
|
// return Err(ParseError::TooManyRepeats);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// * osu-stable treated the first span of the slider
|
// // * osu-stable treated the first span of the slider
|
||||||
// * as a repeat, but no repeats are happening
|
// // * as a repeat, but no repeats are happening
|
||||||
repeats = repeats.saturating_sub(1);
|
// 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()?;
|
||||||
|
|
||||||
for pos in curve_point_iter {
|
// for pos in curve_point_iter {
|
||||||
let mut v = pos.split(':').map(str::parse);
|
// let mut v = pos.split(':').map(str::parse);
|
||||||
|
|
||||||
match (v.next(), v.next()) {
|
// match (v.next(), v.next()) {
|
||||||
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
// (Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||||
_ => return Err(ParseError::InvalidCurvePoints),
|
// _ => return Err(ParseError::InvalidCurvePoints),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
match path_type {
|
// match path_type {
|
||||||
PathType::Linear if curve_points.len() % 2 == 0 => {
|
// PathType::Linear if curve_points.len() % 2 == 0 => {
|
||||||
// Assert that the points are of the form A|B|B|C|C|E
|
// // Assert that the points are of the form A|B|B|C|C|E
|
||||||
if math_util::valid_linear(&curve_points) {
|
// if math_util::valid_linear(&curve_points) {
|
||||||
for i in (2..curve_points.len() - 1).rev().step_by(2) {
|
// for i in (2..curve_points.len() - 1).rev().step_by(2) {
|
||||||
curve_points.remove(i);
|
// curve_points.remove(i);
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
path_type = PathType::Bezier;
|
// path_type = PathType::Bezier;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
PathType::PerfectCurve if curve_points.len() == 3 => {
|
// PathType::PerfectCurve if curve_points.len() == 3 => {
|
||||||
if math_util::is_linear(curve_points[0], curve_points[1], curve_points[2]) {
|
// if math_util::is_linear(curve_points[0], curve_points[1], curve_points[2]) {
|
||||||
path_type = PathType::Linear;
|
// path_type = PathType::Linear;
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
PathType::Catmull => {},
|
// PathType::Catmull => {},
|
||||||
_ => path_type = PathType::Bezier,
|
// _ => path_type = PathType::Bezier,
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Reduce amount of curvepoints but keep the elements evenly spaced.
|
// // Reduce amount of curvepoints but keep the elements evenly spaced.
|
||||||
// Necessary to handle maps like XNOR (2573164) which have
|
// // Necessary to handle maps like XNOR (2573164) which have
|
||||||
// tens of thousands of curvepoints more efficiently.
|
// // tens of thousands of curvepoints more efficiently.
|
||||||
while curve_points.len() > CURVE_POINT_THRESHOLD {
|
// while curve_points.len() > CURVE_POINT_THRESHOLD {
|
||||||
let last = curve_points[curve_points.len() - 1];
|
// let last = curve_points[curve_points.len() - 1];
|
||||||
let last_idx = (curve_points.len() - 1) / 2;
|
// let last_idx = (curve_points.len() - 1) / 2;
|
||||||
|
|
||||||
for i in 1..=last_idx {
|
// for i in 1..=last_idx {
|
||||||
curve_points.swap(i, 2 * i);
|
// curve_points.swap(i, 2 * i);
|
||||||
}
|
// }
|
||||||
|
|
||||||
curve_points[last_idx] = last;
|
// curve_points[last_idx] = last;
|
||||||
curve_points.truncate(last_idx + 1);
|
// curve_points.truncate(last_idx + 1);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if curve_points.is_empty() {
|
// if curve_points.is_empty() {
|
||||||
HitObjectKind::Circle
|
// HitObjectKind::Circle
|
||||||
} else {
|
// } else {
|
||||||
// TODO: Should be Option<f32>?
|
// // TODO: Should be Option<f32>?
|
||||||
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)
|
||||||
.min(MAX_COORDINATE_VALUE);
|
// .min(MAX_COORDINATE_VALUE);
|
||||||
|
|
||||||
HitObjectKind::Slider {
|
// HitObjectKind::Slider {
|
||||||
repeats,
|
// repeats,
|
||||||
pixel_len,
|
// pixel_len,
|
||||||
curve_points,
|
// curve_points,
|
||||||
path_type,
|
// path_type,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[cfg(not(any(
|
// #[cfg(not(any(
|
||||||
feature = "fruits",
|
// feature = "fruits",
|
||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
// all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
)))]
|
// )))]
|
||||||
{
|
// {
|
||||||
let repeats = next_field!(split.nth(1), "repeats").parse::<usize>()?;
|
// let repeats = next_field!(split.nth(1), "repeats").parse::<usize>()?;
|
||||||
let len: f32 = next_field!(split.next(), "pixel len").parse()?;
|
// let len: f32 = next_field!(split.next(), "pixel len").parse()?;
|
||||||
|
|
||||||
HitObjectKind::Slider {
|
// HitObjectKind::Slider {
|
||||||
repeats,
|
// repeats,
|
||||||
pixel_len: len,
|
// pixel_len: len,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else if kind & Self::SPINNER_FLAG > 0 {
|
// } else if kind & Self::SPINNER_FLAG > 0 {
|
||||||
$self.n_spinners += 1;
|
// $self.n_spinners += 1;
|
||||||
let end_time = next_field!(split.next(), "spinner endtime").parse()?;
|
// let end_time = next_field!(split.next(), "spinner endtime").parse()?;
|
||||||
|
|
||||||
HitObjectKind::Spinner { end_time }
|
// HitObjectKind::Spinner { end_time }
|
||||||
} else if kind & Self::HOLD_FLAG > 0 {
|
// } else if kind & Self::HOLD_FLAG > 0 {
|
||||||
$self.n_sliders += 1;
|
// $self.n_sliders += 1;
|
||||||
let mut end = time;
|
// let mut end = time;
|
||||||
|
|
||||||
if let Some(next) = split.next() {
|
// if let Some(next) = split.next() {
|
||||||
end = end.max(next_field!(next.split(':').next(), "hold endtime").parse()?);
|
// end = end.max(next_field!(next.split(':').next(), "hold endtime").parse()?);
|
||||||
}
|
// }
|
||||||
|
|
||||||
HitObjectKind::Hold { end_time: end }
|
// HitObjectKind::Hold { end_time: end }
|
||||||
} else {
|
// } else {
|
||||||
return Err(ParseError::UnknownHitObjectKind);
|
// return Err(ParseError::UnknownHitObjectKind);
|
||||||
};
|
// };
|
||||||
|
|
||||||
$self.hit_objects.push(HitObject {
|
// $self.hit_objects.push(HitObject {
|
||||||
pos,
|
// pos,
|
||||||
start_time: time,
|
// start_time: time,
|
||||||
kind,
|
// kind,
|
||||||
sound, // TODO: omit if not taiko?
|
// sound, // TODO: omit if not taiko?
|
||||||
});
|
// });
|
||||||
|
|
||||||
prev_time = time;
|
// prev_time = time;
|
||||||
$buf.clear();
|
// $buf.clear();
|
||||||
}
|
// }
|
||||||
|
|
||||||
// BUG: If [General] section comes after [HitObjects] then the mode
|
// // BUG: If [General] section comes after [HitObjects] then the mode
|
||||||
// won't be set yet so mania objects won't be sorted properly
|
// // won't be set yet so mania objects won't be sorted properly
|
||||||
if $self.mode == GameMode::MNA {
|
// if $self.mode == GameMode::MNA {
|
||||||
// First a _stable_ sort by time
|
// // First a _stable_ sort by time
|
||||||
sort!(stable $self.hit_objects);
|
// sort!(stable $self.hit_objects);
|
||||||
|
|
||||||
// Then the legacy sort for correct position order
|
// // Then the legacy sort for correct position order
|
||||||
legacy_sort(&mut $self.hit_objects);
|
// legacy_sort(&mut $self.hit_objects);
|
||||||
} else if unsorted {
|
// } else if unsorted {
|
||||||
sort!($self.hit_objects);
|
// sort!($self.hit_objects);
|
||||||
}
|
// }
|
||||||
|
|
||||||
Ok(empty)
|
// Ok(empty)
|
||||||
}};
|
// }};
|
||||||
}
|
// }
|
||||||
|
|
||||||
macro_rules! parse_hitobjects {
|
// macro_rules! parse_hitobjects {
|
||||||
($reader:ident<$inner:ident>) => {
|
// ($reader:ident<$inner:ident>) => {
|
||||||
fn parse_hitobjects<R: $inner>(
|
// fn parse_hitobjects<R: $inner>(
|
||||||
&mut self,
|
// &mut self,
|
||||||
reader: &mut $reader<R>,
|
// reader: &mut $reader<R>,
|
||||||
buf: &mut String,
|
// buf: &mut String,
|
||||||
section: &mut Section,
|
// section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
// ) -> ParseResult<bool> {
|
||||||
parse_hitobjects_body!(self, reader, buf, section)
|
// parse_hitobjects_body!(self, reader, buf, section)
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
(async $reader:ident<$inner:ident>) => {
|
// (async $reader:ident<$inner:ident>) => {
|
||||||
async fn parse_hitobjects<R: $inner + Unpin>(
|
// async fn parse_hitobjects<R: $inner + Unpin>(
|
||||||
&mut self,
|
// &mut self,
|
||||||
reader: &mut $reader<R>,
|
// reader: &mut $reader<R>,
|
||||||
buf: &mut String,
|
// buf: &mut String,
|
||||||
section: &mut Section,
|
// section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
// ) -> ParseResult<bool> {
|
||||||
parse_hitobjects_body!(self, reader, buf, section)
|
// parse_hitobjects_body!(self, reader, buf, section)
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
macro_rules! parse_body {
|
macro_rules! parse_body {
|
||||||
($reader:ident<$inner:ident>: $input:ident) => {{
|
($reader:ident<$inner:ident>: $input:ident) => {{
|
||||||
@@ -711,7 +710,7 @@ pub struct Beatmap {
|
|||||||
pub od: f32,
|
pub od: f32,
|
||||||
pub cs: f32,
|
pub cs: f32,
|
||||||
pub hp: f32,
|
pub hp: f32,
|
||||||
pub sv: f32,
|
pub slider_mult: f32,
|
||||||
pub tick_rate: f32,
|
pub tick_rate: f32,
|
||||||
pub hit_objects: Vec<HitObject>,
|
pub hit_objects: Vec<HitObject>,
|
||||||
|
|
||||||
@@ -727,12 +726,6 @@ pub struct Beatmap {
|
|||||||
|
|
||||||
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
||||||
|
|
||||||
#[cfg(any(
|
|
||||||
feature = "fruits",
|
|
||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
|
||||||
))]
|
|
||||||
const CURVE_POINT_THRESHOLD: usize = 256;
|
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "fruits",
|
feature = "fruits",
|
||||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
@@ -759,7 +752,310 @@ impl Beatmap {
|
|||||||
parse_general!(BufReader<Read>);
|
parse_general!(BufReader<Read>);
|
||||||
parse_difficulty!(BufReader<Read>);
|
parse_difficulty!(BufReader<Read>);
|
||||||
parse_timingpoints!(BufReader<Read>);
|
parse_timingpoints!(BufReader<Read>);
|
||||||
parse_hitobjects!(BufReader<Read>);
|
// parse_hitobjects!(BufReader<Read>);
|
||||||
|
|
||||||
|
// TODO: Remove
|
||||||
|
fn parse_hitobjects<R: Read>(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut BufReader<R>,
|
||||||
|
buf: &mut String,
|
||||||
|
section: &mut Section,
|
||||||
|
) -> ParseResult<bool> {
|
||||||
|
// parse_hitobjects_body!(self, reader, buf, section)
|
||||||
|
|
||||||
|
let mut unsorted = false;
|
||||||
|
let mut prev_time = 0.0;
|
||||||
|
|
||||||
|
let mut empty = true;
|
||||||
|
|
||||||
|
while read_line!(reader, buf)? != 0 {
|
||||||
|
let line = line_prepare!(buf);
|
||||||
|
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
*section = Section::from_str(&line[1..line.len() - 1]);
|
||||||
|
empty = false;
|
||||||
|
buf.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut split = line.split(',');
|
||||||
|
|
||||||
|
let pos = Pos2 {
|
||||||
|
x: split.next().next_field("x pos")?.parse()?,
|
||||||
|
y: split.next().next_field("y pos")?.parse()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let time = split
|
||||||
|
.next()
|
||||||
|
.next_field("hitobject time")?
|
||||||
|
.trim()
|
||||||
|
.parse::<f32>()?
|
||||||
|
.validate()?;
|
||||||
|
|
||||||
|
if !self.hit_objects.is_empty() && time < prev_time {
|
||||||
|
unsorted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind: u8 = split.next().next_field("hitobject kind")?.parse()?;
|
||||||
|
let sound = split.next().map(str::parse).transpose()?.unwrap_or(0);
|
||||||
|
|
||||||
|
let kind = if kind & Self::CIRCLE_FLAG > 0 {
|
||||||
|
self.n_circles += 1;
|
||||||
|
|
||||||
|
HitObjectKind::Circle
|
||||||
|
} else if kind & Self::SLIDER_FLAG > 0 {
|
||||||
|
self.n_sliders += 1;
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
))]
|
||||||
|
{
|
||||||
|
let mut curve_points = Vec::with_capacity(4);
|
||||||
|
curve_points.push(pos);
|
||||||
|
|
||||||
|
let curve_point_iter = split.next().next_field("curve points")?.split("|");
|
||||||
|
let mut repeats: usize = split.next().next_field("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 start_idx = 0;
|
||||||
|
let mut end_idx = 0;
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
|
let point_split: Vec<_> = curve_point_iter.collect();
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
|
||||||
|
while {
|
||||||
|
end_idx += 1;
|
||||||
|
|
||||||
|
end_idx < point_split.len()
|
||||||
|
} {
|
||||||
|
// * Keep incrementing end_idx while it's not the start of a new segment
|
||||||
|
// * (indicated by having a type descriptor of length 1).
|
||||||
|
if point_split[end_idx].len() > 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Multi-segmented sliders DON'T contain the end point as part of the
|
||||||
|
// * current segment as it's assumed to be the start of the next segment.
|
||||||
|
// * The start of the next segment is the index after the type descriptor.
|
||||||
|
let end_point = point_split.get(end_idx + 1).copied();
|
||||||
|
|
||||||
|
convert_points(
|
||||||
|
&point_split[start_idx..end_idx],
|
||||||
|
end_point,
|
||||||
|
first,
|
||||||
|
pos,
|
||||||
|
&mut segments,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
start_idx = end_idx;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_idx > start_idx {
|
||||||
|
convert_points(
|
||||||
|
&point_split[start_idx..end_idx],
|
||||||
|
None,
|
||||||
|
first,
|
||||||
|
pos,
|
||||||
|
&mut segments,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let curve_points = merge_points_lists(segments);
|
||||||
|
|
||||||
|
if curve_points.is_empty() {
|
||||||
|
HitObjectKind::Circle
|
||||||
|
} else {
|
||||||
|
let pixel_len = split
|
||||||
|
.next()
|
||||||
|
.next_field("pixel len")?
|
||||||
|
.parse::<f32>()?
|
||||||
|
.max(0.0)
|
||||||
|
.min(MAX_COORDINATE_VALUE);
|
||||||
|
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
repeats,
|
||||||
|
pixel_len,
|
||||||
|
curve_points,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
)))]
|
||||||
|
{
|
||||||
|
let repeats = next_field!(split.nth(1), "repeats").parse::<usize>()?;
|
||||||
|
let len: f32 = next_field!(split.next(), "pixel len").parse()?;
|
||||||
|
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
repeats,
|
||||||
|
pixel_len: len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if kind & Self::SPINNER_FLAG > 0 {
|
||||||
|
self.n_spinners += 1;
|
||||||
|
let end_time = split.next().next_field("spinner endtime")?.parse()?;
|
||||||
|
|
||||||
|
HitObjectKind::Spinner { end_time }
|
||||||
|
} else if kind & Self::HOLD_FLAG > 0 {
|
||||||
|
self.n_sliders += 1;
|
||||||
|
let mut end = time;
|
||||||
|
|
||||||
|
if let Some(next) = split.next() {
|
||||||
|
end = end.max(next.split(':').next().next_field("hold endtime")?.parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
HitObjectKind::Hold { end_time: end }
|
||||||
|
} else {
|
||||||
|
return Err(ParseError::UnknownHitObjectKind);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.hit_objects.push(HitObject {
|
||||||
|
pos,
|
||||||
|
start_time: time,
|
||||||
|
kind,
|
||||||
|
sound, // TODO: omit if not taiko?
|
||||||
|
});
|
||||||
|
|
||||||
|
prev_time = time;
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUG: If [General] section comes after [HitObjects] then the mode
|
||||||
|
// won't be set yet so mania objects won't be sorted properly
|
||||||
|
if self.mode == GameMode::MNA {
|
||||||
|
// First a _stable_ sort by time
|
||||||
|
sort(&mut self.hit_objects);
|
||||||
|
|
||||||
|
// Then the legacy sort for correct position order
|
||||||
|
legacy_sort(&mut self.hit_objects);
|
||||||
|
} else if unsorted {
|
||||||
|
sort_unstable(&mut self.hit_objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Cleanup
|
||||||
|
fn convert_points(
|
||||||
|
points: &[&str],
|
||||||
|
end_point: Option<&str>,
|
||||||
|
first: bool,
|
||||||
|
offset: Pos2,
|
||||||
|
segments: &mut Vec<Vec<PathControlPoint>>,
|
||||||
|
) -> Result<(), ParseError> {
|
||||||
|
let mut path_kind = PathType::from_str(points[0]);
|
||||||
|
|
||||||
|
let read_offset = first as usize;
|
||||||
|
let readable_points = points.len() - 1;
|
||||||
|
let end_point_len = end_point.is_some() as usize;
|
||||||
|
|
||||||
|
let mut vertices =
|
||||||
|
vec![PathControlPoint::default(); read_offset + readable_points + end_point_len];
|
||||||
|
|
||||||
|
// * Fill any non-read points.
|
||||||
|
// Not necessary since vertices are already initialized
|
||||||
|
|
||||||
|
// * Parse into control points.
|
||||||
|
for i in 1..points.len() {
|
||||||
|
read_point(points[i], offset, &mut vertices[read_offset + i - 1])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * If an endpoint is given, add it to the end.
|
||||||
|
if let Some(end_point) = end_point {
|
||||||
|
read_point(end_point, offset, vertices.last_mut().unwrap())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Edge-case rules (to match stable).
|
||||||
|
if path_kind == PathType::PerfectCurve {
|
||||||
|
if vertices.len() != 3 {
|
||||||
|
path_kind = PathType::Bezier;
|
||||||
|
} else if is_linear(vertices[0].pos, vertices[1].pos, vertices[2].pos) {
|
||||||
|
// * osu-stable special-cased colinear perfect curves to a linear path
|
||||||
|
path_kind = PathType::Linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The first control point must have a definite type.
|
||||||
|
vertices[0].kind = Some(path_kind);
|
||||||
|
|
||||||
|
// * A path can have multiple implicit segments of the same type if
|
||||||
|
// * there are two sequential control points with the same position.
|
||||||
|
// * To handle such cases, this code may return multiple path segments
|
||||||
|
// * with the final control point in each segment having a non-null type.
|
||||||
|
// * For the point string X|1:1|2:2|2:2|3:3, this code returns the segments:
|
||||||
|
// * X: { (1,1), (2, 2) }
|
||||||
|
// * X: { (3, 3) }
|
||||||
|
// * Note: (2, 2) is not returned in the second segments, as it is implicit in the path.
|
||||||
|
let mut start_idx = 0;
|
||||||
|
let mut end_idx = 0;
|
||||||
|
|
||||||
|
while {
|
||||||
|
end_idx += 1;
|
||||||
|
|
||||||
|
end_idx < vertices.len() - end_point_len
|
||||||
|
} {
|
||||||
|
// * Keep incrementing while an implicit segment doesn't need to be started
|
||||||
|
if vertices[end_idx].pos != vertices[end_idx - 1].pos {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The last control point of each segment is not
|
||||||
|
// * allowed to start a new implicit segment.
|
||||||
|
if end_idx == vertices.len() - end_point_len - 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Force a type on the last point, and return
|
||||||
|
// * the current control point set as a segment.
|
||||||
|
vertices[end_idx - 1].kind = Some(path_kind);
|
||||||
|
segments.push(vertices[start_idx..end_idx].to_owned());
|
||||||
|
|
||||||
|
// * Skip the current control point - as it's the same as the one that's just been returned.
|
||||||
|
start_idx = end_idx + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_idx > start_idx {
|
||||||
|
segments.push(vertices[start_idx..end_idx].to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_point(
|
||||||
|
value: &str,
|
||||||
|
start_pos: Pos2,
|
||||||
|
point: &mut PathControlPoint,
|
||||||
|
) -> Result<(), ParseError> {
|
||||||
|
let mut v = value.split(':').map(str::parse);
|
||||||
|
|
||||||
|
match (v.next(), v.next()) {
|
||||||
|
(Some(Ok(x)), Some(Ok(y))) => point.pos = Pos2 { x, y } - start_pos,
|
||||||
|
_ => return Err(ParseError::InvalidCurvePoints),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_points_lists(control_point_list: Vec<Vec<PathControlPoint>>) -> Vec<PathControlPoint> {
|
||||||
|
let total_count = control_point_list.iter().map(Vec::len).sum();
|
||||||
|
let mut merged_list = Vec::with_capacity(total_count);
|
||||||
|
let iter = control_point_list.into_iter().map(Vec::into_iter).flatten();
|
||||||
|
merged_list.extend(iter);
|
||||||
|
|
||||||
|
merged_list
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async_tokio")]
|
#[cfg(feature = "async_tokio")]
|
||||||
@@ -787,6 +1083,12 @@ fn split_colon(line: &str) -> Option<(&str, &str)> {
|
|||||||
Some((split.next()?, split.next()?.trim()))
|
Some((split.next()?, split.next()?.trim()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
|
pub struct PathControlPoint {
|
||||||
|
pub pos: Pos2,
|
||||||
|
pub kind: Option<PathType>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The type of curve of a slider.
|
/// The type of curve of a slider.
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum PathType {
|
pub enum PathType {
|
||||||
@@ -796,17 +1098,14 @@ pub enum PathType {
|
|||||||
PerfectCurve = 3,
|
PerfectCurve = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for PathType {
|
impl PathType {
|
||||||
type Err = ParseError;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Self {
|
||||||
match s {
|
match s {
|
||||||
"L" => Ok(Self::Linear),
|
"L" => Self::Linear,
|
||||||
"C" => Ok(Self::Catmull),
|
"B" => Self::Bezier,
|
||||||
"B" => Ok(Self::Bezier),
|
"P" => Self::PerfectCurve,
|
||||||
"P" => Ok(Self::PerfectCurve),
|
_ => Self::Catmull,
|
||||||
_ => Err(ParseError::InvalidPathType),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,7 +1227,7 @@ mod tests {
|
|||||||
println!("od: {}", map.od);
|
println!("od: {}", map.od);
|
||||||
println!("cs: {}", map.cs);
|
println!("cs: {}", map.cs);
|
||||||
println!("hp: {}", map.hp);
|
println!("hp: {}", map.hp);
|
||||||
println!("sv: {}", map.sv);
|
println!("slider_mult: {}", map.slider_mult);
|
||||||
println!("tick_rate: {}", map.tick_rate);
|
println!("tick_rate: {}", map.tick_rate);
|
||||||
println!("hit_objects: {}", map.hit_objects.len());
|
println!("hit_objects: {}", map.hit_objects.len());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user