livesplit_core/run/parser/
splitterino.rs

1//! Provides the parser for Splitterino splits files.
2
3use crate::{platform::prelude::*, Run, Segment, Time, TimeSpan};
4use alloc::borrow::Cow;
5use core::result::Result as StdResult;
6use serde::Deserialize;
7use serde_json::Error as JsonError;
8
9/// The Error type for splits files that couldn't be parsed by the Splitterino
10/// Parser.
11#[derive(Debug, snafu::Snafu)]
12#[snafu(context(suffix(false)))]
13pub enum Error {
14    /// Failed to parse JSON.
15    Json {
16        /// The underlying error.
17        #[cfg_attr(not(feature = "std"), snafu(source(false)))]
18        source: JsonError,
19    },
20}
21
22/// The Result type for the Splitterino Parser.
23pub type Result<T> = StdResult<T, Error>;
24
25#[derive(Deserialize)]
26struct SplitsFormat<'a> {
27    // #[serde(borrow)]
28    // version: Cow<'a, str>,
29    #[serde(borrow)]
30    splits: Splits<'a>,
31}
32
33/// Format in which splits are getting saved to file or should be transmitted
34#[derive(Deserialize)]
35#[serde(rename_all = "camelCase")]
36struct Splits<'a> {
37    /// The Game Information about this run
38    #[serde(borrow)]
39    game: GameInfo<'a>,
40    /// The delay of how much time the timer should wait when starting a new run in milliseconds
41    start_delay: Option<i64>,
42    /// An array of segments which are associated to these splits
43    #[serde(borrow)]
44    segments: Vec<SplitterinoSegment<'a>>,
45    // /// The timing-method which is used for the splits
46    // timing: SplitterinoTimingMethod,
47}
48
49/// Timing methods which can be used for segment times
50#[derive(Deserialize)]
51#[serde(rename_all = "camelCase")]
52enum SplitterinoTimingMethod {
53    Igt,
54    Rta,
55}
56
57/// Detailed information about the game and run details
58#[derive(Deserialize, Default)]
59#[serde(default)]
60struct GameInfo<'a> {
61    /// Name of the Game that is currently being run
62    #[serde(borrow)]
63    name: Cow<'a, str>,
64    /// Category that is currently being run
65    #[serde(borrow)]
66    category: Cow<'a, str>,
67    /// The Platform on which the Game is being run on
68    #[serde(borrow)]
69    platform: Cow<'a, str>,
70    /// The Region format the game is run in
71    #[serde(borrow)]
72    region: Cow<'a, str>,
73}
74
75/// Describes a single Segment
76#[derive(Deserialize)]
77#[serde(rename_all = "camelCase")]
78struct SplitterinoSegment<'a> {
79    // /// The ID which identifies the Segment
80    // #[serde(borrow)]
81    // id: Cow<'a, str>,
82    /// The name of the Segment
83    #[serde(borrow)]
84    name: Cow<'a, str>,
85    /// The time of the personal best in milliseconds
86    personal_best: Option<SegmentTime>,
87    /// The time of the overall best in milliseconds
88    overall_best: Option<SegmentTime>,
89    // /// If the Segment has been passed successfully
90    // passed: Option<bool>,
91    /// If the Segment has been skipped
92    #[serde(default)]
93    skipped: bool,
94}
95
96/// Format for detailed times with multiple timing methods
97#[derive(Deserialize)]
98struct SegmentTime {
99    /// The detailed time of a Segment for IGT
100    igt: DetailedTime,
101    /// The detailed time of a Segment for RTA
102    rta: DetailedTime,
103}
104
105/// Format in which times can easily be represented and stored
106#[derive(Deserialize)]
107#[serde(rename_all = "camelCase")]
108struct DetailedTime {
109    /// The time of the segment in milliseconds which count towards the run
110    raw_time: u64,
111    /// The time of the segment in milliseconds which were spent paused
112    pause_time: u64,
113}
114
115fn parse_segment_time(SegmentTime { igt, rta }: SegmentTime) -> [TimeSpan; 2] {
116    [
117        TimeSpan::from_milliseconds((rta.raw_time - rta.pause_time) as _),
118        TimeSpan::from_milliseconds((igt.raw_time - igt.pause_time) as _),
119    ]
120}
121
122fn parse_best_segment_time(segment_time: SegmentTime) -> Time {
123    let [rta, igt] = parse_segment_time(segment_time);
124    let mut time = Time::new();
125    if rta != TimeSpan::zero() {
126        time.real_time = Some(rta);
127    }
128    if igt != TimeSpan::zero() {
129        time.game_time = Some(igt);
130    }
131    time
132}
133
134fn parse_split_time(
135    total_rta: &mut TimeSpan,
136    total_igt: &mut TimeSpan,
137    segment_time: SegmentTime,
138) -> Time {
139    let [rta, igt] = parse_segment_time(segment_time);
140
141    let real_time = if rta != TimeSpan::zero() {
142        *total_rta += rta;
143        Some(*total_rta)
144    } else {
145        None
146    };
147
148    let game_time = if igt != TimeSpan::zero() {
149        *total_igt += igt;
150        Some(*total_igt)
151    } else {
152        None
153    };
154
155    Time {
156        real_time,
157        game_time,
158    }
159}
160
161/// Attempts to parse a Splitterino splits file.
162pub fn parse(source: &str) -> Result<Run> {
163    let SplitsFormat::<'_> { splits, .. } =
164        serde_json::from_str(source).map_err(|source| Error::Json { source })?;
165
166    let mut run = Run::new();
167
168    run.set_game_name(splits.game.name);
169    run.set_category_name(splits.game.category);
170    run.metadata_mut().set_platform_name(splits.game.platform);
171    run.metadata_mut().set_region_name(splits.game.region);
172
173    // FIXME: Region may need to be translated to speedrun.com's region.
174    // FIXME: Parse pause times.
175    // FIXME: Parse default timing method, if we ever store that.
176
177    if let Some(start_delay) = splits.start_delay {
178        run.set_offset(TimeSpan::from_milliseconds(-start_delay as _));
179    }
180
181    let (mut total_rta, mut total_igt) = (TimeSpan::zero(), TimeSpan::zero());
182
183    run.segments_mut()
184        .extend(splits.segments.into_iter().map(|split| {
185            let mut segment = Segment::new(split.name);
186
187            if !split.skipped {
188                if let Some(personal_best) = split.personal_best {
189                    segment.set_personal_best_split_time(parse_split_time(
190                        &mut total_rta,
191                        &mut total_igt,
192                        personal_best,
193                    ));
194                }
195            }
196
197            if let Some(overall_best) = split.overall_best {
198                segment.set_best_segment_time(parse_best_segment_time(overall_best));
199            }
200
201            segment
202        }));
203
204    Ok(run)
205}