livesplit_core/run/parser/
source_live_timer.rs

1//! Provides the parser for the SourceLiveTimer splits files.
2
3use crate::{platform::prelude::*, GameTime, Run, Segment, 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
10/// SourceLiveTimer 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 SourceLiveTimer parser.
23pub type Result<T> = StdResult<T, Error>;
24
25#[allow(non_snake_case)]
26#[derive(Deserialize)]
27struct Splits<'a> {
28    #[serde(borrow)]
29    Category: Cow<'a, str>,
30    #[serde(borrow)]
31    RunName: Option<Cow<'a, str>>,
32    Splits: Option<Vec<Split<'a>>>,
33}
34
35#[allow(non_snake_case)]
36#[derive(Deserialize)]
37struct Split<'a> {
38    #[serde(borrow)]
39    Map: Cow<'a, str>,
40    #[serde(borrow)]
41    Name: Option<Cow<'a, str>>,
42    Ticks: Option<u64>,
43    BestSegment: Option<u64>,
44}
45
46fn time_span_from_ticks(is_portal2: bool, ticks: u64) -> TimeSpan {
47    let seconds = if is_portal2 {
48        ticks as f64 / 30.0
49    } else {
50        // Game is either Portal or Half Life 2
51        ticks as f64 / 66.666_666_666_666_7
52    };
53
54    TimeSpan::from_seconds(seconds)
55}
56
57/// Attempts to parse a SourceLiveTimer splits file.
58pub fn parse(source: &str) -> Result<Run> {
59    let splits: Splits<'_> =
60        serde_json::from_str(source).map_err(|source| Error::Json { source })?;
61
62    let mut run = Run::new();
63
64    if splits.Category.starts_with("Portal 2") {
65        run.set_game_name("Portal 2");
66    } else if splits.Category.starts_with("Portal") {
67        run.set_game_name("Portal");
68    } else if splits.Category.starts_with("Half Life 2") {
69        run.set_game_name("Half Life 2");
70    }
71
72    if let Some(run_name) = splits.RunName {
73        if run_name != splits.Category {
74            run.set_category_name(run_name);
75        } else {
76            run.set_category_name(splits.Category);
77        }
78    } else {
79        run.set_category_name(splits.Category);
80    }
81
82    if let Some(segments) = splits.Splits {
83        let is_portal2 = run.category_name().starts_with("Portal 2");
84
85        run.segments_mut().extend(segments.into_iter().map(|split| {
86            let mut segment = Segment::new(split.Name.unwrap_or(split.Map));
87
88            if let Some(ticks) = split.Ticks {
89                let pb_split_time = Some(time_span_from_ticks(is_portal2, ticks));
90                segment.set_personal_best_split_time(GameTime(pb_split_time).into());
91            }
92
93            if let Some(best) = split.BestSegment {
94                let best_segment_time = Some(time_span_from_ticks(is_portal2, best));
95                segment.set_best_segment_time(GameTime(best_segment_time).into());
96            }
97
98            segment
99        }));
100    }
101
102    Ok(run)
103}