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