hail_core 0.3.0

a library for implementing a speedrun timer
Documentation
/*
  Copyright 2024 periwinkle

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

use super::Run;
use crate::types::TimeType;

use serde::Deserialize;

#[derive(Deserialize, Clone, Copy, Default)]
enum TimeTypeV1 {
    Skipped(u128),
    Time(u128),
    #[default]
    None,
}

impl TimeTypeV1 {
    fn val(&self) -> u128 {
        match self {
            &Self::Skipped(t) => t,
            &Self::Time(t) => t,
            Self::None => 0,
        }
    }
}

impl From<TimeTypeV1> for TimeType {
    fn from(tm: TimeTypeV1) -> TimeType {
        if let TimeTypeV1::Time(t) = tm {
            TimeType::Time(t as i128)
        } else {
            TimeType::None
        }
    }
}

#[derive(Deserialize, Clone, Copy, Default, PartialEq)]
enum TimeTypeV2 {
    Time(i128),
    Skipped,
    #[default]
    None,
}

impl TimeTypeV2 {
    pub fn val(&self) -> i128 {
        if let &TimeTypeV2::Time(t) = self {
            t
        } else {
            0
        }
    }
}

impl From<TimeTypeV2> for TimeType {
    fn from(tm: TimeTypeV2) -> TimeType {
        if let TimeTypeV2::Time(t) = tm {
            TimeType::Time(t)
        } else {
            TimeType::None
        }
    }
}

#[derive(Deserialize)]
pub struct RunV1 {
    game_title: String,
    category: String,
    offset: Option<u128>,
    // no longer needed
    // pb: u128,
    splits: Vec<String>,
    pb_times: Vec<u128>,
    gold_times: Vec<u128>,
}

#[derive(Deserialize)]
pub struct RunV2 {
    game_title: String,
    category: String,
    offset: TimeTypeV1,
    // no longer needed
    // pb: TimeTypeV1,
    splits: Vec<String>,
    pb_times: Vec<TimeTypeV1>,
    gold_times: Vec<TimeTypeV1>,
    sum_times: Vec<(u128, TimeTypeV1)>,
}

#[derive(Deserialize)]
pub struct RunV3 {
    game_title: String,
    category: String,
    ingame_time: bool,
    offset: TimeTypeV2,
    segment_names: Vec<String>,
    pb_segments: Vec<TimeTypeV2>,
    gold_segments: Vec<TimeTypeV2>,
    sum_segments: Vec<(usize, TimeTypeV2)>,
}

#[derive(Deserialize)]
pub struct RunV4 {
    game_title: String,
    category: String,
    /// Whether the run saves ingame time.
    ingame_time: bool,
    /// Time that the timer starts at. Can be positive or negative.
    offset: TimeTypeV2,
    segment_names: Vec<String>,
    /// Segments set in real-time for the fastest completed run.
    rta_pb_segments: Vec<TimeTypeV2>,
    /// Segments set in game-time for the fastest completed run.
    igt_pb_segments: Vec<TimeTypeV2>,
    /// Fastest individual segments set in real-time.
    rta_gold_segments: Vec<TimeTypeV2>,
    /// Fastest individual segments set in game-time.
    igt_gold_segments: Vec<TimeTypeV2>,
    rta_sum_segments: Vec<(usize, TimeTypeV2)>,
    igt_sum_segments: Vec<(usize, TimeTypeV2)>,
}

impl From<RunV1> for Run {
    fn from(old: RunV1) -> Run {
        let mut r = Run::default();
        r.game_title = old.game_title;
        r.category = old.category;
        r.offset = old
            .offset
            .map_or(TimeType::None, |t| TimeType::Time(-(t as i128)));
        r.segment_names = old.splits;
        // collect segments into splits
        r.rta_pb_splits = old
            .pb_times
            .iter()
            .scan(TimeType::None, |acc, &time| {
                if time != 0 {
                    *acc += (time as i128).into();
                    Some(*acc)
                } else {
                    Some(TimeType::None)
                }
            })
            .collect();
        r.igt_pb_splits = r.rta_pb_splits.clone();
        r.rta_gold_segments = old.gold_times.iter().map(|&t| (t as i128).into()).collect();
        r.igt_gold_segments = r.rta_gold_segments.clone();
        r.rta_sum_segments = vec![(0, TimeType::None); r.segment_names.len()];
        r.igt_sum_segments = r.rta_sum_segments.clone();
        r
    }
}

impl From<RunV2> for Run {
    fn from(old: RunV2) -> Run {
        let pb_splits: Vec<_> = old
            .pb_times
            .iter()
            .scan(0, |acc, time| match time {
                TimeTypeV1::Skipped(t) => {
                    *acc += t;
                    Some(TimeType::None)
                }
                TimeTypeV1::Time(t) => {
                    *acc += t;
                    Some(TimeType::Time(*acc as i128))
                }
                TimeTypeV1::None => Some(TimeType::Time(*acc as i128)),
            })
            .collect();
        let gold_segments: Vec<_> = old.gold_times.iter().map(|&gold| gold.into()).collect();
        let sum_segments: Vec<_> = old
            .sum_times
            .iter()
            .map(|&(n, t)| (n as usize, t.into()))
            .collect();
        Run {
            game_title: old.game_title,
            category: old.category,
            offset: TimeType::from(-(old.offset.val() as i128)),
            segment_names: old.splits,
            rta_pb_splits: pb_splits.clone(),
            igt_pb_splits: pb_splits,
            rta_gold_segments: gold_segments.clone(),
            igt_gold_segments: gold_segments,
            rta_sum_segments: sum_segments.clone(),
            igt_sum_segments: sum_segments,
            ..Default::default()
        }
    }
}

fn seg_to_split_v2((sum, has_gap): &mut (TimeType, bool), time: &TimeTypeV2) -> Option<TimeType> {
    *sum += TimeType::from(time.val());
    if *time == TimeTypeV2::None {
        *has_gap = true;
    }
    if *has_gap || *time == TimeTypeV2::Skipped {
        Some(TimeType::None)
    } else {
        Some(*sum)
    }
}

impl From<RunV3> for Run {
    fn from(old: RunV3) -> Run {
        let mut r = Run {
            game_title: old.game_title,
            category: old.category,
            ingame_time: old.ingame_time,
            offset: old.offset.into(),
            segment_names: old.segment_names,
            ..Default::default()
        };
        let pb_splits: Vec<_> = old
            .pb_segments
            .iter()
            .scan((TimeType::None, false), seg_to_split_v2)
            .collect();
        r.rta_pb_splits = pb_splits.clone();
        r.rta_gold_segments = old.gold_segments.iter().map(|&t| t.into()).collect();
        r.rta_sum_segments = old
            .sum_segments
            .iter()
            .map(|&(n, t)| (n, t.into()))
            .collect();

        if old.ingame_time {
            r.igt_pb_splits = pb_splits;
            r.igt_gold_segments = old.gold_segments.iter().map(|&t| t.into()).collect();
            r.igt_sum_segments = old
                .sum_segments
                .iter()
                .map(|&(n, t)| (n, t.into()))
                .collect();
        }

        r
    }
}

impl From<RunV4> for Run {
    fn from(old: RunV4) -> Run {
        let mut r = Run {
            game_title: old.game_title,
            category: old.category,
            ingame_time: old.ingame_time,
            offset: old.offset.into(),
            segment_names: old.segment_names,
            rta_gold_segments: old.rta_gold_segments.iter().map(|&t| t.into()).collect(),
            igt_gold_segments: old.igt_gold_segments.iter().map(|&t| t.into()).collect(),
            rta_sum_segments: old
                .rta_sum_segments
                .iter()
                .map(|&(n, t)| (n, t.into()))
                .collect(),
            igt_sum_segments: old
                .igt_sum_segments
                .iter()
                .map(|&(n, t)| (n, t.into()))
                .collect(),
            ..Default::default()
        };
        let rta_pb_splits: Vec<_> = old
            .rta_pb_segments
            .iter()
            .scan((TimeType::None, false), seg_to_split_v2)
            .collect();
        let igt_pb_splits: Vec<_> = old
            .igt_pb_segments
            .iter()
            .scan((TimeType::None, false), seg_to_split_v2)
            .collect();
        r.rta_pb_splits = rta_pb_splits;
        r.igt_pb_splits = igt_pb_splits;
        r
    }
}