parse: cleanup
This commit is contained in:
+2
-2
@@ -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
@@ -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
@@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+319
-211
@@ -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,244 +130,325 @@ 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 section = Section::None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match section {
|
||||||
|
Section::General => section!(map, parse_general, reader, buf, section),
|
||||||
|
Section::Difficulty => section!(map, parse_difficulty, reader, buf, section),
|
||||||
|
Section::TimingPoints => section!(map, parse_timingpoints, reader, buf, section),
|
||||||
|
Section::HitObjects => section!(map, parse_hitobjects, reader, buf, section),
|
||||||
|
Section::None => {
|
||||||
|
if reader.read_line(&mut buf)? == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = line_prepare!(buf);
|
||||||
|
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
section = Section::from_str(&line[1..line.len() - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
||||||
|
|
||||||
|
if key == "Mode" {
|
||||||
|
mode = match value {
|
||||||
|
"0" => Some(GameMode::STD),
|
||||||
|
"1" => Some(GameMode::TKO),
|
||||||
|
"2" => Some(GameMode::CTB),
|
||||||
|
"3" => Some(GameMode::MNA),
|
||||||
|
_ => return Err(ParseError::InvalidMode),
|
||||||
|
};
|
||||||
|
} else if key == "StackLeniency" {
|
||||||
|
stack_leniency = Some(value.parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ar = None;
|
||||||
let mut od = None;
|
let mut od = None;
|
||||||
let mut cs = None;
|
let mut cs = None;
|
||||||
let mut hp = None;
|
let mut hp = None;
|
||||||
let mut sv = None;
|
let mut sv = None;
|
||||||
let mut tick_rate = None;
|
let mut tick_rate = None;
|
||||||
let mut stack_leniency = None;
|
|
||||||
|
|
||||||
let mut section = Section::None;
|
let mut empty = true;
|
||||||
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;
|
while reader.read_line(buf)? != 0 {
|
||||||
|
let line = line_prepare!(buf);
|
||||||
while reader.read_line(&mut buf)? != 0 {
|
|
||||||
let mut line = buf.trim_end();
|
|
||||||
nmbr += 1;
|
|
||||||
|
|
||||||
if line.is_empty()
|
|
||||||
|| line.starts_with("//")
|
|
||||||
|| line.starts_with(' ')
|
|
||||||
|| line.starts_with('_')
|
|
||||||
{
|
|
||||||
buf.clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
buf.clear();
|
buf.clear();
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(idx) = line.find("//") {
|
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
||||||
line = &line[..idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
match section {
|
match key {
|
||||||
Section::General => {
|
"ApproachRate" => ar = Some(value.parse()?),
|
||||||
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
"OverallDifficulty" => od = Some(value.parse()?),
|
||||||
|
"CircleSize" => cs = Some(value.parse()?),
|
||||||
if key == "Mode" {
|
"HPDrainRate" => hp = Some(value.parse()?),
|
||||||
mode = match value {
|
"SliderTickRate" => tick_rate = Some(value.parse()?),
|
||||||
"0" => Some(GameMode::STD),
|
"SliderMultiplier" => sv = Some(value.parse()?),
|
||||||
"1" => Some(GameMode::TKO),
|
_ => {}
|
||||||
"2" => Some(GameMode::CTB),
|
|
||||||
"3" => Some(GameMode::MNA),
|
|
||||||
_ => return Err(ParseError::InvalidMode),
|
|
||||||
};
|
|
||||||
} else if key == "StackLeniency" {
|
|
||||||
stack_leniency = Some(value.parse()?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Section::Difficulty => {
|
|
||||||
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
|
||||||
|
|
||||||
match key {
|
|
||||||
"ApproachRate" => ar = Some(value.parse()?),
|
|
||||||
"OverallDifficulty" => od = Some(value.parse()?),
|
|
||||||
"CircleSize" => cs = Some(value.parse()?),
|
|
||||||
"HPDrainRate" => hp = Some(value.parse()?),
|
|
||||||
"SliderTickRate" => tick_rate = Some(value.parse()?),
|
|
||||||
"SliderMultiplier" => sv = Some(value.parse()?),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Section::TimingPoints => {
|
|
||||||
let mut split = line.split(',');
|
|
||||||
|
|
||||||
let time = offset + next_field!(split.next(), nmbr).trim().parse::<f32>()?;
|
|
||||||
validate_float!(time);
|
|
||||||
|
|
||||||
let beat_len = next_field!(split.next(), nmbr).trim().parse::<f32>()?;
|
|
||||||
|
|
||||||
if beat_len.is_sign_negative() {
|
|
||||||
let point = DifficultyPoint {
|
|
||||||
time,
|
|
||||||
speed_multiplier: -100.0 / beat_len,
|
|
||||||
};
|
|
||||||
|
|
||||||
map.difficulty_points.push(point);
|
|
||||||
|
|
||||||
if time < prev_diff {
|
|
||||||
unsorted_difficulties = true;
|
|
||||||
} else {
|
|
||||||
prev_diff = time;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let point = TimingPoint {
|
|
||||||
time,
|
|
||||||
bpm: 60_000.0 / beat_len,
|
|
||||||
beat_len,
|
|
||||||
};
|
|
||||||
|
|
||||||
map.timing_points.push(point);
|
|
||||||
|
|
||||||
if time < prev_time {
|
|
||||||
unsorted_timings = true;
|
|
||||||
} else {
|
|
||||||
prev_time = time;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section::HitObjects => {
|
|
||||||
let mut split = line.split(',');
|
|
||||||
|
|
||||||
let pos = Pos2 {
|
|
||||||
x: next_field!(split.next(), nmbr).parse()?,
|
|
||||||
y: next_field!(split.next(), nmbr).parse()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let time = offset + next_field!(split.next(), nmbr).trim().parse::<f32>()?;
|
|
||||||
validate_float!(time);
|
|
||||||
|
|
||||||
if !map.hit_objects.is_empty() && time < prev_time {
|
|
||||||
unsorted_hits = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let kind: u8 = next_field!(split.next(), nmbr).parse()?;
|
|
||||||
let sound = split.next().map(str::parse).transpose()?.unwrap_or(0);
|
|
||||||
|
|
||||||
let kind = if kind & Self::CIRCLE_FLAG > 0 {
|
|
||||||
map.n_circles += 1;
|
|
||||||
|
|
||||||
HitObjectKind::Circle
|
|
||||||
} else if kind & Self::SLIDER_FLAG > 0 {
|
|
||||||
map.n_sliders += 1;
|
|
||||||
let mut curve_points = Vec::with_capacity(16);
|
|
||||||
curve_points.push(pos);
|
|
||||||
|
|
||||||
let mut curve_point_iter = next_field!(split.next(), nmbr).split('|');
|
|
||||||
|
|
||||||
let mut path_type: PathType =
|
|
||||||
next_field!(curve_point_iter.next(), nmbr).parse()?;
|
|
||||||
|
|
||||||
for pos in curve_point_iter {
|
|
||||||
let mut v = pos.split(':').map(str::parse);
|
|
||||||
|
|
||||||
match (v.next(), v.next()) {
|
|
||||||
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
|
||||||
_ => return Err(ParseError::InvalidCurvePoints),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if map.version <= 6 && curve_points.len() >= 2 {
|
|
||||||
if path_type == PathType::Linear {
|
|
||||||
path_type = PathType::Bezier;
|
|
||||||
}
|
|
||||||
|
|
||||||
if curve_points.len() == 2
|
|
||||||
&& (pos == curve_points[0] || pos == curve_points[1])
|
|
||||||
{
|
|
||||||
path_type = PathType::Linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if curve_points.is_empty() {
|
|
||||||
HitObjectKind::Circle
|
|
||||||
} else {
|
|
||||||
let repeats = next_field!(split.next(), nmbr).parse::<usize>()?;
|
|
||||||
let len: f32 = next_field!(split.next(), nmbr).parse()?;
|
|
||||||
|
|
||||||
HitObjectKind::Slider {
|
|
||||||
repeats,
|
|
||||||
pixel_len: len,
|
|
||||||
curve_points,
|
|
||||||
path_type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if kind & Self::SPINNER_FLAG > 0 {
|
|
||||||
map.n_spinners += 1;
|
|
||||||
let end_time = next_field!(split.next(), nmbr).parse()?;
|
|
||||||
|
|
||||||
HitObjectKind::Spinner { end_time }
|
|
||||||
} else if kind & Self::HOLD_FLAG > 0 {
|
|
||||||
map.n_sliders += 1;
|
|
||||||
let mut end = time;
|
|
||||||
|
|
||||||
if let Some(next) = split.next() {
|
|
||||||
end = end.max(next_field!(next.split(':').next(), nmbr).parse()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
HitObjectKind::Hold { end_time: end }
|
|
||||||
} else {
|
|
||||||
return Err(ParseError::UnknownHitObjectKind);
|
|
||||||
};
|
|
||||||
|
|
||||||
map.hit_objects.push(HitObject {
|
|
||||||
pos,
|
|
||||||
start_time: time,
|
|
||||||
kind,
|
|
||||||
sound,
|
|
||||||
});
|
|
||||||
|
|
||||||
prev_time = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
Section::None => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.clear();
|
buf.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
map.mode = next_field!(mode, nmbr);
|
self.ar = next_field!(ar);
|
||||||
map.ar = next_field!(ar, nmbr);
|
self.od = next_field!(od);
|
||||||
map.od = next_field!(od, nmbr);
|
self.cs = next_field!(cs);
|
||||||
map.cs = next_field!(cs, nmbr);
|
self.hp = next_field!(hp);
|
||||||
map.hp = next_field!(hp, nmbr);
|
self.sv = next_field!(sv);
|
||||||
map.sv = next_field!(sv, nmbr);
|
self.tick_rate = next_field!(tick_rate);
|
||||||
map.tick_rate = next_field!(tick_rate, nmbr);
|
|
||||||
map.stack_leniency = next_field!(stack_leniency, nmbr);
|
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 time = next_field!(split.next()).trim().parse::<f32>()?;
|
||||||
|
validate_float!(time);
|
||||||
|
|
||||||
|
let beat_len = next_field!(split.next()).trim().parse::<f32>()?;
|
||||||
|
|
||||||
|
if beat_len < 0.0 {
|
||||||
|
let point = DifficultyPoint {
|
||||||
|
time,
|
||||||
|
speed_multiplier: -100.0 / beat_len,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.difficulty_points.push(point);
|
||||||
|
|
||||||
|
if time < prev_diff {
|
||||||
|
unsorted_difficulties = true;
|
||||||
|
} else {
|
||||||
|
prev_diff = time;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let point = TimingPoint {
|
||||||
|
time,
|
||||||
|
bpm: 60_000.0 / beat_len,
|
||||||
|
beat_len,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.timing_points.push(point);
|
||||||
|
|
||||||
|
if time < prev_time {
|
||||||
|
unsorted_timings = true;
|
||||||
|
} else {
|
||||||
|
prev_time = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
if unsorted_timings {
|
if unsorted_timings {
|
||||||
sort!(map.timing_points);
|
sort!(self.timing_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
if unsorted_difficulties {
|
if unsorted_difficulties {
|
||||||
sort!(map.difficulty_points);
|
sort!(self.difficulty_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
if map.mode == GameMode::MNA {
|
Ok(empty)
|
||||||
sort(&mut map.hit_objects);
|
}
|
||||||
} else if unsorted_hits {
|
|
||||||
sort!(map.hit_objects);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut split = line.split(',');
|
||||||
|
|
||||||
|
let pos = Pos2 {
|
||||||
|
x: next_field!(split.next()).parse()?,
|
||||||
|
y: next_field!(split.next()).parse()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let time = next_field!(split.next()).trim().parse::<f32>()?;
|
||||||
|
validate_float!(time);
|
||||||
|
|
||||||
|
if !self.hit_objects.is_empty() && time < prev_time {
|
||||||
|
unsorted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind: u8 = next_field!(split.next()).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;
|
||||||
|
let mut curve_points = Vec::with_capacity(16);
|
||||||
|
curve_points.push(pos);
|
||||||
|
|
||||||
|
let mut curve_point_iter = next_field!(split.next()).split('|');
|
||||||
|
|
||||||
|
let mut path_type: PathType = next_field!(curve_point_iter.next()).parse()?;
|
||||||
|
|
||||||
|
for pos in curve_point_iter {
|
||||||
|
let mut v = pos.split(':').map(str::parse);
|
||||||
|
|
||||||
|
match (v.next(), v.next()) {
|
||||||
|
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||||
|
_ => return Err(ParseError::InvalidCurvePoints),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.version <= 6 && curve_points.len() >= 2 {
|
||||||
|
if path_type == PathType::Linear {
|
||||||
|
path_type = PathType::Bezier;
|
||||||
|
}
|
||||||
|
|
||||||
|
if curve_points.len() == 2 && (pos == curve_points[0] || pos == curve_points[1])
|
||||||
|
{
|
||||||
|
path_type = PathType::Linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if curve_points.is_empty() {
|
||||||
|
HitObjectKind::Circle
|
||||||
|
} else {
|
||||||
|
let repeats = next_field!(split.next()).parse::<usize>()?;
|
||||||
|
let len: f32 = next_field!(split.next()).parse()?;
|
||||||
|
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
repeats,
|
||||||
|
pixel_len: len,
|
||||||
|
curve_points,
|
||||||
|
path_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if kind & Self::SPINNER_FLAG > 0 {
|
||||||
|
self.n_spinners += 1;
|
||||||
|
let end_time = next_field!(split.next()).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_field!(next.split(':').next()).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();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(map)
|
// 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 {
|
||||||
|
sort(&mut self.hit_objects);
|
||||||
|
} else if unsorted {
|
||||||
|
sort!(self.hit_objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user