use super::Run;
use crate::{Result, error::DeError, types::TimeType};
use std::{cmp::min, io::Read};
use roxmltree::{Document, Node};
impl Run {
pub fn from_reader_lss(mut r: impl Read) -> Result<Self> {
let mut s = String::new();
r.read_to_string(&mut s)?;
Self::from_str_lss(&s)
}
fn from_str_lss(r: &str) -> Result<Self> {
let doc = Document::parse(r).map_err(DeError::Xml)?;
let ingame_time = r.contains("GameTime");
let mut r = Run {
ingame_time,
..Default::default()
};
let mut i = 0;
for node in doc.root().descendants() {
match node.tag_name().name() {
"GameName" => {
r.game_title = node
.text()
.map(|t| t.to_owned())
.unwrap_or_else(String::new);
}
"CategoryName" => {
r.category = node
.text()
.map(|t| t.to_owned())
.unwrap_or_else(String::new);
}
"Offset" => r.offset = node.text().map(parse_time).unwrap_or(TimeType::None),
"Segment" => {
add_segment(&node, &mut r, i);
i += 1;
}
_ => {}
}
}
println!("{r:?}");
r.verify()?;
r.normalize();
r.calc_avgs();
r.calc_segments();
Ok(r)
}
}
fn parse_time(time: &str) -> TimeType {
let (neg, t) = if let Some(t) = time.strip_prefix('-') {
(true, t)
} else {
(false, time)
};
let mut s = t.split(':').collect::<Vec<_>>();
let mut r = 0;
let l = s.pop().unwrap();
let (secs, msecs) = l.split_once('.').unwrap_or((l, "0"));
let msecs = format!("{:0<3}", &msecs[0..min(msecs.len(), 3)]);
r += msecs.parse().unwrap_or(0);
r += secs.parse().unwrap_or(0) * 1000;
if !s.is_empty() {
r += s.pop().unwrap().parse().unwrap_or(0) * 60000;
if !s.is_empty() {
r += s.pop().unwrap().parse().unwrap_or(0) * 3600000;
}
}
if neg {
r *= -1
}
TimeType::from(r)
}
fn add_segment(n: &Node, r: &mut Run, i: usize) {
if i != 0 {
r.segment_names.push(String::new());
r.rta_pb_splits.push(TimeType::None);
r.igt_pb_splits.push(TimeType::None);
r.rta_gold_segments.push(TimeType::None);
r.igt_gold_segments.push(TimeType::None);
r.rta_sum_segments.push((0, TimeType::None));
r.igt_sum_segments.push((0, TimeType::None));
r.rta_avg_segments.push(TimeType::None);
r.igt_avg_segments.push(TimeType::None);
}
for node in n.descendants() {
match node.tag_name().name() {
"Name" => {
r.segment_names[i] = node
.text()
.map(|t| t.to_owned())
.unwrap_or_else(String::new)
}
"SplitTime" => {
for d in node.descendants() {
if d.tag_name().name() == "RealTime" {
r.rta_pb_splits[i] = d.text().map(parse_time).unwrap_or(TimeType::None);
} else if d.tag_name().name() == "GameTime" {
r.igt_pb_splits[i] = d.text().map(parse_time).unwrap_or(TimeType::None);
}
}
}
"BestSegmentTime" => {
for d in node.descendants() {
if d.tag_name().name() == "RealTime" {
r.rta_gold_segments[i] = d.text().map(parse_time).unwrap_or(TimeType::None);
} else if d.tag_name().name() == "GameTime" {
r.igt_gold_segments[i] = d.text().map(parse_time).unwrap_or(TimeType::None);
}
}
}
"SegmentHistory" => {
for d in node.descendants() {
if d.tag_name().name() == "RealTime" {
r.rta_sum_segments[i].0 += 1;
r.rta_sum_segments[i].1 += parse_time(d.text().unwrap_or(""));
} else if d.tag_name().name() == "GameTime" {
r.igt_sum_segments[i].0 += 1;
r.igt_sum_segments[i].1 += parse_time(d.text().unwrap_or(""));
}
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_time_full() {
assert_eq!(parse_time("10:01:10.1010000"), TimeType::from(36070101));
assert_eq!(parse_time("1:25:36.4"), TimeType::from(5136400));
assert_eq!(parse_time("1:25:36"), TimeType::from(5136000));
}
#[test]
fn parse_time_mins() {
assert_eq!(parse_time("00001:10.101"), TimeType::from(70101));
assert_eq!(parse_time("1:10.101"), TimeType::from(70101));
}
#[test]
fn parse_time_secs() {
assert_eq!(parse_time("30.54321"), TimeType::from(30543));
assert_eq!(parse_time("301"), TimeType::from(301000));
}
#[test]
fn parse_time_negative() {
assert_eq!(parse_time("-1:25:36"), TimeType::from(-5136000));
assert_eq!(parse_time("-1:10.101"), TimeType::from(-70101));
assert_eq!(parse_time("-301"), TimeType::from(-301000));
}
#[test]
fn rtaonly_lss() {
let r = Run::from_str_lss(include_str!("../../tests/any%.lss")).unwrap();
use TimeType::Time;
let the_same_run = Run {
game_title: "The Legend of Zelda: Breath of the Wild".into(),
category: "Any%".into(),
ingame_time: false,
offset: Time(-2400),
segment_names: vec![
"yellow".into(),
"escbad".into(),
"magnet".into(),
"bobomb battlefield".into(),
"tree launch >:(".into(),
"bananalame/weapon theft".into(),
"ganon platoon".into(),
"idiot".into(),
"pig".into(),
],
rta_pb_splits: vec![
Time(295047),
Time(515607),
Time(727070),
Time(942882),
Time(1133341),
Time(1559189),
Time(1789243),
Time(2007331),
Time(2170346),
],
igt_pb_splits: vec![TimeType::None; 9],
rta_pb_segments: vec![
Time(295047),
Time(220560),
Time(211463),
Time(215812),
Time(190459),
Time(425848),
Time(230054),
Time(218088),
Time(163015),
],
igt_pb_segments: vec![TimeType::None; 9],
rta_gold_segments: vec![
Time(282399),
Time(209727),
Time(151479),
Time(178201),
Time(118043),
Time(419260),
Time(222728),
Time(191937),
Time(163015),
],
igt_gold_segments: vec![TimeType::None; 9],
rta_sum_segments: vec![
(68, Time(20488293)),
(42, Time(10606062)),
(37, Time(7762144)),
(32, Time(7898441)),
(30, Time(5210998)),
(23, Time(15227876)),
(17, Time(5069775)),
(10, Time(4112349)),
(9, Time(1554249)),
],
igt_sum_segments: vec![(0, TimeType::None); 9],
rta_avg_segments: vec![
Time(301298),
Time(252525),
Time(209787),
Time(246826),
Time(173699),
Time(662081),
Time(298222),
Time(411234),
Time(172694),
],
igt_avg_segments: vec![TimeType::None; 9],
};
assert_eq!(r, the_same_run);
}
}