From 6e61a2b675b1fdcb2a1ecc97733436eeba3ac993 Mon Sep 17 00:00:00 2001 From: Refrag Date: Fri, 6 Oct 2023 21:37:52 +0200 Subject: [PATCH 1/4] Run parser: deserialize the AutoSplitterSettings This commit implements the deserialization of the AutoSplitterSettings XML section of a LiveSplit splits file. This can surely use some improvements regarding the compatibility without the auto-splitting feature and some other things. I think it is good as a first step though and we can keep iterating on it. --- .../src/settings/gui.rs | 8 +- src/platform/no_std/mod.rs | 2 + src/run/auto_splitter_settings.rs | 55 +++++++++++ src/run/mod.rs | 23 +++++ src/run/parser/livesplit.rs | 94 +++++++++++++++++-- src/run/saver/livesplit.rs | 39 +++++++- src/util/xml/helper.rs | 9 +- src/util/xml/writer.rs | 16 ++++ 8 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 src/run/auto_splitter_settings.rs diff --git a/crates/livesplit-auto-splitting/src/settings/gui.rs b/crates/livesplit-auto-splitting/src/settings/gui.rs index 37bf883f..24a22c72 100644 --- a/crates/livesplit-auto-splitting/src/settings/gui.rs +++ b/crates/livesplit-auto-splitting/src/settings/gui.rs @@ -2,7 +2,7 @@ use std::sync::Arc; /// A setting widget that is meant to be shown to and modified by the user. #[non_exhaustive] -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct Widget { /// A unique identifier for this setting. This is not meant to be shown to /// the user and is only used to keep track of the setting. This key is used @@ -19,7 +19,7 @@ pub struct Widget { } /// The type of a [`Widget`] and additional information about it. -#[derive(Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum WidgetKind { /// A title that is shown to the user. It doesn't by itself store a value /// and is instead used to group settings together. @@ -51,7 +51,7 @@ pub enum WidgetKind { } /// A filter for a file selection setting. -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub enum FileFilter { /// A filter that matches on the name of the file. Name { @@ -82,7 +82,7 @@ pub enum FileFilter { } /// An option for a choice setting. -#[derive(Clone, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct ChoiceOption { /// The unique identifier of the option. This is not meant to be shown to /// the user and is only used to keep track of the option. This key is used diff --git a/src/platform/no_std/mod.rs b/src/platform/no_std/mod.rs index ca0b8075..0ccc2d96 100644 --- a/src/platform/no_std/mod.rs +++ b/src/platform/no_std/mod.rs @@ -1,8 +1,10 @@ mod time; pub use self::time::*; +#[allow(unused)] pub struct RwLock(core::cell::RefCell); +#[allow(unused)] impl RwLock { pub fn new(value: T) -> Self { Self(core::cell::RefCell::new(value)) diff --git a/src/run/auto_splitter_settings.rs b/src/run/auto_splitter_settings.rs new file mode 100644 index 00000000..2f273b28 --- /dev/null +++ b/src/run/auto_splitter_settings.rs @@ -0,0 +1,55 @@ +use crate::run::parser::livesplit::Version; +use core::fmt::Debug; +use livesplit_auto_splitting::settings; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct AutoSplitterSettings { + pub version: Version, + pub script_path: String, + pub custom_settings: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CustomSetting { + pub id: String, + pub setting_type: settings::WidgetKind, + pub value: settings::Value, +} + +impl AutoSplitterSettings { + pub fn set_version(&mut self, version: Version) { + self.version = version; + } + + pub fn set_script_path(&mut self, script_path: String) { + self.script_path = script_path; + } + + pub fn add_custom_setting(&mut self, custom_setting: CustomSetting) { + self.custom_settings.push(custom_setting); + } +} + +impl CustomSetting { + pub fn new() -> Self { + Self { + id: String::default(), + setting_type: settings::WidgetKind::Bool { + default_value: false, + }, + value: settings::Value::Bool(false), + } + } + + pub fn set_id(&mut self, id: String) { + self.id = id; + } + + pub fn set_setting_type(&mut self, setting_type: settings::WidgetKind) { + self.setting_type = setting_type; + } + + pub fn set_value(&mut self, value: settings::Value) { + self.value = value + } +} diff --git a/src/run/mod.rs b/src/run/mod.rs index 6f24150c..f1cdf8cd 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -15,6 +15,9 @@ //! ``` mod attempt; + +#[cfg(feature = "auto-splitting")] +mod auto_splitter_settings; mod comparisons; pub mod editor; mod linked_layout; @@ -35,6 +38,8 @@ pub use run_metadata::{CustomVariable, RunMetadata}; pub use segment::Segment; pub use segment_history::SegmentHistory; +#[cfg(feature = "auto-splitting")] +use crate::run::auto_splitter_settings::AutoSplitterSettings; use crate::{ comparison::{default_generators, personal_best, ComparisonGenerator, RACE_COMPARISON_PREFIX}, platform::prelude::*, @@ -75,6 +80,8 @@ pub struct Run { custom_comparisons: Vec, comparison_generators: ComparisonGenerators, auto_splitter_settings: String, + #[cfg(feature = "auto-splitting")] + parsed_auto_splitter_settings: AutoSplitterSettings, linked_layout: Option, } @@ -128,6 +135,8 @@ impl Run { custom_comparisons: vec![personal_best::NAME.to_string()], comparison_generators: ComparisonGenerators(default_generators()), auto_splitter_settings: String::new(), + #[cfg(feature = "auto-splitting")] + parsed_auto_splitter_settings: AutoSplitterSettings::default(), linked_layout: None, } } @@ -326,6 +335,20 @@ impl Run { &mut self.auto_splitter_settings } + /// Accesses the Auto Splitter Settings. + #[inline] + #[cfg(feature = "auto-splitting")] + pub fn parsed_auto_splitter_settings(&self) -> &AutoSplitterSettings { + &self.parsed_auto_splitter_settings + } + + /// Accesses the Auto Splitter Settings as mutable. + #[inline] + #[cfg(feature = "auto-splitting")] + pub fn parsed_auto_splitter_settings_mut(&mut self) -> &mut AutoSplitterSettings { + &mut self.parsed_auto_splitter_settings + } + /// Accesses the [`LinkedLayout`] of this `Run`. If a /// [`Layout`](crate::Layout) is linked, it is supposed to be loaded to /// visualize the `Run`. diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs index d36578ea..cf49dc3d 100644 --- a/src/run/parser/livesplit.rs +++ b/src/run/parser/livesplit.rs @@ -9,8 +9,8 @@ use crate::{ xml::{ helper::{ attribute, attribute_escaped_err, end_tag, image, optional_attribute_escaped_err, - parse_attributes, parse_base, parse_children, reencode_children, text, - text_as_escaped_string_err, text_parsed, Error as XmlError, + parse_attributes, parse_base, parse_children, text, text_as_escaped_string_err, + text_parsed, Error as XmlError, }, Reader, }, @@ -18,8 +18,14 @@ use crate::{ AtomicDateTime, DateTime, Run, RunMetadata, Segment, Time, TimeSpan, }; use alloc::borrow::Cow; +use core::fmt::{Display, Formatter}; use core::{mem::MaybeUninit, str}; use time::{Date, Duration, PrimitiveDateTime}; +#[cfg(feature = "auto-splitting")] +use { + crate::run::auto_splitter_settings::{AutoSplitterSettings, CustomSetting}, + livesplit_auto_splitting::settings, +}; /// The Error type for splits files that couldn't be parsed by the LiveSplit /// Parser. @@ -97,8 +103,21 @@ const fn type_hint(v: Result) -> Result { v } -#[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)] -struct Version(u32, u32, u32, u32); +/// The version type for the LiveSplit parser +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq)] +pub struct Version(pub u32, pub u32, pub u32, pub u32); + +impl Default for Version { + fn default() -> Self { + Version(1, 0, 0, 0) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}.{}.{}.{}", self.0, self.1, self.2, self.3) + } +} fn parse_version(version: &str) -> Result { let splits = version.split('.'); @@ -419,6 +438,63 @@ fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Ru } } +#[cfg(feature = "auto-splitting")] +fn parse_auto_splitter_settings( + _version: Version, + reader: &mut Reader<'_>, + run: &mut Run, +) -> Result<()> { + let mut settings = AutoSplitterSettings::default(); + + // The compiler seems to throw a warning that 'attributes' isn't used by default, it actually is though + #[allow(unused_variables)] + parse_children(reader, |reader, tag, attributes| match tag.name() { + "Version" => type_hint(text(reader, |t| { + settings.set_version(parse_version(t.as_ref()).unwrap_or_default()) + })), + "ScriptPath" => type_hint(text(reader, |t| settings.set_script_path(t.to_string()))), + "CustomSettings" => { + parse_children(reader, |reader, _tag, attributes| { + let mut custom_setting = CustomSetting::new(); + + type_hint(parse_attributes(attributes, |k, v| { + match k { + "id" => custom_setting.set_id(v.unescape_str()), + "type" => match v.unescape_str().as_str() { + "bool" => custom_setting.set_setting_type(settings::WidgetKind::Bool { + default_value: false, + }), + _ => custom_setting.set_setting_type(settings::WidgetKind::Bool { + default_value: false, + }), + }, + _ => {} + } + Ok(true) + })) + .ok(); + type_hint(text(reader, |t| { + custom_setting.set_value(settings::Value::Bool( + parse_bool(t.as_ref()).unwrap_or_default(), + )) + })) + .ok(); + settings.add_custom_setting(custom_setting); + Ok::<(), Error>(()) + }) + .ok(); + + Ok(()) + } + _ => Ok(()), + }) + .ok(); + + run.parsed_auto_splitter_settings = settings; + + Ok(()) +} + /// Attempts to parse a LiveSplit splits file. pub fn parse(source: &str) -> Result { let mut reader = Reader::new(source); @@ -475,8 +551,14 @@ pub fn parse(source: &str) -> Result { }) } "AutoSplitterSettings" => { - let settings = run.auto_splitter_settings_mut(); - reencode_children(reader, settings).map_err(Into::into) + #[cfg(not(feature = "auto-splitting"))] + { + let settings = run.auto_splitter_settings_mut(); + crate::util::xml::helper::reencode_children(reader, settings) + .map_err(Into::into) + } + #[cfg(feature = "auto-splitting")] + parse_auto_splitter_settings(version, reader, &mut run) } "LayoutPath" => text(reader, |t| { run.set_linked_layout(if t == "?default" { diff --git a/src/run/saver/livesplit.rs b/src/run/saver/livesplit.rs index 27c6e4db..d3238cee 100644 --- a/src/run/saver/livesplit.rs +++ b/src/run/saver/livesplit.rs @@ -34,6 +34,8 @@ use crate::{ }; use alloc::borrow::Cow; use core::{fmt, mem::MaybeUninit}; +#[cfg(feature = "auto-splitting")] +use livesplit_auto_splitting::settings; use time::UtcOffset; const LSS_IMAGE_HEADER: &[u8; 156] = include_bytes!("lss_image_header.bin"); @@ -306,10 +308,43 @@ pub fn save_run(run: &Run, writer: W) -> fmt::Result { }) })?; - writer.tag_with_text_content( + #[cfg(not(feature = "auto-splitting"))] + return writer.tag_with_text_content( "AutoSplitterSettings", NO_ATTRIBUTES, Text::new_escaped(run.auto_splitter_settings()), - ) + ); + #[cfg(feature = "auto-splitting")] + writer.tag_with_content("AutoSplitterSettings", NO_ATTRIBUTES, |writer| { + writer.tag_with_text_content( + "Version", + NO_ATTRIBUTES, + DisplayAlreadyEscaped(&run.parsed_auto_splitter_settings.version), + )?; + writer.tag_with_text_content( + "ScriptPath", + NO_ATTRIBUTES, + DisplayAlreadyEscaped(&run.parsed_auto_splitter_settings.script_path), + )?; + + scoped_iter( + writer, + "CustomSettings", + &run.parsed_auto_splitter_settings.custom_settings, + |writer, custom_setting| { + let value = match custom_setting.value { + settings::Value::Bool(value) => value, + _ => false, + }; + writer.tag_with_text_content( + "Setting", + [("id", custom_setting.id.as_str()), ("type", "bool")], + bool(value), + ) + }, + )?; + + Ok(()) + }) }) } diff --git a/src/util/xml/helper.rs b/src/util/xml/helper.rs index f2894d9b..f5a3388d 100644 --- a/src/util/xml/helper.rs +++ b/src/util/xml/helper.rs @@ -1,8 +1,12 @@ use crate::platform::prelude::*; use alloc::borrow::Cow; -use core::{fmt, mem::MaybeUninit, str}; +#[cfg(not(feature = "auto-splitting"))] +use core::fmt; +use core::{mem::MaybeUninit, str}; -use super::{Attributes, Event, Reader, TagName, Text, Writer}; +#[cfg(not(feature = "auto-splitting"))] +use super::Writer; +use super::{Attributes, Event, Reader, TagName, Text}; /// The Error type for XML-based splits files that couldn't be parsed. #[derive(Debug, snafu::Snafu)] @@ -117,6 +121,7 @@ where } } +#[cfg(not(feature = "auto-splitting"))] pub fn reencode_children(reader: &mut Reader<'_>, target_buf: &mut String) -> Result<(), Error> { let mut writer = Writer::new_skip_header(target_buf); let mut depth = 0usize; diff --git a/src/util/xml/writer.rs b/src/util/xml/writer.rs index cd8e42de..59d78f78 100644 --- a/src/util/xml/writer.rs +++ b/src/util/xml/writer.rs @@ -1,3 +1,4 @@ +use crate::run::parser::livesplit::Version; use core::fmt::{self, Write}; use crate::util::{ascii_char::AsciiChar, ascii_set::AsciiSet}; @@ -9,6 +10,7 @@ pub struct Writer { } impl Writer { + #[cfg(not(feature = "auto-splitting"))] pub const fn new_skip_header(sink: T) -> Self { Self { sink } } @@ -91,6 +93,7 @@ impl Writer { }) } + #[cfg(not(feature = "auto-splitting"))] pub fn comment(&mut self, text: impl Value) -> fmt::Result { self.sink.write_str("