mirrored slider parsing from osu!lazer
This commit is contained in:
@@ -45,7 +45,7 @@ pub(crate) enum ControlPoint {
|
||||
},
|
||||
Difficulty {
|
||||
time: f32,
|
||||
speed_mult: f32,
|
||||
slider_velocity: f32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -74,7 +74,10 @@ impl<'p> Iterator for ControlPointIter<'p> {
|
||||
self.next_difficulty =
|
||||
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) => {
|
||||
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"))
|
||||
))]
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, convert::identity};
|
||||
use std::{borrow::Cow, cmp::Ordering, convert::identity, f32::consts::PI};
|
||||
|
||||
use crate::{
|
||||
math_util,
|
||||
parse::{PathType, Pos2},
|
||||
parse::{PathControlPoint, PathType, Pos2},
|
||||
};
|
||||
|
||||
const BEZIER_TOLERANCE: f32 = 0.25;
|
||||
const CATMULL_DETAIL: f32 = 50.0;
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
const CATMULL_DETAIL: usize = 50;
|
||||
const CIRCULAR_ARC_TOLERANCE: f32 = 0.1;
|
||||
|
||||
struct BezierBuffers {
|
||||
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]
|
||||
pub(crate) fn new(points: &'p [Pos2], kind: PathType, expected_len: f32) -> Self {
|
||||
match kind {
|
||||
@@ -80,7 +530,7 @@ impl<'p> Curve<'p> {
|
||||
|
||||
if len == 1 {
|
||||
return Self::Bezier {
|
||||
path: points.to_owned(),
|
||||
path: points,
|
||||
lengths: vec![0.0],
|
||||
};
|
||||
}
|
||||
@@ -103,8 +553,7 @@ impl<'p> Curve<'p> {
|
||||
|
||||
// Then calculated cumulative lenghts
|
||||
let mut calculated_len = 0.0;
|
||||
let mut cumulative_len = Vec::new();
|
||||
cumulative_len.push(0.0);
|
||||
let mut cumulative_len = vec![0.0];
|
||||
|
||||
for i in 0..path.len() - 1 {
|
||||
let diff = path[i + 1] - path[i];
|
||||
@@ -280,7 +729,7 @@ impl<'p> Curve<'p> {
|
||||
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
|
||||
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 y4 = -v1.y + 3.0 * (v2.y - v3.y) + v4.y;
|
||||
|
||||
let catmull_detail = CATMULL_DETAIL as f32;
|
||||
|
||||
loop {
|
||||
let t1 = c / CATMULL_DETAIL;
|
||||
let t1 = c / catmull_detail;
|
||||
let t2 = t1 * t1;
|
||||
let t3 = t2 * t1;
|
||||
|
||||
@@ -325,7 +776,7 @@ impl<'p> Curve<'p> {
|
||||
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 t3 = t2 * t1;
|
||||
|
||||
@@ -336,7 +787,7 @@ impl<'p> Curve<'p> {
|
||||
|
||||
c += 1.0;
|
||||
|
||||
if c >= CATMULL_DETAIL {
|
||||
if c >= catmull_detail {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-20
@@ -70,17 +70,17 @@ pub fn stars(
|
||||
pixel_len,
|
||||
repeats,
|
||||
curve_points,
|
||||
path_type,
|
||||
} => {
|
||||
// HR business
|
||||
last_pos
|
||||
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
|
||||
last_pos.replace(
|
||||
h.pos.x + curve_points[curve_points.len() - 1].pos.x - curve_points[0].pos.x,
|
||||
);
|
||||
*last_time = h.start_time;
|
||||
|
||||
// Responsible for timing point values
|
||||
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 {
|
||||
tick_distance /=
|
||||
@@ -88,11 +88,11 @@ pub fn stars(
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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 time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
||||
@@ -103,7 +103,8 @@ pub fn stars(
|
||||
// Tick of the first span
|
||||
if current_distance < target {
|
||||
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;
|
||||
ticks.push((pos, time));
|
||||
current_distance += tick_distance;
|
||||
@@ -129,7 +130,8 @@ pub fn stars(
|
||||
for repeat_id in 1..*repeats {
|
||||
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
||||
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
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
@@ -148,8 +150,8 @@ pub fn stars(
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||
let pos = curve.point_at_distance(dist_end);
|
||||
// let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||
let pos = curve.position_at(1.0); // TODO: what if reversing odd amount?
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
fruits += 1 + *repeats;
|
||||
@@ -301,17 +303,17 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
pixel_len,
|
||||
repeats,
|
||||
curve_points,
|
||||
path_type,
|
||||
} => {
|
||||
// HR business
|
||||
last_pos
|
||||
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
|
||||
last_pos.replace(
|
||||
h.pos.x + curve_points[curve_points.len() - 1].pos.x - curve_points[0].pos.x,
|
||||
);
|
||||
*last_time = h.start_time;
|
||||
|
||||
// Responsible for timing point values
|
||||
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 {
|
||||
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
|
||||
/ (map.sv * slider_state.speed_mult)
|
||||
/ (map.slider_mult * slider_state.speed_mult)
|
||||
/ 100.0;
|
||||
|
||||
// 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 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
|
||||
if current_distance < target {
|
||||
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;
|
||||
ticks.push((pos, time));
|
||||
current_distance += tick_distance;
|
||||
@@ -357,7 +360,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
for repeat_id in 1..*repeats {
|
||||
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
||||
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
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
@@ -374,8 +378,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||
let pos = curve.point_at_distance(dist_end);
|
||||
// let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||
let pos = curve.position_at(1.0); // TODO: what if reversing odd amount?
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
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() {
|
||||
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),
|
||||
};
|
||||
|
||||
@@ -34,7 +37,10 @@ impl<'p> SliderState<'p> {
|
||||
self.beat_len = *beat_len;
|
||||
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();
|
||||
|
||||
@@ -340,7 +340,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> 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/2753127.osu").unwrap();
|
||||
// let file = std::fs::File::open("E:Games/osu!/beatmaps/2571051.osu").unwrap();
|
||||
let map = Beatmap::parse(file).unwrap();
|
||||
|
||||
@@ -350,6 +350,7 @@ fn custom_osu() {
|
||||
let iters = 500;
|
||||
let accum = start.elapsed();
|
||||
|
||||
// * Tiny benchmark
|
||||
// let mut accum = accum;
|
||||
|
||||
// for _ in 0..iters {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
||||
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f32,
|
||||
@@ -47,7 +48,6 @@ impl OsuObject {
|
||||
pixel_len,
|
||||
repeats,
|
||||
curve_points,
|
||||
path_type,
|
||||
} => {
|
||||
// Key values which are computed here
|
||||
let mut lazy_end_pos = h.pos;
|
||||
@@ -56,21 +56,26 @@ impl OsuObject {
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f32;
|
||||
|
||||
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 {
|
||||
tick_distance /=
|
||||
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
|
||||
tick_dist /=
|
||||
(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
|
||||
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.
|
||||
// Increases combo and adjusts `end_pos` and `travel_dist`
|
||||
@@ -86,25 +91,23 @@ impl OsuObject {
|
||||
progress %= 1.0;
|
||||
}
|
||||
|
||||
let curr_dist = pixel_len * progress;
|
||||
let curr_pos = curve.point_at_distance(curr_dist);
|
||||
|
||||
let diff = curr_pos - lazy_end_pos;
|
||||
let curr_pos = curve.position_at(progress);
|
||||
let diff = h.pos + curr_pos - lazy_end_pos;
|
||||
let mut dist = diff.length();
|
||||
|
||||
if dist > approx_follow_circle_radius {
|
||||
// * The cursor would be outside the follow circle, we need to move it
|
||||
dist -= approx_follow_circle_radius;
|
||||
lazy_end_pos += diff.normalize() * dist;
|
||||
|
||||
travel_dist += dist;
|
||||
}
|
||||
};
|
||||
|
||||
let mut current_distance = tick_distance;
|
||||
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
|
||||
let mut current_distance = tick_dist;
|
||||
let time_add = duration * (tick_dist / (pixel_len * span_count));
|
||||
|
||||
let target = pixel_len - tick_distance / 8.0;
|
||||
ticks.reserve((target / tick_distance) as usize);
|
||||
let target = pixel_len - tick_dist / 8.0;
|
||||
ticks.reserve((target / tick_dist) as usize);
|
||||
|
||||
// Tick of the first span
|
||||
if current_distance < target {
|
||||
@@ -112,7 +115,7 @@ impl OsuObject {
|
||||
let time = h.start_time + time_add * tick_idx as f32;
|
||||
compute_vertex(time);
|
||||
ticks.push(time);
|
||||
current_distance += tick_distance;
|
||||
current_distance += tick_dist;
|
||||
|
||||
if current_distance >= target {
|
||||
break;
|
||||
@@ -146,7 +149,7 @@ impl OsuObject {
|
||||
|
||||
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;
|
||||
|
||||
Self {
|
||||
|
||||
@@ -4,7 +4,7 @@ pub(crate) struct SliderState<'p> {
|
||||
control_points: ControlPointIter<'p>,
|
||||
next: Option<ControlPoint>,
|
||||
pub(crate) beat_len: f32,
|
||||
pub(crate) speed_mult: f32,
|
||||
pub(crate) slider_velocity: f32,
|
||||
}
|
||||
|
||||
impl<'p> SliderState<'p> {
|
||||
@@ -12,9 +12,12 @@ impl<'p> SliderState<'p> {
|
||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||
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::Difficulty { speed_mult, .. }) => (1000.0, speed_mult),
|
||||
Some(ControlPoint::Difficulty {
|
||||
slider_velocity: speed_mult,
|
||||
..
|
||||
}) => (1000.0, speed_mult),
|
||||
None => (1000.0, 1.0),
|
||||
};
|
||||
|
||||
@@ -22,7 +25,7 @@ impl<'p> SliderState<'p> {
|
||||
next: control_points.next(),
|
||||
control_points,
|
||||
beat_len,
|
||||
speed_mult,
|
||||
slider_velocity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +35,12 @@ impl<'p> SliderState<'p> {
|
||||
match next {
|
||||
ControlPoint::Timing { 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();
|
||||
@@ -88,10 +94,10 @@ mod test {
|
||||
|
||||
state.update(3.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);
|
||||
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(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
use super::PathType;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// "Intermediate" hitobject created through parsing.
|
||||
@@ -63,8 +61,7 @@ pub enum HitObjectKind {
|
||||
Slider {
|
||||
pixel_len: f32,
|
||||
repeats: usize,
|
||||
curve_points: Vec<Pos2>,
|
||||
path_type: PathType,
|
||||
curve_points: Vec<PathControlPoint>,
|
||||
},
|
||||
#[cfg(not(any(
|
||||
feature = "fruits",
|
||||
|
||||
+530
-231
@@ -1,8 +1,4 @@
|
||||
#[cfg(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
use crate::math_util;
|
||||
use crate::math_util::is_linear;
|
||||
|
||||
mod attributes;
|
||||
mod control_point;
|
||||
@@ -21,7 +17,6 @@ pub use pos2::Pos2;
|
||||
use sort::legacy_sort;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
@@ -32,30 +27,34 @@ use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
|
||||
#[cfg(feature = "async_std")]
|
||||
use async_std::io::{prelude::BufReadExt, BufReader as AsyncBufReader, Read as AsyncRead};
|
||||
|
||||
macro_rules! sort {
|
||||
($slice:expr) => {
|
||||
$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))
|
||||
};
|
||||
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
||||
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal));
|
||||
}
|
||||
|
||||
macro_rules! next_field {
|
||||
($opt:expr, $err:literal) => {
|
||||
$opt.ok_or_else(|| ParseError::MissingField($err))?
|
||||
};
|
||||
fn sort<T: PartialOrd>(slice: &mut [T]) {
|
||||
slice.sort_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal));
|
||||
}
|
||||
|
||||
macro_rules! validate_float {
|
||||
($x:expr) => {{
|
||||
if $x.is_finite() {
|
||||
$x
|
||||
} else {
|
||||
return Err(ParseError::InvalidFloatingPoint);
|
||||
}
|
||||
}};
|
||||
trait OptionExt<T> {
|
||||
fn next_field(self, field: &'static str) -> Result<T, ParseError>;
|
||||
}
|
||||
|
||||
impl<T> OptionExt<T> for Option<T> {
|
||||
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 {
|
||||
@@ -236,12 +235,12 @@ macro_rules! parse_difficulty_body {
|
||||
$buf.clear();
|
||||
}
|
||||
|
||||
$self.od = next_field!(od, "od");
|
||||
$self.cs = next_field!(cs, "cs");
|
||||
$self.hp = next_field!(hp, "hp");
|
||||
$self.od = od.next_field("od")?;
|
||||
$self.cs = cs.next_field("cs")?;
|
||||
$self.hp = hp.next_field("hp")?;
|
||||
$self.ar = ar.unwrap_or($self.od);
|
||||
$self.sv = next_field!(sv, "sv");
|
||||
$self.tick_rate = next_field!(tick_rate, "sv");
|
||||
$self.slider_mult = sv.next_field("sv")?;
|
||||
$self.tick_rate = tick_rate.next_field("tick rate")?;
|
||||
|
||||
Ok(empty)
|
||||
}};
|
||||
@@ -312,14 +311,14 @@ macro_rules! parse_timingpoints_body {
|
||||
|
||||
let mut split = line.split(',');
|
||||
|
||||
let time = next_field!(split.next(), "timing point time")
|
||||
let time = split
|
||||
.next()
|
||||
.next_field("timing point time")?
|
||||
.trim()
|
||||
.parse::<f32>()?;
|
||||
validate_float!(time);
|
||||
.parse::<f32>()?
|
||||
.validate()?;
|
||||
|
||||
let beat_len = next_field!(split.next(), "beat len")
|
||||
.trim()
|
||||
.parse::<f32>()?;
|
||||
let beat_len: f32 = split.next().next_field("beat len")?.trim().parse()?;
|
||||
|
||||
if beat_len < 0.0 {
|
||||
let point = DifficultyPoint {
|
||||
@@ -348,11 +347,11 @@ macro_rules! parse_timingpoints_body {
|
||||
}
|
||||
|
||||
if unsorted_timings {
|
||||
sort!($self.timing_points);
|
||||
sort_unstable(&mut $self.timing_points);
|
||||
}
|
||||
|
||||
if unsorted_difficulties {
|
||||
sort!($self.difficulty_points);
|
||||
sort_unstable(&mut $self.difficulty_points);
|
||||
}
|
||||
|
||||
Ok(empty)
|
||||
@@ -395,217 +394,217 @@ macro_rules! parse_timingpoints {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_hitobjects_body {
|
||||
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
let mut unsorted = false;
|
||||
let mut prev_time = 0.0;
|
||||
// macro_rules! parse_hitobjects_body {
|
||||
// ($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
// let mut unsorted = false;
|
||||
// let mut prev_time = 0.0;
|
||||
|
||||
let mut empty = true;
|
||||
// let mut empty = true;
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
let line = line_prepare!($buf);
|
||||
// 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;
|
||||
}
|
||||
// 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 mut split = line.split(',');
|
||||
|
||||
let pos = Pos2 {
|
||||
x: next_field!(split.next(), "x position").parse()?,
|
||||
y: next_field!(split.next(), "y position").parse()?,
|
||||
};
|
||||
// let pos = Pos2 {
|
||||
// x: next_field!(split.next(), "x position").parse()?,
|
||||
// y: next_field!(split.next(), "y position").parse()?,
|
||||
// };
|
||||
|
||||
let time: f32 = next_field!(split.next(), "hitobject time")
|
||||
.trim()
|
||||
.parse()?;
|
||||
// let time: f32 = next_field!(split.next(), "hitobject time")
|
||||
// .trim()
|
||||
// .parse()?;
|
||||
|
||||
validate_float!(time);
|
||||
// validate_float!(time);
|
||||
|
||||
if !$self.hit_objects.is_empty() && time < prev_time {
|
||||
unsorted = true;
|
||||
}
|
||||
// if !$self.hit_objects.is_empty() && time < prev_time {
|
||||
// unsorted = true;
|
||||
// }
|
||||
|
||||
let kind: u8 = next_field!(split.next(), "hitobject kind").parse()?;
|
||||
let sound = split.next().map(str::parse).transpose()?.unwrap_or(0);
|
||||
// let kind: u8 = next_field!(split.next(), "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;
|
||||
// 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;
|
||||
// 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);
|
||||
// #[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 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()?;
|
||||
// let mut repeats: usize = next_field!(split.next(), "repeats")
|
||||
// .parse()?;
|
||||
|
||||
if repeats > 9000 {
|
||||
return Err(ParseError::TooManyRepeats);
|
||||
}
|
||||
// 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);
|
||||
// // * 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 =
|
||||
next_field!(curve_point_iter.next(), "path kind").parse()?;
|
||||
// let mut path_type: PathType =
|
||||
// next_field!(curve_point_iter.next(), "path kind").parse()?;
|
||||
|
||||
for pos in curve_point_iter {
|
||||
let mut v = pos.split(':').map(str::parse);
|
||||
// for pos in curve_point_iter {
|
||||
// let mut v = pos.split(':').map(str::parse);
|
||||
|
||||
match (v.next(), v.next()) {
|
||||
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||
_ => return Err(ParseError::InvalidCurvePoints),
|
||||
}
|
||||
}
|
||||
// match (v.next(), v.next()) {
|
||||
// (Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||
// _ => return Err(ParseError::InvalidCurvePoints),
|
||||
// }
|
||||
// }
|
||||
|
||||
match path_type {
|
||||
PathType::Linear if curve_points.len() % 2 == 0 => {
|
||||
// Assert that the points are of the form A|B|B|C|C|E
|
||||
if math_util::valid_linear(&curve_points) {
|
||||
for i in (2..curve_points.len() - 1).rev().step_by(2) {
|
||||
curve_points.remove(i);
|
||||
}
|
||||
} else {
|
||||
path_type = PathType::Bezier;
|
||||
}
|
||||
}
|
||||
PathType::PerfectCurve if curve_points.len() == 3 => {
|
||||
if math_util::is_linear(curve_points[0], curve_points[1], curve_points[2]) {
|
||||
path_type = PathType::Linear;
|
||||
}
|
||||
},
|
||||
PathType::Catmull => {},
|
||||
_ => path_type = PathType::Bezier,
|
||||
};
|
||||
// match path_type {
|
||||
// PathType::Linear if curve_points.len() % 2 == 0 => {
|
||||
// // Assert that the points are of the form A|B|B|C|C|E
|
||||
// if math_util::valid_linear(&curve_points) {
|
||||
// for i in (2..curve_points.len() - 1).rev().step_by(2) {
|
||||
// curve_points.remove(i);
|
||||
// }
|
||||
// } else {
|
||||
// path_type = PathType::Bezier;
|
||||
// }
|
||||
// }
|
||||
// PathType::PerfectCurve if curve_points.len() == 3 => {
|
||||
// if math_util::is_linear(curve_points[0], curve_points[1], curve_points[2]) {
|
||||
// path_type = PathType::Linear;
|
||||
// }
|
||||
// },
|
||||
// PathType::Catmull => {},
|
||||
// _ => path_type = PathType::Bezier,
|
||||
// };
|
||||
|
||||
// Reduce amount of curvepoints but keep the elements evenly spaced.
|
||||
// Necessary to handle maps like XNOR (2573164) which have
|
||||
// tens of thousands of curvepoints more efficiently.
|
||||
while curve_points.len() > CURVE_POINT_THRESHOLD {
|
||||
let last = curve_points[curve_points.len() - 1];
|
||||
let last_idx = (curve_points.len() - 1) / 2;
|
||||
// // Reduce amount of curvepoints but keep the elements evenly spaced.
|
||||
// // Necessary to handle maps like XNOR (2573164) which have
|
||||
// // tens of thousands of curvepoints more efficiently.
|
||||
// while curve_points.len() > CURVE_POINT_THRESHOLD {
|
||||
// let last = curve_points[curve_points.len() - 1];
|
||||
// let last_idx = (curve_points.len() - 1) / 2;
|
||||
|
||||
for i in 1..=last_idx {
|
||||
curve_points.swap(i, 2 * i);
|
||||
}
|
||||
// for i in 1..=last_idx {
|
||||
// curve_points.swap(i, 2 * i);
|
||||
// }
|
||||
|
||||
curve_points[last_idx] = last;
|
||||
curve_points.truncate(last_idx + 1);
|
||||
}
|
||||
// curve_points[last_idx] = last;
|
||||
// curve_points.truncate(last_idx + 1);
|
||||
// }
|
||||
|
||||
if curve_points.is_empty() {
|
||||
HitObjectKind::Circle
|
||||
} else {
|
||||
// TODO: Should be Option<f32>?
|
||||
let pixel_len = next_field!(split.next(), "pixel len")
|
||||
.parse::<f32>()?
|
||||
.max(0.0)
|
||||
.min(MAX_COORDINATE_VALUE);
|
||||
// if curve_points.is_empty() {
|
||||
// HitObjectKind::Circle
|
||||
// } else {
|
||||
// // TODO: Should be Option<f32>?
|
||||
// let pixel_len = next_field!(split.next(), "pixel len")
|
||||
// .parse::<f32>()?
|
||||
// .max(0.0)
|
||||
// .min(MAX_COORDINATE_VALUE);
|
||||
|
||||
HitObjectKind::Slider {
|
||||
repeats,
|
||||
pixel_len,
|
||||
curve_points,
|
||||
path_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
// HitObjectKind::Slider {
|
||||
// repeats,
|
||||
// pixel_len,
|
||||
// curve_points,
|
||||
// path_type,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[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()?;
|
||||
// #[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 = next_field!(split.next(), "spinner endtime").parse()?;
|
||||
// HitObjectKind::Slider {
|
||||
// repeats,
|
||||
// pixel_len: len,
|
||||
// }
|
||||
// }
|
||||
// } else if kind & Self::SPINNER_FLAG > 0 {
|
||||
// $self.n_spinners += 1;
|
||||
// let end_time = next_field!(split.next(), "spinner endtime").parse()?;
|
||||
|
||||
HitObjectKind::Spinner { end_time }
|
||||
} else if kind & Self::HOLD_FLAG > 0 {
|
||||
$self.n_sliders += 1;
|
||||
let mut end = time;
|
||||
// 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_field!(next.split(':').next(), "hold endtime").parse()?);
|
||||
}
|
||||
// if let Some(next) = split.next() {
|
||||
// end = end.max(next_field!(next.split(':').next(), "hold endtime").parse()?);
|
||||
// }
|
||||
|
||||
HitObjectKind::Hold { end_time: end }
|
||||
} else {
|
||||
return Err(ParseError::UnknownHitObjectKind);
|
||||
};
|
||||
// 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?
|
||||
});
|
||||
// $self.hit_objects.push(HitObject {
|
||||
// pos,
|
||||
// start_time: time,
|
||||
// kind,
|
||||
// sound, // TODO: omit if not taiko?
|
||||
// });
|
||||
|
||||
prev_time = time;
|
||||
$buf.clear();
|
||||
}
|
||||
// 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!(stable $self.hit_objects);
|
||||
// // 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!(stable $self.hit_objects);
|
||||
|
||||
// Then the legacy sort for correct position order
|
||||
legacy_sort(&mut $self.hit_objects);
|
||||
} else if unsorted {
|
||||
sort!($self.hit_objects);
|
||||
}
|
||||
// // Then the legacy sort for correct position order
|
||||
// legacy_sort(&mut $self.hit_objects);
|
||||
// } else if unsorted {
|
||||
// sort!($self.hit_objects);
|
||||
// }
|
||||
|
||||
Ok(empty)
|
||||
}};
|
||||
}
|
||||
// Ok(empty)
|
||||
// }};
|
||||
// }
|
||||
|
||||
macro_rules! parse_hitobjects {
|
||||
($reader:ident<$inner:ident>) => {
|
||||
fn parse_hitobjects<R: $inner>(
|
||||
&mut self,
|
||||
reader: &mut $reader<R>,
|
||||
buf: &mut String,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, buf, section)
|
||||
}
|
||||
};
|
||||
// macro_rules! parse_hitobjects {
|
||||
// ($reader:ident<$inner:ident>) => {
|
||||
// fn parse_hitobjects<R: $inner>(
|
||||
// &mut self,
|
||||
// reader: &mut $reader<R>,
|
||||
// buf: &mut String,
|
||||
// section: &mut Section,
|
||||
// ) -> ParseResult<bool> {
|
||||
// parse_hitobjects_body!(self, reader, buf, section)
|
||||
// }
|
||||
// };
|
||||
|
||||
(async $reader:ident<$inner:ident>) => {
|
||||
async fn parse_hitobjects<R: $inner + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut $reader<R>,
|
||||
buf: &mut String,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, buf, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
// (async $reader:ident<$inner:ident>) => {
|
||||
// async fn parse_hitobjects<R: $inner + Unpin>(
|
||||
// &mut self,
|
||||
// reader: &mut $reader<R>,
|
||||
// buf: &mut String,
|
||||
// section: &mut Section,
|
||||
// ) -> ParseResult<bool> {
|
||||
// parse_hitobjects_body!(self, reader, buf, section)
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
macro_rules! parse_body {
|
||||
($reader:ident<$inner:ident>: $input:ident) => {{
|
||||
@@ -711,7 +710,7 @@ pub struct Beatmap {
|
||||
pub od: f32,
|
||||
pub cs: f32,
|
||||
pub hp: f32,
|
||||
pub sv: f32,
|
||||
pub slider_mult: f32,
|
||||
pub tick_rate: f32,
|
||||
pub hit_objects: Vec<HitObject>,
|
||||
|
||||
@@ -727,12 +726,6 @@ pub struct Beatmap {
|
||||
|
||||
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(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
@@ -759,7 +752,310 @@ impl Beatmap {
|
||||
parse_general!(BufReader<Read>);
|
||||
parse_difficulty!(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")]
|
||||
@@ -787,6 +1083,12 @@ fn split_colon(line: &str) -> Option<(&str, &str)> {
|
||||
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.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PathType {
|
||||
@@ -796,17 +1098,14 @@ pub enum PathType {
|
||||
PerfectCurve = 3,
|
||||
}
|
||||
|
||||
impl FromStr for PathType {
|
||||
type Err = ParseError;
|
||||
|
||||
impl PathType {
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"L" => Ok(Self::Linear),
|
||||
"C" => Ok(Self::Catmull),
|
||||
"B" => Ok(Self::Bezier),
|
||||
"P" => Ok(Self::PerfectCurve),
|
||||
_ => Err(ParseError::InvalidPathType),
|
||||
"L" => Self::Linear,
|
||||
"B" => Self::Bezier,
|
||||
"P" => Self::PerfectCurve,
|
||||
_ => Self::Catmull,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -928,7 +1227,7 @@ mod tests {
|
||||
println!("od: {}", map.od);
|
||||
println!("cs: {}", map.cs);
|
||||
println!("hp: {}", map.hp);
|
||||
println!("sv: {}", map.sv);
|
||||
println!("slider_mult: {}", map.slider_mult);
|
||||
println!("tick_rate: {}", map.tick_rate);
|
||||
println!("hit_objects: {}", map.hit_objects.len());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user