mod attempt;
mod comparisons;
pub mod editor;
pub mod parser;
mod run_metadata;
pub mod saver;
mod segment;
mod segment_history;
#[cfg(test)]
mod tests;
pub use attempt::Attempt;
pub use comparisons::Comparisons;
pub use editor::{Editor, RenameError};
pub use run_metadata::{CustomVariable, RunMetadata};
pub use segment::Segment;
pub use segment_history::SegmentHistory;
use crate::{
comparison::{default_generators, personal_best, ComparisonGenerator, RACE_COMPARISON_PREFIX},
platform::prelude::*,
settings::Image,
util::PopulateString,
AtomicDateTime, Time, TimeSpan, TimingMethod,
};
use alloc::borrow::Cow;
use core::{cmp::max, fmt};
use hashbrown::HashSet;
#[derive(Clone, Debug, PartialEq)]
pub struct Run {
game_icon: Image,
game_name: String,
category_name: String,
offset: TimeSpan,
attempt_count: u32,
attempt_history: Vec<Attempt>,
metadata: RunMetadata,
has_been_modified: bool,
segments: Vec<Segment>,
custom_comparisons: Vec<String>,
comparison_generators: ComparisonGenerators,
auto_splitter_settings: String,
}
#[derive(Clone, Debug)]
struct ComparisonGenerators(Vec<Box<dyn ComparisonGenerator>>);
impl PartialEq for ComparisonGenerators {
fn eq(&self, other: &ComparisonGenerators) -> bool {
self.0
.iter()
.map(|c| c.name())
.eq(other.0.iter().map(|c| c.name()))
}
}
#[derive(PartialEq, Eq, Debug, snafu::Snafu)]
pub enum ComparisonError {
NameStartsWithRace,
DuplicateName,
}
pub type ComparisonResult<T> = Result<T, ComparisonError>;
impl Run {
#[inline]
pub fn new() -> Self {
Self {
game_icon: Image::default(),
game_name: String::new(),
category_name: String::new(),
offset: TimeSpan::zero(),
attempt_count: 0,
attempt_history: Vec::new(),
metadata: RunMetadata::new(),
has_been_modified: false,
segments: Vec::new(),
custom_comparisons: vec![personal_best::NAME.to_string()],
comparison_generators: ComparisonGenerators(default_generators()),
auto_splitter_settings: String::new(),
}
}
#[inline]
pub fn game_name(&self) -> &str {
&self.game_name
}
#[inline]
pub fn set_game_name<S>(&mut self, name: S)
where
S: PopulateString,
{
name.populate(&mut self.game_name);
}
#[inline]
pub const fn game_icon(&self) -> &Image {
&self.game_icon
}
#[inline]
pub fn set_game_icon<D: Into<Image>>(&mut self, image: D) {
self.game_icon = image.into();
}
#[inline]
pub fn category_name(&self) -> &str {
&self.category_name
}
#[inline]
pub fn set_category_name<S>(&mut self, name: S)
where
S: PopulateString,
{
name.populate(&mut self.category_name);
}
#[inline]
pub const fn attempt_count(&self) -> u32 {
self.attempt_count
}
#[inline]
pub fn set_attempt_count(&mut self, attempts: u32) {
self.attempt_count = attempts;
}
#[inline]
pub const fn metadata(&self) -> &RunMetadata {
&self.metadata
}
#[inline]
pub fn metadata_mut(&mut self) -> &mut RunMetadata {
&mut self.metadata
}
#[inline]
pub fn set_offset(&mut self, offset: TimeSpan) {
self.offset = offset;
}
#[inline]
pub const fn offset(&self) -> TimeSpan {
self.offset
}
pub fn start_next_run(&mut self) {
self.attempt_count += 1;
self.has_been_modified = true;
}
#[inline]
pub fn segments(&self) -> &[Segment] {
&self.segments
}
#[inline]
pub fn segments_mut(&mut self) -> &mut Vec<Segment> {
&mut self.segments
}
#[inline]
pub fn push_segment(&mut self, segment: Segment) {
self.segments.push(segment);
}
#[inline]
pub fn segment(&self, index: usize) -> &Segment {
&self.segments[index]
}
#[inline]
pub fn segment_mut(&mut self, index: usize) -> &mut Segment {
&mut self.segments[index]
}
#[inline]
pub fn attempt_history(&self) -> &[Attempt] {
&self.attempt_history
}
#[inline]
pub fn custom_comparisons(&self) -> &[String] {
&self.custom_comparisons
}
#[inline]
pub fn custom_comparisons_mut(&mut self) -> &mut Vec<String> {
&mut self.custom_comparisons
}
#[inline]
pub fn comparisons(&self) -> ComparisonsIter<'_> {
ComparisonsIter {
custom: &self.custom_comparisons,
generators: &self.comparison_generators.0,
}
}
#[inline]
pub fn comparison_generators(&self) -> &[Box<dyn ComparisonGenerator>] {
&self.comparison_generators.0
}
#[inline]
pub fn comparison_generators_mut(&mut self) -> &mut Vec<Box<dyn ComparisonGenerator>> {
&mut self.comparison_generators.0
}
#[inline]
pub fn auto_splitter_settings(&self) -> &str {
&self.auto_splitter_settings
}
#[inline]
pub fn auto_splitter_settings_mut(&mut self) -> &mut String {
&mut self.auto_splitter_settings
}
#[inline]
pub fn len(&self) -> usize {
self.segments.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
#[inline]
pub fn mark_as_modified(&mut self) {
self.has_been_modified = true;
}
#[inline]
pub fn mark_as_unmodified(&mut self) {
self.has_been_modified = false;
}
#[inline]
pub const fn has_been_modified(&self) -> bool {
self.has_been_modified
}
pub fn add_attempt(
&mut self,
time: Time,
started: Option<AtomicDateTime>,
ended: Option<AtomicDateTime>,
pause_time: Option<TimeSpan>,
) {
let index = self
.attempt_history
.iter()
.map(Attempt::index)
.max()
.unwrap_or(0);
let index = max(0, index + 1);
self.add_attempt_with_index(time, index, started, ended, pause_time);
}
pub fn add_attempt_with_index(
&mut self,
time: Time,
index: i32,
started: Option<AtomicDateTime>,
ended: Option<AtomicDateTime>,
pause_time: Option<TimeSpan>,
) {
let attempt = Attempt::new(index, time, started, ended, pause_time);
self.attempt_history.push(attempt);
}
#[inline]
pub fn clear_run_id(&mut self) {
self.metadata.set_run_id("");
}
#[inline]
pub fn add_custom_comparison<S>(&mut self, comparison: S) -> ComparisonResult<()>
where
S: PopulateString,
{
self.validate_comparison_name(comparison.as_str())?;
self.custom_comparisons.push(comparison.into_string());
Ok(())
}
#[inline]
pub fn regenerate_comparisons(&mut self) {
for generator in &mut self.comparison_generators.0 {
generator.generate(&mut self.segments, &self.attempt_history);
}
}
pub fn extended_file_name(&self, use_extended_category_name: bool) -> String {
let mut extended_name = self.extended_name(use_extended_category_name).into_owned();
extended_name
.retain(|c| !matches!(c, '\\' | '/' | ':' | '*' | '?' | '"' | '<' | '>' | '|'));
extended_name
}
pub fn extended_name(&self, use_extended_category_name: bool) -> Cow<'_, str> {
let mut name = Cow::Borrowed(self.game_name());
let category_name = if use_extended_category_name {
Cow::Owned(self.extended_category_name(false, false, true).to_string())
} else {
Cow::Borrowed(self.category_name())
};
if !category_name.is_empty() {
if !name.is_empty() {
let name = name.to_mut();
name.push_str(" - ");
name.push_str(&category_name);
} else {
name = category_name;
}
}
name
}
pub const fn extended_category_name(
&self,
show_region: bool,
show_platform: bool,
show_variables: bool,
) -> ExtendedCategoryName<'_> {
ExtendedCategoryName {
run: self,
show_region,
show_platform,
show_variables,
}
}
pub fn max_attempt_history_index(&self) -> Option<i32> {
self.attempt_history().iter().map(Attempt::index).max()
}
pub fn fix_splits(&mut self) {
for method in TimingMethod::all() {
self.fix_comparison_times_and_history(method);
}
self.remove_duplicates();
self.remove_none_values();
self.reattach_unattached_segment_history_elements();
}
pub fn clear_history(&mut self) {
self.attempt_history.clear();
for segment in &mut self.segments {
segment.segment_history_mut().clear();
}
}
pub fn clear_times(&mut self) {
self.clear_history();
self.custom_comparisons.retain(|c| c == personal_best::NAME);
for segment in &mut self.segments {
segment.comparisons_mut().clear();
segment.set_best_segment_time(Time::default());
}
self.attempt_count = 0;
self.clear_run_id();
}
fn fix_comparison_times_and_history(&mut self, method: TimingMethod) {
for segment in &mut self.segments {
if segment.best_segment_time_mut()[method].map_or(false, |t| t < TimeSpan::zero()) {
segment.best_segment_time_mut()[method] = None;
}
}
for segment in &mut self.segments {
fix_history_from_none_best_segments(segment, method);
}
for comparison in &self.custom_comparisons {
let mut previous_time = TimeSpan::zero();
for segment in &mut self.segments {
if let Some(mut time) = segment.comparison_mut(comparison)[method] {
if time < previous_time {
time = previous_time;
segment.comparison_mut(comparison)[method] = Some(time);
}
if comparison == personal_best::NAME {
let current_segment = time - previous_time;
if segment.best_segment_time()[method].map_or(true, |t| t > current_segment)
{
segment.best_segment_time_mut()[method] = Some(current_segment);
}
}
previous_time = time;
}
}
}
for segment in &mut self.segments {
fix_history_from_best_segment_times(segment, method);
}
}
fn remove_none_values(&mut self) {
let mut cache = Vec::new();
if let Some(min_index) = self.min_segment_history_index() {
let max_index = self.max_attempt_history_index().unwrap_or(0) + 1;
for run_index in min_index..max_index {
for index in 0..self.len() {
if let Some(element) = self.segments[index].segment_history().get(run_index) {
if element.real_time.is_none() && element.game_time.is_none() {
cache.push(run_index);
} else {
cache.clear();
}
} else {
self.remove_items_from_cache(index, &mut cache);
}
}
let len = self.len();
self.remove_items_from_cache(len, &mut cache);
}
}
}
fn remove_duplicates(&mut self) {
let mut rta_set = HashSet::new();
let mut igt_set = HashSet::new();
for segment in self.segments_mut() {
let history = segment.segment_history_mut();
rta_set.clear();
igt_set.clear();
for &(_, time) in history.iter_actual_runs() {
if let Some(time) = time.real_time {
rta_set.insert(time);
}
if let Some(time) = time.game_time {
igt_set.insert(time);
}
}
history.retain(|&(index, time)| {
if index >= 1 {
return true;
}
let (mut is_none, mut is_unique) = (true, false);
if let Some(time) = time.real_time {
is_unique |= rta_set.insert(time);
is_none = false;
}
if let Some(time) = time.game_time {
is_unique |= igt_set.insert(time);
is_none = false;
}
is_none || is_unique
});
}
}
fn remove_items_from_cache(&mut self, index: usize, cache: &mut Vec<i32>) {
let ind = index - cache.len();
for (index, segment) in cache.drain(..).zip(self.segments_mut()[ind..].iter_mut()) {
segment.segment_history_mut().remove(index);
}
}
pub fn min_segment_history_index(&self) -> Option<i32> {
self.segments
.iter()
.map(|s| s.segment_history().min_index())
.min()
}
pub fn import_pb_into_segment_history(&mut self) {
if let Some(mut index) = self.min_segment_history_index() {
for timing_method in TimingMethod::all() {
index -= 1;
let mut prev_time = TimeSpan::zero();
for segment in self.segments_mut() {
let pb_time = segment.personal_best_split_time()[timing_method];
let time = Time::new()
.with_timing_method(timing_method, pb_time.map(|p| p - prev_time));
segment.segment_history_mut().insert(index, time);
if let Some(time) = pb_time {
prev_time = time;
}
}
}
}
}
pub fn import_best_segment(&mut self, segment_index: usize) {
let best_segment_time = self.segments[segment_index].best_segment_time();
if best_segment_time.real_time.is_some() || best_segment_time.game_time.is_some() {
let index = self.min_segment_history_index().unwrap() - 1;
self.segments[segment_index]
.segment_history_mut()
.insert(index, best_segment_time);
}
}
pub fn update_segment_history(&mut self, current_split_index: usize) {
let mut last_split_time = Time::zero();
let segments = self.segments.iter_mut().take(current_split_index);
let index = self
.attempt_history
.last()
.expect("There is no attempt in the Attempt History.")
.index();
for segment in segments {
let split_time = segment.split_time();
let segment_time = Time::op(split_time, last_split_time, |a, b| a - b);
segment.segment_history_mut().insert(index, segment_time);
if let Some(time) = split_time.real_time {
last_split_time.real_time = Some(time);
}
if let Some(time) = split_time.game_time {
last_split_time.game_time = Some(time);
}
}
}
pub fn validate_comparison_name(&self, new: &str) -> ComparisonResult<()> {
if new.starts_with(RACE_COMPARISON_PREFIX) {
Err(ComparisonError::NameStartsWithRace)
} else if self.comparisons().any(|c| c == new) {
Err(ComparisonError::DuplicateName)
} else {
Ok(())
}
}
fn reattach_unattached_segment_history_elements(&mut self) {
let max_id = self.max_attempt_history_index().unwrap_or_default();
let mut min_id = self.min_segment_history_index().unwrap_or_default();
while let Some(unattached_id) = self
.segments
.iter()
.filter_map(|s| s.segment_history().try_get_max_index())
.filter(|&i| i > max_id)
.max()
{
let reassign_id = min_id - 1;
for segment in self.segments_mut() {
let history = segment.segment_history_mut();
if let Some(time) = history.remove(unattached_id) {
history.insert(reassign_id, time);
}
}
min_id = reassign_id;
}
}
}
impl Default for Run {
fn default() -> Self {
Run::new()
}
}
fn fix_history_from_none_best_segments(segment: &mut Segment, method: TimingMethod) {
if segment.best_segment_time()[method].is_none() {
segment
.segment_history_mut()
.retain(|&(_, time)| time[method].is_none());
}
}
fn fix_history_from_best_segment_times(segment: &mut Segment, method: TimingMethod) {
if let Some(best_segment) = segment.best_segment_time()[method] {
for (_, time) in segment.segment_history_mut().iter_mut() {
if let Some(time) = &mut time[method] {
if *time < best_segment {
*time = best_segment;
}
}
}
}
}
pub struct ComparisonsIter<'a> {
custom: &'a [String],
generators: &'a [Box<dyn ComparisonGenerator>],
}
impl<'a> Iterator for ComparisonsIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if let Some((a, b)) = self.custom.split_first() {
self.custom = b;
Some(a)
} else if let Some((a, b)) = self.generators.split_first() {
self.generators = b;
Some(a.name())
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.custom.len() + self.generators.len();
(len, Some(len))
}
}
impl ExactSizeIterator for ComparisonsIter<'_> {}
pub struct ExtendedCategoryName<'run> {
run: &'run Run,
show_region: bool,
show_platform: bool,
show_variables: bool,
}
impl fmt::Display for ExtendedCategoryName<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let category_name = self.run.category_name.as_str();
if category_name.is_empty() {
return Ok(());
}
let mut is_empty = true;
let mut has_pushed = false;
let (before, after_parenthesis) = if let Some((i, u)) = self
.run
.category_name
.find('(')
.and_then(|i| category_name[i..].find(')').map(|u| (i, i + u)))
{
is_empty = u == i + 1;
category_name.split_at(u)
} else {
(category_name, "")
};
f.write_str(before)?;
let mut push = |values: &[&str]| {
if is_empty {
if !has_pushed {
f.write_str(" (")?;
}
is_empty = false;
} else {
f.write_str(", ")?;
}
for value in values {
f.write_str(value)?;
}
has_pushed = true;
Ok(())
};
if self.show_variables {
for (name, value) in self.run.metadata.speedrun_com_variables() {
let name = name.trim_end_matches('?');
if unicase::eq(value.as_str(), "yes") {
push(&[name])?;
} else if unicase::eq(value.as_str(), "no") {
push(&["No ", value])?;
} else {
push(&[value])?;
}
}
}
if self.show_region {
let region = self.run.metadata.region_name();
if !region.is_empty() {
push(&[region])?;
}
}
if self.show_platform {
let platform = self.run.metadata.platform_name();
let uses_emulator = self.run.metadata.uses_emulator();
match (!platform.is_empty(), uses_emulator) {
(true, true) => push(&[platform, " Emulator"])?,
(true, false) => push(&[platform])?,
(false, true) => push(&["Emulator"])?,
_ => (),
}
}
if !after_parenthesis.is_empty() {
f.write_str(after_parenthesis)?;
} else if !is_empty {
f.write_str(")")?;
}
Ok(())
}
}