Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 88 additions & 73 deletions src/run/parser/opensplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,26 +30,29 @@ struct SplitFilePayload<'a> {
game_name: Cow<'a, str>,
#[serde(borrow)]
game_category: Cow<'a, str>,
segments: Option<Vec<SegmentPayload<'a>>>,
#[serde(default)]
segments: Vec<SegmentPayload<'a>>,
attempts: u32,
runs: Option<Vec<RunPayload<'a>>>,
#[serde(default)]
runs: Vec<RunPayload<'a>>,
#[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<Vec<SplitPayload<'a>>>,
#[serde(default, borrow)]
splits: BTreeMap<Cow<'a, str>, 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,
}

Expand All @@ -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<SegmentPayload<'a>>,
}

fn nullable(real_time: TimeSpan) -> Time {
Expand All @@ -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<SegmentPayload<'a>>) -> Vec<SegmentPayload<'a>> {
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.
Expand All @@ -89,70 +109,65 @@ pub fn parse(source: &str) -> Result<Run> {
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)
Expand Down
2 changes: 1 addition & 1 deletion tests/run_files/OpenSplit.osf
Original file line number Diff line number Diff line change
@@ -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"}
Loading