diff --git a/src/layout/parser/splits.rs b/src/layout/parser/splits.rs index b4ddadc0..7e2697d2 100644 --- a/src/layout/parser/splits.rs +++ b/src/layout/parser/splits.rs @@ -1,14 +1,14 @@ use super::{ - accuracy, comparison_override, end_tag, parse_bool, parse_children, text, text_parsed, - timing_method_override, Error, GradientBuilder, GradientKind, ListGradientKind, Result, + Error, GradientBuilder, GradientKind, ListGradientKind, Result, accuracy, comparison_override, + end_tag, parse_bool, parse_children, text, text_parsed, timing_method_override, }; use crate::{ component::splits::{ self, ColumnKind, ColumnSettings, ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith, - TimeColumn, + TimeColumn, VariableColumn, }, platform::prelude::*, - util::xml::{helper::text_as_escaped_string_err, Reader}, + util::xml::{Reader, helper::text_as_escaped_string_err}, }; pub use crate::component::splits::Component; @@ -49,6 +49,7 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() parse_children(reader, |reader, _, _| { let mut column_name = String::new(); let mut column = TimeColumn::default(); + let mut custom_variable = false; parse_children(reader, |reader, tag, _| match tag.name() { "Name" => text(reader, |v| column_name = v.into_owned()), "Comparison" => { @@ -58,41 +59,44 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() timing_method_override(reader, |v| column.timing_method = v) } "Type" => text_as_escaped_string_err(reader, |v| { - ( - column.start_with, - column.update_with, - column.update_trigger, - ) = match v { - "Delta" => ( - ColumnStartWith::Empty, - ColumnUpdateWith::Delta, - ColumnUpdateTrigger::Contextual, - ), - "SplitTime" => ( - ColumnStartWith::ComparisonTime, - ColumnUpdateWith::SplitTime, - ColumnUpdateTrigger::OnEndingSegment, - ), - "DeltaorSplitTime" => ( - ColumnStartWith::ComparisonTime, - ColumnUpdateWith::DeltaWithFallback, - ColumnUpdateTrigger::Contextual, - ), - "SegmentDelta" => ( - ColumnStartWith::Empty, - ColumnUpdateWith::SegmentDelta, - ColumnUpdateTrigger::Contextual, - ), - "SegmentTime" => ( - ColumnStartWith::ComparisonSegmentTime, - ColumnUpdateWith::SegmentTime, - ColumnUpdateTrigger::OnEndingSegment, - ), - "SegmentDeltaorSegmentTime" => ( - ColumnStartWith::ComparisonSegmentTime, - ColumnUpdateWith::SegmentDeltaWithFallback, - ColumnUpdateTrigger::Contextual, - ), + match v { + "Delta" => { + column.start_with = ColumnStartWith::Empty; + column.update_with = ColumnUpdateWith::Delta; + column.update_trigger = ColumnUpdateTrigger::Contextual; + } + "SplitTime" => { + column.start_with = ColumnStartWith::ComparisonTime; + column.update_with = ColumnUpdateWith::SplitTime; + column.update_trigger = + ColumnUpdateTrigger::OnEndingSegment; + } + "DeltaorSplitTime" => { + column.start_with = ColumnStartWith::ComparisonTime; + column.update_with = + ColumnUpdateWith::DeltaWithFallback; + column.update_trigger = ColumnUpdateTrigger::Contextual; + } + "SegmentDelta" => { + column.start_with = ColumnStartWith::Empty; + column.update_with = ColumnUpdateWith::SegmentDelta; + column.update_trigger = ColumnUpdateTrigger::Contextual; + } + "SegmentTime" => { + column.start_with = + ColumnStartWith::ComparisonSegmentTime; + column.update_with = ColumnUpdateWith::SegmentTime; + column.update_trigger = + ColumnUpdateTrigger::OnEndingSegment; + } + "SegmentDeltaorSegmentTime" => { + column.start_with = + ColumnStartWith::ComparisonSegmentTime; + column.update_with = + ColumnUpdateWith::SegmentDeltaWithFallback; + column.update_trigger = ColumnUpdateTrigger::Contextual; + } + "CustomVariable" => custom_variable = true, _ => return Err(Error::ParseColumnType), }; @@ -103,8 +107,14 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() settings.columns.insert( 0, splits::ColumnSettings { + kind: if custom_variable { + ColumnKind::Variable(VariableColumn { + variable_name: column_name.clone(), + }) + } else { + ColumnKind::Time(column) + }, name: column_name, - kind: ColumnKind::Time(column), }, ); Ok(()) diff --git a/src/layout/parser/text.rs b/src/layout/parser/text.rs index 6bafe107..207d905f 100644 --- a/src/layout/parser/text.rs +++ b/src/layout/parser/text.rs @@ -1,4 +1,4 @@ -use super::{color, end_tag, parse_bool, parse_children, text, GradientBuilder, Result}; +use super::{GradientBuilder, Result, color, end_tag, parse_bool, parse_children, text}; use crate::{component::text::Text, platform::prelude::*, util::xml::Reader}; pub use crate::component::text::Component; @@ -8,6 +8,14 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() let mut background_builder = GradientBuilder::new(); let (mut override_label, mut override_value) = (false, false); let (mut left_center, mut right) = (String::new(), String::new()); + // - Normally, when `custom_variable` is false, + // `Text2`/`right` is interpreted as text to be displayed as-is, + // through `Text::Split` or `Text::Center`. + // - But when `custom_variable` is true, + // `Text2`/`right` is interpreted as the custom variable name, + // and the value of the custom variable is displayed instead, + // through `Text::Variable`. + let mut custom_variable = false; parse_children(reader, |reader, tag, _| { if !background_builder.parse_background(reader, tag.name())? { @@ -19,6 +27,7 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() "Text1" => text(reader, |v| left_center = v.into_owned()), "Text2" => text(reader, |v| right = v.into_owned()), "Display2Rows" => parse_bool(reader, |b| settings.display_two_rows = b), + "CustomVariable" => parse_bool(reader, |b| custom_variable = b), _ => { // FIXME: // Font1 @@ -39,10 +48,11 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() if !override_value { settings.right_color = None; } - settings.text = match (left_center.is_empty(), right.is_empty()) { - (false, false) => Text::Split(left_center, right), - (false, true) => Text::Center(left_center), - _ => Text::Center(right), + settings.text = match (custom_variable, left_center.is_empty(), right.is_empty()) { + (true, lc_empty, _) => Text::Variable(right, !lc_empty), + (false, false, false) => Text::Split(left_center, right), + (false, false, true) => Text::Center(left_center), + (false, true, _) => Text::Center(right), }; settings.background = background_builder.build(); diff --git a/tests/layout_files/custom_variable_ls1l.ls1l b/tests/layout_files/custom_variable_ls1l.ls1l new file mode 100644 index 00000000..7c3e7c49 --- /dev/null +++ b/tests/layout_files/custom_variable_ls1l.ls1l @@ -0,0 +1,306 @@ +{ + "components": [ + { + "Title": { + "background": { + "Vertical": [ + [ + 0.16470589, + 0.16470589, + 0.16470589, + 1.0 + ], + [ + 0.07450981, + 0.07450981, + 0.07450981, + 1.0 + ] + ] + }, + "text_color": null, + "show_game_name": true, + "show_category_name": true, + "show_finished_runs_count": false, + "show_attempt_count": true, + "text_alignment": "Auto", + "display_as_single_line": false, + "display_game_icon": true, + "show_region": false, + "show_platform": false, + "show_variables": true + } + }, + { + "Splits": { + "background": { + "Alternating": [ + [ + 1.0, + 1.0, + 1.0, + 0.0 + ], + [ + 1.0, + 1.0, + 1.0, + 0.04215355 + ] + ] + }, + "visual_split_count": 8, + "split_preview_count": 1, + "show_thin_separators": true, + "separator_last_split": true, + "always_show_last_split": true, + "fill_with_blank_space": true, + "display_two_rows": false, + "current_split_gradient": { + "Vertical": [ + [ + 0.20000002, + 0.45098042, + 0.9568628, + 1.0 + ], + [ + 0.08235294, + 0.20784315, + 0.454902, + 1.0 + ] + ] + }, + "split_time_accuracy": "Seconds", + "segment_time_accuracy": "Seconds", + "delta_time_accuracy": "Tenths", + "delta_drop_decimals": true, + "show_column_labels": false, + "columns": [ + { + "name": "Time", + "start_with": "ComparisonTime", + "update_with": "SplitTime", + "update_trigger": "OnEndingSegment", + "comparison_override": null, + "timing_method": null + }, + { + "name": "+/-", + "start_with": "Empty", + "update_with": "Delta", + "update_trigger": "Contextual", + "comparison_override": null, + "timing_method": null + }, + { + "name": "delta hits", + "variable_name": "delta hits" + }, + { + "name": "segment hits", + "variable_name": "segment hits" + } + ] + } + }, + { + "Text": { + "background": "Transparent", + "display_two_rows": false, + "left_center_color": null, + "right_color": null, + "text": { + "Variable": [ + "hits", + true + ] + } + } + }, + { + "Text": { + "background": "Transparent", + "display_two_rows": false, + "left_center_color": null, + "right_color": null, + "text": { + "Variable": [ + "pb hits", + true + ] + } + } + }, + { + "Timer": { + "background": "Transparent", + "timing_method": "GameTime", + "height": 54, + "color_override": null, + "show_gradient": true, + "digits_format": "SingleDigitSeconds", + "accuracy": "Hundredths", + "is_segment_timer": false + } + }, + { + "Timer": { + "background": "Transparent", + "timing_method": "RealTime", + "height": 39, + "color_override": null, + "show_gradient": true, + "digits_format": "SingleDigitSeconds", + "accuracy": "Hundredths", + "is_segment_timer": false + } + }, + { + "PreviousSegment": { + "background": { + "Vertical": [ + [ + 0.10980393, + 0.10980393, + 0.10980393, + 1.0 + ], + [ + 0.050980397, + 0.050980397, + 0.050980397, + 1.0 + ] + ] + }, + "comparison_override": null, + "display_two_rows": false, + "label_color": null, + "drop_decimals": true, + "accuracy": "Tenths", + "show_possible_time_save": false + } + }, + { + "CurrentPace": { + "background": "Transparent", + "comparison_override": "Best Segments", + "display_two_rows": false, + "label_color": null, + "value_color": null, + "accuracy": "Hundredths" + } + }, + { + "CurrentComparison": { + "background": { + "Vertical": [ + [ + 0.10980393, + 0.10980393, + 0.10980393, + 1.0 + ], + [ + 0.050980397, + 0.050980397, + 0.050980397, + 1.0 + ] + ] + }, + "display_two_rows": false, + "label_color": null, + "value_color": null + } + } + ], + "general": { + "direction": "Vertical", + "timer_font": null, + "times_font": null, + "text_font": null, + "text_shadow": [ + 0.0, + 0.0, + 0.0, + 0.27156216 + ], + "background": { + "Plain": [ + 0.058823533, + 0.058823533, + 0.058823533, + 1.0 + ] + }, + "best_segment_color": [ + 0.8470589, + 0.6862745, + 0.121568635, + 1.0 + ], + "ahead_gaining_time_color": [ + 0.0, + 0.8000001, + 0.21176472, + 1.0 + ], + "ahead_losing_time_color": [ + 0.32156864, + 0.8000001, + 0.45098042, + 1.0 + ], + "behind_gaining_time_color": [ + 0.8000001, + 0.36078432, + 0.32156864, + 1.0 + ], + "behind_losing_time_color": [ + 0.8000001, + 0.07058824, + 0.0, + 1.0 + ], + "not_running_color": [ + 0.6745098, + 0.6745098, + 0.6745098, + 1.0 + ], + "personal_best_color": [ + 0.08627451, + 0.6509804, + 1.0, + 1.0 + ], + "paused_color": [ + 0.4784314, + 0.4784314, + 0.4784314, + 1.0 + ], + "thin_separators_color": [ + 1.0, + 1.0, + 1.0, + 0.07897232 + ], + "separators_color": [ + 1.0, + 1.0, + 1.0, + 0.32670057 + ], + "text_color": [ + 1.0, + 1.0, + 1.0, + 1.0 + ] + } +} diff --git a/tests/layout_files/custom_variable_splits.lsl b/tests/layout_files/custom_variable_splits.lsl new file mode 100644 index 00000000..468b2c0f --- /dev/null +++ b/tests/layout_files/custom_variable_splits.lsl @@ -0,0 +1,278 @@ + + + Vertical + 7 + 647 + 286 + 388 + -1 + -1 + + FFFFFFFF + FF0F0F0F + 00000000 + 03FFFFFF + 24FFFFFF + FF16A6FF + FF00CC36 + FF52CC73 + FFCC5C52 + FFCC1200 + FFD8AF1F + False + FFACACAC + FF7A7A7A + 00000000 + 80000000 + + + + + + + + + + True + True + True + True + SolidColor + + 1 + 0 + 0.59 + False + + + + LiveSplit.Title.dll + + 1.7.3 + True + True + True + False + False + False + + + + False + FFFFFFFF + FF2A2A2A + FF131313 + Vertical + True + False + False + True + 0 + + + + LiveSplit.Splits.dll + + 1.6 + FF3373F4 + FF153574 + 8 + 1 + True + True + True + 20 + Seconds + False + FFFFFFFF + FFFFFFFF + FFFFFFFF + False + FFFFFFFF + FFFFFFFF + FFFFFFFF + False + True + True + 24 + True + 3.6 + Vertical + 00FFFFFF + 01FFFFFF + Alternating + True + Tenths + True + False + FFFFFFFF + False + False + FFFFFFFF + + + 1.5 + segment hits + CustomVariable + Current Comparison + Current Timing Method + + + 1.5 + delta hits + CustomVariable + Current Comparison + Current Timing Method + + + 1.5 + +/- + Delta + Current Comparison + Current Timing Method + + + 1.5 + Time + SplitTime + Current Comparison + Current Timing Method + + + + + + LiveSplit.Text.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + 00FFFFFF + 00FFFFFF + Plain + hits + hits + + + + + + + False + False + False + True + + + + LiveSplit.Text.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + 00FFFFFF + 00FFFFFF + Plain + pb hits + pb hits + + + + + + + False + False + False + True + + + + LiveSplit.Timer.dll + + 1.5 + 69 + 225 + 1.23 + False + True + FFAAAAAA + 00000000 + FF222222 + Plain + False + Game Time + 35 + + + + LiveSplit.Timer.dll + + 1.5 + 50 + 225 + 1.23 + False + True + FFAAAAAA + 00FFFFFF + 00FFFFFF + Plain + False + Real Time + 35 + + + + LiveSplit.PreviousSegment.dll + + 1.6 + FFFFFFFF + False + FF1C1C1C + FF0D0D0D + Vertical + Tenths + True + Current Comparison + False + False + Tenths + + + + LiveSplit.RunPrediction.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + Hundredths + 00FFFFFF + 00FFFFFF + Plain + Best Segments + False + + + + LiveSplit.CurrentComparison.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + FF1C1C1C + FF0D0D0D + Vertical + False + + + + diff --git a/tests/layout_files/custom_variable_subsplits.lsl b/tests/layout_files/custom_variable_subsplits.lsl new file mode 100644 index 00000000..9148ec40 --- /dev/null +++ b/tests/layout_files/custom_variable_subsplits.lsl @@ -0,0 +1,306 @@ + + + Vertical + 5 + 635 + 286 + 398 + -1 + -1 + + FFFFFFFF + FF0F0F0F + 00000000 + 03FFFFFF + 24FFFFFF + FF16A6FF + FF00CC36 + FF52CC73 + FFCC5C52 + FFCC1200 + FFD8AF1F + False + FFACACAC + FF7A7A7A + 00000000 + 80000000 + + + + + + + + + + True + True + True + True + SolidColor + + 1 + 0 + 0.59 + False + + + + LiveSplit.Title.dll + + 1.7.3 + True + True + True + False + False + False + + + + False + FFFFFFFF + FF2A2A2A + FF131313 + Vertical + True + False + False + True + 0 + + + + LiveSplit.Subsplits.dll + + 1.7 + False + FF3373F4 + FF153574 + 8 + 1 + 0 + True + True + True + 20 + Seconds + FFFFFFFF + FFFFFFFF + FFFFFFFF + False + FFFFFFFF + FFFFFFFF + FFFFFFFF + False + True + 24 + True + 6 + Vertical + 00FFFFFF + 01FFFFFF + Alternating + True + Tenths + True + False + FFFFFFFF + Current Comparison + Current Timing Method + False + True + True + False + False + False + False + Plain + True + True + True + True + Vertical + False + True + True + Tenths + True + True + Tenths + 8D000000 + 00FFFFFF + 2BFFFFFF + D8000000 + FFFFFFFF + FFFFFFFF + FF777777 + False + FFFFFFFF + + + 1.5 + segment hits + CustomVariable + Current Comparison + Current Timing Method + + + 1.5 + delta hits + CustomVariable + Current Comparison + Current Timing Method + + + 1.5 + +/- + Delta + Current Comparison + Current Timing Method + + + 1.5 + Time + SplitTime + Current Comparison + Current Timing Method + + + + + + LiveSplit.Text.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + 00FFFFFF + 00FFFFFF + Plain + hits + hits + + + + + + + False + False + False + True + + + + LiveSplit.Text.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + 00FFFFFF + 00FFFFFF + Plain + pb hits + pb hits + + + + + + + False + False + False + True + + + + LiveSplit.Timer.dll + + 1.5 + 69 + 225 + 1.23 + False + True + FFAAAAAA + 00000000 + FF222222 + Plain + False + Game Time + 35 + + + + LiveSplit.Timer.dll + + 1.5 + 50 + 225 + 1.23 + False + True + FFAAAAAA + 00FFFFFF + 00FFFFFF + Plain + False + Real Time + 35 + + + + LiveSplit.PreviousSegment.dll + + 1.6 + FFFFFFFF + False + FF1C1C1C + FF0D0D0D + Vertical + Tenths + True + Current Comparison + False + False + Tenths + + + + LiveSplit.RunPrediction.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + Hundredths + 00FFFFFF + 00FFFFFF + Plain + Best Segments + False + + + + LiveSplit.CurrentComparison.dll + + 1.4 + FFFFFFFF + False + FFFFFFFF + False + FF1C1C1C + FF0D0D0D + Vertical + False + + + + diff --git a/tests/layout_files/mod.rs b/tests/layout_files/mod.rs index ac34b526..5892c178 100644 --- a/tests/layout_files/mod.rs +++ b/tests/layout_files/mod.rs @@ -7,3 +7,6 @@ pub const WSPLIT: &str = include_str!("WSplit.lsl"); pub const WITH_TIMER_DELTA_BACKGROUND: &str = include_str!("WithTimerDeltaBackground.lsl"); pub const WITH_BACKGROUND_IMAGE: &str = include_str!("WithBackgroundImage.lsl"); pub const TEXT_SHADOW: &str = include_str!("TextShadow.ls1l"); +pub const CUSTOM_VARIABLE_SPLITS: &str = include_str!("custom_variable_splits.lsl"); +pub const CUSTOM_VARIABLE_SUBSPLITS: &str = include_str!("custom_variable_subsplits.lsl"); +pub const CUSTOM_VARIABLE_LS1L: &str = include_str!("custom_variable_ls1l.ls1l"); diff --git a/tests/layout_parsing.rs b/tests/layout_parsing.rs index b2b03e63..ec5aad50 100644 --- a/tests/layout_parsing.rs +++ b/tests/layout_parsing.rs @@ -2,13 +2,22 @@ mod layout_files; mod parse { use crate::layout_files; - use livesplit_core::layout::{parser::parse, Layout}; + use livesplit_core::{ + Component, + component::{splits, text}, + layout::{Layout, parser::parse}, + }; #[track_caller] fn livesplit(data: &str) -> Layout { parse(data).unwrap() } + #[track_caller] + fn ls1l(data: &str) -> Layout { + Layout::from_settings(serde_json::from_str(data).unwrap()) + } + #[test] fn all() { livesplit(layout_files::ALL); @@ -34,6 +43,108 @@ mod parse { livesplit(layout_files::WITH_TIMER_DELTA_BACKGROUND); } + #[test] + fn custom_variable_splits() { + let l = livesplit(layout_files::CUSTOM_VARIABLE_SPLITS); + let Some(splits) = l.components.iter().find_map(|c| match c { + Component::Splits(s) => Some(s), + _ => None, + }) else { + panic!("Splits component not found"); + }; + let texts: Vec<_> = l + .components + .iter() + .filter_map(|c| match c { + Component::Text(t) => Some(t), + _ => None, + }) + .collect(); + { + let splits::ColumnKind::Variable(splits::VariableColumn { ref variable_name }) = + splits.settings().columns[2].kind + else { + panic!("expected ColumnKind::Variable"); + }; + assert_eq!(variable_name, "delta hits"); + } + { + let splits::ColumnKind::Variable(splits::VariableColumn { ref variable_name }) = + splits.settings().columns[3].kind + else { + panic!("expected ColumnKind::Variable"); + }; + assert_eq!(variable_name, "segment hits"); + } + { + let text::Text::Variable(ref variable_name, is_split) = texts[0].settings().text else { + panic!("expected Text::Variable"); + }; + assert_eq!(variable_name, "hits"); + assert_eq!(is_split, true); + } + { + let text::Text::Variable(ref variable_name, is_split) = texts[1].settings().text else { + panic!("expected Text::Variable"); + }; + assert_eq!(variable_name, "pb hits"); + assert_eq!(is_split, true); + } + let l1 = ls1l(layout_files::CUSTOM_VARIABLE_LS1L); + assert_eq!(serde_json::to_string(&l.settings()).ok(), serde_json::to_string(&l1.settings()).ok()); + } + + #[test] + fn custom_variable_subsplits() { + let l = livesplit(layout_files::CUSTOM_VARIABLE_SUBSPLITS); + let Some(splits) = l.components.iter().find_map(|c| match c { + Component::Splits(s) => Some(s), + _ => None, + }) else { + panic!("Splits component not found"); + }; + let texts: Vec<_> = l + .components + .iter() + .filter_map(|c| match c { + Component::Text(t) => Some(t), + _ => None, + }) + .collect(); + { + let splits::ColumnKind::Variable(splits::VariableColumn { ref variable_name }) = + splits.settings().columns[2].kind + else { + panic!("expected ColumnKind::Variable"); + }; + assert_eq!(variable_name, "delta hits"); + } + { + let splits::ColumnKind::Variable(splits::VariableColumn { ref variable_name }) = + splits.settings().columns[3].kind + else { + panic!("expected ColumnKind::Variable"); + }; + assert_eq!(variable_name, "segment hits"); + } + { + let text::Text::Variable(ref variable_name, is_split) = texts[0].settings().text else { + panic!("expected Text::Variable"); + }; + assert_eq!(variable_name, "hits"); + assert_eq!(is_split, true); + } + { + let text::Text::Variable(ref variable_name, is_split) = texts[1].settings().text else { + panic!("expected Text::Variable"); + }; + assert_eq!(variable_name, "pb hits"); + assert_eq!(is_split, true); + } + let l1 = ls1l(layout_files::CUSTOM_VARIABLE_LS1L); + assert_eq!(serde_json::to_string(&l.settings()).ok(), serde_json::to_string(&l1.settings()).ok()); + } + #[test] fn assert_order_of_default_columns() { use livesplit_core::component::splits;