parse: cleanup

This commit is contained in:
MaxOhn
2021-01-17 11:00:18 +01:00
parent bd69e1782a
commit 6cce937be8
5 changed files with 332 additions and 233 deletions
+2 -2
View File
@@ -391,7 +391,7 @@ mod tests {
#[test] #[test]
#[ignore] #[ignore]
fn fruits_single() { fn fruits_single() {
let file = match File::open("E:/Games/osu!/beatmaps/2206596.osu") { let file = match File::open("E:/Games/osu!/beatmaps/1974968.osu") {
Ok(file) => file, Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why), Err(why) => panic!("Could not open file: {}", why),
}; };
@@ -401,7 +401,7 @@ mod tests {
Err(why) => panic!("Error while parsing map: {}", why), Err(why) => panic!("Error while parsing map: {}", why),
}; };
let result = PpCalculator::new(&map).mods(64).calculate(); let result = PpCalculator::new(&map).mods(0).calculate();
println!("Stars: {}", result.stars); println!("Stars: {}", result.stars);
println!("PP: {}", result.pp); println!("PP: {}", result.pp);
+5 -1
View File
@@ -94,7 +94,11 @@ mod tests {
Err(why) => panic!("Error while parsing map: {}", why), Err(why) => panic!("Error while parsing map: {}", why),
}; };
let result = PpCalculator::new(&map).mods(0).calculate(); // println!("timings:\n{:#?}", map.timing_points);
// println!("---");
// println!("difficulties:\n{:#?}", map.difficulty_points);
let result = PpCalculator::new(&map).mods(256).calculate();
println!("Stars: {}", result.stars); println!("Stars: {}", result.stars);
println!("PP: {}", result.pp); println!("PP: {}", result.pp);
@@ -177,6 +177,7 @@ mod tests {
use std::fs::File; use std::fs::File;
#[test] #[test]
#[ignore]
fn no_sliders_single_stars() { fn no_sliders_single_stars() {
let file = match File::open("E:/Games/osu!/beatmaps/70090.osu") { let file = match File::open("E:/Games/osu!/beatmaps/70090.osu") {
Ok(file) => file, Ok(file) => file,
@@ -257,6 +258,7 @@ mod tests {
} }
#[test] #[test]
#[ignore]
fn no_sliders_single_pp() { fn no_sliders_single_pp() {
let file = match File::open("E:/Games/osu!/beatmaps/1241370.osu") { let file = match File::open("E:/Games/osu!/beatmaps/1241370.osu") {
Ok(file) => file, Ok(file) => file,
+4 -19
View File
@@ -14,17 +14,12 @@ pub enum ParseError {
BadLine, BadLine,
InvalidCurvePoints, InvalidCurvePoints,
InvalidInteger, InvalidInteger,
InvalidEffectFlag,
InvalidFloatingPoint, InvalidFloatingPoint,
InvalidMode, InvalidMode,
InvalidPathType, InvalidPathType,
InvalidTimingSignature, InvalidTimingSignature,
MissingField(usize), MissingField,
UnsupportedMode,
UnknownHitObjectKind, UnknownHitObjectKind,
NoHitobjects,
UnsortedHitobjects,
UnsortedTimingPoints,
} }
impl fmt::Display for ParseError { impl fmt::Display for ParseError {
@@ -37,17 +32,12 @@ impl fmt::Display for ParseError {
Self::BadLine => f.write_str("line not in `Key:Value` pattern"), Self::BadLine => f.write_str("line not in `Key:Value` pattern"),
Self::InvalidCurvePoints => f.write_str("invalid curve point"), Self::InvalidCurvePoints => f.write_str("invalid curve point"),
Self::InvalidInteger => f.write_str("invalid integer"), Self::InvalidInteger => f.write_str("invalid integer"),
Self::InvalidEffectFlag => f.write_str("invalid effect flag"), Self::InvalidFloatingPoint => f.write_str("invalid float number"),
Self::InvalidFloatingPoint => f.write_str("invalid floating-point number"),
Self::InvalidMode => f.write_str("invalid mode"), Self::InvalidMode => f.write_str("invalid mode"),
Self::InvalidPathType => f.write_str("invalid path type"), Self::InvalidPathType => f.write_str("invalid path type"),
Self::InvalidTimingSignature => f.write_str("invalid timing signature"), Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
Self::MissingField(line) => write!(f, "missing field on line {}", line), Self::MissingField => f.write_str("missing field"),
Self::UnsupportedMode => f.write_str("unsupported osu! mode"),
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"), Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
Self::NoHitobjects => f.write_str("beatmap has no hitobjects"),
Self::UnsortedHitobjects => f.write_str("hitobjects are not sorted by time"),
Self::UnsortedTimingPoints => f.write_str("timing points are not sorted by time"),
} }
} }
} }
@@ -60,17 +50,12 @@ impl StdError for ParseError {
Self::BadLine => None, Self::BadLine => None,
Self::InvalidCurvePoints => None, Self::InvalidCurvePoints => None,
Self::InvalidInteger => None, Self::InvalidInteger => None,
Self::InvalidEffectFlag => None,
Self::InvalidFloatingPoint => None, Self::InvalidFloatingPoint => None,
Self::InvalidMode => None, Self::InvalidMode => None,
Self::InvalidPathType => None, Self::InvalidPathType => None,
Self::InvalidTimingSignature => None, Self::InvalidTimingSignature => None,
Self::MissingField(_) => None, Self::MissingField => None,
Self::UnsupportedMode => None,
Self::UnknownHitObjectKind => None, Self::UnknownHitObjectKind => None,
Self::NoHitobjects => None,
Self::UnsortedHitobjects => None,
Self::UnsortedTimingPoints => None,
} }
} }
} }
+204 -96
View File
@@ -25,8 +25,8 @@ macro_rules! sort {
} }
macro_rules! next_field { macro_rules! next_field {
($opt:expr, $nmbr:ident) => { ($opt:expr) => {
$opt.ok_or(ParseError::MissingField($nmbr))? $opt.ok_or(ParseError::MissingField)?
}; };
} }
@@ -40,6 +40,35 @@ macro_rules! validate_float {
}}; }};
} }
macro_rules! line_prepare {
($buf:ident) => {{
let mut line = $buf.trim_end();
if line.is_empty()
|| line.starts_with("//")
|| line.starts_with(' ')
|| line.starts_with('_')
{
$buf.clear();
continue;
}
if let Some(idx) = line.find("//") {
line = &line[..idx];
}
line
}};
}
macro_rules! section {
($map:ident, $func:ident, $reader:ident, $buf:ident, $section:ident) => {
if $map.$func(&mut $reader, &mut $buf, &mut $section)? {
break;
}
};
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum GameMode { pub enum GameMode {
STD = 0, STD = 0,
@@ -101,55 +130,56 @@ impl Beatmap {
None => return Err(ParseError::IncorrectFileHeader), None => return Err(ParseError::IncorrectFileHeader),
}; };
// version 4 and lower had an incorrect offset (stable has this set as 24ms off)
let offset = if map.version < 5 { 24.0 } else { 0.0 };
buf.clear(); buf.clear();
map.hit_objects.reserve(256); map.hit_objects.reserve(256);
let mut mode = None;
let mut ar = None;
let mut od = None;
let mut cs = None;
let mut hp = None;
let mut sv = None;
let mut tick_rate = None;
let mut stack_leniency = None;
let mut section = Section::None; let mut section = Section::None;
let mut prev_time = 0.0;
let mut prev_diff = 0.0;
let mut unsorted_timings = false;
let mut unsorted_difficulties = false;
let mut unsorted_hits = false;
let mut nmbr = 1; loop {
match section {
while reader.read_line(&mut buf)? != 0 { Section::General => section!(map, parse_general, reader, buf, section),
let mut line = buf.trim_end(); Section::Difficulty => section!(map, parse_difficulty, reader, buf, section),
nmbr += 1; Section::TimingPoints => section!(map, parse_timingpoints, reader, buf, section),
Section::HitObjects => section!(map, parse_hitobjects, reader, buf, section),
if line.is_empty() Section::None => {
|| line.starts_with("//") if reader.read_line(&mut buf)? == 0 {
|| line.starts_with(' ') break;
|| line.starts_with('_')
{
buf.clear();
continue;
} }
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]);
}
buf.clear(); buf.clear();
continue; }
}
} }
if let Some(idx) = line.find("//") { Ok(map)
line = &line[..idx]; }
fn parse_general<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
section: &mut Section,
) -> ParseResult<bool> {
let mut mode = None;
let mut stack_leniency = None;
let mut empty = true;
while reader.read_line(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;
} }
match section {
Section::General => {
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?; let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
if key == "Mode" { if key == "Mode" {
@@ -163,8 +193,41 @@ impl Beatmap {
} else if key == "StackLeniency" { } else if key == "StackLeniency" {
stack_leniency = Some(value.parse()?); stack_leniency = Some(value.parse()?);
} }
buf.clear();
} }
Section::Difficulty => {
self.mode = next_field!(mode);
self.stack_leniency = next_field!(stack_leniency);
Ok(empty)
}
fn parse_difficulty<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
section: &mut Section,
) -> ParseResult<bool> {
let mut ar = None;
let mut od = None;
let mut cs = None;
let mut hp = None;
let mut sv = None;
let mut tick_rate = None;
let mut empty = true;
while reader.read_line(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 (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?; let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
match key { match key {
@@ -176,22 +239,58 @@ impl Beatmap {
"SliderMultiplier" => sv = Some(value.parse()?), "SliderMultiplier" => sv = Some(value.parse()?),
_ => {} _ => {}
} }
buf.clear();
} }
Section::TimingPoints => {
self.ar = next_field!(ar);
self.od = next_field!(od);
self.cs = next_field!(cs);
self.hp = next_field!(hp);
self.sv = next_field!(sv);
self.tick_rate = next_field!(tick_rate);
Ok(empty)
}
fn parse_timingpoints<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
section: &mut Section,
) -> ParseResult<bool> {
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;
while reader.read_line(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 mut split = line.split(',');
let time = offset + next_field!(split.next(), nmbr).trim().parse::<f32>()?; let time = next_field!(split.next()).trim().parse::<f32>()?;
validate_float!(time); validate_float!(time);
let beat_len = next_field!(split.next(), nmbr).trim().parse::<f32>()?; let beat_len = next_field!(split.next()).trim().parse::<f32>()?;
if beat_len.is_sign_negative() { if beat_len < 0.0 {
let point = DifficultyPoint { let point = DifficultyPoint {
time, time,
speed_multiplier: -100.0 / beat_len, speed_multiplier: -100.0 / beat_len,
}; };
map.difficulty_points.push(point); self.difficulty_points.push(point);
if time < prev_diff { if time < prev_diff {
unsorted_difficulties = true; unsorted_difficulties = true;
@@ -205,7 +304,7 @@ impl Beatmap {
beat_len, beat_len,
}; };
map.timing_points.push(point); self.timing_points.push(point);
if time < prev_time { if time < prev_time {
unsorted_timings = true; unsorted_timings = true;
@@ -213,39 +312,71 @@ impl Beatmap {
prev_time = time; prev_time = time;
} }
} }
buf.clear();
}
if unsorted_timings {
sort!(self.timing_points);
}
if unsorted_difficulties {
sort!(self.difficulty_points);
}
Ok(empty)
}
fn parse_hitobjects<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
section: &mut Section,
) -> ParseResult<bool> {
let mut unsorted = false;
let mut prev_time = 0.0;
let mut empty = true;
while reader.read_line(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;
} }
Section::HitObjects => {
let mut split = line.split(','); let mut split = line.split(',');
let pos = Pos2 { let pos = Pos2 {
x: next_field!(split.next(), nmbr).parse()?, x: next_field!(split.next()).parse()?,
y: next_field!(split.next(), nmbr).parse()?, y: next_field!(split.next()).parse()?,
}; };
let time = offset + next_field!(split.next(), nmbr).trim().parse::<f32>()?; let time = next_field!(split.next()).trim().parse::<f32>()?;
validate_float!(time); validate_float!(time);
if !map.hit_objects.is_empty() && time < prev_time { if !self.hit_objects.is_empty() && time < prev_time {
unsorted_hits = true; unsorted = true;
} }
let kind: u8 = next_field!(split.next(), nmbr).parse()?; let kind: u8 = next_field!(split.next()).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 {
map.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 {
map.n_sliders += 1; self.n_sliders += 1;
let mut curve_points = Vec::with_capacity(16); let mut curve_points = Vec::with_capacity(16);
curve_points.push(pos); curve_points.push(pos);
let mut curve_point_iter = next_field!(split.next(), nmbr).split('|'); let mut curve_point_iter = next_field!(split.next()).split('|');
let mut path_type: PathType = let mut path_type: PathType = next_field!(curve_point_iter.next()).parse()?;
next_field!(curve_point_iter.next(), nmbr).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);
@@ -256,13 +387,12 @@ impl Beatmap {
} }
} }
if map.version <= 6 && curve_points.len() >= 2 { if self.version <= 6 && curve_points.len() >= 2 {
if path_type == PathType::Linear { if path_type == PathType::Linear {
path_type = PathType::Bezier; path_type = PathType::Bezier;
} }
if curve_points.len() == 2 if curve_points.len() == 2 && (pos == curve_points[0] || pos == curve_points[1])
&& (pos == curve_points[0] || pos == curve_points[1])
{ {
path_type = PathType::Linear; path_type = PathType::Linear;
} }
@@ -271,8 +401,8 @@ impl Beatmap {
if curve_points.is_empty() { if curve_points.is_empty() {
HitObjectKind::Circle HitObjectKind::Circle
} else { } else {
let repeats = next_field!(split.next(), nmbr).parse::<usize>()?; let repeats = next_field!(split.next()).parse::<usize>()?;
let len: f32 = next_field!(split.next(), nmbr).parse()?; let len: f32 = next_field!(split.next()).parse()?;
HitObjectKind::Slider { HitObjectKind::Slider {
repeats, repeats,
@@ -282,16 +412,16 @@ impl Beatmap {
} }
} }
} else if kind & Self::SPINNER_FLAG > 0 { } else if kind & Self::SPINNER_FLAG > 0 {
map.n_spinners += 1; self.n_spinners += 1;
let end_time = next_field!(split.next(), nmbr).parse()?; let end_time = next_field!(split.next()).parse()?;
HitObjectKind::Spinner { end_time } HitObjectKind::Spinner { end_time }
} else if kind & Self::HOLD_FLAG > 0 { } else if kind & Self::HOLD_FLAG > 0 {
map.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(), nmbr).parse()?); end = end.max(next_field!(next.split(':').next()).parse()?);
} }
HitObjectKind::Hold { end_time: end } HitObjectKind::Hold { end_time: end }
@@ -299,7 +429,7 @@ impl Beatmap {
return Err(ParseError::UnknownHitObjectKind); return Err(ParseError::UnknownHitObjectKind);
}; };
map.hit_objects.push(HitObject { self.hit_objects.push(HitObject {
pos, pos,
start_time: time, start_time: time,
kind, kind,
@@ -307,38 +437,18 @@ impl Beatmap {
}); });
prev_time = time; prev_time = time;
}
Section::None => {}
}
buf.clear(); buf.clear();
} }
map.mode = next_field!(mode, nmbr); // BUG: If [General] section comes after [HitObjects] then the mode
map.ar = next_field!(ar, nmbr); // won't be set yet so mania objects won't be sorted properly
map.od = next_field!(od, nmbr); if self.mode == GameMode::MNA {
map.cs = next_field!(cs, nmbr); sort(&mut self.hit_objects);
map.hp = next_field!(hp, nmbr); } else if unsorted {
map.sv = next_field!(sv, nmbr); sort!(self.hit_objects);
map.tick_rate = next_field!(tick_rate, nmbr);
map.stack_leniency = next_field!(stack_leniency, nmbr);
if unsorted_timings {
sort!(map.timing_points);
} }
if unsorted_difficulties { Ok(empty)
sort!(map.difficulty_points);
}
if map.mode == GameMode::MNA {
sort(&mut map.hit_objects);
} else if unsorted_hits {
sort!(map.hit_objects);
}
Ok(map)
} }
#[inline] #[inline]
@@ -406,7 +516,7 @@ mod tests {
#[test] #[test]
fn parsing() { fn parsing() {
let file = match File::open("E:/Games/osu!/beatmaps/2223745.osu") { let file = match File::open("./maps/1241370.osu") {
Ok(file) => file, Ok(file) => file,
Err(why) => panic!("Could not read file: {}", why), Err(why) => panic!("Could not read file: {}", why),
}; };
@@ -430,7 +540,5 @@ mod tests {
println!("hit_objects: {}", map.hit_objects.len()); println!("hit_objects: {}", map.hit_objects.len());
println!("timing_points: {}", map.timing_points.len()); println!("timing_points: {}", map.timing_points.len());
println!("difficulty_points: {}", map.difficulty_points.len()); println!("difficulty_points: {}", map.difficulty_points.len());
assert_eq!(2 + 2, 4);
} }
} }