livesplit_core/run/parser/
splitterz.rs

1//! Provides the parser for SplitterZ splits files.
2
3use crate::{timing, RealTime, Run, Segment, TimeSpan};
4use alloc::borrow::Cow;
5use core::{num::ParseIntError, result::Result as StdResult};
6use snafu::ResultExt;
7
8/// The Error type for splits files that couldn't be parsed by the SplitterZ
9/// Parser.
10#[derive(Debug, snafu::Snafu)]
11#[snafu(context(suffix(false)))]
12pub enum Error {
13    /// An empty splits file was provided.
14    Empty,
15    /// Expected the name of the category, but didn't find it.
16    ExpectedCategoryName,
17    /// Expected the attempt count, but didn't find it.
18    ExpectedAttemptCount,
19    /// Failed to parse the amount of attempts.
20    ParseAttemptCount {
21        /// The underlying error.
22        source: ParseIntError,
23    },
24    /// Expected the name of the segment, but didn't find it.
25    ExpectedSegmentName,
26    /// Expected the split time, but didn't find it.
27    ExpectedSplitTime,
28    /// Failed to parse a split time.
29    ParseSplitTime {
30        /// The underlying error.
31        source: timing::ParseError,
32    },
33    /// Expected the best segment time, but didn't find it.
34    ExpectedBestSegment,
35    /// Failed to parse a best segment time.
36    ParseBestSegment {
37        /// The underlying error.
38        source: timing::ParseError,
39    },
40}
41
42/// The Result type for the SplitterZ parser.
43pub type Result<T> = StdResult<T, Error>;
44
45fn unescape(text: &str) -> Cow<'_, str> {
46    if text.contains('‡') {
47        text.replace('‡', ",").into()
48    } else {
49        text.into()
50    }
51}
52
53/// Attempts to parse a SplitterZ splits file. In addition to the source to
54/// parse, you need to specify if additional files for the icons should be
55/// loaded from the file system. If you are using livesplit-core in a
56/// server-like environment, set this to `false`. Only client-side applications
57/// should set this to `true`.
58pub fn parse(source: &str, #[allow(unused)] load_icons: bool) -> Result<Run> {
59    let mut run = Run::new();
60
61    #[cfg(feature = "std")]
62    let mut icon_buf = Vec::new();
63
64    let mut lines = source.lines();
65    let line = lines.next().ok_or(Error::Empty)?;
66    let mut splits = line.split(',');
67    run.set_category_name(unescape(splits.next().ok_or(Error::ExpectedCategoryName)?));
68    run.set_attempt_count(
69        splits
70            .next()
71            .ok_or(Error::ExpectedAttemptCount)?
72            .parse()
73            .context(ParseAttemptCount)?,
74    );
75
76    for line in &mut lines {
77        if line.is_empty() {
78            break;
79        }
80
81        let mut splits = line.split(',');
82
83        let mut segment = Segment::new(unescape(splits.next().ok_or(Error::ExpectedSegmentName)?));
84
85        let time: TimeSpan = splits
86            .next()
87            .ok_or(Error::ExpectedSplitTime)?
88            .parse()
89            .context(ParseSplitTime)?;
90        if time != TimeSpan::zero() {
91            segment.set_personal_best_split_time(RealTime(Some(time)).into());
92        }
93
94        let time: TimeSpan = splits
95            .next()
96            .ok_or(Error::ExpectedBestSegment)?
97            .parse()
98            .context(ParseBestSegment)?;
99        if time != TimeSpan::zero() {
100            segment.set_best_segment_time(RealTime(Some(time)).into());
101        }
102
103        #[cfg(feature = "std")]
104        if load_icons {
105            if let Some(icon_path) = splits.next() {
106                if !icon_path.is_empty() {
107                    if let Ok(image) = crate::settings::Image::from_file(
108                        unescape(icon_path).as_ref(),
109                        &mut icon_buf,
110                    ) {
111                        segment.set_icon(image);
112                    }
113                }
114            }
115        }
116
117        run.push_segment(segment);
118    }
119
120    for line in lines {
121        if let Some(counter) = line.split(',').next() {
122            run.metadata_mut()
123                .custom_variable_mut(unescape(counter))
124                .permanent()
125                .set_value("0");
126        }
127        // The other two lines are not that useful. The number is how much to
128        // increment the counter each time and the other is a boolean that does
129        // not seem to be exposed to the UI.
130    }
131
132    Ok(run)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn counters() {
141        const RUN: &str = r#"Run Title:,1
142SegmentName,0:00:00.00,0.00
143SegmentName,0:00:00.00,0.00
144SegmentName,0:00:00.00,0.00
145
146Counter,1,True
147Counter2,1,True
148"#;
149
150        let run = parse(RUN, false).unwrap();
151        assert_eq!(run.len(), 3);
152        assert_eq!(run.metadata.custom_variable_value("Counter"), Some("0"));
153        assert_eq!(run.metadata.custom_variable_value("Counter2"), Some("0"));
154    }
155}