Skip to content

Commit

Permalink
Allow copying comparisons (LiveSplit#812)
Browse files Browse the repository at this point in the history
This adds functionality to the run editor to allow copying comparisons.
This is not only useful for copying custom comparisons, but also for
"baking" the times of a generated comparison, such as the `Latest Run` or
the `Average Segments` to a custom comparison that you can keep around
as long as you want. This is somewhat also meant to replace the
functionality of storing the current run as a Personal Best. Instead you
can just store the `Latest Run` as a custom comparison after you are
done with it.
  • Loading branch information
CryZe authored and AlexKnauth committed Jun 13, 2024
1 parent 4f0f5e7 commit 707ee63
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 40 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ libc = { version = "0.2.101", optional = true }
seahash = "4.1.0"

[target.'cfg(windows)'.dev-dependencies]
sysinfo = { version = "0.30.0", default-features = false }
winreg = "0.52.0"

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
criterion = "0.5.0"
Expand Down
14 changes: 13 additions & 1 deletion capi/src/run_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ pub unsafe extern "C" fn RunEditor_active_parse_and_set_comparison_time(
}

/// Adds a new custom comparison. It can't be added if it starts with
/// `[Race]` or already exists.
/// `[Race]` or it already exists.
#[no_mangle]
pub unsafe extern "C" fn RunEditor_add_comparison(
this: &mut RunEditor,
Expand Down Expand Up @@ -425,6 +425,18 @@ pub unsafe extern "C" fn RunEditor_parse_and_generate_goal_comparison(
this.parse_and_generate_goal_comparison(str(time)).is_ok()
}

/// Copies a comparison with the given name as a new custom comparison with the
/// new name provided. It can't be added if it starts with `[Race]` or it
/// already exists. The old comparison needs to exist.
#[no_mangle]
pub unsafe extern "C" fn RunEditor_copy_comparison(
this: &mut RunEditor,
old_name: *const c_char,
new_name: *const c_char,
) -> bool {
this.copy_comparison(str(old_name), str(new_name)).is_ok()
}

/// Clears out the Attempt History and the Segment Histories of all the
/// segments.
#[no_mangle]
Expand Down
2 changes: 1 addition & 1 deletion src/comparison/latest_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub const NAME: &str = "Latest Run";

fn generate(segments: &mut [Segment], method: TimingMethod) {
let mut attempt_id = None;
for segment in segments.iter_mut().rev() {
for segment in segments.iter().rev() {
if let Some(max_index) = segment.segment_history().try_get_max_index() {
attempt_id = Some(max_index);
break;
Expand Down
42 changes: 37 additions & 5 deletions src/run/editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! current state of the editor as state objects that can be visualized by any
//! kind of User Interface.

use super::{ComparisonError, ComparisonResult, LinkedLayout};
use super::{AddComparisonError, CopyComparisonError, LinkedLayout};
use crate::{
comparison,
platform::prelude::*,
Expand Down Expand Up @@ -63,7 +63,7 @@ pub enum RenameError {
/// Name was invalid.
InvalidName {
/// The underlying error.
source: ComparisonError,
source: AddComparisonError,
},
}

Expand Down Expand Up @@ -743,8 +743,11 @@ impl Editor {
}

/// Adds a new custom comparison. It can't be added if it starts with
/// `[Race]` or already exists.
pub fn add_comparison<S: PopulateString>(&mut self, comparison: S) -> ComparisonResult<()> {
/// `[Race]` or it already exists.
pub fn add_comparison<S: PopulateString>(
&mut self,
comparison: S,
) -> Result<(), AddComparisonError> {
self.run.add_custom_comparison(comparison)?;
self.fix();
Ok(())
Expand All @@ -753,7 +756,11 @@ impl Editor {
/// 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(&mut self, run: &Run, comparison: &str) -> ComparisonResult<()> {
pub fn import_comparison(
&mut self,
run: &Run,
comparison: &str,
) -> Result<(), AddComparisonError> {
self.run.add_custom_comparison(comparison)?;

let mut remaining_segments = self.run.segments_mut().as_mut_slice();
Expand Down Expand Up @@ -908,6 +915,31 @@ impl Editor {
Ok(())
}

/// Copies a comparison with the given name as a new custom comparison with
/// the new name provided. It can't be added if it starts with `[Race]` or
/// it already exists. The old comparison needs to exist.
pub fn copy_comparison(
&mut self,
old_name: &str,
new_name: &str,
) -> Result<(), CopyComparisonError> {
if !self.run.comparisons().any(|c| c == old_name) {
return Err(CopyComparisonError::NoSuchComparison);
}

self.run
.add_custom_comparison(new_name)
.map_err(|source| CopyComparisonError::AddComparison { source })?;

for segment in self.run.segments_mut() {
*segment.comparison_mut(new_name) = segment.comparison(old_name);
}

self.raise_run_edited();

Ok(())
}

/// Clears out the Attempt History and the Segment Histories of all the
/// segments.
pub fn clear_history(&mut self) {
Expand Down
61 changes: 55 additions & 6 deletions src/run/editor/tests/comparison.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::run::{ComparisonError, Editor, RenameError};
use crate::{Run, Segment};
use crate::{
comparison::{best_segments, personal_best},
run::{AddComparisonError, CopyComparisonError, Editor, RenameError},
Run, Segment,
};

#[test]
fn adding_a_new_comparison_works() {
Expand All @@ -15,8 +18,54 @@ fn adding_a_duplicate_fails() {
let mut run = Run::new();
run.push_segment(Segment::new("s"));
let mut editor = Editor::new(run).unwrap();
let c = editor.add_comparison("Best Segments");
assert_eq!(c, Err(ComparisonError::DuplicateName));
let c = editor.add_comparison(best_segments::NAME);
assert_eq!(c, Err(AddComparisonError::DuplicateName));
}

#[test]
fn copying_a_comparison_works() {
let mut run = Run::new();
run.push_segment(Segment::new("s"));
let mut editor = Editor::new(run).unwrap();
let c = editor.copy_comparison(personal_best::NAME, "My Comparison");
assert_eq!(c, Ok(()));
}

#[test]
fn copying_a_duplicate_fails() {
let mut run = Run::new();
run.push_segment(Segment::new("s"));
let mut editor = Editor::new(run).unwrap();
let c = editor.copy_comparison(personal_best::NAME, best_segments::NAME);
assert_eq!(
c,
Err(CopyComparisonError::AddComparison {
source: AddComparisonError::DuplicateName,
})
);
}

#[test]
fn copying_to_a_race_name_fails() {
let mut run = Run::new();
run.push_segment(Segment::new("s"));
let mut editor = Editor::new(run).unwrap();
let c = editor.copy_comparison(personal_best::NAME, "[Race]Custom");
assert_eq!(
c,
Err(CopyComparisonError::AddComparison {
source: AddComparisonError::NameStartsWithRace,
})
);
}

#[test]
fn copying_an_inexistent_comparison_fails() {
let mut run = Run::new();
run.push_segment(Segment::new("s"));
let mut editor = Editor::new(run).unwrap();
let c = editor.copy_comparison("My Comparison", "My Comparison 2");
assert_eq!(c, Err(CopyComparisonError::NoSuchComparison));
}

#[test]
Expand Down Expand Up @@ -49,7 +98,7 @@ fn renaming_to_a_race_name_fails() {
assert_eq!(
c,
Err(RenameError::InvalidName {
source: ComparisonError::NameStartsWithRace
source: AddComparisonError::NameStartsWithRace
})
);
}
Expand All @@ -65,7 +114,7 @@ fn renaming_to_an_existing_name_fails() {
assert_eq!(
c,
Err(RenameError::InvalidName {
source: ComparisonError::DuplicateName
source: AddComparisonError::DuplicateName
})
);
}
Expand Down
25 changes: 17 additions & 8 deletions src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,26 @@ impl PartialEq for ComparisonGenerators {
}
}

/// Error type for an invalid comparison name
/// Error type for an invalid comparison name to be added.
#[derive(PartialEq, Eq, Debug, snafu::Snafu)]
pub enum ComparisonError {
pub enum AddComparisonError {
/// Comparison name starts with "\[Race\]".
NameStartsWithRace,
/// Comparison name is a duplicate.
DuplicateName,
}

/// Result type for an invalid comparison name
pub type ComparisonResult<T> = Result<T, ComparisonError>;
/// Error type for copying a comparison.
#[derive(PartialEq, Eq, Debug, snafu::Snafu)]
pub enum CopyComparisonError {
/// There is no comparison with the provided name to copy.
NoSuchComparison,
/// The new comparison could not be added.
AddComparison {
/// The underlying error.
source: AddComparisonError,
},
}

impl Run {
/// Creates a new Run object with no segments.
Expand Down Expand Up @@ -485,7 +494,7 @@ impl Run {
/// Adds a new custom comparison. If a custom comparison with that name
/// already exists, it is not added.
#[inline]
pub fn add_custom_comparison<S>(&mut self, comparison: S) -> ComparisonResult<()>
pub fn add_custom_comparison<S>(&mut self, comparison: S) -> Result<(), AddComparisonError>
where
S: PopulateString,
{
Expand Down Expand Up @@ -798,11 +807,11 @@ impl Run {

/// Checks a given name against the current comparisons in the Run to
/// ensure that it is valid for use.
pub fn validate_comparison_name(&self, new: &str) -> ComparisonResult<()> {
pub fn validate_comparison_name(&self, new: &str) -> Result<(), AddComparisonError> {
if new.starts_with(RACE_COMPARISON_PREFIX) {
Err(ComparisonError::NameStartsWithRace)
Err(AddComparisonError::NameStartsWithRace)
} else if self.comparisons().any(|c| c == new) {
Err(ComparisonError::DuplicateName)
Err(AddComparisonError::DuplicateName)
} else {
Ok(())
}
Expand Down
12 changes: 6 additions & 6 deletions src/run/parser/livesplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::{
platform::prelude::*,
run::{ComparisonError, LinkedLayout},
run::{AddComparisonError, LinkedLayout},
settings::Image,
util::{
ascii_char::AsciiChar,
Expand Down Expand Up @@ -58,7 +58,7 @@ pub enum Error {
/// Parsed comparison has an invalid name.
InvalidComparisonName {
/// The underlying error.
source: ComparisonError,
source: AddComparisonError,
},
/// Failed to parse a boolean.
ParseBool,
Expand Down Expand Up @@ -88,8 +88,8 @@ impl From<crate::timing::ParseError> for Error {
}
}

impl From<ComparisonError> for Error {
fn from(source: ComparisonError) -> Self {
impl From<AddComparisonError> for Error {
fn from(source: AddComparisonError) -> Self {
Self::InvalidComparisonName { source }
}
}
Expand Down Expand Up @@ -314,10 +314,10 @@ fn parse_segment(
} else {
time_old(reader, |t| *segment.comparison_mut(&comparison) = t)?;
}
if let Err(ComparisonError::NameStartsWithRace) =
if let Err(AddComparisonError::NameStartsWithRace) =
run.add_custom_comparison(comparison)
{
return Err(ComparisonError::NameStartsWithRace.into());
return Err(AddComparisonError::NameStartsWithRace.into());
}
Ok(())
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/run/parser/time_split_tracker.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Provides the parser for Time Split Tracker splits files.

use super::super::ComparisonError;
use super::super::AddComparisonError;
use crate::{
comparison::RACE_COMPARISON_PREFIX,
platform::{path::Path, prelude::*},
Expand Down Expand Up @@ -124,13 +124,13 @@ pub fn parse(
loop {
match run.add_custom_comparison(&**comparison) {
Ok(_) => break,
Err(ComparisonError::DuplicateName) => {
Err(AddComparisonError::DuplicateName) => {
let comparison = comparison.to_mut();
comparison.drain(orig_len..);
let _ = write!(comparison, " {number}");
number += 1;
}
Err(ComparisonError::NameStartsWithRace) => {
Err(AddComparisonError::NameStartsWithRace) => {
let comparison = comparison.to_mut();
// After removing the `[Race]`, there might be some
// whitespace we want to trim too.
Expand Down
4 changes: 2 additions & 2 deletions src/run/tests/comparison.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::run::{ComparisonError, Run};
use crate::run::{AddComparisonError, Run};

#[test]
fn adding_a_new_comparison_works() {
Expand All @@ -11,5 +11,5 @@ fn adding_a_new_comparison_works() {
fn adding_a_duplicate_fails() {
let mut run = Run::new();
let c = run.add_custom_comparison("Best Segments");
assert_eq!(c, Err(ComparisonError::DuplicateName));
assert_eq!(c, Err(AddComparisonError::DuplicateName));
}
19 changes: 12 additions & 7 deletions tests/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,18 @@ fn default() {
#[cfg(all(feature = "font-loading", windows))]
#[test]
fn font_fallback() {
let build_number: u64 = sysinfo::System::kernel_version().unwrap().parse().unwrap();
let hklm = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE);
let cur_ver = hklm
.open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")
.unwrap();
let build_number: String = cur_ver.get_value("CurrentBuildNumber").unwrap();
let build_number: u64 = build_number.parse().unwrap();
let revision: u32 = cur_ver.get_value("UBR").unwrap();

if build_number < 22000 {
// The hash is different before Windows 11.
if (build_number, revision) < (22631, 3672) {
// The hash is different before Windows 11 23H2 (end of May 2024 update).
println!(
"Skipping font fallback test on Windows with build number {}.",
build_number
"Skipping font fallback test on Windows with build number {build_number}.{revision}.",
);
return;
}
Expand Down Expand Up @@ -122,8 +127,8 @@ fn font_fallback() {
check(
&state,
&image_cache,
"d908fda633352ba5",
"299f188d2a8ccf5d",
"e3c55d333d082bab",
"267615d875c8cf61",
"font_fallback",
);
}
Expand Down

0 comments on commit 707ee63

Please sign in to comment.