mirrored slider parsing from osu!lazer

This commit is contained in:
MaxOhn
2021-11-05 14:44:14 +01:00
parent 8cb14d88ba
commit bc8d6d70f8
9 changed files with 1100 additions and 330 deletions
+5 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+8 -2
View File
@@ -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();
+2 -1
View File
@@ -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 {
+24 -21
View File
@@ -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 {
+14 -8
View File
@@ -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);
}
}
+2 -5
View File
@@ -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
View File
@@ -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());