Skip to content

Commit 4ebb4bb

Browse files
authored
Make invalid timer states unrepresentable (#818)
This refactors the internals of the `Timer`, such that it's impossible for different fields to contain conflicting information. This is done by factoring out an `ActiveAttempt` struct that only exists when there is an active attempt. Additionally inside it differentiates between the attempt having ended or not. The not ended state has a split index and an optional pause time, whereas the ended state has the date time of when the attempt ended. This does not yet touch the public API of the `Timer`. Factoring out the notion of an active attempt and making sure invalid states are unrepresentable is done so we can soon explore the possibility of storing an active attempt in the splits file.
1 parent 7786111 commit 4ebb4bb

File tree

6 files changed

+458
-250
lines changed

6 files changed

+458
-250
lines changed

src/run/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -740,10 +740,10 @@ impl Run {
740740
/// # Panics
741741
///
742742
/// This panics if there is no attempt in the Attempt History.
743-
pub fn update_segment_history(&mut self, current_split_index: usize) {
743+
pub fn update_segment_history(&mut self, segments_count: usize) {
744744
let mut previous_split_time = Time::zero();
745745

746-
let segments = self.segments.iter_mut().take(current_split_index);
746+
let segments = &mut self.segments[..segments_count];
747747
let index = self
748748
.attempt_history
749749
.last()

src/timing/timer/active_attempt.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
use crate::{AtomicDateTime, Run, Time, TimeSpan, TimeStamp, TimingMethod};
2+
3+
#[derive(Debug, Clone)]
4+
pub struct ActiveAttempt {
5+
pub state: State,
6+
pub attempt_started: AtomicDateTime,
7+
pub start_time: TimeStamp,
8+
pub start_time_with_offset: TimeStamp,
9+
// This gets adjusted after resuming
10+
pub adjusted_start_time: TimeStamp,
11+
pub game_time_paused_at: Option<TimeSpan>,
12+
pub loading_times: Option<TimeSpan>,
13+
}
14+
15+
#[derive(Debug, Clone)]
16+
pub enum State {
17+
NotEnded {
18+
current_split_index: usize,
19+
time_paused_at: Option<TimeSpan>,
20+
},
21+
Ended {
22+
attempt_ended: AtomicDateTime,
23+
},
24+
}
25+
26+
pub struct TimerTime {
27+
pub real_time: TimeSpan,
28+
pub game_time: Option<TimeSpan>,
29+
}
30+
31+
impl From<TimerTime> for Time {
32+
fn from(time: TimerTime) -> Self {
33+
Time {
34+
real_time: Some(time.real_time),
35+
game_time: time.game_time,
36+
}
37+
}
38+
}
39+
40+
impl ActiveAttempt {
41+
pub fn current_time(&self, run: &Run) -> TimerTime {
42+
let real_time = match self.state {
43+
State::Ended { .. } => {
44+
let Time {
45+
real_time,
46+
game_time,
47+
} = run.segments().last().unwrap().split_time();
48+
49+
return TimerTime {
50+
real_time: real_time.unwrap_or_default(),
51+
game_time,
52+
};
53+
}
54+
State::NotEnded { time_paused_at, .. } => {
55+
time_paused_at.unwrap_or_else(|| TimeStamp::now() - self.adjusted_start_time)
56+
}
57+
};
58+
59+
let game_time = self
60+
.game_time_paused_at
61+
.or_else(|| Some(real_time - self.loading_times?));
62+
63+
TimerTime {
64+
real_time,
65+
game_time,
66+
}
67+
}
68+
69+
pub fn get_pause_time(&self) -> Option<TimeSpan> {
70+
if let State::NotEnded {
71+
time_paused_at: Some(pause_time),
72+
..
73+
} = self.state
74+
{
75+
return Some(TimeStamp::now() - self.start_time_with_offset - pause_time);
76+
}
77+
78+
if self.start_time_with_offset != self.adjusted_start_time {
79+
Some(self.start_time_with_offset - self.adjusted_start_time)
80+
} else {
81+
None
82+
}
83+
}
84+
85+
pub fn set_loading_times(&mut self, time: TimeSpan, run: &Run) {
86+
self.loading_times = Some(time);
87+
if self.game_time_paused_at.is_some() {
88+
self.game_time_paused_at = Some(self.current_time(run).real_time - time);
89+
}
90+
}
91+
92+
pub fn prepare_split(&mut self, run: &Run) -> Option<(usize, Time)> {
93+
let State::NotEnded {
94+
current_split_index,
95+
time_paused_at: None,
96+
} = &mut self.state
97+
else {
98+
return None;
99+
};
100+
101+
let real_time = TimeStamp::now() - self.adjusted_start_time;
102+
103+
if real_time < TimeSpan::zero() {
104+
return None;
105+
}
106+
107+
let game_time = self
108+
.game_time_paused_at
109+
.or_else(|| Some(real_time - self.loading_times?));
110+
111+
let previous_split_index = *current_split_index;
112+
*current_split_index += 1;
113+
114+
if *current_split_index == run.len() {
115+
self.state = State::Ended {
116+
attempt_ended: AtomicDateTime::now(),
117+
};
118+
}
119+
120+
Some((
121+
previous_split_index,
122+
Time {
123+
real_time: Some(real_time),
124+
game_time,
125+
},
126+
))
127+
}
128+
129+
pub const fn current_split_index(&self) -> Option<usize> {
130+
match self.state {
131+
State::NotEnded {
132+
current_split_index,
133+
..
134+
} => Some(current_split_index),
135+
State::Ended { .. } => None,
136+
}
137+
}
138+
139+
pub fn current_split_index_mut(&mut self) -> Option<&mut usize> {
140+
match &mut self.state {
141+
State::NotEnded {
142+
current_split_index,
143+
..
144+
} => Some(current_split_index),
145+
State::Ended { .. } => None,
146+
}
147+
}
148+
149+
pub fn current_split_index_overflowing(&self, run: &Run) -> usize {
150+
match self.state {
151+
State::NotEnded {
152+
current_split_index,
153+
..
154+
} => current_split_index,
155+
State::Ended { .. } => run.len(),
156+
}
157+
}
158+
159+
pub fn update_times(&self, run: &mut Run, timing_method: TimingMethod) {
160+
self.update_attempt_history(run);
161+
update_best_segments(run);
162+
update_pb_splits(run, timing_method);
163+
run.update_segment_history(self.current_split_index_overflowing(run));
164+
}
165+
166+
pub fn update_attempt_history(&self, run: &mut Run) {
167+
let (attempt_ended, time) = match self.state {
168+
State::NotEnded { .. } => (AtomicDateTime::now(), Time::new()),
169+
State::Ended { attempt_ended } => {
170+
(attempt_ended, run.segments().last().unwrap().split_time())
171+
}
172+
};
173+
174+
let pause_time = self.get_pause_time();
175+
176+
run.add_attempt(
177+
time,
178+
Some(self.attempt_started),
179+
Some(attempt_ended),
180+
pause_time,
181+
);
182+
}
183+
}
184+
185+
fn update_best_segments(run: &mut Run) {
186+
let mut previous_split_time_rta = Some(TimeSpan::zero());
187+
let mut previous_split_time_game_time = Some(TimeSpan::zero());
188+
189+
for split in run.segments_mut() {
190+
let mut new_best_segment = split.best_segment_time();
191+
if let Some(split_time) = split.split_time().real_time {
192+
let current_segment = previous_split_time_rta.map(|previous| split_time - previous);
193+
previous_split_time_rta = Some(split_time);
194+
if split
195+
.best_segment_time()
196+
.real_time
197+
.map_or(true, |b| current_segment.is_some_and(|c| c < b))
198+
{
199+
new_best_segment.real_time = current_segment;
200+
}
201+
}
202+
if let Some(split_time) = split.split_time().game_time {
203+
let current_segment =
204+
previous_split_time_game_time.map(|previous| split_time - previous);
205+
previous_split_time_game_time = Some(split_time);
206+
if split
207+
.best_segment_time()
208+
.game_time
209+
.map_or(true, |b| current_segment.is_some_and(|c| c < b))
210+
{
211+
new_best_segment.game_time = current_segment;
212+
}
213+
}
214+
split.set_best_segment_time(new_best_segment);
215+
}
216+
}
217+
218+
fn update_pb_splits(run: &mut Run, method: TimingMethod) {
219+
let (split_time, pb_split_time) = {
220+
let last_segment = run.segments().last().unwrap();
221+
(
222+
last_segment.split_time()[method],
223+
last_segment.personal_best_split_time()[method],
224+
)
225+
};
226+
if split_time.is_some_and(|s| pb_split_time.map_or(true, |pb| s < pb)) {
227+
super::set_run_as_pb(run);
228+
}
229+
}

0 commit comments

Comments
 (0)