Skip to content

Commit 9cf1c3e

Browse files
committed
Port the Sum of Best Cleaning Algorithm
1 parent c45fb56 commit 9cf1c3e

File tree

6 files changed

+309
-1
lines changed

6 files changed

+309
-1
lines changed

capi/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub mod title_component;
5757
pub mod total_playtime_component_state;
5858
pub mod total_playtime_component;
5959
pub mod run_editor;
60+
pub mod sum_of_best_cleaner;
6061
pub mod shared_timer;
6162
pub mod timer_read_lock;
6263
pub mod timer_write_lock;

capi/src/run_editor.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use livesplit_core::{Run, RunEditor, TimingMethod};
22
use super::{acc, acc_mut, alloc, output_vec, own, str, Json};
33
use run::OwnedRun;
4+
use sum_of_best_cleaner::OwnedSumOfBestCleaner;
45
use libc::c_char;
56
use std::{ptr, slice};
67

@@ -230,3 +231,10 @@ pub unsafe extern "C" fn RunEditor_clear_history(this: *mut RunEditor) {
230231
pub unsafe extern "C" fn RunEditor_clear_times(this: *mut RunEditor) {
231232
acc_mut(this).clear_times();
232233
}
234+
235+
#[no_mangle]
236+
pub unsafe extern "C" fn RunEditor_clean_sum_of_best(
237+
this: *mut RunEditor,
238+
) -> OwnedSumOfBestCleaner {
239+
alloc(acc_mut(this).clean_sum_of_best())
240+
}

capi/src/sum_of_best_cleaner.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use livesplit_core::run::editor::cleaning::{PotentialCleanUp, SumOfBestCleaner};
2+
use super::{acc, acc_mut, alloc, output_str_with, own, own_drop};
3+
use libc::c_char;
4+
use std::fmt::Write;
5+
use std::ptr;
6+
7+
pub type OwnedSumOfBestCleaner = *mut SumOfBestCleaner<'static>;
8+
pub type OwnedPotentialCleanUp = *mut PotentialCleanUp<'static>;
9+
pub type NullableOwnedPotentialCleanUp = OwnedPotentialCleanUp;
10+
11+
#[no_mangle]
12+
pub unsafe extern "C" fn SumOfBestCleaner_drop(this: OwnedSumOfBestCleaner) {
13+
own_drop(this);
14+
}
15+
16+
#[no_mangle]
17+
pub unsafe extern "C" fn SumOfBestCleaner_next_potential_clean_up(
18+
this: *mut SumOfBestCleaner<'static>,
19+
) -> NullableOwnedPotentialCleanUp {
20+
acc_mut(this)
21+
.next_potential_clean_up()
22+
.map_or_else(ptr::null_mut, alloc)
23+
}
24+
25+
#[no_mangle]
26+
pub unsafe extern "C" fn SumOfBestCleaner_apply(
27+
this: *mut SumOfBestCleaner<'static>,
28+
clean_up: OwnedPotentialCleanUp,
29+
) {
30+
acc_mut(this).apply(own(clean_up).into());
31+
}
32+
33+
#[no_mangle]
34+
pub unsafe extern "C" fn PotentialCleanUp_drop(this: OwnedPotentialCleanUp) {
35+
own_drop(this);
36+
}
37+
38+
#[no_mangle]
39+
pub unsafe extern "C" fn PotentialCleanUp_message(
40+
this: *const PotentialCleanUp<'static>,
41+
) -> *const c_char {
42+
output_str_with(|s| write!(s, "{}", acc(this)).unwrap())
43+
}

src/analysis/sum_of_segments/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ fn track_personal_best_run(
9191
(0, Time::default())
9292
}
9393

94-
fn track_branch(
94+
pub fn track_branch(
9595
segments: &[Segment],
9696
current_time: Option<TimeSpan>,
9797
segment_index: usize,

src/run/editor/cleaning.rs

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
use {Attempt, Run, Segment, TimeSpan, TimingMethod};
2+
use analysis::sum_of_segments::{best, track_branch};
3+
use std::mem::replace;
4+
use std::fmt;
5+
use time::formatter::{Short, TimeFormatter};
6+
use chrono::Local;
7+
8+
pub struct SumOfBestCleaner<'r> {
9+
run: &'r mut Run,
10+
predictions: Vec<Option<TimeSpan>>,
11+
state: State,
12+
}
13+
14+
enum State {
15+
Poisoned,
16+
Done,
17+
WithTimingMethod(TimingMethod),
18+
IteratingRun(IteratingRunState),
19+
IteratingHistory(IteratingHistoryState),
20+
}
21+
22+
struct IteratingRunState {
23+
method: TimingMethod,
24+
segment_index: usize,
25+
}
26+
27+
struct IteratingHistoryState {
28+
parent: IteratingRunState,
29+
current_time: Option<TimeSpan>,
30+
skip_count: usize,
31+
}
32+
33+
pub struct PotentialCleanUp<'r> {
34+
starting_segment: Option<&'r Segment>,
35+
ending_segment: &'r Segment,
36+
time_between: TimeSpan,
37+
combined_sum_of_best: Option<TimeSpan>,
38+
attempt: &'r Attempt,
39+
method: TimingMethod,
40+
clean_up: CleanUp,
41+
}
42+
43+
pub struct CleanUp {
44+
ending_index: usize,
45+
run_index: i32,
46+
}
47+
48+
impl<'r> fmt::Display for PotentialCleanUp<'r> {
49+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50+
let short = Short::new();
51+
52+
let method = match self.method {
53+
TimingMethod::RealTime => "Real Time",
54+
TimingMethod::GameTime => "Game Time",
55+
};
56+
57+
write!(
58+
f,
59+
"You had a {} segment time of {} between ",
60+
method,
61+
short.format(self.time_between)
62+
)?;
63+
64+
if let Some(starting_segment) = self.starting_segment {
65+
write!(f, "{}", starting_segment.name())?;
66+
} else {
67+
write!(f, "the start of the run")?;
68+
}
69+
70+
write!(f, " and {}", self.ending_segment.name())?;
71+
72+
if let Some(combined) = self.combined_sum_of_best {
73+
write!(
74+
f,
75+
", which is faster than the Combined Best Segments of {}",
76+
short.format(combined)
77+
)?;
78+
}
79+
80+
if let Some(ended) = self.attempt.ended() {
81+
write!(
82+
f,
83+
" in a run on {}",
84+
ended.time.with_timezone(&Local).format("%F")
85+
)?;
86+
}
87+
88+
write!(
89+
f,
90+
". Do you think that this segment time is inaccurate and should be removed?"
91+
)
92+
}
93+
}
94+
95+
impl<'a> From<PotentialCleanUp<'a>> for CleanUp {
96+
fn from(potential: PotentialCleanUp) -> Self {
97+
potential.clean_up
98+
}
99+
}
100+
101+
impl<'r> SumOfBestCleaner<'r> {
102+
pub fn new(run: &'r mut Run) -> Self {
103+
let predictions = Vec::with_capacity(run.len() + 1);
104+
Self {
105+
run,
106+
predictions,
107+
state: State::WithTimingMethod(TimingMethod::RealTime),
108+
}
109+
}
110+
111+
pub fn apply(&mut self, clean_up: CleanUp) {
112+
self.run
113+
.segment_mut(clean_up.ending_index)
114+
.segment_history_mut()
115+
.remove(clean_up.run_index);
116+
117+
self.run.mark_as_changed();
118+
}
119+
120+
pub fn next_potential_clean_up(&mut self) -> Option<PotentialCleanUp> {
121+
loop {
122+
match replace(&mut self.state, State::Poisoned) {
123+
State::Poisoned => unreachable!(),
124+
State::Done => return None,
125+
State::WithTimingMethod(method) => {
126+
next_timing_method(&self.run, &mut self.predictions, method);
127+
self.state = State::IteratingRun(IteratingRunState {
128+
method: method,
129+
segment_index: 0,
130+
});
131+
}
132+
State::IteratingRun(state) => {
133+
self.state = if state.segment_index < self.run.len() {
134+
let current_time = self.predictions[state.segment_index];
135+
State::IteratingHistory(IteratingHistoryState {
136+
parent: state,
137+
current_time,
138+
skip_count: 0,
139+
})
140+
} else if state.method == TimingMethod::RealTime {
141+
State::WithTimingMethod(TimingMethod::GameTime)
142+
} else {
143+
State::Done
144+
};
145+
}
146+
State::IteratingHistory(state) => {
147+
let iter = self.run
148+
.segment(state.parent.segment_index)
149+
.segment_history()
150+
.iter()
151+
.enumerate()
152+
.skip(state.skip_count);
153+
154+
for (skip_count, &(run_index, time)) in iter {
155+
if time[state.parent.method].is_none() {
156+
let (prediction_index, prediction_time) = track_branch(
157+
self.run.segments(),
158+
state.current_time,
159+
state.parent.segment_index + 1,
160+
run_index,
161+
state.parent.method,
162+
);
163+
if prediction_index > 0 {
164+
if let Some(question) = check_prediction(
165+
&self.run,
166+
&self.predictions,
167+
prediction_time[state.parent.method],
168+
state.parent.segment_index as isize - 1,
169+
prediction_index - 1,
170+
run_index,
171+
state.parent.method,
172+
) {
173+
self.state = State::IteratingHistory(IteratingHistoryState {
174+
skip_count: skip_count + 1,
175+
..state
176+
});
177+
return Some(question);
178+
}
179+
}
180+
}
181+
}
182+
self.state = State::IteratingRun(IteratingRunState {
183+
method: state.parent.method,
184+
segment_index: state.parent.segment_index + 1,
185+
});
186+
}
187+
};
188+
}
189+
}
190+
}
191+
192+
fn check_prediction<'a>(
193+
run: &'a Run,
194+
predictions: &[Option<TimeSpan>],
195+
predicted_time: Option<TimeSpan>,
196+
starting_index: isize,
197+
ending_index: usize,
198+
run_index: i32,
199+
method: TimingMethod,
200+
) -> Option<PotentialCleanUp<'a>> {
201+
if let Some(predicted_time) = predicted_time {
202+
if predictions[ending_index + 1].map_or(true, |t| predicted_time < t) {
203+
if let Some(segment_history_element) =
204+
run.segment(ending_index).segment_history().get(run_index)
205+
{
206+
return Some(PotentialCleanUp {
207+
starting_segment: if starting_index >= 0 {
208+
Some(run.segment(starting_index as usize))
209+
} else {
210+
None
211+
},
212+
ending_segment: run.segment(ending_index),
213+
time_between: segment_history_element[method]
214+
.expect("Cleanup path is shorter but doesn't have a time"),
215+
combined_sum_of_best: predictions[ending_index + 1].map(|t| {
216+
t -
217+
predictions[(starting_index + 1) as usize]
218+
.expect("Start time must not be empty")
219+
}),
220+
attempt: run.attempt_history()
221+
.iter()
222+
.find(|attempt| attempt.index() == run_index)
223+
.expect("The attempt has to exist"),
224+
method,
225+
clean_up: CleanUp {
226+
ending_index,
227+
run_index,
228+
},
229+
});
230+
}
231+
}
232+
}
233+
None
234+
}
235+
236+
fn next_timing_method(run: &Run, predictions: &mut Vec<Option<TimeSpan>>, method: TimingMethod) {
237+
let segments = run.segments();
238+
239+
predictions.clear();
240+
predictions.resize(segments.len() + 1, None);
241+
best::calculate(
242+
segments,
243+
0,
244+
segments.len(),
245+
predictions,
246+
true,
247+
false,
248+
method,
249+
);
250+
}

src/run/editor/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ use std::mem::swap;
33
use {unicase, Image, Run, Segment, Time, TimeSpan, TimingMethod};
44
use time::ParseError as ParseTimeSpanError;
55

6+
pub mod cleaning;
67
mod segment_row;
78
mod state;
89
#[cfg(test)]
910
mod tests;
1011

1112
pub use self::segment_row::SegmentRow;
1213
pub use self::state::{Buttons as ButtonsState, Segment as SegmentState, State};
14+
pub use self::cleaning::SumOfBestCleaner;
1315

1416
quick_error! {
1517
#[derive(Debug)]
@@ -563,6 +565,10 @@ impl Editor {
563565
self.run.clear_times();
564566
self.fix();
565567
}
568+
569+
pub fn clean_sum_of_best(&mut self) -> SumOfBestCleaner {
570+
SumOfBestCleaner::new(&mut self.run)
571+
}
566572
}
567573

568574
fn validate_comparison_name(run: &Run, comparison: &str) -> bool {

0 commit comments

Comments
 (0)