wip osu pp update

This commit is contained in:
MaxOhn
2022-10-07 18:46:17 +02:00
parent fae3a0359c
commit 5eb8dab588
34 changed files with 2415 additions and 1931 deletions
+1 -20
View File
@@ -1,9 +1,4 @@
use std::{
error::Error as StdError,
fmt,
io::Error as IoError,
num::{ParseFloatError, ParseIntError},
};
use std::{error::Error as StdError, fmt, io::Error as IoError, num::ParseFloatError};
/// `Result<_, ParseError>`
pub type ParseResult<T> = Result<T, ParseError>;
@@ -22,14 +17,10 @@ pub enum ParseError {
InvalidCurvePoints,
/// Expected a decimal number, got something else.
InvalidDecimalNumber,
/// Expected an integer, got something else.
InvalidInteger,
/// Failed to parse game mode.
InvalidMode,
/// Expected an additional field.
MissingField(&'static str),
/// Reject maps with too many repeat points.
TooManyRepeats,
/// Failed to recognized specified type for hitobjects.
UnknownHitObjectKind,
}
@@ -43,11 +34,9 @@ impl fmt::Display for ParseError {
}
Self::BadLine => f.write_str("line not in `Key:Value` pattern"),
Self::InvalidCurvePoints => f.write_str("invalid curve point"),
Self::InvalidInteger => f.write_str("invalid integer"),
Self::InvalidDecimalNumber => f.write_str("invalid float number"),
Self::InvalidMode => f.write_str("invalid mode"),
Self::MissingField(field) => write!(f, "missing field `{}`", field),
Self::TooManyRepeats => f.write_str("repeat count is way too high"),
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
}
}
@@ -60,11 +49,9 @@ impl StdError for ParseError {
Self::IncorrectFileHeader => None,
Self::BadLine => None,
Self::InvalidCurvePoints => None,
Self::InvalidInteger => None,
Self::InvalidDecimalNumber => None,
Self::InvalidMode => None,
Self::MissingField(_) => None,
Self::TooManyRepeats => None,
Self::UnknownHitObjectKind => None,
}
}
@@ -76,12 +63,6 @@ impl From<IoError> for ParseError {
}
}
impl From<ParseIntError> for ParseError {
fn from(_: ParseIntError) -> Self {
Self::InvalidInteger
}
}
impl From<ParseFloatError> for ParseError {
fn from(_: ParseFloatError) -> Self {
Self::InvalidDecimalNumber
+1 -1
View File
@@ -69,7 +69,7 @@ pub enum HitObjectKind {
/// A full slider object.
Slider {
/// Total length of the slider in pixels.
pixel_len: f64,
pixel_len: Option<f64>,
/// The amount of repeat points of the slider.
repeats: usize,
/// The control points of the slider.
+198 -96
View File
@@ -14,7 +14,10 @@ pub use slider_parsing::*;
use reader::FileReader;
pub(crate) use sort::legacy_sort;
use std::cmp::Ordering;
use std::{
cmp::Ordering,
ops::{ControlFlow, Neg},
};
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
use std::{fs::File, io::Read};
@@ -44,18 +47,33 @@ impl<T> OptionExt<T> for Option<T> {
}
}
trait FloatExt: Sized {
fn validate(self) -> Result<Self, ParseError>;
}
trait InRange: Sized + Copy + Neg<Output = Self> + PartialOrd {
const LIMIT: Self;
impl FloatExt for f64 {
fn validate(self) -> Result<Self, ParseError> {
self.is_finite()
.then(|| self)
.ok_or(ParseError::InvalidDecimalNumber)
fn is_in_range(&self) -> bool {
(-Self::LIMIT..=Self::LIMIT).contains(self)
}
fn is_in_custom_range(&self, limit: Self) -> bool {
(-limit..=limit).contains(self)
}
}
impl InRange for i32 {
const LIMIT: Self = i32::MAX;
}
impl InRange for f32 {
const LIMIT: Self = i32::MAX as f32;
}
impl InRange for f64 {
const LIMIT: Self = i32::MAX as f64;
}
const MAX_COORDINATE_VALUE: i32 = 131_072;
const KIAI_FLAG: i32 = 1 << 0;
macro_rules! section {
($map:ident, $func:ident, $reader:ident, $section:ident) => {{
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
@@ -201,14 +219,11 @@ macro_rules! parse_events_body {
macro_rules! parse_timingpoints_body {
($self:ident, $reader:ident, $section:ident) => {{
let mut unsorted_timings = false;
let mut unsorted_difficulties = false;
let mut prev_diff = 0.0;
let mut prev_time = 0.0;
let mut empty = true;
let mut pending_diff_points_time = 0.0;
let mut pending_diff_point = None;
while next_line!($reader)? != 0 {
if let Some(bytes) = $reader.get_section() {
*$section = Section::from_bytes(bytes);
@@ -219,65 +234,125 @@ macro_rules! parse_timingpoints_body {
let line = $reader.get_line()?;
let mut split = line.split(',');
let time = split
let time: f64 = split
.next()
.next_field("timing point time")?
.trim()
.parse::<f64>()?
.validate()?;
.parse()?;
if !time.is_in_range() {
continue;
}
// * beatLength is allowed to be NaN to handle an edge case in which
// * some beatmaps use NaN slider velocity to disable slider tick
// * generation (see LegacyDifficultyControlPoint).
let beat_len: f64 = split.next().next_field("beat len")?.trim().parse()?;
let timing_change = split.nth(4).and_then(|value| value.bytes().next());
let effect_flags = split.next().and_then(|value| value.bytes().next());
let kiai = matches!(effect_flags, Some(b'1'));
if !(beat_len.is_in_range() || beat_len.is_nan()) {
continue;
}
if matches!(timing_change, Some(b'1') | None) {
let beat_len = beat_len.clamp(6.0, 60_000.0);
let mut timing_change = true;
let mut kiai = false;
enum Status {
Ok,
Err,
}
fn parse_remaining<'s, I>(
mut split: I,
timing_change: &mut bool,
kiai: &mut bool,
) -> Status
where
I: Iterator<Item = &'s str>,
{
match split
.next()
.filter(|&sig| !sig.starts_with('0'))
.map(str::parse::<i32>)
{
Some(Ok(time_sig)) if !time_sig.is_in_range() || time_sig < 1 => {
return Status::Err
}
Some(Ok(_)) => {}
None => return Status::Ok,
Some(Err(_)) => return Status::Err,
}
match split.next().map(str::parse::<i32>) {
Some(Ok(sample_set)) if !sample_set.is_in_range() => return Status::Err,
Some(Ok(_)) => {}
None => return Status::Ok,
Some(Err(_)) => return Status::Err,
}
match split.next().map(str::parse::<i32>) {
Some(Ok(custom_sample)) if !custom_sample.is_in_range() => return Status::Err,
Some(Ok(_)) => {}
None => return Status::Ok,
Some(Err(_)) => return Status::Err,
}
match split.next().map(str::parse::<i32>) {
Some(Ok(sample_volume)) if !sample_volume.is_in_range() => return Status::Err,
Some(Ok(_)) => {}
None => return Status::Ok,
Some(Err(_)) => return Status::Err,
}
if let Some(byte) = split.next().and_then(|value| value.bytes().next()) {
*timing_change = byte == b'1';
} else {
return Status::Ok;
}
match split.next().map(str::parse::<i32>) {
Some(Ok(effect_flags)) if !effect_flags.is_in_range() => return Status::Err,
Some(Ok(effect_flags)) => *kiai = (effect_flags & KIAI_FLAG) > 0,
None => return Status::Ok,
Some(Err(_)) => return Status::Err,
}
Status::Ok
}
if let Status::Err = parse_remaining(split, &mut timing_change, &mut kiai) {
continue;
}
if timing_change {
let point = TimingPoint {
time,
beat_len,
beat_len: beat_len.clamp(6.0, 60_000.0),
kiai,
};
$self.timing_points.push(point);
}
if time < prev_time {
unsorted_timings = true;
} else {
prev_time = time;
}
// * If beatLength is NaN, speedMultiplier should still be 1
// * because all comparisons against NaN are false.
let speed_multiplier = if beat_len < 0.0 {
(100.0 / -beat_len)
} else {
let speed_multiplier = if beat_len < 0.0 {
(-100.0 / beat_len).clamp(0.1, 10.0)
} else {
1.0
};
1.0
};
let point = DifficultyPoint {
time,
speed_multiplier,
kiai,
};
$self.difficulty_points.push(point);
if time < prev_diff {
unsorted_difficulties = true;
} else {
prev_diff = time;
if time != pending_diff_points_time {
if let Some(point) = pending_diff_point.take() {
$self.difficulty_points.push_if_not_redundant(point);
}
}
pending_diff_point = Some(DifficultyPoint::new(time, beat_len, speed_multiplier, kiai));
pending_diff_points_time = time;
}
if unsorted_timings {
sort_unstable(&mut $self.timing_points);
}
if unsorted_difficulties {
sort_unstable(&mut $self.difficulty_points);
}
$self.timing_points.dedup_by_key(|point| point.time);
$self.difficulty_points.dedup_by_key(|point| point.time);
Ok(empty)
}};
@@ -308,24 +383,36 @@ macro_rules! parse_hitobjects_body {
let line = $reader.get_line()?;
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 x: f32 = split.next().next_field("x pos")?.parse()?;
let y: f32 = split.next().next_field("y pos")?.parse()?;
let time = split
.next()
.next_field("hitobject time")?
.trim()
.parse::<f64>()?
.validate()?;
if !(x.is_in_custom_range(MAX_COORDINATE_VALUE as f32)
&& y.is_in_custom_range(MAX_COORDINATE_VALUE as f32))
{
continue;
}
let pos = Pos2 { x, y };
let time: f64 = split.next().next_field("hitobject time")?.trim().parse()?;
if !time.is_in_range() {
continue;
}
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: u8 = match split.next().next_field("hitobject kind")?.parse() {
Ok(kind) => kind,
Err(_) => continue,
};
let sound: u8 = match split.next().next_field("sound")?.parse() {
Ok(sound) => sound,
Err(_) => continue,
};
let kind = if kind & Self::CIRCLE_FLAG > 0 {
$self.n_circles += 1;
@@ -337,15 +424,13 @@ macro_rules! parse_hitobjects_body {
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 repeats = match split.next().next_field("repeats")?.parse::<usize>() {
// * osu-stable treated the first span of the slider
// * as a repeat, but no repeats are happening
Ok(repeats @ 0..=9000) => repeats.saturating_sub(1),
Ok(_) | Err(_) => continue,
};
let mut start_idx = 0;
let mut end_idx = 0;
@@ -402,25 +487,25 @@ macro_rules! parse_hitobjects_body {
if control_points.is_empty() {
HitObjectKind::Circle
} else {
let pixel_len = split
.next()
.next_field("pixel len")?
.parse::<f64>()?
.max(0.0)
.min(MAX_COORDINATE_VALUE);
let pixel_len = match split.next().map(str::parse::<f64>) {
Some(Ok(len)) if len.is_in_custom_range(MAX_COORDINATE_VALUE as f64) => {
(len != 0.0).then_some(len)
}
Some(_) => continue,
None => None,
};
let edge_sounds_opt = split.next().map(|sounds| {
sounds
.split('|')
.take(repeats + 2)
.map(parse_custom_sound)
.collect::<Result<Vec<_>, _>>()
.collect()
});
let edge_sounds = match edge_sounds_opt {
None => Vec::new(),
Some(Ok(sounds)) => sounds,
Some(Err(err)) => return Err(err),
Some(sounds) => sounds,
};
HitObjectKind::Slider {
@@ -432,18 +517,27 @@ macro_rules! parse_hitobjects_body {
}
} else if kind & Self::SPINNER_FLAG > 0 {
$self.n_spinners += 1;
let end_time = split.next().next_field("spinner endtime")?.parse()?;
let end_time = match split.next().next_field("spinner endtime")?.parse::<f64>() {
Ok(end_time) => end_time.max(0.0),
Err(_) => continue,
};
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()?);
}
let end_time = match split
.next()
.and_then(|next| next.split(':').next())
.map(str::parse::<f64>)
{
Some(Ok(time_)) if time_.is_in_range() => time_.max(time),
Some(_) => continue,
None => time,
};
HitObjectKind::Hold { end_time: end }
HitObjectKind::Hold { end_time }
} else {
return Err(ParseError::UnknownHitObjectKind);
};
@@ -453,6 +547,7 @@ macro_rules! parse_hitobjects_body {
start_time: time,
kind,
});
$self.sounds.push(sound);
prev_time = time;
@@ -476,12 +571,21 @@ macro_rules! parse_hitobjects_body {
}};
}
// Required for maps with slider edge sound values above 255 e.g. map id 80799
fn parse_custom_sound(sound: &str) -> ParseResult<u8> {
sound.bytes().try_fold(0_u8, |sound, byte| match byte {
b'0'..=b'9' => Ok(sound.wrapping_mul(10).wrapping_add((byte & 0xF) as u8)),
_ => Err(ParseError::InvalidInteger),
})
// Required for maps with slider edge sound values above 255 e.g. map /b/80799
fn parse_custom_sound(sound: &str) -> u8 {
fn fold_str(sound: &str) -> ControlFlow<u8, u8> {
sound.bytes().try_fold(0_u8, |sound, byte| match byte {
b'0'..=b'9' => {
ControlFlow::Continue(sound.wrapping_mul(10).wrapping_add((byte & 0xF) as u8))
}
_ => ControlFlow::Break(0),
})
}
match fold_str(sound) {
ControlFlow::Continue(n) => n,
ControlFlow::Break(n) => n,
}
}
macro_rules! parse_body {
@@ -539,8 +643,6 @@ mod slider_parsing {
use super::Pos2;
pub(super) const MAX_COORDINATE_VALUE: f64 = 131_072.0;
pub(super) fn convert_points(
points: &[&str],
end_point: Option<&str>,
+8 -4
View File
@@ -32,13 +32,13 @@ impl Pos2 {
/// Return the position's length.
#[inline]
pub fn length(&self) -> f32 {
self.x.hypot(self.y)
((self.x * self.x + self.y * self.y) as f64).sqrt() as f32
}
/// Return the dot product.
#[inline]
pub fn dot(&self, other: Self) -> f32 {
self.x.mul_add(other.x, self.y * other.y)
(self.x * other.x) + (self.y * other.y)
}
/// Return the distance to another position.
@@ -49,8 +49,12 @@ impl Pos2 {
/// Normalize the coordinates with respect to the vector's length.
#[inline]
pub fn normalize(self) -> Pos2 {
self / self.length()
pub fn normalize(mut self) -> Pos2 {
let scale = self.length().recip();
self.x *= scale;
self.y *= scale;
self
}
}
+2 -2
View File
@@ -138,7 +138,7 @@ impl<R> FileReader<R> {
.and_then(|idx| {
self.buf[idx..]
.starts_with(b"osu file format v")
.then(|| idx + 17)
.then_some(idx + 17)
})
.map(|idx| {
let mut n = 0;
@@ -235,7 +235,7 @@ impl<R> FileReader<R> {
.iter()
.enumerate()
.rev()
.find_map(|(i, byte)| (!matches!(byte, b' ' | b'\t')).then(|| i + 1))
.find_map(|(i, byte)| (!matches!(byte, b' ' | b'\t')).then_some(i + 1))
.unwrap_or(0);
self.buf.truncate(len);