enable runtime features again
This commit is contained in:
@@ -45,6 +45,7 @@ features = ["io-util"]
|
|||||||
|
|
||||||
[dev-dependencies.tokio]
|
[dev-dependencies.tokio]
|
||||||
version = "1.2"
|
version = "1.2"
|
||||||
|
default-features = false
|
||||||
features = ["fs", "rt"]
|
features = ["fs", "rt"]
|
||||||
|
|
||||||
[dev-dependencies.async-std]
|
[dev-dependencies.async-std]
|
||||||
|
|||||||
@@ -47,3 +47,50 @@ impl PerformanceAttributes {
|
|||||||
self.pp
|
self.pp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_osu() {
|
||||||
|
use std::{fs::File, time::Instant};
|
||||||
|
|
||||||
|
use crate::{Beatmap, OsuPP};
|
||||||
|
|
||||||
|
let path = "./maps/2753127.osu";
|
||||||
|
// let path = "E:Games/osu!/beatmaps/2571051.osu";
|
||||||
|
let file = File::open(path).unwrap();
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let map = Beatmap::parse(file).unwrap();
|
||||||
|
|
||||||
|
let iters = 100;
|
||||||
|
let accum = start.elapsed();
|
||||||
|
|
||||||
|
// * Tiny benchmark for map parsing
|
||||||
|
let mut accum = accum;
|
||||||
|
|
||||||
|
for _ in 0..iters {
|
||||||
|
let file = File::open(path).unwrap();
|
||||||
|
let start = Instant::now();
|
||||||
|
let _map = Beatmap::parse(file).unwrap();
|
||||||
|
accum += start.elapsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Parsing average: {:?}", accum / iters);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let result = OsuPP::new(&map).mods(0).calculate();
|
||||||
|
|
||||||
|
let iters = 100;
|
||||||
|
let accum = start.elapsed();
|
||||||
|
|
||||||
|
// * Tiny benchmark for pp calculation
|
||||||
|
// let mut accum = accum;
|
||||||
|
|
||||||
|
// for _ in 0..iters {
|
||||||
|
// let start = Instant::now();
|
||||||
|
// let _result = OsuPP::new(&map).mods(0).calculate();
|
||||||
|
// accum += start.elapsed();
|
||||||
|
// }
|
||||||
|
|
||||||
|
println!("{:#?}", result);
|
||||||
|
println!("Calculation average: {:?}", accum / iters);
|
||||||
|
}
|
||||||
|
|||||||
@@ -336,48 +336,3 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
strains,
|
strains,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn custom_osu() {
|
|
||||||
use std::{fs::File, time::Instant};
|
|
||||||
|
|
||||||
let path = "./maps/2753127.osu";
|
|
||||||
// let path = "E:Games/osu!/beatmaps/2571051.osu";
|
|
||||||
let file = File::open(path).unwrap();
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
let map = Beatmap::parse(file).unwrap();
|
|
||||||
|
|
||||||
let iters = 100;
|
|
||||||
let accum = start.elapsed();
|
|
||||||
|
|
||||||
// * Tiny benchmark for map parsing
|
|
||||||
let mut accum = accum;
|
|
||||||
|
|
||||||
for _ in 0..iters {
|
|
||||||
let file = File::open(path).unwrap();
|
|
||||||
let start = Instant::now();
|
|
||||||
let _map = Beatmap::parse(file).unwrap();
|
|
||||||
accum += start.elapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Parsing average: {:?}", accum / iters);
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
let result = crate::OsuPP::new(&map).mods(0).calculate();
|
|
||||||
|
|
||||||
let iters = 100;
|
|
||||||
let accum = start.elapsed();
|
|
||||||
|
|
||||||
// * Tiny benchmark for pp calculation
|
|
||||||
// let mut accum = accum;
|
|
||||||
|
|
||||||
// for _ in 0..iters {
|
|
||||||
// let start = Instant::now();
|
|
||||||
// let _result = crate::OsuPP::new(&map).mods(0).calculate();
|
|
||||||
// accum += start.elapsed();
|
|
||||||
// }
|
|
||||||
|
|
||||||
println!("{:#?}", result);
|
|
||||||
println!("Calculation average: {:?}", accum / iters);
|
|
||||||
}
|
|
||||||
|
|||||||
+189
-382
@@ -391,217 +391,223 @@ 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;
|
#[cfg(feature = "sliders")]
|
||||||
|
// `point_split` will be of type `Vec<&str>
|
||||||
|
// with each element having its lifetime bound to `buf`.
|
||||||
|
// To cirvumvent this, `point_split_raw` will contain
|
||||||
|
// the actual `&str` elements transmuted into `usize`.
|
||||||
|
let mut point_split_raw: Vec<usize> = Vec::new();
|
||||||
|
|
||||||
// while read_line!($reader, $buf)? != 0 {
|
#[cfg(feature = "sliders")]
|
||||||
// let line = line_prepare!($buf);
|
// Buffer to re-use for all sliders
|
||||||
|
let mut vertices = Vec::new();
|
||||||
|
|
||||||
// if line.starts_with('[') && line.ends_with(']') {
|
while read_line!($reader, $buf)? != 0 {
|
||||||
// *$section = Section::from_str(&line[1..line.len() - 1]);
|
let line = line_prepare!($buf);
|
||||||
// empty = false;
|
|
||||||
// $buf.clear();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let mut split = line.split(',');
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
*$section = Section::from_str(&line[1..line.len() - 1]);
|
||||||
|
empty = false;
|
||||||
|
$buf.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// let pos = Pos2 {
|
let mut split = line.split(',');
|
||||||
// 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")
|
let pos = Pos2 {
|
||||||
// .trim()
|
x: split.next().next_field("x pos")?.parse()?,
|
||||||
// .parse()?;
|
y: split.next().next_field("y pos")?.parse()?,
|
||||||
|
};
|
||||||
|
|
||||||
// validate_float!(time);
|
let time = split
|
||||||
|
.next()
|
||||||
|
.next_field("hitobject time")?
|
||||||
|
.trim()
|
||||||
|
.parse::<f32>()?
|
||||||
|
.validate()?;
|
||||||
|
|
||||||
// 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 = split.next().next_field("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(feature = "sliders")]
|
||||||
// feature = "fruits",
|
{
|
||||||
// all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
let mut control_points = Vec::new();
|
||||||
// ))]
|
|
||||||
// {
|
|
||||||
// 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 control_point_iter = split.next().next_field("control points")?.split('|');
|
||||||
|
let mut repeats: usize = split.next().next_field("repeats")?.parse()?;
|
||||||
|
|
||||||
// let mut repeats: usize = next_field!(split.next(), "repeats")
|
if repeats > 9000 {
|
||||||
// .parse()?;
|
return Err(ParseError::TooManyRepeats);
|
||||||
|
}
|
||||||
|
|
||||||
// if repeats > 9000 {
|
// * osu-stable treated the first span of the slider
|
||||||
// return Err(ParseError::TooManyRepeats);
|
// * as a repeat, but no repeats are happening
|
||||||
// }
|
repeats = repeats.saturating_sub(1);
|
||||||
|
|
||||||
// // * osu-stable treated the first span of the slider
|
let mut start_idx = 0;
|
||||||
// // * as a repeat, but no repeats are happening
|
let mut end_idx = 0;
|
||||||
// repeats = repeats.saturating_sub(1);
|
let mut first = true;
|
||||||
|
|
||||||
// let mut path_type: PathType =
|
// SAFETY: `Vec<usize>` and `Vec<&str>` have the same size and layout.
|
||||||
// next_field!(curve_point_iter.next(), "path kind").parse()?;
|
let point_split: &mut Vec<&str> =
|
||||||
|
unsafe { std::mem::transmute(&mut point_split_raw) };
|
||||||
|
|
||||||
// for pos in curve_point_iter {
|
point_split.clear();
|
||||||
// let mut v = pos.split(':').map(str::parse);
|
point_split.extend(control_point_iter);
|
||||||
|
|
||||||
// match (v.next(), v.next()) {
|
#[allow(clippy::blocks_in_if_conditions)]
|
||||||
// (Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
while {
|
||||||
// _ => return Err(ParseError::InvalidCurvePoints),
|
end_idx += 1;
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// match path_type {
|
end_idx < point_split.len()
|
||||||
// PathType::Linear if curve_points.len() % 2 == 0 => {
|
} {
|
||||||
// // Assert that the points are of the form A|B|B|C|C|E
|
// * Keep incrementing end_idx while it's not the start of a new segment
|
||||||
// if math_util::valid_linear(&curve_points) {
|
// * (indicated by having a type descriptor of length 1).
|
||||||
// for i in (2..curve_points.len() - 1).rev().step_by(2) {
|
if point_split[end_idx].len() > 1 {
|
||||||
// curve_points.remove(i);
|
continue;
|
||||||
// }
|
}
|
||||||
// } 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.
|
// * Multi-segmented sliders DON'T contain the end point as part of the
|
||||||
// // Necessary to handle maps like XNOR (2573164) which have
|
// * current segment as it's assumed to be the start of the next segment.
|
||||||
// // tens of thousands of curvepoints more efficiently.
|
// * The start of the next segment is the index after the type descriptor.
|
||||||
// while curve_points.len() > CURVE_POINT_THRESHOLD {
|
let end_point = point_split.get(end_idx + 1).copied();
|
||||||
// let last = curve_points[curve_points.len() - 1];
|
|
||||||
// let last_idx = (curve_points.len() - 1) / 2;
|
|
||||||
|
|
||||||
// for i in 1..=last_idx {
|
convert_points(
|
||||||
// curve_points.swap(i, 2 * i);
|
&point_split[start_idx..end_idx],
|
||||||
// }
|
end_point,
|
||||||
|
first,
|
||||||
|
pos,
|
||||||
|
&mut control_points,
|
||||||
|
&mut vertices,
|
||||||
|
)?;
|
||||||
|
|
||||||
// curve_points[last_idx] = last;
|
start_idx = end_idx;
|
||||||
// curve_points.truncate(last_idx + 1);
|
first = false;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if curve_points.is_empty() {
|
if end_idx > start_idx {
|
||||||
// HitObjectKind::Circle
|
convert_points(
|
||||||
// } else {
|
&point_split[start_idx..end_idx],
|
||||||
// // TODO: Should be Option<f32>?
|
None,
|
||||||
// let pixel_len = next_field!(split.next(), "pixel len")
|
first,
|
||||||
// .parse::<f32>()?
|
pos,
|
||||||
// .max(0.0)
|
&mut control_points,
|
||||||
// .min(MAX_COORDINATE_VALUE);
|
&mut vertices,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
// HitObjectKind::Slider {
|
if control_points.is_empty() {
|
||||||
// repeats,
|
HitObjectKind::Circle
|
||||||
// pixel_len,
|
} else {
|
||||||
// curve_points,
|
let pixel_len = split
|
||||||
// path_type,
|
.next()
|
||||||
// }
|
.next_field("pixel len")?
|
||||||
// }
|
.parse::<f32>()?
|
||||||
// }
|
.max(0.0)
|
||||||
|
.min(MAX_COORDINATE_VALUE);
|
||||||
|
|
||||||
// #[cfg(not(any(
|
HitObjectKind::Slider {
|
||||||
// feature = "fruits",
|
repeats,
|
||||||
// all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
pixel_len,
|
||||||
// )))]
|
control_points,
|
||||||
// {
|
}
|
||||||
// let repeats = next_field!(split.nth(1), "repeats").parse::<usize>()?;
|
}
|
||||||
// let len: f32 = next_field!(split.next(), "pixel len").parse()?;
|
}
|
||||||
|
|
||||||
// HitObjectKind::Slider {
|
#[cfg(not(feature = "sliders"))]
|
||||||
// repeats,
|
{
|
||||||
// pixel_len: len,
|
let repeats = split.nth(1).next_field("repeats")?.parse()?;
|
||||||
// }
|
let pixel_len = split.next().next_field("pixel len")?.parse()?;
|
||||||
// }
|
|
||||||
// } 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 }
|
HitObjectKind::Slider { repeats, pixel_len }
|
||||||
// } else if kind & Self::HOLD_FLAG > 0 {
|
}
|
||||||
// $self.n_sliders += 1;
|
} else if kind & Self::SPINNER_FLAG > 0 {
|
||||||
// let mut end = time;
|
$self.n_spinners += 1;
|
||||||
|
let end_time = split.next().next_field("spinner endtime")?.parse()?;
|
||||||
|
|
||||||
// if let Some(next) = split.next() {
|
HitObjectKind::Spinner { end_time }
|
||||||
// end = end.max(next_field!(next.split(':').next(), "hold endtime").parse()?);
|
} else if kind & Self::HOLD_FLAG > 0 {
|
||||||
// }
|
$self.n_sliders += 1;
|
||||||
|
let mut end = time;
|
||||||
|
|
||||||
// HitObjectKind::Hold { end_time: end }
|
if let Some(next) = split.next() {
|
||||||
// } else {
|
end = end.max(next.split(':').next().next_field("hold endtime")?.parse()?);
|
||||||
// return Err(ParseError::UnknownHitObjectKind);
|
}
|
||||||
// };
|
|
||||||
|
|
||||||
// $self.hit_objects.push(HitObject {
|
HitObjectKind::Hold { end_time: end }
|
||||||
// pos,
|
} else {
|
||||||
// start_time: time,
|
return Err(ParseError::UnknownHitObjectKind);
|
||||||
// kind,
|
};
|
||||||
// sound, // TODO: omit if not taiko?
|
|
||||||
// });
|
|
||||||
|
|
||||||
// prev_time = time;
|
$self.hit_objects.push(HitObject {
|
||||||
// $buf.clear();
|
pos,
|
||||||
// }
|
start_time: time,
|
||||||
|
kind,
|
||||||
|
sound,
|
||||||
|
});
|
||||||
|
|
||||||
// // BUG: If [General] section comes after [HitObjects] then the mode
|
prev_time = time;
|
||||||
// // won't be set yet so mania objects won't be sorted properly
|
$buf.clear();
|
||||||
// if $self.mode == GameMode::MNA {
|
}
|
||||||
// // First a _stable_ sort by time
|
|
||||||
// sort!(stable $self.hit_objects);
|
|
||||||
|
|
||||||
// // Then the legacy sort for correct position order
|
// BUG: If [General] section comes after [HitObjects] then the mode
|
||||||
// legacy_sort(&mut $self.hit_objects);
|
// won't be set yet so mania objects won't be sorted properly
|
||||||
// } else if unsorted {
|
if $self.mode == GameMode::MNA {
|
||||||
// sort!($self.hit_objects);
|
// First a _stable_ sort by time
|
||||||
// }
|
$self
|
||||||
|
.hit_objects
|
||||||
|
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
// Ok(empty)
|
// Then the legacy sort for correct position order
|
||||||
// }};
|
legacy_sort(&mut $self.hit_objects);
|
||||||
// }
|
} else if unsorted {
|
||||||
|
sort_unstable(&mut $self.hit_objects);
|
||||||
|
}
|
||||||
|
|
||||||
// macro_rules! parse_hitobjects {
|
Ok(empty)
|
||||||
// ($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>) => {
|
macro_rules! parse_hitobjects {
|
||||||
// async fn parse_hitobjects<R: $inner + Unpin>(
|
($reader:ident<$inner:ident>) => {
|
||||||
// &mut self,
|
fn parse_hitobjects<R: $inner>(
|
||||||
// reader: &mut $reader<R>,
|
&mut self,
|
||||||
// buf: &mut String,
|
reader: &mut $reader<R>,
|
||||||
// section: &mut Section,
|
buf: &mut String,
|
||||||
// ) -> ParseResult<bool> {
|
section: &mut Section,
|
||||||
// parse_hitobjects_body!(self, reader, buf, 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 {
|
macro_rules! parse_body {
|
||||||
($reader:ident<$inner:ident>: $input:ident) => {{
|
($reader:ident<$inner:ident>: $input:ident) => {{
|
||||||
@@ -737,214 +743,6 @@ impl Beatmap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
|
||||||
impl Beatmap {
|
|
||||||
parse!(BufReader<Read>);
|
|
||||||
parse_general!(BufReader<Read>);
|
|
||||||
parse_difficulty!(BufReader<Read>);
|
|
||||||
parse_timingpoints!(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;
|
|
||||||
|
|
||||||
#[cfg(feature = "sliders")]
|
|
||||||
// `point_split` will be of type `Vec<&str>
|
|
||||||
// with each element having its lifetime bound to `buf`.
|
|
||||||
// To cirvumvent this, `point_split_raw` will contain
|
|
||||||
// the actual `&str` elements transmuted into `usize`.
|
|
||||||
let mut point_split_raw: Vec<usize> = Vec::new();
|
|
||||||
|
|
||||||
#[cfg(feature = "sliders")]
|
|
||||||
// Buffer to re-use for all sliders
|
|
||||||
let mut vertices = Vec::new();
|
|
||||||
|
|
||||||
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(feature = "sliders")]
|
|
||||||
{
|
|
||||||
let mut control_points = Vec::new();
|
|
||||||
|
|
||||||
let control_point_iter = split.next().next_field("control 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;
|
|
||||||
|
|
||||||
// SAFETY: `Vec<usize>` and `Vec<&str>` have the same size and layout.
|
|
||||||
let point_split: &mut Vec<&str> =
|
|
||||||
unsafe { std::mem::transmute(&mut point_split_raw) };
|
|
||||||
|
|
||||||
point_split.clear();
|
|
||||||
point_split.extend(control_point_iter);
|
|
||||||
|
|
||||||
#[allow(clippy::blocks_in_if_conditions)]
|
|
||||||
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 control_points,
|
|
||||||
&mut vertices,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
start_idx = end_idx;
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if end_idx > start_idx {
|
|
||||||
convert_points(
|
|
||||||
&point_split[start_idx..end_idx],
|
|
||||||
None,
|
|
||||||
first,
|
|
||||||
pos,
|
|
||||||
&mut control_points,
|
|
||||||
&mut vertices,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if control_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,
|
|
||||||
control_points,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "sliders"))]
|
|
||||||
{
|
|
||||||
let repeats = split.nth(1).next_field("repeats")?.parse()?;
|
|
||||||
let pixel_len = split.next().next_field("pixel len")?.parse()?;
|
|
||||||
|
|
||||||
HitObjectKind::Slider { repeats, pixel_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,
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
self.hit_objects
|
|
||||||
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "sliders")]
|
#[cfg(feature = "sliders")]
|
||||||
mod osu_fruits {
|
mod osu_fruits {
|
||||||
use crate::{math_util::is_linear, ParseError};
|
use crate::{math_util::is_linear, ParseError};
|
||||||
@@ -1087,6 +885,15 @@ mod osu_fruits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
|
impl Beatmap {
|
||||||
|
parse!(BufReader<Read>);
|
||||||
|
parse_general!(BufReader<Read>);
|
||||||
|
parse_difficulty!(BufReader<Read>);
|
||||||
|
parse_timingpoints!(BufReader<Read>);
|
||||||
|
parse_hitobjects!(BufReader<Read>);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async_tokio")]
|
#[cfg(feature = "async_tokio")]
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
parse!(async BufReader<AsyncRead>);
|
parse!(async BufReader<AsyncRead>);
|
||||||
|
|||||||
Reference in New Issue
Block a user