use super::super::ComparisonError;
use crate::{
comparison::RACE_COMPARISON_PREFIX,
platform::{
path::{Path, PathBuf},
prelude::*,
},
timing, RealTime, Run, Segment, Time, TimeSpan,
};
#[cfg(feature = "std")]
use crate::{settings::Image, AtomicDateTime};
use alloc::borrow::Cow;
use core::{fmt::Write, num::ParseIntError, result::Result as StdResult};
use snafu::{OptionExt, ResultExt};
#[derive(Debug, snafu::Snafu)]
#[snafu(context(suffix(false)))]
pub enum Error {
Empty,
ExpectedAttemptCount,
ParseAttemptCount {
source: ParseIntError,
},
ExpectedOffset,
ParseOffset {
source: timing::ParseError,
},
ExpectedTitleLine,
ExpectedCategoryName,
ExpectedSegmentName,
ExpectedBestSegment,
ParseBestSegment {
source: timing::ParseError,
},
ExpectedComparisonTime,
ParseComparisonTime {
source: timing::ParseError,
},
ExpectedIconLine,
}
pub type Result<T> = StdResult<T, Error>;
fn parse_time_optional(time: &str) -> StdResult<Option<TimeSpan>, timing::ParseError> {
let time: TimeSpan = time.parse()?;
if time == TimeSpan::zero() {
Ok(None)
} else {
Ok(Some(time))
}
}
pub fn parse(
source: &str,
#[allow(unused)] path_for_loading_other_files: Option<&Path>,
) -> Result<Run> {
let mut run = Run::new();
#[cfg(feature = "std")]
let mut buf = Vec::new();
let mut lines = source.lines();
let line = lines.next().context(Empty)?;
let mut splits = line.split('\t');
let attempt_count = splits.next().context(ExpectedAttemptCount)?;
if !attempt_count.is_empty() {
run.set_attempt_count(attempt_count.parse().context(ParseAttemptCount)?);
}
run.set_offset(
splits
.next()
.context(ExpectedOffset)?
.parse()
.context(ParseOffset)?,
);
#[cfg(feature = "std")]
let mut path = path_for_loading_other_files.map(Path::to_path_buf);
#[cfg(feature = "std")]
catch! {
let path = path.as_mut()?;
path.set_file_name(splits.next()?);
let image = Image::from_file(path, &mut buf).ok()?;
run.set_game_icon(image);
};
let line = lines.next().context(ExpectedTitleLine)?;
let mut splits = line.split('\t');
run.set_category_name(splits.next().context(ExpectedCategoryName)?);
splits.next();
let mut comparisons = splits.map(Cow::Borrowed).collect::<Vec<_>>();
for comparison in &mut comparisons {
let orig_len = comparison.len();
let mut number = 2;
loop {
match run.add_custom_comparison(&**comparison) {
Ok(_) => break,
Err(ComparisonError::DuplicateName) => {
let comparison = comparison.to_mut();
comparison.drain(orig_len..);
let _ = write!(comparison, " {number}");
number += 1;
}
Err(ComparisonError::NameStartsWithRace) => {
let comparison = comparison.to_mut();
let len_after_trimming = comparison[RACE_COMPARISON_PREFIX.len()..]
.trim_start()
.len();
let shrunk_by = comparison.len() - len_after_trimming;
comparison.drain(..shrunk_by);
}
}
}
}
while let Some(line) = lines.next() {
if line.is_empty() {
continue;
}
let mut splits = line.split('\t');
let mut segment = Segment::new(splits.next().context(ExpectedSegmentName)?);
let best_segment = parse_time_optional(splits.next().context(ExpectedBestSegment)?)
.context(ParseBestSegment)?;
segment.set_best_segment_time(RealTime(best_segment).into());
let mut pb_time = Time::new();
for comparison in &comparisons {
let time = segment.comparison_mut(comparison);
pb_time.real_time = parse_time_optional(splits.next().context(ExpectedComparisonTime)?)
.context(ParseComparisonTime)?;
time.real_time = pb_time.real_time;
}
segment.set_personal_best_split_time(pb_time);
let _line = lines.next().context(ExpectedIconLine)?;
#[cfg(feature = "std")]
catch! {
let file = _line.trim_end();
if !file.is_empty() {
let path = path.as_mut()?;
path.set_file_name(file);
let image = Image::from_file(path, &mut buf).ok()?;
segment.set_icon(image);
}
};
run.push_segment(segment);
}
#[cfg(feature = "std")]
parse_history(&mut run, path).ok();
Ok(run)
}
#[cfg(feature = "std")]
fn parse_history(run: &mut Run, path: Option<PathBuf>) -> StdResult<(), ()> {
if let Some(mut path) = path {
path.set_extension("");
let mut path = path.into_os_string();
path.push("-RunLog.txt");
let path = PathBuf::from(path);
let file = std::fs::read_to_string(path).map_err(drop)?;
let mut lines = file.lines();
let mut attempt_id = 1;
lines.next();
for line in lines {
let mut splits = line.split('\t');
let time_stamp = splits.next().ok_or(())?;
let started = parse_date(time_stamp).ok_or(())?;
let completed = splits.next().ok_or(())? == "C";
let split_times: Vec<_> = splits
.map(parse_time_optional)
.collect::<StdResult<_, _>>()
.map_err(drop)?;
let mut final_time = Time::default();
let mut ended = None;
if completed {
catch! {
let last_split_time = split_times.last()?;
final_time.real_time = *last_split_time;
let final_time = final_time.real_time?;
let ended_date = started + final_time.to_duration();
ended = Some(AtomicDateTime::new(ended_date, false));
};
}
run.add_attempt_with_index(
final_time,
attempt_id,
Some(AtomicDateTime::new(started, false)),
ended,
None,
);
let mut last_split = TimeSpan::zero();
for (segment, current_split) in
run.segments_mut().iter_mut().zip(split_times.into_iter())
{
let mut segment_time = Time::default();
if let Some(current_split) = current_split {
segment_time.real_time = Some(current_split - last_split);
last_split = current_split;
}
segment
.segment_history_mut()
.insert(attempt_id, segment_time);
if catch! {
segment_time.real_time? < segment.best_segment_time().real_time?
}
.unwrap_or(false)
{
segment.set_best_segment_time(segment_time);
}
}
attempt_id += 1;
}
}
Ok(())
}
#[cfg(feature = "std")]
fn parse_date(text: &str) -> Option<crate::DateTime> {
let (year, rem) = text.split_once('/')?;
let (month, rem) = rem.split_once('/')?;
let (day, rem) = rem.split_once(' ')?;
let (hour, minute) = rem.split_once(':')?;
Some(
time::PrimitiveDateTime::new(
time::Date::from_calendar_date(
year.parse().ok()?,
month.parse::<u8>().ok()?.try_into().ok()?,
day.parse().ok()?,
)
.ok()?,
time::Time::from_hms(hour.parse().ok()?, minute.parse().ok()?, 0).ok()?,
)
.assume_utc(),
)
}