livesplit_core/run/parser/
wsplit.rs

1//! Provides the parser for WSplit splits files.
2
3use crate::{RealTime, Run, Segment, TimeSpan};
4use core::{
5    num::{ParseFloatError, ParseIntError},
6    result::Result as StdResult,
7};
8use snafu::ResultExt;
9
10/// The Error type for splits files that couldn't be parsed by the WSplit
11/// Parser.
12#[derive(Debug, snafu::Snafu)]
13#[snafu(context(suffix(false)))]
14pub enum Error {
15    /// Expected the name of the segment, but didn't find it.
16    ExpectedSegmentName,
17    /// Expected the old time, but didn't find it.
18    ExpectedOldTime,
19    /// Expected the split time, but didn't find it.
20    ExpectedPbTime,
21    /// Expected the best segment time, but didn't find it.
22    ExpectedBestTime,
23    /// Failed to parse the amount of attempts.
24    Attempt {
25        /// The underlying error.
26        source: ParseIntError,
27    },
28    /// Failed to parse the time the timer starts at.
29    Offset {
30        /// The underlying error.
31        source: ParseFloatError,
32    },
33    /// Failed to parse the personal best split time of a segment.
34    PbTime {
35        /// The underlying error.
36        source: ParseFloatError,
37    },
38    /// Failed to parse the best segment time of a segment.
39    BestSegment {
40        /// The underlying error.
41        source: ParseFloatError,
42    },
43    /// Failed to parse the "Old Run" time of a segment.
44    OldTime {
45        /// The underlying error.
46        source: ParseFloatError,
47    },
48}
49
50/// The Result type for the WSplit Parser.
51pub type Result<T> = StdResult<T, Error>;
52
53/// Attempts to parse a WSplit splits file. In addition to the source to parse,
54/// you need to specify if additional files for the icons should be loaded from
55/// the file system. If you are using livesplit-core in a server-like
56/// environment, set this to `false`. Only client-side applications should set
57/// this to `true`.
58pub fn parse(source: &str, #[allow(unused)] load_icons: bool) -> Result<Run> {
59    let mut run = Run::new();
60    #[cfg(feature = "std")]
61    let mut icon_buf = Vec::new();
62    #[cfg(feature = "std")]
63    let mut icons_list = Vec::new();
64    let mut old_run_exists = false;
65    let mut goal = None;
66
67    for line in source.lines() {
68        if line.is_empty() {
69            continue;
70        }
71
72        if let Some(title) = line.strip_prefix("Title=") {
73            run.set_category_name(title);
74        } else if let Some(attempts) = line.strip_prefix("Attempts=") {
75            run.set_attempt_count(attempts.parse().context(Attempt)?);
76        } else if let Some(offset) = line.strip_prefix("Offset=") {
77            if !offset.is_empty() {
78                run.set_offset(TimeSpan::from_milliseconds(
79                    -offset.parse::<f64>().context(Offset)?,
80                ));
81            }
82        } else if line.starts_with("Size=") {
83            // Ignore
84        } else if let Some(_icons) = line.strip_prefix("Icons=") {
85            #[cfg(feature = "std")]
86            if load_icons {
87                icons_list.clear();
88                for path in _icons.split(',') {
89                    if path.len() >= 2 {
90                        let path = &path[1..path.len() - 1];
91                        if let Ok(image) = crate::settings::Image::from_file(path, &mut icon_buf) {
92                            icons_list.push(image);
93                            continue;
94                        }
95                    }
96                    icons_list.push(Default::default());
97                }
98            }
99        } else if let Some(goal_value) = line.strip_prefix("Goal=") {
100            if !goal_value.is_empty() {
101                goal = Some(goal_value);
102            }
103        } else {
104            // must be a split Kappa
105            let mut split_info = line.split(',');
106
107            let segment_name = split_info.next().ok_or(Error::ExpectedSegmentName)?;
108            let old_time = split_info.next().ok_or(Error::ExpectedOldTime)?;
109            let pb_time = split_info.next().ok_or(Error::ExpectedPbTime)?;
110            let best_time = split_info.next().ok_or(Error::ExpectedBestTime)?;
111
112            let pb_time = TimeSpan::from_seconds(pb_time.parse().context(PbTime)?);
113            let best_time = TimeSpan::from_seconds(best_time.parse().context(BestSegment)?);
114            let old_time = TimeSpan::from_seconds(old_time.parse().context(OldTime)?);
115
116            let mut segment = Segment::new(segment_name);
117
118            if pb_time != TimeSpan::zero() {
119                segment.set_personal_best_split_time(RealTime(Some(pb_time)).into());
120            }
121            if best_time != TimeSpan::zero() {
122                segment.set_best_segment_time(RealTime(Some(best_time)).into());
123            }
124            if old_time != TimeSpan::zero() {
125                *segment.comparison_mut("Old Run") = RealTime(Some(old_time)).into();
126                old_run_exists = true;
127            }
128
129            run.push_segment(segment);
130        }
131    }
132
133    if let Some(goal) = goal {
134        if run.category_name().is_empty() {
135            run.set_category_name(goal);
136        } else {
137            run.metadata_mut()
138                .custom_variable_mut("Goal")
139                .permanent()
140                .set_value(goal);
141        }
142    }
143
144    if old_run_exists {
145        run.add_custom_comparison("Old Run").unwrap();
146    }
147
148    #[cfg(feature = "std")]
149    for (icon, segment) in icons_list.into_iter().zip(run.segments_mut().iter_mut()) {
150        segment.set_icon(icon);
151    }
152
153    Ok(run)
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn goal_parsing() {
162        const RUN: &str = r#"Title=WarioWare, Inc
163Attempts=1
164Offset=0
165Size=374,61
166Goal=sub 2h
167Introduction,0,85.48,85.48
168Jimmy,0,219.68,134.2
169"#;
170
171        let run = parse(RUN, false).unwrap();
172        assert_eq!(run.category_name(), "WarioWare, Inc");
173        assert_eq!(run.metadata().custom_variable_value("Goal"), Some("sub 2h"));
174        assert_eq!(run.len(), 2);
175    }
176
177    #[test]
178    fn skip_goal_if_its_empty() {
179        const RUN: &str = r#"Title=WarioWare, Inc
180Attempts=1
181Offset=0
182Size=374,61
183Goal=
184Introduction,0,85.48,85.48
185Jimmy,0,219.68,134.2
186"#;
187
188        let run = parse(RUN, false).unwrap();
189        assert_eq!(run.category_name(), "WarioWare, Inc");
190        assert_eq!(run.len(), 2);
191    }
192}