livesplit_core/run/parser/
face_split.rs

1//! Provides the parser for FaceSplit splits files.
2
3use crate::{timing, RealTime, Run, Segment, Time, TimeSpan};
4use alloc::borrow::Cow;
5use core::{num::ParseIntError, result::Result as StdResult};
6use snafu::{OptionExt, ResultExt};
7
8/// The Error type for splits files that couldn't be parsed by the FaceSplit
9/// Parser.
10#[derive(Debug, snafu::Snafu)]
11#[snafu(context(suffix(false)))]
12pub enum Error {
13    /// Expected the title, but didn't find it.
14    ExpectedTitle,
15    /// Expected the goal, but didn't find it.
16    ExpectedGoal,
17    /// Expected the attempt count, but didn't find it.
18    ExpectedAttemptCount,
19    /// Failed to parse the attempt count.
20    ParseAttemptCount {
21        /// The underlying error.
22        source: ParseIntError,
23    },
24    /// Expected the name of a segment, but didn't find it.
25    ExpectedSegmentName,
26    /// Expected the split time of a segment, but didn't find it.
27    ExpectedSplitTime,
28    /// Failed to parse the split time of a segment.
29    ParseSplitTime {
30        /// The underlying error.
31        source: timing::ParseError,
32    },
33    /// Expected the best segment time of a segment, but didn't find it.
34    ExpectedBestSegmentTime,
35    /// Failed to parse the best segment time of a segment.
36    ParseBestSegmentTime {
37        /// The underlying error.
38        source: timing::ParseError,
39    },
40}
41
42/// The Result type for the FaceSplit Parser.
43pub type Result<T> = StdResult<T, Error>;
44
45fn parse_time(time: &str) -> StdResult<Time, timing::ParseError> {
46    // Replace "," by "." as "," wouldn't parse
47    let time: TimeSpan = replace(time, ",", ".").parse()?;
48    // Skipped is stored as a zero time in FaceSplit Splits
49    if time == TimeSpan::zero() {
50        Ok(Time::default())
51    } else {
52        Ok(RealTime(Some(time)).into())
53    }
54}
55
56fn replace<'text>(text: &'text str, a: &str, b: &str) -> Cow<'text, str> {
57    if text.contains(a) {
58        text.replace(a, b).into()
59    } else {
60        text.into()
61    }
62}
63
64/// Attempts to parse a FaceSplit splits file. In addition to the source to
65/// parse, you need to specify if additional files for the icons should be
66/// loaded from the file system. If you are using livesplit-core in a
67/// server-like environment, set this to `false`. Only client-side applications
68/// should set this to `true`.
69pub fn parse(source: &str, #[allow(unused)] load_icons: bool) -> Result<Run> {
70    let mut run = Run::new();
71    #[cfg(feature = "std")]
72    let mut icon_buf = Vec::new();
73    let mut lines = source.lines();
74
75    run.set_category_name(lines.next().context(ExpectedTitle)?);
76
77    let goal = lines.next().context(ExpectedGoal)?;
78    if !goal.trim_start().is_empty() {
79        run.metadata_mut()
80            .custom_variable_mut("Goal")
81            .permanent()
82            .set_value(goal);
83    }
84
85    run.set_attempt_count(
86        lines
87            .next()
88            .context(ExpectedAttemptCount)?
89            .parse()
90            .context(ParseAttemptCount)?,
91    );
92    lines.next(); // FIXME: Store runs completed somehow
93
94    for line in lines {
95        let mut splits = line.splitn(5, '-');
96
97        let segment_name = replace(splits.next().context(ExpectedSegmentName)?, r#""?""#, "-");
98        let mut segment = Segment::new(segment_name);
99
100        let split_time =
101            parse_time(splits.next().context(ExpectedSplitTime)?).context(ParseSplitTime)?;
102        segment.set_personal_best_split_time(split_time);
103
104        let best_segment = parse_time(splits.next().context(ExpectedBestSegmentTime)?)
105            .context(ParseBestSegmentTime)?;
106        segment.set_best_segment_time(best_segment);
107
108        splits.next(); // Skip Segment Time
109
110        #[cfg(feature = "std")]
111        if load_icons {
112            if let Some(icon_path) = splits.next() {
113                if !icon_path.is_empty() {
114                    if let Ok(image) = crate::settings::Image::from_file(icon_path, &mut icon_buf) {
115                        segment.set_icon(image);
116                    }
117                }
118            }
119        }
120
121        run.push_segment(segment);
122    }
123
124    Ok(run)
125}