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/.
*/

#[cfg(feature = "serde")]
mod dump;
#[cfg(feature = "serde")]
pub use dump::{StateDump, TimerDump};

mod timer;
pub use timer::HailTimer;

use crate::{
    Run,
    types::{
        Comparison, SplitStatus, StateChange, StateChangeRequest, TimeType, TimerState,
        TimingMethod,
    },
};

use std::cell::RefCell;

/// State of a speedrun timer.
///
/// Requests to change state are queued by calls to [`request`](Self::request), then executed in the order they were requested
/// by a call to [`update`](Self::update).
///
/// Requests to make invalid state transitions will be ignored.
///
/// Tracks in-game time independently of real time. In-game time can be paused independently of real time, but
/// pausing real time will prevent in-game time from updating, regardless of whether in-game time is paused.
#[derive(Debug, Clone)]
pub struct HailState {
    pub run: Run,
    pub run_changed: bool,
    /// The real time timer, contains information about the real time segments and times.
    pub rta: HailTimer,
    rta_state: TimerState,
    /// The in-game timer, contains information about the in-game segments and times.
    pub igt: HailTimer,
    igt_state: TimerState,
    pub current_seg: usize,
    pub comparison: Comparison,
    requests: RefCell<Vec<StateChangeRequest>>,
    /// Every state transition that occurred in the last call to [`update`](Self::update).
    pub changes: Vec<StateChange>,
}

impl HailState {
    /// Create a new `HailState` from a [`Run`].
    pub fn new(run: Run) -> Self {
        let rta = HailTimer::new(&run, TimingMethod::Rta);
        let igt = HailTimer::new(&run, TimingMethod::Igt);
        Self {
            run,
            run_changed: false,
            rta,
            rta_state: TimerState::NotRunning,
            igt,
            igt_state: TimerState::NotRunning,
            current_seg: 0,
            comparison: Comparison::PersonalBest,
            requests: RefCell::new(Vec::new()),
            changes: vec![StateChange::Reset],
        }
    }
    /// Create a [`StateDump`] representing the current state.
    #[cfg(feature = "serde")]
    pub fn create_dump(&self) -> StateDump {
        StateDump {
            run: self.run.clone(),
            comparison: self.comparison,
            current_seg: self.current_seg,
            rta: self.rta.create_dump(),
            igt: self.igt.create_dump(),
            igt_paused: self.is_igt_paused(),
        }
    }
    /// Create a new `HailState` from a [`StateDump`].
    ///
    /// The created state will be paused, but otherwise identical to the state that was dumped.
    #[cfg(feature = "serde")]
    pub fn from_dump(d: &StateDump) -> Self {
        let rta = HailTimer::from_dump(d, &d.run, TimingMethod::Rta);

        // if we don't care about igt, dont bother restoring it, just create a new one.
        let igt = if d.run.ingame_time {
            HailTimer::from_dump(d, &d.run, TimingMethod::Igt)
        } else {
            HailTimer::new(&d.run, TimingMethod::Igt)
        };

        Self {
            run: d.run.clone(),
            run_changed: true,
            rta,
            rta_state: TimerState::Paused,
            igt,
            igt_state: if d.igt_paused {
                TimerState::Paused
            } else {
                TimerState::Running
            },
            current_seg: d.current_seg,
            comparison: d.comparison,
            requests: RefCell::new(Vec::new()),
            changes: vec![StateChange::Reset],
        }
    }
    /// Update the state.
    ///
    /// Should be called each frame. Processes all requests that have been
    /// made, updates timing data, and updates the state as
    /// well as generates a vec of the changes that occurred.
    pub fn update(&mut self) {
        use StateChangeRequest as SCR;
        use TimerState as TS;

        self.changes.clear();

        if self.rta_state == TS::Running {
            self.rta.update_timing();
            if self.igt_state == TS::Running {
                self.igt.update_timing();
            }
        } else if self.rta_state == TS::Offset {
            self.rta.update_timing();
            self.igt.update_timing();
            if self.rta.active_time >= 0 {
                self.rta_state = TS::Running;
                self.igt_state = TS::Running;
                self.changes
                    .push(StateChange::StartSeg { segment: Some(0) });
            }
        }

        for r in self.requests.borrow().iter() {
            match (r, self.rta_state, self.igt_state) {
                (SCR::Pause, TS::Running, i) => {
                    self.rta.save_adj();
                    if i != TS::Paused {
                        self.igt.save_adj();
                    }
                    // rta pause forces igt pause
                    // igt pause only set by PauseIGT req
                    self.rta_state = TS::Paused;
                    self.changes.push(StateChange::Pause);
                }
                (SCR::Pause, TS::Paused, i) => {
                    self.rta_state = TS::Running;
                    self.rta.tare();
                    if i != TS::Paused {
                        self.igt.tare();
                    }
                    self.changes.push(StateChange::Unpause);
                }
                (SCR::PauseIGT, _, TS::Running) => {
                    self.igt_state = TS::Paused;
                    self.igt.save_adj();
                    self.changes.push(StateChange::PauseIGT);
                }
                (SCR::UnpauseIGT, _, TS::Paused) => {
                    self.igt_state = TS::Running;
                    self.igt.tare();
                    self.changes.push(StateChange::UnpauseIGT);
                }
                (SCR::Split, TS::Running, TS::Running | TS::Paused) => {
                    let rta_seg_time = self.rta.split();
                    self.run
                        .add_segment_attempt(self.current_seg, rta_seg_time, TimingMethod::Rta);

                    // only save igt attempts if igt recording is enabled
                    if self.run.ingame_time {
                        let igt_seg_time = self.igt.split();
                        self.run.add_segment_attempt(
                            self.current_seg,
                            igt_seg_time,
                            TimingMethod::Igt,
                        );
                    }

                    self.run_changed = true;

                    self.changes.push(StateChange::FinishSeg {
                        segment: self.current_seg,
                    });

                    if self.last_segment() {
                        self.rta_state = TS::Finished;
                        self.igt_state = TS::Finished;
                        let rta_prev_pb = self.run.pb(TimingMethod::Rta);
                        if *self.rta.run_split_times.last().unwrap() < rta_prev_pb
                            || !rta_prev_pb.is_time()
                        {
                            self.run.rta_pb_splits = self.rta.run_split_times.clone();
                        }
                        let mut prev = TimeType::None;
                        for (i, &spl) in self.run.rta_pb_splits.iter().enumerate() {
                            if self.rta.run_pb_statuses[i] == SplitStatus::Gold {
                                self.run.rta_gold_segments[i] = spl - prev;
                            }
                            prev = spl;
                        }

                        // only save igt segments if igt recording is enabled
                        if self.run.ingame_time {
                            let igt_prev_pb = self.run.pb(TimingMethod::Igt);
                            if *self.igt.run_split_times.last().unwrap() < igt_prev_pb
                                || !igt_prev_pb.is_time()
                            {
                                self.run.igt_pb_splits = self.igt.run_split_times.clone();
                            }
                            let mut prev = TimeType::None;
                            for (i, &spl) in self.igt.run_split_times.iter().enumerate() {
                                if self.igt.run_pb_statuses[i] == SplitStatus::Gold {
                                    self.run.igt_gold_segments[i] = spl - prev;
                                }
                                prev = spl;
                            }
                        }
                        // recalculate exposed segment times as the split times may have changed
                        self.run.calc_segments();
                    } else {
                        self.changes.push(StateChange::ChangeSeg {
                            old: self.current_seg,
                            new: self.current_seg + 1,
                        });
                        self.current_seg += 1;
                        self.changes.push(StateChange::StartSeg {
                            segment: Some(self.current_seg),
                        });
                    }
                }
                (SCR::Split, TS::NotRunning, TS::NotRunning) => {
                    self.rta.tare();
                    self.igt.tare();
                    let chg_seg;
                    if self.run.offset.val() < 0 {
                        self.rta_state = TS::Offset;
                        self.igt_state = TS::Offset;
                        chg_seg = None;
                    } else {
                        self.rta_state = TS::Running;
                        self.igt_state = TS::Running;
                        chg_seg = Some(0);
                    }
                    self.changes
                        .push(StateChange::StartSeg { segment: chg_seg });
                }
                (SCR::Unsplit, TS::Running, TS::Running | TS::Paused) if self.current_seg != 0 => {
                    self.changes.push(StateChange::ChangeSeg {
                        old: self.current_seg,
                        new: self.current_seg - 1,
                    });

                    self.current_seg -= 1;

                    let rta_seg_time = self.rta.unsplit();
                    self.run.remove_segment_attempt(
                        self.current_seg,
                        rta_seg_time,
                        TimingMethod::Rta,
                    );

                    if self.run.ingame_time {
                        let igt_seg_time = self.igt.unsplit();
                        self.run.remove_segment_attempt(
                            self.current_seg,
                            igt_seg_time,
                            TimingMethod::Igt,
                        );
                    }
                }
                (SCR::Skip, TS::Running, TS::Running | TS::Paused) if !self.last_segment() => {
                    self.changes.push(StateChange::ChangeSeg {
                        old: self.current_seg,
                        new: self.current_seg + 1,
                    });
                    self.rta.skip();
                    if self.run.ingame_time {
                        self.igt.skip();
                    }
                    self.current_seg += 1;
                }
                (SCR::Reset, _, _) => {
                    self.rta_state = TS::NotRunning;
                    // save golds
                    let mut prev = TimeType::None;
                    for (i, &spl) in self.rta.run_split_times.iter().enumerate() {
                        if self.rta.run_pb_statuses[i] == SplitStatus::Gold {
                            self.run.rta_gold_segments[i] = spl - prev;
                        }
                        prev = spl;
                    }
                    self.rta = HailTimer::new(&self.run, TimingMethod::Rta);
                    self.igt_state = TS::NotRunning;
                    if self.run.ingame_time {
                        let mut prev = TimeType::None;
                        for (i, &spl) in self.igt.run_split_times.iter().enumerate() {
                            if self.igt.run_pb_statuses[i] == SplitStatus::Gold {
                                self.run.igt_gold_segments[i] = spl - prev;
                            }
                            prev = spl;
                        }
                    }
                    self.igt = HailTimer::new(&self.run, TimingMethod::Igt);
                    self.current_seg = 0;
                    self.changes.push(StateChange::Reset);
                }
                (&SCR::SetComparison(c), _, _) => {
                    self.changes.push(StateChange::ChangeComparison {
                        old: self.comparison,
                        new: c,
                    });
                    self.comparison = c;
                }
                (&SCR::SetIGT(t), _, _) => {
                    self.igt.set_time(t);
                }
                _ => {}
            }
        }

        if self.rta_state == TS::Running {
            self.rta.update_status();
            if self.igt_state == TS::Running {
                self.igt.update_status();
            }
        }

        self.requests.borrow_mut().clear();
    }
    /// Check whether the specified timer is running (i.e. the time is changing).
    pub fn is_running(&self, method: TimingMethod) -> bool {
        if method == TimingMethod::Rta {
            self.is_rta_running()
        } else {
            self.is_igt_running()
        }
    }
    /// Check whether the RTA timer is running (i.e. the time is changing).
    pub fn is_rta_running(&self) -> bool {
        self.rta_state == TimerState::Running
    }
    /// Check whether the IGT timer is running (i.e. the time is changing).
    pub fn is_igt_running(&self) -> bool {
        self.is_rta_running() && self.igt_state == TimerState::Running
    }
    /// Check whether the IGT timer has been independently paused
    /// (i.e. not just forced to pause by RTA).
    pub fn is_igt_paused(&self) -> bool {
        self.igt_state == TimerState::Paused
    }
    /// Add a [request](crate::types::StateChangeRequest) to the queue.
    pub fn request(&self, rq: StateChangeRequest) {
        self.requests.borrow_mut().push(rq);
    }
    /// Check whether the timer is in a negative offset.
    pub fn in_offset(&self) -> bool {
        self.rta_state == TimerState::Offset
    }

    fn last_segment(&self) -> bool {
        self.current_seg + 1 == self.run.segment_names.len()
    }
}