From 2f6e651b9f7f294213150e62a3d16626de249e45 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Fri, 20 Feb 2026 18:54:59 +0100 Subject: [PATCH] Update OpenSplit Parser It matches the current master branch now: https://github.com/ZellyDev-Games/OpenSplit/blob/e10713484d47fdaa58ce5ae1eadca76effd6c862/session/splitfile.go --- src/run/parser/opensplit.rs | 161 +++++++++++++++++++--------------- tests/run_files/OpenSplit.osf | 2 +- 2 files changed, 89 insertions(+), 74 deletions(-) diff --git a/src/run/parser/opensplit.rs b/src/run/parser/opensplit.rs index 7a5463cf..baaff0d3 100644 --- a/src/run/parser/opensplit.rs +++ b/src/run/parser/opensplit.rs @@ -3,7 +3,7 @@ // https://github.com/ZellyDev-Games/OpenSplit use crate::{Run, Segment, Time, TimeSpan, platform::prelude::*}; -use alloc::borrow::Cow; +use alloc::{borrow::Cow, collections::BTreeMap}; use core::result::Result as StdResult; use serde_derive::Deserialize; use serde_json::Error as JsonError; @@ -30,26 +30,29 @@ struct SplitFilePayload<'a> { game_name: Cow<'a, str>, #[serde(borrow)] game_category: Cow<'a, str>, - segments: Option>>, + #[serde(default)] + segments: Vec>, attempts: u32, - runs: Option>>, + #[serde(default)] + runs: Vec>, + #[serde(default)] + offset: i64, + #[serde(default, borrow)] + platform: Cow<'a, str>, } #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] struct RunPayload<'a> { total_time: i64, completed: bool, - #[serde(borrow)] - split_payloads: Option>>, + #[serde(default, borrow)] + splits: BTreeMap, SplitPayload>, } #[derive(Deserialize)] -struct SplitPayload<'a> { - #[serde(borrow)] - split_segment_id: Cow<'a, str>, - // FIXME: Is current_time the correct field? - // current_time: TimeSpan, +struct SplitPayload { + #[allow(dead_code)] + current_cumulative: i64, current_duration: i64, } @@ -59,9 +62,10 @@ struct SegmentPayload<'a> { id: Cow<'a, str>, #[serde(borrow)] name: Cow<'a, str>, - best_time: TimeSpan, - // FIXME: Would need to be stored as part of the segment history - // average_time: TimeSpan, + gold: i64, + pb: i64, + #[serde(default, borrow)] + children: Vec>, } fn nullable(real_time: TimeSpan) -> Time { @@ -75,8 +79,24 @@ fn nullable(real_time: TimeSpan) -> Time { Time::new().with_real_time(real_time) } -fn integer_time(nanos: i64) -> TimeSpan { - crate::platform::Duration::nanoseconds(nanos).into() +fn integer_time(milliseconds: i64) -> TimeSpan { + crate::platform::Duration::milliseconds(milliseconds).into() +} + +fn flatten_leaf_segments<'a>(segments: Vec>) -> Vec> { + let mut leaf_segments = Vec::with_capacity(segments.len()); + let mut stack = Vec::with_capacity(segments.len()); + stack.extend(segments.into_iter().rev()); + + while let Some(mut segment) = stack.pop() { + if segment.children.is_empty() { + leaf_segments.push(segment); + } else { + stack.extend(segment.children.drain(..).rev()); + } + } + + leaf_segments } /// Attempts to parse an OpenSplit splits file. @@ -89,70 +109,65 @@ pub fn parse(source: &str) -> Result { run.set_game_name(splits.game_name); run.set_category_name(splits.game_category); run.set_attempt_count(splits.attempts); + run.set_offset(integer_time(splits.offset)); + run.metadata_mut().set_platform_name(splits.platform); - if let Some(segments) = splits.segments { - let mut segment_ids = Vec::with_capacity(segments.len()); + let leaf_segments = flatten_leaf_segments(splits.segments); - for segment_payload in segments { - segment_ids.push(segment_payload.id); - let mut segment = Segment::new(segment_payload.name); - segment.set_personal_best_split_time(nullable(segment_payload.best_time)); - run.push_segment(segment); - } + let mut segment_ids = Vec::with_capacity(leaf_segments.len()); + let mut cumulative_pb = TimeSpan::zero(); - let mut attempt_history_index = 1; - - if let Some(runs) = splits.runs { - for run_payload in runs { - run.add_attempt_with_index( - Time::new().with_real_time(if run_payload.completed { - Some(integer_time(run_payload.total_time)) - } else { - None - }), - attempt_history_index, - None, - None, - None, - ); - - let mut current_time = 0; - let mut previous_idx = None; - - if let Some(split_payloads) = run_payload.split_payloads { - for split_payload in split_payloads { - if let Some(idx) = segment_ids - .iter() - .position(|id| *id == split_payload.split_segment_id) - && previous_idx.is_none_or(|prev| idx > prev) - { - let segment_time = split_payload.current_duration - current_time; - - run.segments_mut()[idx].segment_history_mut().insert( - attempt_history_index, - Time::new().with_real_time(Some(integer_time(segment_time))), - ); - - current_time = split_payload.current_duration; - previous_idx = Some(idx); - } - } - } - - attempt_history_index += 1; - } - } + for segment_payload in leaf_segments { + segment_ids.push(segment_payload.id); + + let mut segment = Segment::new(segment_payload.name); + + segment.set_best_segment_time(nullable(integer_time(segment_payload.gold))); + + cumulative_pb += integer_time(segment_payload.pb); + segment.set_personal_best_split_time(Time::new().with_real_time( + if segment_payload.pb != 0 { + Some(cumulative_pb) + } else { + None + }, + )); + + run.push_segment(segment); } - for segment in run.segments_mut() { - if let Some(segment_time) = segment - .segment_history() + let mut attempt_history_index = 1; + + for run_payload in splits.runs { + run.add_attempt_with_index( + Time::new().with_real_time(if run_payload.completed { + Some(integer_time(run_payload.total_time)) + } else { + None + }), + attempt_history_index, + None, + None, + None, + ); + + let last = segment_ids .iter() - .filter_map(|(_, time)| time.real_time) - .min() - { - segment.set_best_segment_time(Time::new().with_real_time(Some(segment_time))); + .enumerate() + .rfind(|(_, segment_id)| run_payload.splits.contains_key(*segment_id)) + .map_or(0, |(index, _)| index + 1); + + for (segment_id, segment) in segment_ids[..last].iter().zip(run.segments_mut()) { + let mut time = Time::new(); + if let Some(split_payload) = run_payload.splits.get(segment_id) { + time = time.with_real_time(Some(integer_time(split_payload.current_duration))); + } + segment + .segment_history_mut() + .insert(attempt_history_index, time); } + + attempt_history_index += 1; } Ok(run) diff --git a/tests/run_files/OpenSplit.osf b/tests/run_files/OpenSplit.osf index 457d89c8..5abf2800 100644 --- a/tests/run_files/OpenSplit.osf +++ b/tests/run_files/OpenSplit.osf @@ -1 +1 @@ -{"id":"f348af3a-6dd5-4d56-b8d2-e2ee9f34d225","version":1,"game_name":"Foo","game_category":"Bar","segments":[{"id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","name":"A","best_time":"0:01:00.00","average_time":"0:00:00.00"},{"id":"ee1f9846-c925-4a35-909b-d9c781bab09a","name":"B","best_time":"0:02:00.00","average_time":"0:00:00.00"}],"attempts":9,"runs":[{"id":"7de244fb-d7d2-494a-83f7-e287da7484eb","splitFileVersion":1,"totalTime":13186244361,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:10.45","current_duration":10450254457},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:13.17","current_duration":13170253759}]},{"id":"f9a42348-ecbf-4bdd-9836-f750f078a15d","splitFileVersion":1,"totalTime":0,"completed":false,"splitPayloads":null},{"id":"f9a42348-ecbf-4bdd-9836-f750f078a15d","splitFileVersion":1,"totalTime":0,"completed":false,"splitPayloads":null},{"id":"f70c7e2a-0d2b-4a45-ac67-2a5584c5ec3c","splitFileVersion":1,"totalTime":2848150445,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:01.45","current_duration":1454764695},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:02.83","current_duration":2834764981}]},{"id":"22747b03-fd61-4c44-9e33-c36cb4d00bed","splitFileVersion":1,"totalTime":0,"completed":false,"splitPayloads":null},{"id":"f1a96f85-f9e3-477a-883f-68285b8b08c1","splitFileVersion":1,"totalTime":5029069465,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:02.39","current_duration":2395045999},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:05.02","current_duration":5021429184}]},{"id":"e47b283c-fa10-42ef-bb33-de6a34051826","splitFileVersion":1,"totalTime":4298421810,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:02.97","current_duration":2979723253},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:04.27","current_duration":4279724348}]},{"id":"ca05f580-0fb2-42ba-8c1d-3b1c751f8385","splitFileVersion":1,"totalTime":2274795393,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:01.24","current_duration":1242464622},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:02.26","current_duration":2262464771}]},{"id":"e1da3ec4-7161-400b-8d66-206ca375a191","splitFileVersion":1,"totalTime":1914949468,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:00.84","current_duration":840096903},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:01.90","current_duration":1900097400}]},{"id":"3a9acc19-0960-406d-bf49-efcdd14c9c57","splitFileVersion":1,"totalTime":1603571992,"completed":true,"splitPayloads":[{"split_index":0,"split_segment_id":"0fc8317a-454a-4799-95f9-9bb4eb45a2b6","current_time":"0:00:00.87","current_duration":870589603},{"split_index":1,"split_segment_id":"ee1f9846-c925-4a35-909b-d9c781bab09a","current_time":"0:00:01.59","current_duration":1590587830}]}]} +{"id":"dd58a2a7-3474-4fc9-9b80-df1d0795663c","version":0,"attempts":2,"game_name":"Game","game_category":"Category","window_x":10,"window_y":135,"window_height":550,"window_width":350,"runs":[{"id":"f4994c82-cfaf-4bd8-9353-b8ed233c45ff","split_file_version":0,"total_time":13782,"splits":{"3c9c6862-da93-4476-a53e-9d44a48a62d7":{"split_segment_id":"3c9c6862-da93-4476-a53e-9d44a48a62d7","current_cumulative":11562,"current_duration":6140},"797aeacb-0042-4cd0-aaf4-c381ecc05494":{"split_segment_id":"797aeacb-0042-4cd0-aaf4-c381ecc05494","current_cumulative":13782,"current_duration":2220},"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b":{"split_segment_id":"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b","current_cumulative":5422,"current_duration":5422}},"leaf_segments":null,"completed":false},{"id":"afcd5acd-2222-483f-bace-46f42dc32992","split_file_version":0,"total_time":5618,"splits":{"3c9c6862-da93-4476-a53e-9d44a48a62d7":{"split_segment_id":"3c9c6862-da93-4476-a53e-9d44a48a62d7","current_cumulative":3298,"current_duration":2120},"797aeacb-0042-4cd0-aaf4-c381ecc05494":{"split_segment_id":"797aeacb-0042-4cd0-aaf4-c381ecc05494","current_cumulative":5618,"current_duration":2320},"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b":{"split_segment_id":"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b","current_cumulative":1178,"current_duration":1178}},"leaf_segments":null,"completed":false}],"segments":[{"id":"baddf90c-5638-4216-81e3-396701dfd280","name":"Foo","gold":0,"average":0,"pb":0,"children":[{"id":"ef731019-58f0-4c0b-87b8-297c2ec20223","name":"Sub","gold":0,"average":0,"pb":0,"children":[{"id":"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b","name":"Another Sub","gold":1178,"average":3300,"pb":1178,"children":[]}]}]},{"id":"3c9c6862-da93-4476-a53e-9d44a48a62d7","name":"Baz","gold":2120,"average":4130,"pb":2120,"children":[]},{"id":"797aeacb-0042-4cd0-aaf4-c381ecc05494","name":"End","gold":2220,"average":2270,"pb":2320,"children":[]}],"sob":5518,"pb":{"id":"afcd5acd-2222-483f-bace-46f42dc32992","split_file_version":0,"total_time":5618,"splits":{"3c9c6862-da93-4476-a53e-9d44a48a62d7":{"split_segment_id":"3c9c6862-da93-4476-a53e-9d44a48a62d7","current_cumulative":3298,"current_duration":2120},"797aeacb-0042-4cd0-aaf4-c381ecc05494":{"split_segment_id":"797aeacb-0042-4cd0-aaf4-c381ecc05494","current_cumulative":5618,"current_duration":2320},"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b":{"split_segment_id":"d48dfdd8-8bc7-4b4a-a07d-030e048a8e3b","current_cumulative":1178,"current_duration":1178}},"leaf_segments":null,"completed":false},"offset":0,"platform":"GameCube"}