Repository: LiveSplit/livesplit-core
Pull request #118 (2018-03-26 00:06:11)

Correct Portal 2 Live Timer parsing
Correct Map Names & order
Handle merging of consecutive maps with the same name

CryZe: Maybe we need something like cargo bomb / crater to parse all splits on splits i/o whenever we make changes to the parsers, so we know we don't break something.
CryZe: Like for example you changed the order, but I'm pretty sure this used to be the correct order. But maybe not. Who knows.
hallcristobal: It might have been at one point, i'm not sure I don't really follow Portal. I just know this is the order that I saw on https://wiki.sourceruns.org/wiki/Portal_2_Maps and in the PLT source
CryZe: Also it looks like Splitter Bomb couldn't even find a single successfully uploaded Portal 2 Live Timer splits file on splits i/o. So I guess there's no proof it ever worked with any actual splits file.

Patch:
diff --git a/src/run/parser/portal2_live_timer.rs b/src/run/parser/portal2_live_timer.rs
index 4065b62..387070b 100644
--- a/src/run/parser/portal2_live_timer.rs
+++ b/src/run/parser/portal2_live_timer.rs
@@ -69,8 +69,8 @@ static CHAPTERS: [(&str, &[&str]); 9] = [
             "sp_a2_ricochet",
             "sp_a2_bridge_intro",
             "sp_a2_bridge_the_gap",
-            "sp_a2_laser_relays",
             "sp_a2_turret_intro",
+            "sp_a2_laser_relays",
             "sp_a2_turret_blocker",
             "sp_a2_laser_vs_turret",
             "sp_a2_pull_the_rug",
@@ -113,8 +113,6 @@ static CHAPTERS: [(&str, &[&str]); 9] = [
         &[
             "sp_a3_speed_ramp",
             "sp_a3_speed_flings",
-            "sp_a3_speed_flings",
-            "sp_a3_speed_flings",
             "sp_a3_portal_intro",
             "sp_a3_end",
         ],
@@ -133,7 +131,6 @@ static CHAPTERS: [(&str, &[&str]); 9] = [
             "sp_a4_laser_platform",
             "sp_a4_speed_tb_catch",
             "sp_a4_jump_polarity",
-            "sp_a4_jump_polarity",
         ],
     ),
     (
@@ -154,23 +151,38 @@ pub fn parse<R: BufRead>(source: R) -> Result<Run> {
     run.set_game_name("Portal 2");
     run.set_category_name("Any%");
 
-    let mut lines = source.lines().peekable();
+    let mut lines = source.lines();
     lines.next(); // Skip the header
 
     let mut aggregate_ticks = 0.0;
 
+    let mut line = lines.next().ok_or(Error::ExpectedMap)??;
     for &(chapter_name, maps) in &CHAPTERS {
         for &map in maps {
-            let line = lines.next().ok_or(Error::ExpectedMap)??;
-            let mut splits = line.split(',');
-            let map_name = splits.next().ok_or(Error::ExpectedMapName)?;
-            if map_name != map {
-                return Err(Error::ExpectedDifferentMapName);
+            {
+                let mut splits = line.split(',');
+                let map_name = splits.next().ok_or(Error::ExpectedMapName)?;
+                if map_name != map {
+                    return Err(Error::ExpectedDifferentMapName);
+                }
+                let start_ticks: f64 = splits.next().ok_or(Error::ExpectedStartTicks)?.parse()?;
+                let end_ticks: f64 = splits.next().ok_or(Error::ExpectedEndTicks)?.parse()?;
+                let map_ticks = end_ticks - start_ticks;
+                aggregate_ticks += map_ticks;
+            }
+
+            while let Some(Ok(next_line)) = lines.next() {
+                line = next_line;
+                let mut splits = line.split(',');
+                if splits.next() == Some(map) {
+                    let start_ticks: f64 = splits.next().ok_or(Error::ExpectedStartTicks)?.parse()?;
+                    let end_ticks: f64 = splits.next().ok_or(Error::ExpectedEndTicks)?.parse()?;
+                    let map_ticks = end_ticks - start_ticks;
+                    aggregate_ticks += map_ticks;
+                } else {
+                    break;
+                }
             }
-            let start_ticks: f64 = splits.next().ok_or(Error::ExpectedStartTicks)?.parse()?;
-            let end_ticks: f64 = splits.next().ok_or(Error::ExpectedEndTicks)?.parse()?;
-            let map_ticks = end_ticks - start_ticks;
-            aggregate_ticks += map_ticks;
         }
 
         let time = GameTime(Some(TimeSpan::from_seconds(aggregate_ticks / 60.0))).into();


Test patch:
diff --git a/tests/parsing.rs b/tests/parsing.rs
index c71c001..dd28428 100644
--- a/tests/parsing.rs
+++ b/tests/parsing.rs
@@ -2,8 +2,8 @@ extern crate livesplit_core;
 
 mod parse {
     use livesplit_core::run::parser::{
-        composite, livesplit, llanfair, llanfair2, llanfair_gered, source_live_timer, splitterz,
-        time_split_tracker, urn, worstrun, wsplit, TimerKind,
+        composite, livesplit, llanfair, llanfair2, llanfair_gered, portal2_live_timer,
+        source_live_timer, splitterz, time_split_tracker, urn, worstrun, wsplit, TimerKind,
     };
     use livesplit_core::Run;
     use std::fs::File;
@@ -143,11 +143,28 @@ mod parse {
         source_live_timer::parse(file("tests/run_files/source_live_timer2.json")).unwrap();
     }
 
+    #[test]
+    fn portal2_live_timer() {
+        portal2_live_timer::parse(file("tests/run_files/portal2_live_timer1.csv")).unwrap();
+    }
+
+    #[test]
+    fn portal2_live_timer2() {
+        portal2_live_timer::parse(file("tests/run_files/portal2_live_timer2.csv")).unwrap();
+    }
+
     #[test]
     fn worstrun() {
         worstrun::parse(file("tests/run_files/worstrun.json")).unwrap();
     }
 
+    #[test]
+    fn portal2_live_timer_prefers_parsing_as_itself() {
+        let run =
+            composite::parse(file("tests/run_files/portal2_live_timer1.csv"), None, false).unwrap();
+        assert_eq!(run.kind, TimerKind::Portal2LiveTimer);
+    }
+
     #[test]
     fn worstrun_prefers_parsing_as_itself() {
         let run = composite::parse(file("tests/run_files/worstrun.json"), None, false).unwrap();
diff --git a/tests/run_files/portal2_live_timer1.csv b/tests/run_files/portal2_live_timer1.csv
new file mode 100644
index 0000000..fd0511a
--- /dev/null
+++ b/tests/run_files/portal2_live_timer1.csv
@@ -0,0 +1,63 @@
+map,tick_start,tick_stop
+sp_a1_intro1,686,21159
+sp_a1_intro2,0,3172
+sp_a1_intro3,0,3684
+sp_a1_intro4,0,3344
+sp_a1_intro5,0,3104
+sp_a1_intro6,0,4126
+sp_a1_intro7,0,6278
+sp_a1_wakeup,0,11096
+sp_a2_intro,0,7132
+sp_a2_laser_intro,0,2398
+sp_a2_laser_stairs,0,2796
+sp_a2_dual_lasers,0,2872
+sp_a2_laser_over_goo,0,3270
+sp_a2_catapult_intro,0,3220
+sp_a2_trust_fling,0,4560
+sp_a2_pit_flings,0,4928
+sp_a2_fizzler_intro,0,2690
+sp_a2_sphere_peek,0,8408
+sp_a2_ricochet,0,4514
+sp_a2_bridge_intro,0,5450
+sp_a2_bridge_the_gap,0,6122
+sp_a2_turret_intro,0,4624
+sp_a2_laser_relays,0,4816
+sp_a2_turret_blocker,0,4260
+sp_a2_laser_vs_turret,0,3310
+sp_a2_pull_the_rug,0,2954
+sp_a2_column_blocker,0,8314
+sp_a2_laser_chaining,0,7860
+sp_a2_triple_laser,0,2514
+sp_a2_bts1,0,4540
+sp_a2_bts2,0,4722
+sp_a2_bts3,0,8716
+sp_a2_bts4,0,11638
+sp_a2_bts5,0,5404
+sp_a2_bts6,0,3112
+sp_a2_core,0,21478
+sp_a3_00,0,4666
+sp_a3_01,0,9168
+sp_a3_03,0,9574
+sp_a3_jump_intro,0,10338
+sp_a3_bomb_flings,0,5664
+sp_a3_crazy_box,0,8980
+sp_a3_transition01,0,8206
+sp_a3_speed_ramp,0,12536
+sp_a3_speed_flings,0,13414
+sp_a3_portal_intro,0,14676
+sp_a3_end,0,8700
+sp_a4_intro,0,10526
+sp_a4_tb_intro,0,3166
+sp_a4_tb_trust_drop,0,3980
+sp_a4_tb_wall_button,0,5494
+sp_a4_tb_polarity,0,4814
+sp_a4_tb_catch,0,4162
+sp_a4_stop_the_box,0,4650
+sp_a4_laser_catapult,0,7770
+sp_a4_laser_platform,0,8802
+sp_a4_speed_tb_catch,0,8576
+sp_a4_jump_polarity,0,9394
+sp_a4_finale1,0,6572
+sp_a4_finale2,0,12360
+sp_a4_finale3,0,9890
+sp_a4_finale4,0,18134
diff --git a/tests/run_files/portal2_live_timer2.csv b/tests/run_files/portal2_live_timer2.csv
new file mode 100644
index 0000000..f3f0eec
--- /dev/null
+++ b/tests/run_files/portal2_live_timer2.csv
@@ -0,0 +1,67 @@
+map,tick_start,tick_stop
+sp_a1_intro1,686,21387
+sp_a1_intro2,0,3276
+sp_a1_intro3,0,3664
+sp_a1_intro4,0,3174
+sp_a1_intro5,0,3084
+sp_a1_intro6,0,4112
+sp_a1_intro7,0,6706
+sp_a1_wakeup,0,11062
+sp_a2_intro,0,7054
+sp_a2_laser_intro,0,2372
+sp_a2_laser_stairs,0,2788
+sp_a2_dual_lasers,0,2394
+sp_a2_laser_over_goo,0,3074
+sp_a2_catapult_intro,0,3414
+sp_a2_trust_fling,0,4624
+sp_a2_pit_flings,0,4752
+sp_a2_fizzler_intro,0,3184
+sp_a2_sphere_peek,0,7144
+sp_a2_ricochet,0,4552
+sp_a2_bridge_intro,0,5148
+sp_a2_bridge_the_gap,0,6082
+sp_a2_turret_intro,0,4340
+sp_a2_laser_relays,0,3552
+sp_a2_turret_blocker,0,4020
+sp_a2_laser_vs_turret,0,3412
+sp_a2_pull_the_rug,0,2412
+sp_a2_column_blocker,0,1672
+sp_a2_column_blocker,0,6517
+sp_a2_laser_chaining,0,6470
+sp_a2_triple_laser,0,2420
+sp_a2_bts1,0,4674
+sp_a2_bts2,0,4408
+sp_a2_bts3,0,8102
+sp_a2_bts4,0,11312
+sp_a2_bts5,0,5534
+sp_a2_bts6,0,3112
+sp_a2_core,0,21288
+sp_a3_00,0,4666
+sp_a3_01,0,9068
+sp_a3_03,0,8902
+sp_a3_jump_intro,0,9336
+sp_a3_bomb_flings,0,5702
+sp_a3_crazy_box,0,8810
+sp_a3_transition01,0,7838
+sp_a3_speed_ramp,0,8688
+sp_a3_speed_flings,0,9208
+sp_a3_speed_flings,0,5551
+sp_a3_portal_intro,0,14798
+sp_a3_end,0,17552
+sp_a4_intro,0,10722
+sp_a4_tb_intro,0,2988
+sp_a4_tb_trust_drop,0,3830
+sp_a4_tb_wall_button,0,5822
+sp_a4_tb_polarity,0,3220
+sp_a4_tb_catch,0,3444
+sp_a4_stop_the_box,0,3946
+sp_a4_laser_catapult,0,7582
+sp_a4_laser_platform,0,9220
+sp_a4_speed_tb_catch,0,5670
+sp_a4_speed_tb_catch,0,2011
+sp_a4_speed_tb_catch,0,961
+sp_a4_jump_polarity,0,6610
+sp_a4_finale1,0,6302
+sp_a4_finale2,0,8970
+sp_a4_finale3,0,8278
+sp_a4_finale4,0,19386