Skip to content

Commit c28443e

Browse files
committed
Implement parsing the segment groups
Parsing the segment groups works now and it automatically makes sure the ranges are sorted and don't overlap. There are two remaining problems here though. One is that the group name doesn't differentiate between being empty and not existing here. I don't think we want to differentiate those two case long term anyway as it doesn't make much sense in the Run Editor to do so either. The other problem is that it parses segment groups right now that may be out of the total amount of segments' range. That shouldn't cause any major problems, but those ranges probably don't interact well with the Run Editor, so we need to be wary of that.
1 parent ab5208b commit c28443e

File tree

7 files changed

+79
-11
lines changed

7 files changed

+79
-11
lines changed

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
#![allow(
1313
clippy::block_in_if_condition_stmt,
1414
clippy::redundant_closure_call,
15-
clippy::new_ret_no_self
15+
clippy::new_ret_no_self,
16+
clippy::single_char_pattern, // https://github.com/rust-lang/rust-clippy/issues/3813
1617
)]
1718

1819
//! livesplit-core is a library that provides a lot of functionality for creating a speedrun timer.

src/run/parser/livesplit.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,9 @@ pub fn parse<R: BufRead>(source: R, path: Option<PathBuf>) -> Result<Run> {
438438
let mut run = Run::new();
439439

440440
let mut required_flags = 0u8;
441+
let mut version = Version(1, 0, 0, 0);
441442

442443
parse_base(reader, &mut buf, b"Run", |reader, tag| {
443-
let mut version = Version(1, 0, 0, 0);
444444
type_hint(optional_attribute_err(&tag, b"version", |t| {
445445
version = parse_version(t)?;
446446
Ok(())
@@ -480,6 +480,38 @@ pub fn parse<R: BufRead>(source: R, path: Option<PathBuf>) -> Result<Run> {
480480
end_tag(reader, tag.into_buf())
481481
}
482482
})
483+
} else if tag.name() == b"SegmentGroups" {
484+
let mut unordered_groups = Vec::new();
485+
type_hint(parse_children(reader, tag.into_buf(), |reader, tag| {
486+
if tag.name() == b"SegmentGroup" {
487+
let (mut start, mut end) = (0, 0);
488+
type_hint(attribute_err(&tag, b"start", |t| {
489+
start = t.parse()?;
490+
Ok(())
491+
}))?;
492+
type_hint(attribute_err(&tag, b"end", |t| {
493+
end = t.parse()?;
494+
Ok(())
495+
}))?;
496+
497+
// TODO: An empty name is not equivalent to None outside
498+
// of parsing.
499+
let mut name = String::new();
500+
type_hint(text(reader, tag.into_buf(), |t| name = t.into_owned()))?;
501+
502+
unordered_groups.push(SegmentGroup::new_lossy(
503+
start,
504+
end,
505+
if name.is_empty() { None } else { Some(name) },
506+
));
507+
508+
Ok(())
509+
} else {
510+
end_tag(reader, tag.into_buf())
511+
}
512+
}))?;
513+
*run.segment_groups_mut() = SegmentGroups::from_vec_lossy(unordered_groups);
514+
Ok(())
483515
} else if tag.name() == b"AutoSplitterSettings" {
484516
let settings = run.auto_splitter_settings_mut();
485517
reencode_children(reader, tag.into_buf(), settings).map_err(Into::into)
@@ -493,21 +525,23 @@ pub fn parse<R: BufRead>(source: R, path: Option<PathBuf>) -> Result<Run> {
493525
return Err(Error::Xml(XmlError::ElementNotFound));
494526
}
495527

496-
import_subsplits(&mut run);
528+
if version < Version(1, 8, 0, 0) {
529+
import_legacy_subsplits(&mut run);
530+
}
497531

498532
run.set_path(path);
499533

500534
Ok(run)
501535
}
502536

503-
fn import_subsplits(run: &mut Run) {
537+
fn import_legacy_subsplits(run: &mut Run) {
504538
let mut groups = SegmentGroups::new();
505539
let mut current_group_start = None;
506540

507541
for (index, segment) in run.segments_mut().iter_mut().enumerate() {
508542
let name = segment.name_mut();
509543

510-
if name.starts_with('-') {
544+
if name.starts_with("-") {
511545
if current_group_start.is_none() {
512546
current_group_start = Some(index);
513547
}
@@ -517,7 +551,7 @@ fn import_subsplits(run: &mut Run) {
517551
.take()
518552
.map(|range_start| SegmentGroup::new(range_start, index + 1, None).unwrap());
519553

520-
if name.starts_with('{') {
554+
if name.starts_with("{") {
521555
let mut iter = name[1..].splitn(2, '}');
522556
if let (Some(group_name), Some(segment_name)) = (iter.next(), iter.next()) {
523557
let segment_name = segment_name.trim_start();

src/run/parser/shit_split.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub fn parse<R: BufRead>(source: R) -> Result<Run> {
4646

4747
let mut splits = line.split('|');
4848
let category_name = splits.next().ok_or(Error::ExpectedCategoryName)?;
49-
if !category_name.starts_with('#') {
49+
if !category_name.starts_with("#") {
5050
return Err(Error::ExpectedCategoryName);
5151
}
5252

@@ -68,7 +68,7 @@ pub fn parse<R: BufRead>(source: R) -> Result<Run> {
6868
let mut has_acts = false;
6969
while let Some(line) = next_line {
7070
let line = line?;
71-
if line.starts_with('*') {
71+
if line.starts_with("*") {
7272
run.push_segment(Segment::new(&line[1..]));
7373
has_acts = true;
7474
next_line = lines.next();

src/run/segment_groups.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ impl SegmentGroup {
2323
}
2424
}
2525

26+
pub fn new_lossy(start: usize, end: usize, name: Option<String>) -> Self {
27+
Self {
28+
start,
29+
end: end.max(start + 1),
30+
name,
31+
}
32+
}
33+
2634
pub fn set_name(&mut self, name: Option<String>) {
2735
self.name = name;
2836
}
@@ -48,6 +56,17 @@ impl SegmentGroups {
4856
Default::default()
4957
}
5058

59+
pub fn from_vec_lossy(mut unordered_groups: Vec<SegmentGroup>) -> Self {
60+
unordered_groups.sort_unstable_by_key(|g| g.start);
61+
let mut min_start = 0;
62+
for group in &mut unordered_groups {
63+
group.start = group.start.max(min_start);
64+
group.end = group.end.max(group.start + 1);
65+
min_start = group.end;
66+
}
67+
Self(unordered_groups)
68+
}
69+
5170
// TODO: Implement iterator instead (look at SegmentHistory)
5271
pub fn groups(&self) -> &[SegmentGroup] {
5372
&self.0

src/timing/time_span.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ impl FromStr for TimeSpan {
7979
type Err = ParseError;
8080

8181
fn from_str(mut text: &str) -> Result<Self, ParseError> {
82-
let factor = if text.starts_with('-') {
82+
let factor = if text.starts_with("-") {
8383
text = &text[1..];
8484
-1.0
85-
} else if text.starts_with('−') {
85+
} else if text.starts_with("−") {
8686
text = &text[3..];
8787
-1.0
8888
} else {

tests/run_files/segment_groups.lss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?xml version="1.0" encoding="UTF-8"?><Run version="1.8.0"><GameIcon/><GameName>Celeste</GameName><CategoryName>Any%</CategoryName><Metadata><Run id=""/><Platform usesEmulator="False">PC</Platform><Region/><Variables><Variable name="Game Version">1.2.1.5</Variable></Variables></Metadata><Offset>00:00:00.0000000</Offset><AttemptCount>0</AttemptCount><AttemptHistory/><Segments><Segment><Name>Prologue</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Crossing</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Chasm</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Forsaken City</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Intervention</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Awake</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Old Site</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Huge Mess</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Elevator Shaft</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Presidential Suite</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Celestial Resort</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Shrine</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Old Trial</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Cliff Face</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Golden Ridge</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Depths</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Unravelling</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Search</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Rescue</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Mirror Temple</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Lake</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Hollows</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Reflection</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Rock Bottom</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Resolution</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>Reflection</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>500M</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>1000M</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>1500M</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>2000M</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>2500M</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>3000M</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment><Segment><Name>The Summit</Name><Icon/><SplitTimes><SplitTime name="Personal Best"/></SplitTimes><BestSegmentTime/><SegmentHistory/></Segment></Segments><SegmentGroups><SegmentGroup start="1" end="4"/><SegmentGroup start="4" end="7"/><SegmentGroup start="7" end="11"/><SegmentGroup start="11" end="15"/><SegmentGroup start="15" end="20"/><SegmentGroup start="20" end="26"/><SegmentGroup start="26" end="33"/></SegmentGroups><AutoSplitterSettings><Splits><Split>Prologue</Split><Split>Chapter1Checkpoint1</Split><Split>Chapter1Checkpoint2</Split><Split>Chapter1</Split><Split>Chapter2Checkpoint1</Split><Split>Chapter2Checkpoint2</Split><Split>Chapter2</Split><Split>Chapter3Checkpoint1</Split><Split>Chapter3Checkpoint2</Split><Split>Chapter3Checkpoint3</Split><Split>Chapter3</Split><Split>Chapter4Checkpoint1</Split><Split>Chapter4Checkpoint2</Split><Split>Chapter4Checkpoint3</Split><Split>Chapter4</Split><Split>Chapter5Checkpoint1</Split><Split>Chapter5Checkpoint2</Split><Split>Chapter5Checkpoint3</Split><Split>Chapter5Checkpoint4</Split><Split>Chapter5</Split><Split>Chapter6Checkpoint1</Split><Split>Chapter6Checkpoint2</Split><Split>Chapter6Checkpoint3</Split><Split>Chapter6Checkpoint4</Split><Split>Chapter6Checkpoint5</Split><Split>Chapter6</Split><Split>Chapter7Checkpoint1</Split><Split>Chapter7Checkpoint2</Split><Split>Chapter7Checkpoint3</Split><Split>Chapter7Checkpoint4</Split><Split>Chapter7Checkpoint5</Split><Split>Chapter7Checkpoint6</Split><Split>Chapter7</Split><Split>Epilogue</Split></Splits></AutoSplitterSettings></Run>

tests/split_parsing.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod parse {
44
source_live_timer, splits_io, splitterz, time_split_tracker, urn, worstrun, wsplit,
55
TimerKind,
66
};
7-
use livesplit_core::{analysis::total_playtime, Run, TimeSpan};
7+
use livesplit_core::{analysis::total_playtime, run::SegmentGroup, Run, TimeSpan};
88
use std::fs::File;
99
use std::io::{BufReader, Cursor};
1010

@@ -66,6 +66,19 @@ mod parse {
6666
livesplit("tests/run_files/Celeste - Any% (1.2.1.5).lss");
6767
}
6868

69+
#[test]
70+
fn segment_groups() {
71+
let run = livesplit("tests/run_files/segment_groups.lss");
72+
let groups = run.segment_groups().groups();
73+
assert_eq!(groups[0], SegmentGroup::new(1, 4, None).unwrap());
74+
assert_eq!(groups[1], SegmentGroup::new(4, 7, None).unwrap());
75+
assert_eq!(groups[2], SegmentGroup::new(7, 11, None).unwrap());
76+
assert_eq!(groups[3], SegmentGroup::new(11, 15, None).unwrap());
77+
assert_eq!(groups[4], SegmentGroup::new(15, 20, None).unwrap());
78+
assert_eq!(groups[5], SegmentGroup::new(20, 26, None).unwrap());
79+
assert_eq!(groups[6], SegmentGroup::new(26, 33, None).unwrap());
80+
}
81+
6982
#[test]
7083
fn livesplit_attempt_ended_bug() {
7184
let run = livesplit("tests/run_files/livesplit_attempt_ended_bug.lss");

0 commit comments

Comments
 (0)