use std::{
error::Error,
fs::File,
io,
io::{BufRead, BufReader, Cursor},
ops::ControlFlow,
path::Path,
};
use crate::{format_version, reader::Decoder, section::Section};
pub fn from_path<D: DecodeBeatmap>(path: impl AsRef<Path>) -> Result<D, io::Error> {
File::open(path).map(BufReader::new).and_then(D::decode)
}
pub fn from_bytes<D: DecodeBeatmap>(bytes: &[u8]) -> Result<D, io::Error> {
D::decode(Cursor::new(bytes))
}
pub fn from_str<D: DecodeBeatmap>(s: &str) -> Result<D, io::Error> {
D::decode(Cursor::new(s))
}
pub trait DecodeState: Sized {
fn create(version: i32) -> Self;
}
pub trait DecodeBeatmap: Sized {
type Error: Error;
type State: DecodeState + Into<Self>;
fn decode<R: BufRead>(src: R) -> Result<Self, io::Error> {
let mut reader = Decoder::new(src)?;
let (version, use_curr_line) = parse_version(&mut reader)?;
let mut state =
Self::State::create(version.unwrap_or(format_version::LATEST_FORMAT_VERSION));
let Some(mut section) = parse_first_section(&mut reader, use_curr_line)? else {
return Ok(state.into());
};
loop {
let parse_fn = match section {
Section::General => Self::parse_general,
Section::Editor => Self::parse_editor,
Section::Metadata => Self::parse_metadata,
Section::Difficulty => Self::parse_difficulty,
Section::Events => Self::parse_events,
Section::TimingPoints => Self::parse_timing_points,
Section::Colors => Self::parse_colors,
Section::HitObjects => Self::parse_hit_objects,
Section::Variables => Self::parse_variables,
Section::CatchTheBeat => Self::parse_catch_the_beat,
Section::Mania => Self::parse_mania,
};
match parse_section::<_, Self>(&mut reader, &mut state, parse_fn) {
Ok(SectionFlow::Continue(next)) => section = next,
Ok(SectionFlow::Break(())) => break,
Err(err) => return Err(err),
}
}
Ok(state.into())
}
fn should_skip_line(line: &str) -> bool {
line.is_empty() || line.trim_start().starts_with("//")
}
#[allow(unused_variables)]
fn parse_general(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_editor(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_metadata(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_difficulty(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_events(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_timing_points(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_colors(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_hit_objects(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_variables(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_catch_the_beat(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
#[allow(unused_variables)]
fn parse_mania(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
}
struct UseCurrentLine(bool);
fn parse_version<R>(reader: &mut Decoder<R>) -> Result<(Option<i32>, UseCurrentLine), io::Error>
where
R: BufRead,
{
loop {
let (version, use_curr_line) = match reader.read_line() {
Ok(Some(line)) => match format_version::try_version_from_line(line) {
ControlFlow::Continue(()) => continue,
ControlFlow::Break(Ok(version)) => (Some(version), false),
#[allow(unused)]
ControlFlow::Break(Err(err)) => {
#[cfg(feature = "tracing")]
{
tracing::error!("Failed to parse format version: {err}");
log_error_cause(&err);
}
(None, true)
}
},
Ok(None) => (None, false),
Err(err) => return Err(err),
};
return Ok((version, UseCurrentLine(use_curr_line)));
}
}
fn parse_first_section<R: BufRead>(
reader: &mut Decoder<R>,
UseCurrentLine(use_curr_line): UseCurrentLine,
) -> Result<Option<Section>, io::Error> {
if use_curr_line {
if let opt @ Some(_) = Section::try_from_line(reader.curr_line()) {
return Ok(opt);
}
}
loop {
match reader.read_line() {
Ok(Some(line)) => {
if let Some(section) = Section::try_from_line(line) {
return Ok(Some(section));
}
}
Ok(None) => return Ok(None),
Err(err) => return Err(err),
}
}
}
type SectionFlow = ControlFlow<(), Section>;
fn parse_section<R, D>(
reader: &mut Decoder<R>,
state: &mut D::State,
f: fn(&mut D::State, &str) -> Result<(), D::Error>,
) -> Result<SectionFlow, io::Error>
where
R: BufRead,
D: DecodeBeatmap,
{
loop {
match reader.read_line() {
Ok(Some(line)) => {
if D::should_skip_line(line) {
continue;
}
if let Some(next) = Section::try_from_line(line) {
return Ok(SectionFlow::Continue(next));
}
#[allow(unused)]
let res = f(state, line);
#[cfg(feature = "tracing")]
if let Err(err) = res {
tracing::error!("Failed to process line {line:?}: {err}");
log_error_cause(&err);
}
}
Ok(None) => return Ok(SectionFlow::Break(())),
Err(err) => return Err(err),
}
}
}
#[cfg(feature = "tracing")]
fn log_error_cause(mut err: &dyn Error) {
while let Some(src) = err.source() {
tracing::error!(" - caused by: {src}");
err = src;
}
}