From 8135b5333931621f6b153b0ee56a090f47257764 Mon Sep 17 00:00:00 2001 From: Refrag Date: Fri, 6 Oct 2023 21:37:52 +0200 Subject: [PATCH] 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/auto_splitting/mod.rs | 35 +- src/platform/no_std/mod.rs | 2 + src/run/auto_splitter_settings.rs | 24 ++ src/run/mod.rs | 43 +++ src/run/parser/livesplit.rs | 326 +++++++++++++++++- src/run/saver/livesplit.rs | 226 +++++++++++- src/timing/timer/mod.rs | 9 + src/util/xml/helper.rs | 6 +- src/util/xml/writer.rs | 11 + 10 files changed, 666 insertions(+), 24 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/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index 9ffaa7e5..eccce98a 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -559,7 +559,8 @@ use crate::{ use arc_swap::ArcSwapOption; pub use livesplit_auto_splitting::{settings, wasi_path}; use livesplit_auto_splitting::{ - AutoSplitter, Config, CreationError, LogLevel, Timer as AutoSplitTimer, TimerState, + AutoSplitter, CompiledAutoSplitter, Config, CreationError, LogLevel, + Timer as AutoSplitTimer, TimerState, }; use snafu::Snafu; use std::{ @@ -567,7 +568,7 @@ use std::{ path::PathBuf, sync::{ mpsc::{self, Receiver, RecvTimeoutError, Sender}, - Condvar, Mutex, + Condvar, Mutex, RwLock, }, thread, time::{Duration, Instant}, @@ -602,6 +603,7 @@ pub struct Runtime { shared_state: Arc>, changed_sender: Sender<()>, runtime: livesplit_auto_splitting::Runtime, + compiled_auto_splitter: RwLock>, } struct SharedState { @@ -676,6 +678,7 @@ impl Runtime { changed_sender, // TODO: unwrap? runtime: livesplit_auto_splitting::Runtime::new(Config::default()).unwrap(), + compiled_auto_splitter: RwLock::new(None), } } @@ -683,11 +686,24 @@ impl Runtime { pub fn load(&self, path: PathBuf, timer: T) -> Result<(), Error> { let data = fs::read(path).map_err(|e| Error::ReadFileFailed { source: e })?; - let auto_splitter = self + let compiled_auto_splitter = self .runtime .compile(&data) - .map_err(|e| Error::LoadFailed { source: e })? - .instantiate(Timer(timer), None, None) + .map_err(|e| Error::LoadFailed { source: e })?; + self.instantiate(&compiled_auto_splitter, timer)?; + *self.compiled_auto_splitter.write().unwrap() = Some(compiled_auto_splitter); + Ok(()) + } + + /// Instantiates the compiled auto splitter. + fn instantiate( + &self, + compiled_auto_splitter: &CompiledAutoSplitter, + timer: T, + ) -> Result<(), Error> { + let settings_map = timer.get_timer().run().auto_splitter_settings_map_load(); + let auto_splitter = compiled_auto_splitter + .instantiate(Timer(timer), settings_map, None) .map_err(|e| Error::LoadFailed { source: e })?; self.shared_state @@ -710,6 +726,15 @@ impl Runtime { .map_err(|_| Error::ThreadStopped) } + /// Reloads the auto splitter without re-compiling. + pub fn reload(&self, timer: T) -> Result<(), Error> { + self.unload()?; + if let Some(compiled_auto_splitter) = self.compiled_auto_splitter.read().unwrap().as_ref() { + self.instantiate(compiled_auto_splitter, timer)?; + } + Ok(()) + } + /// Accesses a copy of the currently stored settings. The auto splitter can /// change these at any time. If you intend to make modifications to the /// settings, you need to set them again via 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..a5d3f08d --- /dev/null +++ b/src/run/auto_splitter_settings.rs @@ -0,0 +1,24 @@ +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: settings::Map, +} + +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 set_custom_settings(&mut self, custom_settings: settings::Map) { + self.custom_settings = custom_settings; + } +} diff --git a/src/run/mod.rs b/src/run/mod.rs index 6f24150c..5938dad1 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: Option, 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: None, linked_layout: None, } } @@ -326,6 +335,40 @@ impl Run { &mut self.auto_splitter_settings } + /// Loads a copy of the Auto Splitter Settings as a settings map. + #[inline] + #[cfg(feature = "auto-splitting")] + pub fn auto_splitter_settings_map_load( + &self, + ) -> Option { + if let Some(p) = &self.parsed_auto_splitter_settings { + return Some(p.custom_settings.clone()); + } + None + } + + /// Stores a settings map into the parsed auto splitter settings. + #[cfg(feature = "auto-splitting")] + pub fn auto_splitter_settings_map_store( + &mut self, + settings_map: livesplit_auto_splitting::settings::Map, + ) { + let p = &mut self.parsed_auto_splitter_settings; + match p { + None => { + if settings_map.is_empty() { + return; + } + let mut a = AutoSplitterSettings::default(); + a.set_custom_settings(settings_map); + *p = Some(a); + } + Some(a) => { + a.set_custom_settings(settings_map); + } + } + } + /// 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..4ed6930d 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, crate::util::xml::Attributes, + 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,142 @@ fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Ru } } +fn parse_auto_splitter_settings( + _version: Version, + reader: &mut Reader<'_>, + run: &mut Run, +) -> Result<()> { + crate::util::xml::helper::reencode_children(reader, run.auto_splitter_settings_mut()) + .map_err(Into::::into)?; + + #[cfg(feature = "auto-splitting")] + let mut reader = Reader::new(run.auto_splitter_settings()); + + #[cfg(feature = "auto-splitting")] + let mut any_parsed = false; + #[cfg(feature = "auto-splitting")] + let mut settings = AutoSplitterSettings::default(); + + #[cfg(feature = "auto-splitting")] + // The compiler seems to throw a warning that 'attributes' isn't used by default, it actually is though + #[allow(unused_variables)] + parse_children(&mut reader, |reader, tag, attributes| match tag.name() { + "Version" => type_hint(text(reader, |t| { + any_parsed = true; + settings.set_version(parse_version(t.as_ref()).unwrap_or_default()) + })), + "ScriptPath" => type_hint(text(reader, |t| { + any_parsed = true; + settings.set_script_path(t.to_string()) + })), + "CustomSettings" => { + any_parsed = true; + settings.set_custom_settings(parse_settings_map(reader)); + Ok(()) + } + _ => Ok(()), + }) + .ok(); + + #[cfg(feature = "auto-splitting")] + if any_parsed { + run.parsed_auto_splitter_settings = Some(settings); + } + + Ok(()) +} + +#[cfg(feature = "auto-splitting")] +fn parse_settings_map(reader: &mut Reader<'_>) -> settings::Map { + let mut settings_map = settings::Map::new(); + + parse_children(reader, |reader, _tag, attributes| { + if let (Some(id), Some(value)) = parse_settings_entry(reader, attributes) { + settings_map.insert(id.into(), value); + } + Ok::<(), Error>(()) + }) + .ok(); + + settings_map +} + +#[cfg(feature = "auto-splitting")] +fn parse_settings_list(reader: &mut Reader<'_>) -> settings::List { + let mut settings_list = settings::List::new(); + + parse_children(reader, |reader, _tag, attributes| { + if let (_, Some(value)) = parse_settings_entry(reader, attributes) { + settings_list.push(value); + } + Ok::<(), Error>(()) + }) + .ok(); + + settings_list +} + +#[cfg(feature = "auto-splitting")] +fn parse_settings_entry( + reader: &mut Reader<'_>, + attributes: Attributes<'_>, +) -> (Option, Option) { + let mut id = None; + let mut setting_type = None; + let mut string_value = None; + type_hint(parse_attributes(attributes, |k, v| { + match k { + "id" => id = Some(v.unescape_str()), + "type" => setting_type = Some(v.unescape_str()), + "value" => string_value = Some(v.unescape_str()), + _ => {} + } + Ok(true) + })) + .ok(); + let Some(setting_type) = setting_type else { + return (id, None); + }; + let value = match setting_type.as_str() { + "bool" => { + let mut b = bool::default(); + type_hint(text(reader, |t| { + b = parse_bool(t.as_ref()).unwrap_or_default(); + })) + .ok(); + Some(settings::Value::Bool(b)) + } + "i64" => { + let mut i = i64::default(); + type_hint(text(reader, |t| { + i = t.as_ref().parse().unwrap_or_default(); + })) + .ok(); + Some(settings::Value::I64(i)) + } + "f64" => { + let mut f = f64::default(); + type_hint(text(reader, |t| { + f = t.as_ref().parse().unwrap_or_default(); + })) + .ok(); + Some(settings::Value::F64(f)) + } + "string" => { + let mut s = String::default(); + type_hint(text(reader, |t| { + s = t.to_string(); + })) + .ok(); + Some(settings::Value::String(string_value.unwrap_or(s).into())) + } + "map" => Some(settings::Value::Map(parse_settings_map(reader))), + "list" => Some(settings::Value::List(parse_settings_list(reader))), + _ => None, + }; + (id, value) +} + /// Attempts to parse a LiveSplit splits file. pub fn parse(source: &str) -> Result { let mut reader = Reader::new(source); @@ -474,10 +629,7 @@ pub fn parse(source: &str) -> Result { } }) } - "AutoSplitterSettings" => { - let settings = run.auto_splitter_settings_mut(); - reencode_children(reader, settings).map_err(Into::into) - } + "AutoSplitterSettings" => parse_auto_splitter_settings(version, reader, &mut run), "LayoutPath" => text(reader, |t| { run.set_linked_layout(if t == "?default" { Some(LinkedLayout::Default) @@ -503,6 +655,8 @@ pub fn parse(source: &str) -> Result { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "auto-splitting")] + use livesplit_auto_splitting::settings; #[test] fn time_span_parsing() { @@ -524,4 +678,160 @@ mod tests { parse_time_span("NaN.23:34:56.789").unwrap_err(); parse_time_span("Inf.23:34:56.789").unwrap_err(); } + + #[cfg(feature = "auto-splitting")] + #[test] + fn test_parse_settings() { + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + True + True + True + "# + )), + { + let mut m = settings::Map::new(); + m.insert("start".into(), settings::Value::Bool(true)); + m.insert("split".into(), settings::Value::Bool(true)); + m.insert("remove_loads".into(), settings::Value::Bool(true)); + m + }, + ); + + assert_eq!( + parse_settings_list(&mut Reader::new( + r#""# + )), + { + let mut l = settings::List::new(); + l.push(settings::Value::String("KingsPass".into())); + l + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#""# + )), + { + let mut m = settings::Map::new(); + m.insert( + "splits_0_item".into(), + settings::Value::String("KingsPass".into()), + ); + m + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + + True + + + "# + )), + { + let mut map = settings::Map::new(); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + + map.insert("inner_map".into(), settings::Value::Map(inner_map)); + map + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + + True + + + True + + + + "# + )), + { + let mut map = settings::Map::new(); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + inner_map.insert("recursive".into(), settings::Value::Map(inner_map.clone())); + + map.insert("lolol".into(), settings::Value::Map(inner_map)); + map + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + + True + + + True + + + + "# + )), + { + let mut map = settings::Map::new(); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + inner_map.insert("recursive".into(), settings::Value::Map(inner_map.clone())); + + map.insert("lolol".into(), settings::Value::Map(inner_map)); + map + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + True + True + True + + True + + + True + + + + + "# + )), + { + let mut map = settings::Map::new(); + map.insert("level32_bool".into(), settings::Value::Bool(true)); + map.insert("other_setting".into(), settings::Value::Bool(true)); + map.insert("level12_bool".into(), settings::Value::Bool(true)); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + inner_map.insert("recursive".into(), settings::Value::Map(inner_map.clone())); + + map.insert("lolol".into(), settings::Value::Map(inner_map)); + map.insert( + "okok".into(), + settings::Value::String("hello, you seem to like true!".into()), + ); + map + }, + ); + } } diff --git a/src/run/saver/livesplit.rs b/src/run/saver/livesplit.rs index 27c6e4db..790e13f3 100644 --- a/src/run/saver/livesplit.rs +++ b/src/run/saver/livesplit.rs @@ -24,6 +24,8 @@ //! livesplit::save_run(&run, IoWrite(writer)).expect("Couldn't save the splits file"); //! ``` +#[cfg(feature = "auto-splitting")] +use crate::run::AutoSplitterSettings; use crate::{ platform::prelude::*, run::LinkedLayout, @@ -34,6 +36,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 +310,222 @@ pub fn save_run(run: &Run, writer: W) -> fmt::Result { }) })?; - writer.tag_with_text_content( - "AutoSplitterSettings", - NO_ATTRIBUTES, - Text::new_escaped(run.auto_splitter_settings()), - ) + write_run_auto_splitter_settings(writer, run) + }) +} + +fn write_run_auto_splitter_settings( + writer: &mut Writer, + run: &Run, +) -> fmt::Result { + #[cfg(feature = "auto-splitting")] + if let Some(AutoSplitterSettings { + version, + script_path, + custom_settings, + }) = &run.parsed_auto_splitter_settings + { + return writer.tag_with_content("AutoSplitterSettings", NO_ATTRIBUTES, |writer| { + writer.tag_with_text_content( + "Version", + NO_ATTRIBUTES, + DisplayAlreadyEscaped(version), + )?; + writer.tag_with_text_content( + "ScriptPath", + NO_ATTRIBUTES, + DisplayAlreadyEscaped(script_path), + )?; + + write_settings_map(writer, "CustomSettings", vec![], custom_settings)?; + + Ok(()) + }); + } + + writer.tag_with_text_content( + "AutoSplitterSettings", + NO_ATTRIBUTES, + Text::new_escaped(run.auto_splitter_settings()), + ) +} + +#[cfg(feature = "auto-splitting")] +fn write_settings_map( + writer: &mut Writer, + tag: &str, + attrs: Vec<(&str, &str)>, + map: &settings::Map, +) -> fmt::Result { + writer.tag_with_content(tag, attrs, |writer| { + for (id, value) in map.iter() { + write_settings_entry(writer, vec![("id", id)], value).ok(); + } + Ok(()) }) } + +#[cfg(feature = "auto-splitting")] +fn write_settings_list( + writer: &mut Writer, + tag: &str, + attrs: Vec<(&str, &str)>, + map: &settings::List, +) -> fmt::Result { + writer.tag_with_content(tag, attrs, |writer| { + for value in map.iter() { + write_settings_entry(writer, vec![], value).ok(); + } + Ok(()) + }) +} + +#[cfg(feature = "auto-splitting")] +fn write_settings_entry<'a, W>( + writer: &mut Writer, + mut attrs: Vec<(&str, &'a str)>, + value: &'a settings::Value, +) -> fmt::Result +where + W: fmt::Write, +{ + match value { + settings::Value::Map(m) => { + attrs.push(("type", "map")); + write_settings_map(writer, "Setting", attrs, m) + } + settings::Value::List(l) => { + attrs.push(("type", "list")); + write_settings_list(writer, "Setting", attrs, l) + } + settings::Value::Bool(b) => { + attrs.push(("type", "bool")); + writer.tag_with_text_content("Setting", attrs, bool(*b)) + } + settings::Value::I64(i) => { + attrs.push(("type", "i64")); + writer.tag_with_text_content("Setting", attrs, Text::new_escaped(&i.to_string())) + } + settings::Value::F64(f) => { + attrs.push(("type", "f64")); + writer.tag_with_text_content("Setting", attrs, Text::new_escaped(&f.to_string())) + } + settings::Value::String(s) => { + attrs.push(("type", "string")); + attrs.push(("value", s)); + writer.empty_tag("Setting", attrs) + } + _ => writer.empty_tag("Setting", attrs), + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "auto-splitting")] + use super::*; + #[cfg(feature = "auto-splitting")] + use livesplit_auto_splitting::settings; + + #[cfg(feature = "auto-splitting")] + #[test] + fn test_write_settings() { + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + write_settings_entry(&mut writer, vec![], &settings::Value::Bool(true)).ok(); + s + }, + r#"True"# + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + write_settings_entry( + &mut writer, + vec![("id", "start")], + &settings::Value::Bool(true), + ) + .ok(); + s + }, + r#"True"# + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + let mut m = settings::Map::new(); + m.insert("start".into(), settings::Value::Bool(true)); + m.insert("split".into(), settings::Value::Bool(true)); + m.insert("remove_loads".into(), settings::Value::Bool(true)); + write_settings_map(&mut writer, "CustomSettings", vec![], &m).ok(); + s + }, + format!( + "{}{}{}{}{}", + r#""#, + r#"True"#, + r#"True"#, + r#"True"#, + r#""#, + ) + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + let mut m = settings::Map::new(); + m.insert("first".into(), settings::Value::Bool(true)); + m.insert("second".into(), settings::Value::String("bar".into())); + write_settings_entry( + &mut writer, + vec![("id", "inner_map")], + &settings::Value::Map(m), + ) + .ok(); + s + }, + format!( + "{}{}{}{}", + r#""#, + r#"True"#, + r#""#, + r#""#, + ) + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + let mut m = settings::Map::new(); + m.insert("first".into(), settings::Value::Bool(true)); + m.insert("second".into(), settings::Value::String("bar".into())); + m.insert("recursive".into(), settings::Value::Map(m.clone())); + write_settings_entry( + &mut writer, + vec![("id", "inner_map")], + &settings::Value::Map(m), + ) + .ok(); + s + }, + format!( + "{}{}{}{}{}{}{}{}", + r#""#, + r#"True"#, + r#""#, + r#""#, + r#"True"#, + r#""#, + r#""#, + r#""#, + ) + ); + } +} diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs index 012d8e7b..3dfd61a7 100644 --- a/src/timing/timer/mod.rs +++ b/src/timing/timer/mod.rs @@ -168,6 +168,15 @@ impl Timer { &self.run } + /// Stores a settings map into the parsed auto splitter settings. + #[cfg(feature = "auto-splitting")] + pub fn run_auto_splitter_settings_map_store( + &mut self, + settings_map: livesplit_auto_splitting::settings::Map, + ) { + self.run.auto_splitter_settings_map_store(settings_map); + } + /// Marks the Run as unmodified, so that it is known that all the changes /// have been saved. #[inline] diff --git a/src/util/xml/helper.rs b/src/util/xml/helper.rs index f2894d9b..c01da593 100644 --- a/src/util/xml/helper.rs +++ b/src/util/xml/helper.rs @@ -1,8 +1,10 @@ use crate::platform::prelude::*; use alloc::borrow::Cow; -use core::{fmt, mem::MaybeUninit, str}; +use core::fmt; +use core::{mem::MaybeUninit, str}; -use super::{Attributes, Event, Reader, TagName, Text, Writer}; +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)] diff --git a/src/util/xml/writer.rs b/src/util/xml/writer.rs index cd8e42de..31645ee3 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}; @@ -204,6 +205,16 @@ impl Value for Text<'_> { } } +impl Value for Version { + fn write_escaped(self, sink: &mut T) -> fmt::Result { + write!(sink, "{}.{}.{}.{}", self.0, self.1, self.2, self.3) + } + + fn is_empty(&self) -> bool { + self.0 == 0 && self.1 == 0 && self.2 == 0 && self.3 == 0 + } +} + pub struct DisplayAlreadyEscaped(pub T); impl Value for DisplayAlreadyEscaped {