livesplit_core/run/parser/
time_split_tracker.rs1use super::super::ComparisonError;
4use crate::{
5 comparison::RACE_COMPARISON_PREFIX,
6 platform::{
7 path::{Path, PathBuf},
8 prelude::*,
9 },
10 timing, RealTime, Run, Segment, Time, TimeSpan,
11};
12#[cfg(feature = "std")]
13use crate::{settings::Image, AtomicDateTime};
14use alloc::borrow::Cow;
15use core::{fmt::Write, num::ParseIntError, result::Result as StdResult};
16use snafu::{OptionExt, ResultExt};
17
18#[derive(Debug, snafu::Snafu)]
21#[snafu(context(suffix(false)))]
22pub enum Error {
23 Empty,
25 ExpectedAttemptCount,
27 ParseAttemptCount {
29 source: ParseIntError,
31 },
32 ExpectedOffset,
34 ParseOffset {
36 source: timing::ParseError,
38 },
39 ExpectedTitleLine,
41 ExpectedCategoryName,
43 ExpectedSegmentName,
45 ExpectedBestSegment,
47 ParseBestSegment {
49 source: timing::ParseError,
51 },
52 ExpectedComparisonTime,
54 ParseComparisonTime {
56 source: timing::ParseError,
58 },
59 ExpectedIconLine,
61}
62
63pub type Result<T> = StdResult<T, Error>;
65
66fn parse_time_optional(time: &str) -> StdResult<Option<TimeSpan>, timing::ParseError> {
67 let time: TimeSpan = time.parse()?;
68 if time == TimeSpan::zero() {
69 Ok(None)
70 } else {
71 Ok(Some(time))
72 }
73}
74
75pub fn parse(
81 source: &str,
82 #[allow(unused)] path_for_loading_other_files: Option<&Path>,
83) -> Result<Run> {
84 let mut run = Run::new();
85 #[cfg(feature = "std")]
86 let mut buf = Vec::new();
87
88 let mut lines = source.lines();
89
90 let line = lines.next().context(Empty)?;
91 let mut splits = line.split('\t');
92
93 let attempt_count = splits.next().context(ExpectedAttemptCount)?;
94 if !attempt_count.is_empty() {
95 run.set_attempt_count(attempt_count.parse().context(ParseAttemptCount)?);
96 }
97
98 run.set_offset(
99 splits
100 .next()
101 .context(ExpectedOffset)?
102 .parse()
103 .context(ParseOffset)?,
104 );
105
106 #[cfg(feature = "std")]
107 let mut path = path_for_loading_other_files.map(Path::to_path_buf);
108
109 #[cfg(feature = "std")]
110 catch! {
111 let path = path.as_mut()?;
112 path.set_file_name(splits.next()?);
113 let image = Image::from_file(path, &mut buf).ok()?;
114 run.set_game_icon(image);
115 };
116
117 let line = lines.next().context(ExpectedTitleLine)?;
118 let mut splits = line.split('\t');
119 run.set_category_name(splits.next().context(ExpectedCategoryName)?);
120 splits.next(); let mut comparisons = splits.map(Cow::Borrowed).collect::<Vec<_>>();
123
124 for comparison in &mut comparisons {
125 let orig_len = comparison.len();
126 let mut number = 2;
127 loop {
128 match run.add_custom_comparison(&**comparison) {
129 Ok(_) => break,
130 Err(ComparisonError::DuplicateName) => {
131 let comparison = comparison.to_mut();
132 comparison.drain(orig_len..);
133 let _ = write!(comparison, " {number}");
134 number += 1;
135 }
136 Err(ComparisonError::NameStartsWithRace) => {
137 let comparison = comparison.to_mut();
138 let len_after_trimming = comparison[RACE_COMPARISON_PREFIX.len()..]
141 .trim_start()
142 .len();
143 let shrunk_by = comparison.len() - len_after_trimming;
144 comparison.drain(..shrunk_by);
145 }
146 }
147 }
148 }
149
150 while let Some(line) = lines.next() {
151 if line.is_empty() {
152 continue;
153 }
154
155 let mut splits = line.split('\t');
156 let mut segment = Segment::new(splits.next().context(ExpectedSegmentName)?);
157 let best_segment = parse_time_optional(splits.next().context(ExpectedBestSegment)?)
158 .context(ParseBestSegment)?;
159 segment.set_best_segment_time(RealTime(best_segment).into());
160
161 let mut pb_time = Time::new();
162 for comparison in &comparisons {
163 let time = segment.comparison_mut(comparison);
164 pb_time.real_time = parse_time_optional(splits.next().context(ExpectedComparisonTime)?)
165 .context(ParseComparisonTime)?;
166 time.real_time = pb_time.real_time;
167 }
168 segment.set_personal_best_split_time(pb_time);
169
170 let _line = lines.next().context(ExpectedIconLine)?;
171
172 #[cfg(feature = "std")]
173 catch! {
174 let file = _line.trim_end();
175 if !file.is_empty() {
176 let path = path.as_mut()?;
177 path.set_file_name(file);
178 let image = Image::from_file(path, &mut buf).ok()?;
179 segment.set_icon(image);
180 }
181 };
182
183 run.push_segment(segment);
184 }
185
186 #[cfg(feature = "std")]
187 parse_history(&mut run, path).ok();
188
189 Ok(run)
190}
191
192#[cfg(feature = "std")]
193fn parse_history(run: &mut Run, path: Option<PathBuf>) -> StdResult<(), ()> {
194 if let Some(mut path) = path {
195 path.set_extension("");
196 let mut path = path.into_os_string();
197 path.push("-RunLog.txt");
198 let path = PathBuf::from(path);
199
200 let file = std::fs::read_to_string(path).map_err(drop)?;
201 let mut lines = file.lines();
202 let mut attempt_id = 1;
203
204 lines.next(); for line in lines {
207 let mut splits = line.split('\t');
208 let time_stamp = splits.next().ok_or(())?;
209 let started = parse_date(time_stamp).ok_or(())?;
210 let completed = splits.next().ok_or(())? == "C";
211 let split_times: Vec<_> = splits
212 .map(parse_time_optional)
213 .collect::<StdResult<_, _>>()
214 .map_err(drop)?;
215 let mut final_time = Time::default();
216 let mut ended = None;
217 if completed {
218 catch! {
219 let last_split_time = split_times.last()?;
220 final_time.real_time = *last_split_time;
221 let final_time = final_time.real_time?;
222 let ended_date = started + final_time.to_duration();
223 ended = Some(AtomicDateTime::new(ended_date, false));
224 };
225 }
226
227 run.add_attempt_with_index(
228 final_time,
229 attempt_id,
230 Some(AtomicDateTime::new(started, false)),
231 ended,
232 None,
233 );
234
235 let mut last_split = TimeSpan::zero();
236 for (segment, current_split) in
237 run.segments_mut().iter_mut().zip(split_times.into_iter())
238 {
239 let mut segment_time = Time::default();
240 if let Some(current_split) = current_split {
241 segment_time.real_time = Some(current_split - last_split);
242 last_split = current_split;
243 }
244
245 segment
246 .segment_history_mut()
247 .insert(attempt_id, segment_time);
248
249 if catch! {
250 segment_time.real_time? < segment.best_segment_time().real_time?
251 }
252 .unwrap_or(false)
253 {
254 segment.set_best_segment_time(segment_time);
255 }
256 }
257
258 attempt_id += 1;
259 }
260 }
261 Ok(())
262}
263
264#[cfg(feature = "std")]
265fn parse_date(text: &str) -> Option<crate::DateTime> {
266 let (year, rem) = text.split_once('/')?;
267 let (month, rem) = rem.split_once('/')?;
268 let (day, rem) = rem.split_once(' ')?;
269 let (hour, minute) = rem.split_once(':')?;
270 Some(
271 time::PrimitiveDateTime::new(
272 time::Date::from_calendar_date(
273 year.parse().ok()?,
274 month.parse::<u8>().ok()?.try_into().ok()?,
275 day.parse().ok()?,
276 )
277 .ok()?,
278 time::Time::from_hms(hour.parse().ok()?, minute.parse().ok()?, 0).ok()?,
279 )
280 .assume_utc(),
281 )
282}