livesplit_core/run/parser/
wsplit.rs1use crate::{RealTime, Run, Segment, TimeSpan};
4use core::{
5 num::{ParseFloatError, ParseIntError},
6 result::Result as StdResult,
7};
8use snafu::ResultExt;
9
10#[derive(Debug, snafu::Snafu)]
13#[snafu(context(suffix(false)))]
14pub enum Error {
15 ExpectedSegmentName,
17 ExpectedOldTime,
19 ExpectedPbTime,
21 ExpectedBestTime,
23 Attempt {
25 source: ParseIntError,
27 },
28 Offset {
30 source: ParseFloatError,
32 },
33 PbTime {
35 source: ParseFloatError,
37 },
38 BestSegment {
40 source: ParseFloatError,
42 },
43 OldTime {
45 source: ParseFloatError,
47 },
48}
49
50pub type Result<T> = StdResult<T, Error>;
52
53pub 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 } 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 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}