livesplit_core/run/
mod.rs

1//! The run module provides everything necessary for working with a `Run`, like parsing and saving or editing them.
2//!
3//! # Examples
4//!
5//! ```
6//! use livesplit_core::run::{Run, Segment};
7//!
8//! let mut run = Run::new();
9//!
10//! run.set_game_name("Super Mario Odyssey");
11//! run.set_category_name("Darker Side");
12//!
13//! run.push_segment(Segment::new("Cap Kingdom"));
14//! run.push_segment(Segment::new("Cascade Kingdom"));
15//! ```
16
17mod attempt;
18mod comparisons;
19pub mod editor;
20pub mod parser;
21mod run_metadata;
22pub mod saver;
23mod segment;
24mod segment_history;
25
26#[cfg(test)]
27mod tests;
28
29pub use attempt::Attempt;
30pub use comparisons::Comparisons;
31pub use editor::{Editor, RenameError};
32pub use run_metadata::{CustomVariable, RunMetadata};
33pub use segment::Segment;
34pub use segment_history::SegmentHistory;
35
36use crate::{
37    comparison::{default_generators, personal_best, ComparisonGenerator, RACE_COMPARISON_PREFIX},
38    platform::prelude::*,
39    settings::Image,
40    util::PopulateString,
41    AtomicDateTime, Time, TimeSpan, TimingMethod,
42};
43use alloc::borrow::Cow;
44use core::{cmp::max, fmt};
45use hashbrown::HashSet;
46
47/// A Run stores the split times for a specific game and category of a runner.
48///
49/// # Examples
50///
51/// ```
52/// use livesplit_core::{Run, Segment};
53///
54/// let mut run = Run::new();
55///
56/// run.set_game_name("Super Mario Odyssey");
57/// run.set_category_name("Darker Side");
58///
59/// run.push_segment(Segment::new("Cap Kingdom"));
60/// run.push_segment(Segment::new("Cascade Kingdom"));
61/// ```
62#[derive(Clone, Debug, PartialEq)]
63pub struct Run {
64    game_icon: Image,
65    game_name: String,
66    category_name: String,
67    offset: TimeSpan,
68    attempt_count: u32,
69    attempt_history: Vec<Attempt>,
70    metadata: RunMetadata,
71    has_been_modified: bool,
72    segments: Vec<Segment>,
73    custom_comparisons: Vec<String>,
74    comparison_generators: ComparisonGenerators,
75    auto_splitter_settings: String,
76}
77
78#[derive(Clone, Debug)]
79struct ComparisonGenerators(Vec<Box<dyn ComparisonGenerator>>);
80
81impl PartialEq for ComparisonGenerators {
82    fn eq(&self, other: &ComparisonGenerators) -> bool {
83        self.0
84            .iter()
85            .map(|c| c.name())
86            .eq(other.0.iter().map(|c| c.name()))
87    }
88}
89
90/// Error type for an invalid comparison name
91#[derive(PartialEq, Eq, Debug, snafu::Snafu)]
92pub enum ComparisonError {
93    /// Comparison name starts with "\[Race\]".
94    NameStartsWithRace,
95    /// Comparison name is a duplicate.
96    DuplicateName,
97}
98
99/// Result type for an invalid comparison name
100pub type ComparisonResult<T> = Result<T, ComparisonError>;
101
102impl Run {
103    /// Creates a new Run object with no segments.
104    #[inline]
105    pub fn new() -> Self {
106        Self {
107            game_icon: Image::default(),
108            game_name: String::new(),
109            category_name: String::new(),
110            offset: TimeSpan::zero(),
111            attempt_count: 0,
112            attempt_history: Vec::new(),
113            metadata: RunMetadata::new(),
114            has_been_modified: false,
115            segments: Vec::new(),
116            custom_comparisons: vec![personal_best::NAME.to_string()],
117            comparison_generators: ComparisonGenerators(default_generators()),
118            auto_splitter_settings: String::new(),
119        }
120    }
121
122    /// Accesses the name of the game this Run is for.
123    #[inline]
124    pub fn game_name(&self) -> &str {
125        &self.game_name
126    }
127
128    /// Sets the name of the game this Run is for.
129    #[inline]
130    pub fn set_game_name<S>(&mut self, name: S)
131    where
132        S: PopulateString,
133    {
134        name.populate(&mut self.game_name);
135    }
136
137    /// Accesses the game's icon.
138    #[inline]
139    pub const fn game_icon(&self) -> &Image {
140        &self.game_icon
141    }
142
143    /// Sets the game's icon.
144    #[inline]
145    pub fn set_game_icon<D: Into<Image>>(&mut self, image: D) {
146        self.game_icon = image.into();
147    }
148
149    /// Accesses the name of the category this Run is for.
150    #[inline]
151    pub fn category_name(&self) -> &str {
152        &self.category_name
153    }
154
155    /// Sets the name of the category this Run is for.
156    #[inline]
157    pub fn set_category_name<S>(&mut self, name: S)
158    where
159        S: PopulateString,
160    {
161        name.populate(&mut self.category_name);
162    }
163
164    /// Returns the amount of runs that have been attempted with these splits.
165    #[inline]
166    pub const fn attempt_count(&self) -> u32 {
167        self.attempt_count
168    }
169
170    /// Sets the amount of runs that have been attempted with these splits.
171    #[inline]
172    pub fn set_attempt_count(&mut self, attempts: u32) {
173        self.attempt_count = attempts;
174    }
175
176    /// Accesses additional metadata of this Run, like the platform and region
177    /// of the game.
178    #[inline]
179    pub const fn metadata(&self) -> &RunMetadata {
180        &self.metadata
181    }
182
183    /// Grants mutable access to the additional metadata of this Run, like the
184    /// platform and region of the game.
185    #[inline]
186    pub fn metadata_mut(&mut self) -> &mut RunMetadata {
187        &mut self.metadata
188    }
189
190    /// Sets the time an attempt of this Run should start at.
191    #[inline]
192    pub fn set_offset(&mut self, offset: TimeSpan) {
193        self.offset = offset;
194    }
195
196    /// Accesses the time an attempt of this Run should start at.
197    #[inline]
198    pub const fn offset(&self) -> TimeSpan {
199        self.offset
200    }
201
202    /// Marks a Run that a new Attempt has started. If you use it with a Timer,
203    /// this is done automatically.
204    pub fn start_next_run(&mut self) {
205        self.attempt_count += 1;
206        self.has_been_modified = true;
207    }
208
209    /// Accesses the Segments of this Run object.
210    #[inline]
211    pub fn segments(&self) -> &[Segment] {
212        &self.segments
213    }
214
215    /// Grants mutable access to the Segments of this Run object.
216    #[inline]
217    pub fn segments_mut(&mut self) -> &mut Vec<Segment> {
218        &mut self.segments
219    }
220
221    /// Pushes the segment provided to the end of the list of segments of this Run.
222    #[inline]
223    pub fn push_segment(&mut self, segment: Segment) {
224        self.segments.push(segment);
225    }
226
227    /// Accesses a certain segment of this Run.
228    ///
229    /// # Panics
230    ///
231    /// Panics if the index is out of bounds.
232    #[inline]
233    pub fn segment(&self, index: usize) -> &Segment {
234        &self.segments[index]
235    }
236
237    /// Mutably accesses a certain segment of this Run.
238    ///
239    /// # Panics
240    ///
241    /// Panics if the index is out of bounds.
242    #[inline]
243    pub fn segment_mut(&mut self, index: usize) -> &mut Segment {
244        &mut self.segments[index]
245    }
246
247    /// Accesses the history of all the runs that have been attempted. This does
248    /// not store the actual segment times, just the overall attempt
249    /// information. Information about the individual segments is stored within
250    /// each segment.
251    #[inline]
252    pub fn attempt_history(&self) -> &[Attempt] {
253        &self.attempt_history
254    }
255
256    /// Accesses the custom comparisons that are stored in this Run. This
257    /// includes `Personal Best` but excludes all the other Comparison
258    /// Generators.
259    #[inline]
260    pub fn custom_comparisons(&self) -> &[String] {
261        &self.custom_comparisons
262    }
263
264    /// Grants mutable access to the custom comparisons that are stored in this
265    /// Run.  This includes `Personal Best` but excludes all the other
266    /// Comparison Generators.
267    ///
268    /// # Warning
269    ///
270    /// You may not delete the `Personal Best` comparison.
271    #[inline]
272    pub fn custom_comparisons_mut(&mut self) -> &mut Vec<String> {
273        &mut self.custom_comparisons
274    }
275
276    /// Accesses an iterator that iterates over all the comparisons. This
277    /// includes both the custom comparisons defined by the user and the
278    /// Comparison Generators.
279    #[inline]
280    pub fn comparisons(&self) -> ComparisonsIter<'_> {
281        ComparisonsIter {
282            custom: &self.custom_comparisons,
283            generators: &self.comparison_generators.0,
284        }
285    }
286
287    /// Accesses the Comparison Generators in use by this Run.
288    #[inline]
289    pub fn comparison_generators(&self) -> &[Box<dyn ComparisonGenerator>] {
290        &self.comparison_generators.0
291    }
292
293    /// Grants mutable access to the Comparison Generators in use by this Run.
294    #[inline]
295    pub fn comparison_generators_mut(&mut self) -> &mut Vec<Box<dyn ComparisonGenerator>> {
296        &mut self.comparison_generators.0
297    }
298
299    /// Accesses the Auto Splitter Settings that are encoded as XML.
300    #[inline]
301    pub fn auto_splitter_settings(&self) -> &str {
302        &self.auto_splitter_settings
303    }
304
305    /// Grants mutable access to the XML encoded Auto Splitter Settings.
306    ///
307    /// # Warning
308    ///
309    /// You need to ensure that the Auto Splitter Settings are encoded as data
310    /// that would be valid as an interior of an XML element.
311    #[inline]
312    pub fn auto_splitter_settings_mut(&mut self) -> &mut String {
313        &mut self.auto_splitter_settings
314    }
315
316    /// Returns the amount of segments stored in this Run.
317    #[inline]
318    pub fn len(&self) -> usize {
319        self.segments.len()
320    }
321
322    /// Returns `true` if there's no segments stored in this Run.
323    #[inline]
324    pub fn is_empty(&self) -> bool {
325        self.segments.is_empty()
326    }
327
328    /// Marks the Run as modified, so that it is known that there are changes
329    /// that should be saved.
330    #[inline]
331    pub fn mark_as_modified(&mut self) {
332        self.has_been_modified = true;
333    }
334
335    /// Marks the Run as unmodified, so that it is known that all the changes
336    /// have been saved.
337    #[inline]
338    pub fn mark_as_unmodified(&mut self) {
339        self.has_been_modified = false;
340    }
341
342    /// Returns whether the Run has been modified and should be saved so that
343    /// the changes don't get lost.
344    #[inline]
345    pub const fn has_been_modified(&self) -> bool {
346        self.has_been_modified
347    }
348
349    /// Adds a new Attempt to the Run's Attempt History. This is automatically
350    /// done if the Run is used with a Timer.
351    pub fn add_attempt(
352        &mut self,
353        time: Time,
354        started: Option<AtomicDateTime>,
355        ended: Option<AtomicDateTime>,
356        pause_time: Option<TimeSpan>,
357    ) {
358        let index = self
359            .attempt_history
360            .iter()
361            .map(Attempt::index)
362            .max()
363            .unwrap_or(0);
364        let index = max(0, index + 1);
365        self.add_attempt_with_index(time, index, started, ended, pause_time);
366    }
367
368    /// Adds a new Attempt to the Run's Attempt History with a predetermined
369    /// History Index.
370    ///
371    /// # Warning
372    ///
373    /// This index may not overlap with an index that is already in the Attempt
374    /// History.
375    pub fn add_attempt_with_index(
376        &mut self,
377        time: Time,
378        index: i32,
379        started: Option<AtomicDateTime>,
380        ended: Option<AtomicDateTime>,
381        pause_time: Option<TimeSpan>,
382    ) {
383        let attempt = Attempt::new(index, time, started, ended, pause_time);
384        self.attempt_history.push(attempt);
385    }
386
387    /// Clears the speedrun.com Run ID of this Run, as the current Run does not
388    /// reflect the run on speedrun.com anymore. This may be the case if a new
389    /// Personal Best is achieved for example.
390    #[inline]
391    pub fn clear_run_id(&mut self) {
392        self.metadata.set_run_id("");
393    }
394
395    /// Adds a new custom comparison. If a custom comparison with that name
396    /// already exists, it is not added.
397    #[inline]
398    pub fn add_custom_comparison<S>(&mut self, comparison: S) -> ComparisonResult<()>
399    where
400        S: PopulateString,
401    {
402        self.validate_comparison_name(comparison.as_str())?;
403        self.custom_comparisons.push(comparison.into_string());
404        Ok(())
405    }
406
407    /// Recalculates all the comparison times the Comparison Generators provide.
408    #[inline]
409    pub fn regenerate_comparisons(&mut self) {
410        for generator in &mut self.comparison_generators.0 {
411            generator.generate(&mut self.segments, &self.attempt_history);
412        }
413    }
414
415    /// Returns a file name (without the extension) suitable for this Run that
416    /// is built the following way:
417    ///
418    /// Game Name - Category Name
419    ///
420    /// If either is empty, the dash is omitted. Special characters that cause
421    /// problems in file names are also omitted. If an extended category name is
422    /// used, the variables of the category are appended in a parenthesis.
423    pub fn extended_file_name(&self, use_extended_category_name: bool) -> String {
424        let mut extended_name = self.extended_name(use_extended_category_name).into_owned();
425        extended_name
426            .retain(|c| !matches!(c, '\\' | '/' | ':' | '*' | '?' | '"' | '<' | '>' | '|'));
427        extended_name
428    }
429
430    /// Returns a name suitable for this Run that is built the following way:
431    ///
432    /// Game Name - Category Name
433    ///
434    /// If either is empty, the dash is omitted. If an extended category name is
435    /// used, the variables of the category are appended in a parenthesis.
436    pub fn extended_name(&self, use_extended_category_name: bool) -> Cow<'_, str> {
437        let mut name = Cow::Borrowed(self.game_name());
438
439        let category_name = if use_extended_category_name {
440            Cow::Owned(self.extended_category_name(false, false, true).to_string())
441        } else {
442            Cow::Borrowed(self.category_name())
443        };
444
445        if !category_name.is_empty() {
446            if !name.is_empty() {
447                let name = name.to_mut();
448                name.push_str(" - ");
449                name.push_str(&category_name);
450            } else {
451                name = category_name;
452            }
453        }
454
455        name
456    }
457
458    /// Returns an extended category name that possibly includes the region,
459    /// platform and variables, depending on the arguments provided. The
460    /// returned object implements `Display` where it lazily formats the
461    /// extended category name. An extended category name may look like this:
462    ///
463    /// Any% (No Tuner, JPN, Wii Emulator)
464    pub const fn extended_category_name(
465        &self,
466        show_region: bool,
467        show_platform: bool,
468        show_variables: bool,
469    ) -> ExtendedCategoryName<'_> {
470        ExtendedCategoryName {
471            run: self,
472            show_region,
473            show_platform,
474            show_variables,
475        }
476    }
477
478    /// Returns the maximum index currently in use by the Attempt History. This
479    /// mostly serves as a helper function for the Timer.
480    pub fn max_attempt_history_index(&self) -> Option<i32> {
481        self.attempt_history().iter().map(Attempt::index).max()
482    }
483
484    /// Applies some fixing algorithms on the Run. This includes fixing the
485    /// comparison times and history, removing duplicates in the segment
486    /// histories and removing empty times.
487    pub fn fix_splits(&mut self) {
488        for method in TimingMethod::all() {
489            self.fix_comparison_times_and_history(method);
490        }
491        self.remove_duplicates();
492        self.remove_none_values();
493        self.reattach_unattached_segment_history_elements();
494    }
495
496    /// Clears out the Attempt History and the Segment Histories of all the segments.
497    pub fn clear_history(&mut self) {
498        self.attempt_history.clear();
499        for segment in &mut self.segments {
500            segment.segment_history_mut().clear();
501        }
502    }
503
504    /// Clears out the Attempt History, the Segment Histories, all the times,
505    /// sets the Attempt Count to 0 and clears the speedrun.com run id
506    /// association. All Custom Comparisons other than `Personal Best` are
507    /// deleted as well.
508    pub fn clear_times(&mut self) {
509        self.clear_history();
510        self.custom_comparisons.retain(|c| c == personal_best::NAME);
511        for segment in &mut self.segments {
512            segment.comparisons_mut().clear();
513            segment.set_best_segment_time(Time::default());
514        }
515        self.attempt_count = 0;
516        self.clear_run_id();
517    }
518
519    fn fix_comparison_times_and_history(&mut self, method: TimingMethod) {
520        // Remove negative Best Segment Times
521        for segment in &mut self.segments {
522            if segment.best_segment_time_mut()[method].map_or(false, |t| t < TimeSpan::zero()) {
523                segment.best_segment_time_mut()[method] = None;
524            }
525        }
526
527        for segment in &mut self.segments {
528            fix_history_from_none_best_segments(segment, method);
529        }
530
531        for comparison in &self.custom_comparisons {
532            let mut previous_time = TimeSpan::zero();
533            for segment in &mut self.segments {
534                if let Some(mut time) = segment.comparison_mut(comparison)[method] {
535                    // Prevent comparison times from decreasing from one split to the next
536                    if time < previous_time {
537                        time = previous_time;
538                        segment.comparison_mut(comparison)[method] = Some(time);
539                    }
540
541                    // Fix Best Segment time if the PB segment is faster
542                    if comparison == personal_best::NAME {
543                        let current_segment = time - previous_time;
544                        if segment.best_segment_time()[method].map_or(true, |t| t > current_segment)
545                        {
546                            segment.best_segment_time_mut()[method] = Some(current_segment);
547                        }
548                    }
549
550                    previous_time = time;
551                }
552            }
553        }
554
555        for segment in &mut self.segments {
556            fix_history_from_best_segment_times(segment, method);
557        }
558    }
559
560    fn remove_none_values(&mut self) {
561        let mut cache = Vec::new();
562        if let Some(min_index) = self.min_segment_history_index() {
563            let max_index = self.max_attempt_history_index().unwrap_or(0) + 1;
564            for run_index in min_index..max_index {
565                for index in 0..self.len() {
566                    if let Some(element) = self.segments[index].segment_history().get(run_index) {
567                        if element.real_time.is_none() && element.game_time.is_none() {
568                            cache.push(run_index);
569                        } else {
570                            cache.clear();
571                        }
572                    } else {
573                        // Remove None times in history that aren't followed by a non-None time
574                        self.remove_items_from_cache(index, &mut cache);
575                    }
576                }
577                let len = self.len();
578                self.remove_items_from_cache(len, &mut cache);
579            }
580        }
581    }
582
583    fn remove_duplicates(&mut self) {
584        let mut rta_set = HashSet::new();
585        let mut igt_set = HashSet::new();
586
587        for segment in self.segments_mut() {
588            let history = segment.segment_history_mut();
589
590            rta_set.clear();
591            igt_set.clear();
592
593            for &(_, time) in history.iter_actual_runs() {
594                if let Some(time) = time.real_time {
595                    rta_set.insert(time);
596                }
597                if let Some(time) = time.game_time {
598                    igt_set.insert(time);
599                }
600            }
601
602            history.retain(|&(index, time)| {
603                if index >= 1 {
604                    return true;
605                }
606
607                let (mut is_none, mut is_unique) = (true, false);
608                if let Some(time) = time.real_time {
609                    is_unique |= rta_set.insert(time);
610                    is_none = false;
611                }
612
613                if let Some(time) = time.game_time {
614                    is_unique |= igt_set.insert(time);
615                    is_none = false;
616                }
617
618                is_none || is_unique
619            });
620        }
621    }
622
623    fn remove_items_from_cache(&mut self, index: usize, cache: &mut Vec<i32>) {
624        let ind = index - cache.len();
625        for (index, segment) in cache.drain(..).zip(self.segments_mut()[ind..].iter_mut()) {
626            segment.segment_history_mut().remove(index);
627        }
628    }
629
630    /// Returns the minimum index in use by all the Segment Histories. `None` is
631    /// returned if the Run has no segments.
632    pub fn min_segment_history_index(&self) -> Option<i32> {
633        self.segments
634            .iter()
635            .map(|s| s.segment_history().min_index())
636            .min()
637    }
638
639    /// Fixes the Segment History by calculating the segment times from the
640    /// Personal Best times and adding those to the Segment History.
641    pub fn import_pb_into_segment_history(&mut self) {
642        if let Some(mut index) = self.min_segment_history_index() {
643            for timing_method in TimingMethod::all() {
644                index -= 1;
645                let mut prev_time = TimeSpan::zero();
646
647                for segment in self.segments_mut() {
648                    // Import the PB splits into the history
649                    let pb_time = segment.personal_best_split_time()[timing_method];
650                    let time = Time::new()
651                        .with_timing_method(timing_method, pb_time.map(|p| p - prev_time));
652                    segment.segment_history_mut().insert(index, time);
653
654                    if let Some(time) = pb_time {
655                        prev_time = time;
656                    }
657                }
658            }
659        }
660    }
661
662    /// Fixes a segment's Segment History by adding its Best Segment Time to its
663    /// Segment History.
664    ///
665    /// # Panics
666    ///
667    /// This panics if the segment index provided is out of bounds.
668    pub fn import_best_segment(&mut self, segment_index: usize) {
669        let best_segment_time = self.segments[segment_index].best_segment_time();
670        if best_segment_time.real_time.is_some() || best_segment_time.game_time.is_some() {
671            // We can unwrap here because due to the fact that we can access the
672            // best_segment_time of some segment, at least one exists.
673            let index = self.min_segment_history_index().unwrap() - 1;
674            self.segments[segment_index]
675                .segment_history_mut()
676                .insert(index, best_segment_time);
677        }
678    }
679
680    /// Updates the Segment History by adding the split times of the most recent
681    /// attempt up to the provided current split index to the Segment History.
682    ///
683    /// # Panics
684    ///
685    /// This panics if there is no attempt in the Attempt History.
686    pub fn update_segment_history(&mut self, current_split_index: usize) {
687        let mut last_split_time = Time::zero();
688
689        let segments = self.segments.iter_mut().take(current_split_index);
690        let index = self
691            .attempt_history
692            .last()
693            .expect("There is no attempt in the Attempt History.")
694            .index();
695
696        for segment in segments {
697            let split_time = segment.split_time();
698            let segment_time = Time::op(split_time, last_split_time, |a, b| a - b);
699            segment.segment_history_mut().insert(index, segment_time);
700            if let Some(time) = split_time.real_time {
701                last_split_time.real_time = Some(time);
702            }
703            if let Some(time) = split_time.game_time {
704                last_split_time.game_time = Some(time);
705            }
706        }
707    }
708
709    /// Checks a given name against the current comparisons in the Run to
710    /// ensure that it is valid for use.
711    pub fn validate_comparison_name(&self, new: &str) -> ComparisonResult<()> {
712        if new.starts_with(RACE_COMPARISON_PREFIX) {
713            Err(ComparisonError::NameStartsWithRace)
714        } else if self.comparisons().any(|c| c == new) {
715            Err(ComparisonError::DuplicateName)
716        } else {
717            Ok(())
718        }
719    }
720
721    fn reattach_unattached_segment_history_elements(&mut self) {
722        let max_id = self.max_attempt_history_index().unwrap_or_default();
723        let mut min_id = self.min_segment_history_index().unwrap_or_default();
724
725        while let Some(unattached_id) = self
726            .segments
727            .iter()
728            .filter_map(|s| s.segment_history().try_get_max_index())
729            .filter(|&i| i > max_id)
730            .max()
731        {
732            let reassign_id = min_id - 1;
733
734            for segment in self.segments_mut() {
735                let history = segment.segment_history_mut();
736                if let Some(time) = history.remove(unattached_id) {
737                    history.insert(reassign_id, time);
738                }
739            }
740
741            min_id = reassign_id;
742        }
743    }
744}
745
746impl Default for Run {
747    fn default() -> Self {
748        Run::new()
749    }
750}
751
752fn fix_history_from_none_best_segments(segment: &mut Segment, method: TimingMethod) {
753    // Only do anything if the Best Segment Time is gone for the Segment in question
754    if segment.best_segment_time()[method].is_none() {
755        // Keep only the skipped segments
756        segment
757            .segment_history_mut()
758            .retain(|&(_, time)| time[method].is_none());
759    }
760}
761
762fn fix_history_from_best_segment_times(segment: &mut Segment, method: TimingMethod) {
763    if let Some(best_segment) = segment.best_segment_time()[method] {
764        for (_, time) in segment.segment_history_mut().iter_mut() {
765            // Make sure no times in the history are lower than the Best Segment
766            if let Some(time) = &mut time[method] {
767                if *time < best_segment {
768                    *time = best_segment;
769                }
770            }
771        }
772    }
773}
774
775/// Iterator that iterates over all the comparisons. This includes both the
776/// custom comparisons defined by the user and the Comparison Generators.
777pub struct ComparisonsIter<'a> {
778    custom: &'a [String],
779    generators: &'a [Box<dyn ComparisonGenerator>],
780}
781
782impl<'a> Iterator for ComparisonsIter<'a> {
783    type Item = &'a str;
784
785    fn next(&mut self) -> Option<&'a str> {
786        if let Some((a, b)) = self.custom.split_first() {
787            self.custom = b;
788            Some(a)
789        } else if let Some((a, b)) = self.generators.split_first() {
790            self.generators = b;
791            Some(a.name())
792        } else {
793            None
794        }
795    }
796
797    fn size_hint(&self) -> (usize, Option<usize>) {
798        let len = self.custom.len() + self.generators.len();
799        (len, Some(len))
800    }
801}
802
803impl ExactSizeIterator for ComparisonsIter<'_> {}
804
805/// Lazily formats an extended category name via the `Display` trait. It's a
806/// category name that possibly includes the region, platform and variables,
807/// depending on the arguments provided. An extended category name may look like
808/// this:
809///
810/// Any% (No Tuner, JPN, Wii Emulator)
811pub struct ExtendedCategoryName<'run> {
812    run: &'run Run,
813    show_region: bool,
814    show_platform: bool,
815    show_variables: bool,
816}
817
818impl fmt::Display for ExtendedCategoryName<'_> {
819    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820        let category_name = self.run.category_name.as_str();
821
822        if category_name.is_empty() {
823            return Ok(());
824        }
825
826        let mut is_empty = true;
827        let mut has_pushed = false;
828
829        let (before, after_parenthesis) = if let Some((i, u)) = self
830            .run
831            .category_name
832            .find('(')
833            .and_then(|i| category_name[i..].find(')').map(|u| (i, i + u)))
834        {
835            is_empty = u == i + 1;
836            category_name.split_at(u)
837        } else {
838            (category_name, "")
839        };
840        f.write_str(before)?;
841
842        let mut push = |values: &[&str]| {
843            if is_empty {
844                if !has_pushed {
845                    f.write_str(" (")?;
846                }
847                is_empty = false;
848            } else {
849                f.write_str(", ")?;
850            }
851            for value in values {
852                f.write_str(value)?;
853            }
854            has_pushed = true;
855            Ok(())
856        };
857
858        if self.show_variables {
859            for (name, value) in self.run.metadata.speedrun_com_variables() {
860                let name = name.trim_end_matches('?');
861
862                if unicase::eq(value.as_str(), "yes") {
863                    push(&[name])?;
864                } else if unicase::eq(value.as_str(), "no") {
865                    push(&["No ", value])?;
866                } else {
867                    push(&[value])?;
868                }
869            }
870        }
871
872        if self.show_region {
873            let region = self.run.metadata.region_name();
874            if !region.is_empty() {
875                push(&[region])?;
876            }
877        }
878
879        if self.show_platform {
880            let platform = self.run.metadata.platform_name();
881            let uses_emulator = self.run.metadata.uses_emulator();
882
883            match (!platform.is_empty(), uses_emulator) {
884                (true, true) => push(&[platform, " Emulator"])?,
885                (true, false) => push(&[platform])?,
886                (false, true) => push(&["Emulator"])?,
887                _ => (),
888            }
889        }
890
891        if !after_parenthesis.is_empty() {
892            f.write_str(after_parenthesis)?;
893        } else if !is_empty {
894            f.write_str(")")?;
895        }
896
897        Ok(())
898    }
899}