forked from LiveSplit/livesplit-core
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmod.rs
944 lines (825 loc) · 32.5 KB
/
mod.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
//! The editor module provides an editor for Run objects. The editor ensures
//! that all the different invariants of the Run objects are upheld no matter
//! what kind of operations are being applied to the Run. It provides the
//! current state of the editor as state objects that can be visualized by any
//! kind of User Interface.
use super::{ComparisonError, ComparisonResult};
use crate::platform::prelude::*;
use crate::timing::ParseError as ParseTimeSpanError;
use crate::{
comparison,
settings::{CachedImageId, Image},
Run, Segment, Time, TimeSpan, TimingMethod,
};
use core::mem::swap;
use core::num::ParseIntError;
use odds::slice::rotate_left;
use snafu::{OptionExt, ResultExt};
pub mod cleaning;
mod fuzzy_list;
mod segment_row;
mod state;
#[cfg(test)]
mod tests;
pub use self::cleaning::SumOfBestCleaner;
pub use self::fuzzy_list::FuzzyList;
pub use self::segment_row::SegmentRow;
pub use self::state::{Buttons as ButtonsState, Segment as SegmentState, State};
/// Describes an Error that occurred while parsing a time.
#[derive(Debug, snafu::Snafu)]
pub enum ParseError {
/// Couldn't parse the time.
ParseTime {
/// The underlying error.
source: ParseTimeSpanError,
},
/// Negative times are not allowed here.
NegativeTimeNotAllowed,
/// Empty times are not allowed here.
EmptyTimeNotAllowed,
}
/// Describes an Error that occurred while opening the Run Editor.
#[derive(Debug, snafu::Snafu)]
pub enum OpenError {
/// The Run Editor couldn't be opened because an empty run with no
/// segments was provided.
EmptyRun,
}
/// Error type for a failed Rename.
#[derive(PartialEq, Debug, snafu::Snafu)]
pub enum RenameError {
/// Old Comparison was not found during rename.
OldNameNotFound,
/// Name was invalid.
InvalidName {
/// The underlying error.
source: ComparisonError,
},
}
/// The Run Editor allows modifying Runs while ensuring that all the different
/// invariants of the Run objects are upheld no matter what kind of operations
/// are being applied to the Run. It provides the current state of the editor as
/// state objects that can be visualized by any kind of User Interface.
pub struct Editor {
run: Run,
selected_method: TimingMethod,
selected_segments: Vec<usize>,
previous_personal_best_time: Time,
game_icon_id: CachedImageId,
segment_icon_ids: Vec<CachedImageId>,
segment_times: Vec<Option<TimeSpan>>,
}
impl Editor {
/// Creates a new Run Editor that modifies the Run provided. Creation of the
/// Run Editor fails when a Run with no segments is provided.
pub fn new(mut run: Run) -> Result<Self, OpenError> {
run.fix_splits();
let len = run.len();
if len == 0 {
return Err(OpenError::EmptyRun);
}
let personal_best_time = run.segments().last().unwrap().personal_best_split_time();
let mut editor = Self {
run,
selected_method: TimingMethod::RealTime,
selected_segments: vec![0],
previous_personal_best_time: personal_best_time,
game_icon_id: CachedImageId::default(),
segment_icon_ids: Vec::with_capacity(len),
segment_times: Vec::with_capacity(len),
};
editor.update_segment_list();
Ok(editor)
}
/// Closes the Run Editor and gives back access to the modified Run object.
/// In case you want to implement a Cancel Button, just drop the Run object
/// you get here.
pub fn close(self) -> Run {
self.run
}
/// Accesses the Run being edited by the Run Editor.
#[inline]
pub fn run(&self) -> &Run {
&self.run
}
/// Accesses the timing method that is currently selected for being
/// modified.
pub fn selected_timing_method(&self) -> TimingMethod {
self.selected_method
}
/// Selects a different timing method for being modified.
pub fn select_timing_method(&mut self, method: TimingMethod) {
self.selected_method = method;
self.update_segment_list();
}
fn active_segment_index(&self) -> usize {
*self.selected_segments.last().unwrap()
}
/// Grants mutable access to the actively selected segment row. You can
/// select multiple segment rows at the same time, but only the one that is
/// most recently selected is the active segment.
pub fn active_segment(&mut self) -> SegmentRow<'_> {
SegmentRow::new(self.active_segment_index(), self)
}
/// Unselects the segment with the given index. If it's not selected or the
/// index is out of bounds, nothing happens. The segment is not unselected,
/// when it is the only segment that is selected. If the active segment is
/// unselected, the most recently selected segment remaining becomes the
/// active segment.
pub fn unselect(&mut self, index: usize) {
self.selected_segments.retain(|&i| i != index);
if self.selected_segments.is_empty() {
self.selected_segments.push(index);
}
}
/// In addition to the segments that are already selected, the segment with
/// the given index is being selected. The segment chosen also becomes the
/// active segment.
///
/// # Panics
///
/// This panics if the index of the segment provided is out of bounds.
pub fn select_additionally(&mut self, index: usize) {
if index >= self.run.len() {
panic!("Index out of bounds for segment selection.");
}
self.selected_segments.retain(|&i| i != index);
self.selected_segments.push(index);
}
/// Selects the segment with the given index. All other segments are
/// unselected. The segment chosen also becomes the active segment.
///
/// # Panics
///
/// This panics if the index of the segment provided is out of bounds.
pub fn select_only(&mut self, index: usize) {
if index >= self.run.len() {
panic!("Index out of bounds for segment selection.");
}
self.selected_segments.clear();
self.selected_segments.push(index);
}
fn raise_run_edited(&mut self) {
self.run.mark_as_modified();
}
/// Accesses the name of the game.
pub fn game_name(&self) -> &str {
self.run.game_name()
}
/// Sets the name of the game.
pub fn set_game_name<S>(&mut self, name: S)
where
S: AsRef<str>,
{
self.run.set_game_name(name);
self.raise_run_edited();
self.run.clear_run_id();
}
/// Accesses the name of the category.
pub fn category_name(&self) -> &str {
self.run.category_name()
}
/// Sets the name of the category.
pub fn set_category_name<S>(&mut self, name: S)
where
S: AsRef<str>,
{
self.run.set_category_name(name);
self.raise_run_edited();
self.run.clear_run_id();
}
/// Accesses the timer offset. The timer offset specifies the time, the
/// timer starts at when starting a new attempt.
pub fn offset(&self) -> TimeSpan {
self.run.offset()
}
/// Sets the timer offset. The timer offset specifies the time, the timer
/// starts at when starting a new attempt.
pub fn set_offset(&mut self, offset: TimeSpan) {
self.run.set_offset(offset);
self.raise_run_edited();
}
/// Parses and sets the timer offset from the string provided. The timer
/// offset specifies the time, the timer starts at when starting a new
/// attempt.
pub fn parse_and_set_offset<S>(&mut self, offset: S) -> Result<(), ParseError>
where
S: AsRef<str>,
{
self.set_offset(offset.as_ref().parse().context(ParseTime)?);
Ok(())
}
/// Accesses the attempt count.
pub fn attempt_count(&self) -> u32 {
self.run.attempt_count()
}
/// Sets the attempt count. Changing this has no affect on the attempt
/// history or the segment history. This number is mostly just a visual
/// number for the runner.
pub fn set_attempt_count(&mut self, attempts: u32) {
self.run.set_attempt_count(attempts);
self.raise_run_edited();
}
/// Parses and sets the attempt count from the string provided. Changing
/// this has no affect on the attempt history or the segment history. This
/// number is mostly just a visual number for the runner.
pub fn parse_and_set_attempt_count<S>(&mut self, attempts: S) -> Result<(), ParseIntError>
where
S: AsRef<str>,
{
self.set_attempt_count(attempts.as_ref().parse()?);
Ok(())
}
/// Accesses the game's icon.
pub fn game_icon(&self) -> &Image {
self.run.game_icon()
}
/// Sets the game's icon.
pub fn set_game_icon<D: Into<Image>>(&mut self, image: D) {
self.run.set_game_icon(image);
self.raise_run_edited();
}
/// Removes the game's icon.
pub fn remove_game_icon(&mut self) {
self.run.set_game_icon(&[]);
self.raise_run_edited();
}
/// Accesses all the custom comparisons that exist on the Run.
pub fn custom_comparisons(&self) -> &[String] {
self.run.custom_comparisons()
}
/// Sets the speedrun.com Run ID of the run. You need to ensure that the
/// record on speedrun.com matches up with the Personal Best of this run.
/// This may be empty if there's no association.
pub fn set_run_id<S>(&mut self, id: S)
where
S: AsRef<str>,
{
self.run.metadata_mut().set_run_id(id);
self.raise_run_edited();
}
/// Sets the name of the region this game is from. This may be empty if it's
/// not specified.
pub fn set_region_name<S>(&mut self, name: S)
where
S: AsRef<str>,
{
self.run.metadata_mut().set_region_name(name);
self.metadata_modified();
}
/// Sets the name of the platform this game is run on. This may be empty if
/// it's not specified.
pub fn set_platform_name<S>(&mut self, name: S)
where
S: AsRef<str>,
{
self.run.metadata_mut().set_platform_name(name);
self.metadata_modified();
}
/// Specifies whether this speedrun is done on an emulator. Keep in mind
/// that `false` may also mean that this information is simply not known.
pub fn set_emulator_usage(&mut self, uses_emulator: bool) {
self.run.metadata_mut().set_emulator_usage(uses_emulator);
self.metadata_modified();
}
/// Sets the speedrun.com variable with the name specified to the value
/// specified. A variable is an arbitrary key value pair storing additional
/// information about the category. An example of this may be whether
/// Amiibos are used in this category. If the variable doesn't exist yet, it
/// is being inserted.
pub fn set_speedrun_com_variable<N, V>(&mut self, name: N, value: V)
where
N: Into<String>,
V: Into<String>,
{
self.run
.metadata_mut()
.set_speedrun_com_variable(name, value);
self.metadata_modified();
}
/// Removes the speedrun.com variable with the name specified.
pub fn remove_speedrun_com_variable<S>(&mut self, name: S)
where
S: AsRef<str>,
{
self.run.metadata_mut().remove_speedrun_com_variable(name);
self.metadata_modified();
}
/// Adds a new permanent custom variable. If there's a temporary variable
/// with the same name, it gets turned into a permanent variable and its
/// value stays. If a permanent variable with the name already exists,
/// nothing happens.
pub fn add_custom_variable<N>(&mut self, name: N)
where
N: Into<String>,
{
self.run
.metadata_mut()
.custom_variable_mut(name)
.permanent();
self.raise_run_edited();
}
/// Sets the value of a custom variable with the name specified. If the
/// custom variable does not exist, or is not a permanent variable, nothing
/// happens.
pub fn set_custom_variable<N, V>(&mut self, name: N, value: V)
where
N: Into<String>,
V: AsRef<str>,
{
let name = name.into();
if self.run.metadata().custom_variable(&name).is_none() {
return;
}
let variable = self.run.metadata_mut().custom_variable_mut(name);
if variable.is_permanent {
variable.value.clear();
variable.value.push_str(value.as_ref());
self.raise_run_edited();
}
}
/// Removes the custom variable with the name specified. If the custom
/// variable does not exist, or is not a permanent variable, nothing
/// happens.
pub fn remove_custom_variable<N>(&mut self, name: N)
where
N: AsRef<str>,
{
let name = name.as_ref();
if let Some(variable) = self.run.metadata().custom_variable(name) {
if variable.is_permanent {
self.run.metadata_mut().remove_custom_variable(name);
self.raise_run_edited();
}
}
}
/// Resets all the Metadata Information.
pub fn clear_metadata(&mut self) {
self.run.metadata_mut().clear();
self.raise_run_edited();
}
fn metadata_modified(&mut self) {
self.run.clear_run_id();
self.raise_run_edited();
}
fn times_modified(&mut self) {
let pb_split_time = self
.run
.segments()
.last()
.unwrap()
.personal_best_split_time();
if pb_split_time != self.previous_personal_best_time {
self.run.clear_run_id();
self.previous_personal_best_time = pb_split_time;
}
self.raise_run_edited();
}
fn fix(&mut self) {
self.run.fix_splits();
self.update_segment_list();
self.raise_run_edited();
}
fn update_segment_list(&mut self) {
let method = self.selected_method;
let mut previous_time = Some(TimeSpan::zero());
self.segment_times.clear();
for segment in self.run.segments() {
let split_time = segment.personal_best_split_time()[method];
self.segment_times
.push(catch! { split_time? - previous_time? });
if split_time.is_some() {
previous_time = split_time;
}
}
}
fn fix_splits_from_segments(&mut self) {
let method = self.selected_method;
let mut previous_time = Some(TimeSpan::zero());
for (segment_time, segment) in self
.segment_times
.iter_mut()
.zip(self.run.segments_mut().iter_mut())
{
{
let time = segment.personal_best_split_time_mut();
time[method] = catch! { previous_time? + (*segment_time)? };
}
if segment_time.is_some() {
previous_time = segment.personal_best_split_time()[method];
}
}
}
/// Inserts a new empty segment above the active segment and adjusts the
/// Run's history information accordingly. The newly created segment is then
/// the only selected segment and also the active segment.
pub fn insert_segment_above(&mut self) {
let active_segment = self.active_segment_index();
let mut segment = Segment::new("");
self.run.import_best_segment(active_segment);
let max_index = self.run.max_attempt_history_index().unwrap_or(0);
let min_index = self.run.min_segment_history_index().unwrap();
for x in min_index..=max_index {
segment.segment_history_mut().insert(x, Default::default());
}
self.run.segments_mut().insert(active_segment, segment);
self.select_only(active_segment);
self.times_modified();
self.fix();
}
/// Inserts a new empty segment below the active segment and adjusts the
/// Run's history information accordingly. The newly created segment is then
/// the only selected segment and also the active segment.
pub fn insert_segment_below(&mut self) {
let active_segment = self.active_segment_index();
let next_segment = active_segment + 1;
let mut segment = Segment::new("");
if next_segment < self.run.len() {
self.run.import_best_segment(next_segment);
}
let max_index = self.run.max_attempt_history_index().unwrap_or(0);
let min_index = self.run.min_segment_history_index().unwrap();
for x in min_index..=max_index {
segment.segment_history_mut().insert(x, Default::default());
}
self.run.segments_mut().insert(next_segment, segment);
self.select_only(next_segment);
self.times_modified();
self.fix();
}
fn fix_after_deletion(&mut self, index: usize) {
self.fix_with_timing_method(index, TimingMethod::RealTime);
self.fix_with_timing_method(index, TimingMethod::GameTime);
}
fn fix_with_timing_method(&mut self, index: usize, method: TimingMethod) {
let current_index = index + 1;
if current_index >= self.run.len() {
return;
}
let max_index = self.run.max_attempt_history_index().unwrap_or(0);
let min_index = self.run.min_segment_history_index().unwrap();
for run_index in min_index..=max_index {
// If a history element isn't there in the segment that's deleted
// remove it from the next segment's history as well
if let Some(segment_history_element) =
self.run.segment(index).segment_history().get(run_index)
{
let current_segment = segment_history_element[method];
if let Some(current_segment) = current_segment {
for current_index in current_index..self.run.len() {
// Add the removed segment's history times to the next
// non None times
if let Some(Some(segment)) = self
.run
.segment_mut(current_index)
.segment_history_mut()
.get_mut(run_index)
.map(|t| &mut t[method])
{
*segment += current_segment;
break;
}
}
}
} else {
self.run
.segment_mut(current_index)
.segment_history_mut()
.remove(run_index);
}
}
// Set the new Best Segment time to be the sum of the two Best Segments
let min_best_segment = catch! {
self.run.segment(index).best_segment_time()[method]?
+ self.run.segment(current_index).best_segment_time()[method]?
};
if let Some(mut min_best_segment) = min_best_segment {
// Use any element in the history that has a lower time than this
// sum
for time in self
.run
.segment(current_index)
.segment_history()
.iter()
.filter_map(|&(_, t)| t[method])
{
if time < min_best_segment {
min_best_segment = time;
}
}
self.run.segment_mut(current_index).best_segment_time_mut()[method] =
Some(min_best_segment);
}
}
/// Checks if the currently selected segments can be removed. If all
/// segments are selected, they can't be removed.
pub fn can_remove_segments(&self) -> bool {
self.run.len() > self.selected_segments.len()
}
/// Removes all the selected segments, unless all of them are selected. The
/// run's information is automatically adjusted properly. The next
/// not-to-be-removed segment after the active segment becomes the new
/// active segment. If there's none, then the next not-to-be-removed segment
/// before the active segment, becomes the new active segment.
pub fn remove_segments(&mut self) {
if !self.can_remove_segments() {
return;
}
let mut removed = 0;
for i in 0..self.run.len() {
if self.selected_segments.contains(&i) {
let segment_index = i - removed;
self.fix_after_deletion(segment_index);
self.run.segments_mut().remove(segment_index);
removed += 1;
}
}
let selected_segment = self.active_segment_index();
let above_count = self
.selected_segments
.iter()
.filter(|&&i| i < selected_segment)
.count();
let mut new_index = selected_segment - above_count;
if new_index >= self.run.len() {
new_index = self.run.len() - 1;
}
self.selected_segments.clear();
self.selected_segments.push(new_index);
self.times_modified();
self.fix();
}
fn switch_segments(&mut self, index: usize) {
let max_index = self.run.max_attempt_history_index().unwrap_or(0);
let min_index = self.run.min_segment_history_index().unwrap();
// Use split_at to prove that the 3 segments are distinct
let (a, b) = self.run.segments_mut().split_at_mut(index);
let previous = a.last();
let (a, b) = b.split_at_mut(1);
let first = &mut a[0];
let second = &mut b[0];
for run_index in min_index..=max_index {
// Remove both segment history elements if one of them has a None
// time and the other has has a non None time
let first_history = first.segment_history().get(run_index);
let second_history = second.segment_history().get(run_index);
if let (Some(first_history), Some(second_history)) = (first_history, second_history) {
if first_history.real_time.is_some() != second_history.real_time.is_some()
|| first_history.game_time.is_some() != second_history.game_time.is_some()
{
first.segment_history_mut().remove(run_index);
second.segment_history_mut().remove(run_index);
}
}
}
for (comparison, first_time) in first.comparisons_mut().iter_mut() {
// Fix the comparison times based on the new positions of the two
// segments
let previous_time = previous
.map(|p| p.comparison(comparison))
.unwrap_or_else(Time::zero);
let second_time = second.comparison_mut(comparison);
let first_segment_time = *first_time - previous_time;
let second_segment_time = *second_time - *first_time;
*second_time = previous_time + second_segment_time;
*first_time = *second_time + first_segment_time;
}
swap(first, second);
}
/// Checks if the currently selected segments can be moved up. If any one of
/// the selected segments is the first segment, then they can't be moved.
pub fn can_move_segments_up(&self) -> bool {
!self.selected_segments.iter().any(|&s| s == 0)
}
/// Moves all the selected segments up, unless the first segment is
/// selected. The run's information is automatically adjusted properly. The
/// active segment stays the active segment.
pub fn move_segments_up(&mut self) {
if !self.can_move_segments_up() {
return;
}
for i in 0..self.run.len() - 1 {
if self.selected_segments.contains(&(i + 1)) {
self.switch_segments(i);
}
}
for segment in &mut self.selected_segments {
*segment = segment.saturating_sub(1);
}
self.times_modified();
self.fix();
}
/// Checks if the currently selected segments can be moved down. If any one
/// of the selected segments is the last segment, then they can't be moved.
pub fn can_move_segments_down(&self) -> bool {
let last_index = self.run.len() - 1;
!self.selected_segments.iter().any(|&s| s == last_index)
}
/// Moves all the selected segments down, unless the last segment is
/// selected. The run's information is automatically adjusted properly. The
/// active segment stays the active segment.
pub fn move_segments_down(&mut self) {
if !self.can_move_segments_down() {
return;
}
for i in (0..self.run.len() - 1).rev() {
if self.selected_segments.contains(&i) {
self.switch_segments(i);
}
}
for segment in &mut self.selected_segments {
if *segment < self.run.len() - 1 {
*segment += 1;
}
}
self.times_modified();
self.fix();
}
/// Adds a new custom comparison. It can't be added if it starts with
/// `[Race]` or already exists.
pub fn add_comparison<S: Into<String>>(&mut self, comparison: S) -> ComparisonResult<()> {
let comparison = comparison.into();
self.run.add_custom_comparison(comparison)?;
self.fix();
Ok(())
}
/// Imports the Personal Best from the provided run as a comparison. The
/// comparison can't be added if its name starts with `[Race]` or it already
/// exists.
pub fn import_comparison<S: Into<String>>(
&mut self,
run: &Run,
comparison: S,
) -> ComparisonResult<()> {
let comparison = comparison.into();
self.run.add_custom_comparison(comparison.as_str())?;
let mut remaining_segments = self.run.segments_mut().as_mut_slice();
for segment in run.segments().iter().take(run.len().saturating_sub(1)) {
if let Some((segment_index, my_segment)) = remaining_segments
.iter_mut()
.enumerate()
.find(|(_, s)| unicase::eq(segment.name(), s.name()))
{
*my_segment.comparison_mut(&comparison) = segment.personal_best_split_time();
remaining_segments = &mut remaining_segments[segment_index + 1..];
}
}
if let (Some(my_segment), Some(segment)) =
(self.run.segments_mut().last_mut(), run.segments().last())
{
*my_segment.comparison_mut(&comparison) = segment.personal_best_split_time();
}
self.fix();
Ok(())
}
/// Removes the chosen custom comparison. You can't remove a Comparison
/// Generator's Comparison or the Personal Best.
pub fn remove_comparison(&mut self, comparison: &str) {
if comparison == comparison::personal_best::NAME {
return;
}
self.run
.custom_comparisons_mut()
.retain(|c| c != comparison);
if self.run.comparisons().any(|c| c == comparison) {
return;
}
for segment in self.run.segments_mut() {
segment.comparisons_mut().remove(comparison);
}
self.fix();
}
/// Renames a comparison. The comparison can't be renamed if the new name of
/// the comparison starts with `[Race]` or it already exists.
pub fn rename_comparison(&mut self, old: &str, new: &str) -> Result<(), RenameError> {
if old == new {
return Ok(());
}
self.run
.validate_comparison_name(new)
.context(InvalidName)?;
{
let comparison_name = self
.run
.custom_comparisons_mut()
.iter_mut()
.find(|c| *c == old)
.context(OldNameNotFound)?;
comparison_name.clear();
comparison_name.push_str(new);
}
for segment in self.run.segments_mut() {
if let Some(time) = segment.comparisons_mut().remove(old) {
*segment.comparison_mut(new) = time;
}
}
self.fix();
Ok(())
}
/// Reorders the custom comparisons by moving the comparison with the
/// `src_index` specified to the `dst_index` specified. Returns `Err(())` if
/// one of the indices is invalid. The indices are based on the
/// `comparison_names` field of the Run Editor's `State`.
pub fn move_comparison(&mut self, src_index: usize, dst_index: usize) -> Result<(), ()> {
let comparisons = self.run.custom_comparisons_mut();
let (src_index, dst_index) = (src_index + 1, dst_index + 1);
if src_index >= comparisons.len() || dst_index >= comparisons.len() {
return Err(());
}
if src_index == dst_index {
return Ok(());
}
if src_index > dst_index {
rotate_left(
&mut comparisons[dst_index..=src_index],
src_index - dst_index,
);
} else {
rotate_left(&mut comparisons[src_index..=dst_index], 1);
}
self.raise_run_edited();
Ok(())
}
/// Generates a custom goal comparison based on the goal time provided. The
/// comparison's times are automatically balanced based on the runner's
/// history such that it roughly represents what split times for the goal
/// time would roughly look like. Since it is populated by the runner's
/// history, only goal times within the sum of the best segments and the sum
/// of the worst segments are supported. Everything else is automatically
/// capped by that range. The comparison is only populated for the selected
/// timing method. The other timing method's comparison times are not
/// modified by this, so you can call this again with the other timing
/// method to generate the comparison times for both timing methods.
pub fn generate_goal_comparison(&mut self, time: TimeSpan) {
if !self
.run
.custom_comparisons()
.iter()
.any(|c| c == comparison::goal::NAME)
{
self.run
.custom_comparisons_mut()
.push(String::from(comparison::goal::NAME));
}
comparison::goal::generate_for_timing_method(
&mut self.run.segments_mut(),
self.selected_method,
time,
comparison::goal::NAME,
);
self.raise_run_edited();
}
/// Parses a goal time and generates a custom goal comparison based on the
/// parsed value. The comparison's times are automatically balanced based on
/// the runner's history such that it roughly represents what split times
/// for the goal time would roughly look like. Since it is populated by the
/// runner's history, only goal times within the sum of the best segments
/// and the sum of the worst segments are supported. Everything else is
/// automatically capped by that range. The comparison is only populated for
/// the selected timing method. The other timing method's comparison times
/// are not modified by this, so you can call this again with the other
/// timing method to generate the comparison times for both timing methods.
pub fn parse_and_generate_goal_comparison<S>(&mut self, time: S) -> Result<(), ParseError>
where
S: AsRef<str>,
{
self.generate_goal_comparison(
parse_positive(time)?.ok_or(ParseError::EmptyTimeNotAllowed)?,
);
Ok(())
}
/// Clears out the Attempt History and the Segment Histories of all the
/// segments.
pub fn clear_history(&mut self) {
self.run.clear_history();
self.fix();
}
/// Clears out the Attempt History, the Segment Histories, all the times,
/// sets the Attempt Count to 0 and clears the speedrun.com run id
/// association. All Custom Comparisons other than `Personal Best` are
/// deleted as well.
pub fn clear_times(&mut self) {
self.run.clear_times();
self.fix();
}
/// Creates a Sum of Best Cleaner which allows you to interactively remove
/// potential issues in the segment history that lead to an inaccurate Sum
/// of Best. If you skip a split, whenever you will do the next split, the
/// combined segment time might be faster than the sum of the individual
/// best segments. The Sum of Best Cleaner will point out all of these and
/// allows you to delete them individually if any of them seem wrong.
pub fn clean_sum_of_best(&mut self) -> SumOfBestCleaner<'_> {
SumOfBestCleaner::new(&mut self.run)
}
}
fn parse_positive<S>(time: S) -> Result<Option<TimeSpan>, ParseError>
where
S: AsRef<str>,
{
let time = TimeSpan::parse_opt(time).context(ParseTime)?;
if time.map_or(false, |t| t < TimeSpan::zero()) {
Err(ParseError::NegativeTimeNotAllowed)
} else {
Ok(time)
}
}