livesplit_core/run/parser/
splits_io.rs

1//! Provides the parser for generic Splits I/O splits files.
2
3use crate::{
4    platform::prelude::*, util::PopulateString, Run, Segment as LiveSplitSegment, Time, TimeSpan,
5};
6use alloc::borrow::Cow;
7use core::result::Result as StdResult;
8use serde::{Deserialize, Serialize};
9use serde_json::Error as JsonError;
10
11/// The Error type for splits files that couldn't be parsed by the generic
12/// Splits I/O Parser.
13#[derive(Debug, snafu::Snafu)]
14#[snafu(context(suffix(false)))]
15pub enum Error {
16    /// Failed to parse JSON.
17    Json {
18        /// The underlying error.
19        #[cfg_attr(not(feature = "std"), snafu(source(false)))]
20        source: JsonError,
21    },
22}
23
24/// The Result type for the generic Splits I/O Parser.
25pub type Result<T> = StdResult<T, Error>;
26
27/// Duration holds a realtime duration and a gametime duration.
28#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
29#[serde(rename = "duration")]
30struct Duration {
31    /// Gametime (Milliseconds) is a duration of milliseconds in game-world time.
32    #[serde(rename = "gametimeMS")]
33    gametime_ms: Option<f64>,
34    /// Realtime (Milliseconds) is a duration of milliseconds in real-world time.
35    #[serde(rename = "realtimeMS")]
36    realtime_ms: Option<f64>,
37}
38/// Run Time represents a moment inside a run, and indicates the duration of the run so far at that
39/// moment. It holds a realtime run duration so far and a gametime run duration so far.
40#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
41#[serde(rename = "runTime")]
42struct RunTime {
43    /// Gametime (Milliseconds) is a duration a run so far in milliseconds.
44    #[serde(rename = "gametimeMS")]
45    gametime_ms: Option<f64>,
46    /// Realtime (Milliseconds) is a duration of a run so far in milliseconds.
47    #[serde(rename = "realtimeMS")]
48    realtime_ms: Option<f64>,
49}
50#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
51struct Attempt {
52    /// Attempt Number is the number of lifetime attempts the runner will have made after this one.
53    /// The Attempt Number for an attempt is a label, not an index; the first attempt for a
54    /// category has an Attempt Number of 1 (not 0).
55    #[serde(rename = "attemptNumber")]
56    attempt_number: i64,
57    duration: Option<Duration>,
58}
59#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
60struct Attempts {
61    /// Histories is an array of previous attempts by this runner of this category.
62    histories: Option<Vec<Attempt>>,
63    /// Total holds the total number of attempts for this category.
64    total: Option<u32>,
65}
66#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
67struct CategoryLinks<'a> {
68    /// Speedrun.com ID specifies the category's Speedrun.com ID.
69    #[serde(rename = "speedruncomID")]
70    #[serde(borrow)]
71    speedruncom_id: Option<Cow<'a, str>>,
72    /// Splits I/O ID specifies the category's Splits I/O ID.
73    #[serde(rename = "splitsioID")]
74    #[serde(borrow)]
75    splitsio_id: Option<Cow<'a, str>>,
76}
77#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
78struct Category<'a> {
79    /// Links specifies the category's identity in other services.
80    links: Option<CategoryLinks<'a>>,
81    /// Longname is a human-readable category name, intended for display to users.
82    #[serde(borrow)]
83    longname: Cow<'a, str>,
84    /// Shortname is a machine-readable category name, intended for use in APIs, databases, URLs,
85    /// and filenames.
86    #[serde(borrow)]
87    shortname: Option<Cow<'a, str>>,
88}
89#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
90struct GameLinks<'a> {
91    /// Speedrun.com ID specifies the game's Speedrun.com ID.
92    #[serde(rename = "speedruncomID")]
93    #[serde(borrow)]
94    speedruncom_id: Option<Cow<'a, str>>,
95    /// Splits I/O ID specifies the game's Splits I/O ID.
96    #[serde(rename = "splitsioID")]
97    #[serde(borrow)]
98    splitsio_id: Option<Cow<'a, str>>,
99}
100#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
101struct Game<'a> {
102    /// Links specifies the game's identity in other services.
103    links: Option<GameLinks<'a>>,
104    /// Longname is a human-readable game name, intended for display to users.
105    #[serde(borrow)]
106    longname: Cow<'a, str>,
107    /// Shortname is a machine-readable game name, intended for use in APIs, databases, URLs, and
108    /// filenames.
109    #[serde(borrow)]
110    shortname: Option<Cow<'a, str>>,
111}
112#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
113struct RunLinks<'a> {
114    /// Speedrun.com ID is the run's ID on Speedrun.com. This can be used to communicate with the
115    /// Speedrun.com API.
116    #[serde(rename = "speedruncomID")]
117    #[serde(borrow)]
118    speedruncom_id: Option<Cow<'a, str>>,
119    /// Splits I/O ID is the run's ID on Splits I/O. This can be used to communicate with the
120    /// Splits I/O API.
121    #[serde(rename = "splitsioID")]
122    #[serde(borrow)]
123    splitsio_id: Option<Cow<'a, str>>,
124}
125#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
126struct Pause<'a> {
127    /// Ended At is the date and time at which the pause was ended, specified in RFC 3339 format.
128    #[serde(rename = "endedAt")]
129    #[serde(borrow)]
130    ended_at: Option<Cow<'a, str>>,
131    /// Started At is the date and time at which the pause was started, specified in RFC 3339
132    /// format.
133    #[serde(rename = "startedAt")]
134    #[serde(borrow)]
135    started_at: Cow<'a, str>,
136}
137#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
138struct RunnerLinks<'a> {
139    /// Speedrun.com ID specifies the runner's Speedrun.com ID.
140    #[serde(rename = "speedruncomID")]
141    #[serde(borrow)]
142    speedruncom_id: Option<Cow<'a, str>>,
143    /// Splits I/O ID specifies the runner's Splits I/O ID.
144    #[serde(rename = "splitsioID")]
145    #[serde(borrow)]
146    splitsio_id: Option<Cow<'a, str>>,
147    /// Twitch ID specifies the runner's Twitch ID.
148    #[serde(rename = "twitchID")]
149    #[serde(borrow)]
150    twitch_id: Option<Cow<'a, str>>,
151    /// Twitter ID specifies the runner's Twitter ID.
152    #[serde(rename = "twitterID")]
153    #[serde(borrow)]
154    twitter_id: Option<Cow<'a, str>>,
155}
156#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
157struct Runner<'a> {
158    /// Links specifies the runner's identity in other services.
159    links: Option<RunnerLinks<'a>>,
160    /// Longname is a human-readable runner name, intended for display to users.
161    #[serde(borrow)]
162    longname: Option<Cow<'a, str>>,
163    /// Shortname is a machine-readable runner name, intended for use in APIs, databases, URLs, and
164    /// filenames.
165    #[serde(borrow)]
166    shortname: Cow<'a, str>,
167}
168#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
169struct SegmentHistoryElement {
170    /// Attempt Number is the number of lifetime attempts the runner will have made on this
171    /// category after this one. Generally these attempt numbers should correspond to those in
172    /// Attempts -> History, although a number given here may not be present there if the run was
173    /// reset before completion.
174    #[serde(rename = "attemptNumber")]
175    attempt_number: i64,
176    #[serde(rename = "endedAt")]
177    ended_at: Option<RunTime>,
178    /// Is Reset should be true if the runner reset the run during this segment. If so, this and
179    /// all future segments' Ended Ats for this run are ignored.
180    #[serde(rename = "isReset")]
181    is_reset: Option<bool>,
182    /// Is Skipped should be true if the runner skipped over the split that ends this segment,
183    /// rather than splitting. If so, this segment's Ended At is ignored.
184    #[serde(rename = "isSkipped")]
185    is_skipped: Option<bool>,
186}
187#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
188struct Segment<'a> {
189    #[serde(rename = "bestDuration")]
190    best_duration: Option<Duration>,
191    #[serde(rename = "endedAt")]
192    ended_at: Option<RunTime>,
193    /// Histories is an array of previous completions of this segment by this runner.
194    histories: Option<Vec<SegmentHistoryElement>>,
195    /// Is Reset should be true if the runner reset the run during this segment. If so, this and
196    /// all future segments' Ended Ats for this run are ignored.
197    #[serde(rename = "isReset")]
198    is_reset: Option<bool>,
199    /// Is Skipped should be true if the runner skipped over the split that ends this segment,
200    /// rather than splitting. If so, this segment's Ended At is ignored.
201    #[serde(rename = "isSkipped")]
202    is_skipped: Option<bool>,
203    /// Name is the runner-provided name of this segment
204    #[serde(borrow)]
205    name: Option<Cow<'a, str>>,
206}
207#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
208struct Timer<'a> {
209    /// Longname is a human-readable timer name, intended for display to users.
210    #[serde(borrow)]
211    longname: Cow<'a, str>,
212    /// Shortname is a machine-readable timer name, intended for use in APIs, databases, URLs, and
213    /// filenames.
214    #[serde(borrow)]
215    shortname: Cow<'a, str>,
216    /// Version is the version of the timer used to record this run. Semantic Versioning is
217    /// strongly recommended but not enforced.
218    #[serde(borrow)]
219    version: Cow<'a, str>,
220    /// Website is the URL for the timer's website.
221    #[serde(borrow)]
222    website: Option<Cow<'a, str>>,
223}
224#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
225struct Splits<'a> {
226    /// Schema Version specifies which version of the Splits I/O JSON Schema is being used. This
227    /// schema specifies only v1.0.0.
228    #[serde(rename = "_schemaVersion")]
229    #[serde(borrow)]
230    _schemaversion: Cow<'a, str>,
231    /// Attempts contains historical information about previous runs by this runner in this
232    /// category.
233    attempts: Option<Attempts>,
234    /// Category specifies information about the category being run.
235    category: Option<Category<'a>>,
236    /// Ended At is the date and time at which the run was ended, specified in RFC 3339 format.
237    #[serde(rename = "endedAt")]
238    #[serde(borrow)]
239    ended_at: Option<Cow<'a, str>>,
240    /// Game specifies information about the game being run.
241    game: Option<Game<'a>>,
242    /// Image URL is the location of an image associated with this run. Often this is a screenshot
243    /// of the timer at run completion, but can be anything the runner wants displayed alongside
244    /// the run.
245    #[serde(rename = "imageURL")]
246    #[serde(borrow)]
247    image_url: Option<Cow<'a, str>>,
248    /// Links specifies the run's identity in other services.
249    links: Option<RunLinks<'a>>,
250    /// Pauses holds runner-caused pauses that took place during the run.
251    pauses: Option<Vec<Pause<'a>>>,
252    /// Runners is an array of people who participated in this run. Some games and categories call
253    /// for cooperative play, but otherwise this will usually be just one person.
254    runners: Option<Vec<Runner<'a>>>,
255    /// Segments is an array of all segments for this run.
256    segments: Option<Vec<Segment<'a>>>,
257    /// Started At is the date and time at which the run was started, specified in RFC 3339 format.
258    #[serde(rename = "startedAt")]
259    #[serde(borrow)]
260    started_at: Option<Cow<'a, str>>,
261    /// Timer holds information about the timer used to record the run.
262    timer: Timer<'a>,
263    /// Video URL is the location of a VOD of the run.
264    #[serde(rename = "videoURL")]
265    #[serde(borrow)]
266    video_url: Option<Cow<'a, str>>,
267}
268
269impl From<Option<Duration>> for Time {
270    fn from(duration: Option<Duration>) -> Time {
271        duration.map(Into::into).unwrap_or_default()
272    }
273}
274
275impl From<Duration> for Time {
276    fn from(duration: Duration) -> Time {
277        let mut time = Time::new();
278        if let Some(ms) = duration.realtime_ms {
279            time.real_time = Some(TimeSpan::from_milliseconds(ms));
280        }
281        if let Some(ms) = duration.gametime_ms {
282            time.game_time = Some(TimeSpan::from_milliseconds(ms));
283        }
284        time
285    }
286}
287
288impl From<Option<RunTime>> for Time {
289    fn from(time: Option<RunTime>) -> Time {
290        time.map(Into::into).unwrap_or_default()
291    }
292}
293
294impl From<RunTime> for Time {
295    fn from(run_time: RunTime) -> Time {
296        let mut time = Time::new();
297        if let Some(ms) = run_time.realtime_ms {
298            time.real_time = Some(TimeSpan::from_milliseconds(ms));
299        }
300        if let Some(ms) = run_time.gametime_ms {
301            time.game_time = Some(TimeSpan::from_milliseconds(ms));
302        }
303        time
304    }
305}
306
307/// Attempts to parse a generic Splits I/O splits file.
308pub fn parse(source: &str) -> Result<(Run, Cow<'_, str>)> {
309    let splits: Splits<'_> =
310        serde_json::from_str(source).map_err(|source| Error::Json { source })?;
311
312    let mut run = Run::new();
313
314    if let Some(game) = splits.game {
315        run.set_game_name(game.longname);
316    }
317    if let Some(category) = splits.category {
318        run.set_category_name(category.longname);
319    }
320    if let Some(attempts) = splits.attempts {
321        if let Some(total) = attempts.total {
322            run.set_attempt_count(total);
323        }
324        for attempt in attempts.histories.into_iter().flatten() {
325            run.add_attempt_with_index(
326                attempt.duration.into(),
327                attempt.attempt_number as i32,
328                None,
329                None,
330                None,
331            );
332        }
333    }
334
335    if let Some(runner) = splits
336        .runners
337        .and_then(|runners| runners.into_iter().next())
338    {
339        let name = runner.longname.unwrap_or(runner.shortname);
340        if !name.trim_start().is_empty() {
341            run.metadata_mut()
342                .custom_variable_mut("Runner")
343                .permanent()
344                .set_value(name);
345        }
346        if let Some(links) = runner.links {
347            if let Some(twitter_id) = links.twitter_id {
348                run.metadata_mut()
349                    .custom_variable_mut("Twitter")
350                    .permanent()
351                    .set_value(twitter_id);
352            }
353            if let Some(twitch_id) = links.twitch_id {
354                run.metadata_mut()
355                    .custom_variable_mut("Twitch")
356                    .permanent()
357                    .set_value(twitch_id);
358            }
359            if let Some(speedruncom_id) = links.speedruncom_id {
360                run.metadata_mut()
361                    .custom_variable_mut("speedrun.com")
362                    .permanent()
363                    .set_value(speedruncom_id);
364            }
365            if let Some(splitsio_id) = links.splitsio_id {
366                run.metadata_mut()
367                    .custom_variable_mut("Splits I/O")
368                    .permanent()
369                    .set_value(splitsio_id);
370            }
371        }
372    }
373
374    if let Some(segments) = splits.segments {
375        run.segments_mut().extend(segments.into_iter().map(|split| {
376            let mut segment = LiveSplitSegment::new(split.name.unwrap_or_default());
377            segment.set_personal_best_split_time(split.ended_at.into());
378            segment.set_best_segment_time(split.best_duration.into());
379            if let Some(mut history) = split.histories {
380                let segment_history = segment.segment_history_mut();
381                history.sort_unstable_by_key(|x| x.attempt_number);
382                for element in history {
383                    segment_history.insert(element.attempt_number as i32, element.ended_at.into());
384                }
385            }
386            segment
387        }));
388    }
389
390    if let Some(links) = splits.links {
391        if let Some(link) = links.speedruncom_id {
392            link.populate(&mut run.metadata_mut().run_id);
393        }
394    }
395
396    let timer = if splits.timer.longname.is_empty() {
397        "Generic Timer".into()
398    } else {
399        splits.timer.longname
400    };
401
402    Ok((run, timer))
403}