diff --git a/StarterNG.Tests/SettingKeyCatalogTests.cs b/StarterNG.Tests/SettingKeyCatalogTests.cs new file mode 100644 index 0000000..440ba81 --- /dev/null +++ b/StarterNG.Tests/SettingKeyCatalogTests.cs @@ -0,0 +1,37 @@ +using StarterNG.Infrastructure.Settings; + +namespace StarterNG.Tests; + +public class SettingKeyCatalogTests +{ + [Fact] + public void A_clamped_key_is_annotated_with_its_default_and_range() + { + Assert.Equal("dynamiclights = 7 (0-7)", SettingKeyCatalog.Annotate("dynamiclights")); + Assert.Equal("starter.bufferscale = 100 (50-200)", SettingKeyCatalog.Annotate("starter.bufferscale")); + } + + [Fact] + public void An_unclamped_key_is_annotated_with_its_default_alone() + { + Assert.Equal("maxtexturesize = 4096", SettingKeyCatalog.Annotate("maxtexturesize")); + Assert.Equal("vsync = false", SettingKeyCatalog.Annotate("vsync")); + } + + [Fact] + public void A_composite_or_unknown_key_is_shown_as_written() + { + Assert.Equal("shadowtune", SettingKeyCatalog.Annotate("shadowtune")); + Assert.Equal("", SettingKeyCatalog.Annotate(null)); + } + + [Fact] + public void The_annotation_matches_what_the_serializer_actually_defaults_to() + { + var settings = new StarterNG.Domain.Settings.SimulatorSettings(); + new SettingsSerializer().Read(settings, new StarterNG.Classes.ConfigFile()); + + Assert.Equal($"maxtexturesize = {settings.MaxTextureSize}", SettingKeyCatalog.Annotate("maxtexturesize")); + Assert.Equal($"fieldofview = {settings.FieldOfView} (15-75)", SettingKeyCatalog.Annotate("fieldofview")); + } +} diff --git a/StarterNG/Controls/SettingRow.cs b/StarterNG/Controls/SettingRow.cs index 83219e3..3135314 100644 --- a/StarterNG/Controls/SettingRow.cs +++ b/StarterNG/Controls/SettingRow.cs @@ -5,7 +5,8 @@ namespace StarterNG.Controls; /// /// One line of the settings panel: label on the left, the control itself in the -/// middle, a value read-out on the right and an optional description underneath. +/// middle, a value read-out on the right, and underneath the eu07.ini key this +/// row writes plus an optional description. /// Replaces the hand-rolled StackPanel/DockPanel pattern that used to be repeated /// for every setting. /// @@ -17,6 +18,15 @@ public class SettingRow : ContentControl public static readonly StyledProperty DescriptionProperty = AvaloniaProperty.Register(nameof(Description)); + /// + /// The eu07.ini key this row reads and writes, shown verbatim so a user can + /// match what they see here against the file, the wiki or a forum post. Rows + /// that are starter-only preferences carry their "starter.*" key just the + /// same; rows that write no key at all leave this empty. + /// + public static readonly StyledProperty ConfigKeyProperty = + AvaloniaProperty.Register(nameof(ConfigKey)); + public static readonly StyledProperty ValueTextProperty = AvaloniaProperty.Register(nameof(ValueText)); @@ -32,6 +42,12 @@ public class SettingRow : ContentControl set => SetValue(DescriptionProperty, value); } + public string? ConfigKey + { + get => GetValue(ConfigKeyProperty); + set => SetValue(ConfigKeyProperty, value); + } + public string? ValueText { get => GetValue(ValueTextProperty); @@ -49,6 +65,7 @@ public class SettingRow : ContentControl if (change.Property == LabelProperty || change.Property == DescriptionProperty || + change.Property == ConfigKeyProperty || change.Property == ValueTextProperty || change.Property == ContentProperty) UpdatePseudoClasses(); @@ -62,6 +79,7 @@ public class SettingRow : ContentControl { PseudoClasses.Set(":no-label", string.IsNullOrEmpty(Label)); PseudoClasses.Set(":no-description", string.IsNullOrEmpty(Description)); + PseudoClasses.Set(":no-key", string.IsNullOrEmpty(ConfigKey)); PseudoClasses.Set(":no-value", string.IsNullOrEmpty(ValueText)); // A read-only row is a label and a reading with no widget between them. The // reading then takes the middle column too, so a long path ellipsises instead diff --git a/StarterNG/Infrastructure/Settings/SettingKeyCatalog.cs b/StarterNG/Infrastructure/Settings/SettingKeyCatalog.cs new file mode 100644 index 0000000..f9c681f --- /dev/null +++ b/StarterNG/Infrastructure/Settings/SettingKeyCatalog.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; + +namespace StarterNG.Infrastructure.Settings; + +/// What the settings screen can state about a key without describing it. +/// The eu07.ini key itself. +/// The value the simulator assumes when the key is absent. +/// "lo-hi" when the starter clamps the value, otherwise null. +public readonly record struct SettingKeyInfo(string Key, string? Default, string? Range); + +/// +/// The default and the accepted range of every eu07.ini key the starter writes. +/// +/// +/// Read straight off - these are the same +/// literals that method defaults and clamps with - so the settings screen can +/// state a fact about each key rather than a description of it. If a default or +/// a clamp changes in the serializer, it changes here too. +/// +public static class SettingKeyCatalog +{ + private static readonly Dictionary Entries = new() + { + ["ai.trainman"] = new("ai.trainman", "true", null), + ["anisotropicfiltering"] = new("anisotropicfiltering", "8", null), + ["async.trainThreads"] = new("async.trainThreads", "0", null), + ["brakespeed"] = new("brakespeed", "3.0", null), + ["brakestep"] = new("brakestep", "1.0", null), + ["compresstex"] = new("compresstex", "true", null), + ["convertmodels"] = new("convertmodels", "0", null), + ["crashdamage"] = new("crashdamage", "true", null), + ["debugmode"] = new("debugmode", "false", null), + ["dynamiclights"] = new("dynamiclights", "7", "0-7"), + ["enabletraction"] = new("enabletraction", "true", null), + ["feedbackmode"] = new("feedbackmode", "1", "0-5"), + ["feedbackport"] = new("feedbackport", "888", null), + ["fieldofview"] = new("fieldofview", "45", "15-75"), + ["fpslimit"] = new("fpslimit", "30", "1-100"), + ["friction"] = new("friction", "1.0", null), + ["fullphysics"] = new("fullphysics", "true", null), + ["fullscreen"] = new("fullscreen", "false", null), + ["fullscreenwindowed"] = new("fullscreenwindowed", "false", null), + ["gfx.drawrange.factor.max"] = new("gfx.drawrange.factor.max", "3.0", null), + ["gfx.envmap.enabled"] = new("gfx.envmap.enabled", "true", null), + ["gfx.extraeffects"] = new("gfx.extraeffects", "true", null), + ["gfx.postfx.chromaticaberration.enabled"] = new("gfx.postfx.chromaticaberration.enabled", "true", null), + ["gfx.postfx.motionblur.enabled"] = new("gfx.postfx.motionblur.enabled", "true", null), + ["gfx.reflections.fidelity"] = new("gfx.reflections.fidelity", "0", "0-2"), + ["gfx.reflections.framerate"] = new("gfx.reflections.framerate", "60", null), + ["gfx.resource.move"] = new("gfx.resource.move", "false", null), + ["gfx.resource.sweep"] = new("gfx.resource.sweep", "true", null), + ["gfx.shadergamma"] = new("gfx.shadergamma", "false", null), + ["gfx.shadow.angle.min"] = new("gfx.shadow.angle.min", "0.2", null), + ["gfx.shadow.rank.cutoff"] = new("gfx.shadow.rank.cutoff", "3", null), + ["gfx.shadows.cab.range"] = new("gfx.shadows.cab.range", "0", null), + ["gfx.skippipeline"] = new("gfx.skippipeline", "false", null), + ["gfx.skiprendering"] = new("gfx.skiprendering", "false", null), + ["gfx.smoke"] = new("gfx.smoke", "true", null), + ["gfx.smoke.fidelity"] = new("gfx.smoke.fidelity", "1", "1-4"), + ["gfx.usegles"] = new("gfx.usegles", "false", null), + ["height"] = new("height", "720", null), + ["inactivepause"] = new("inactivepause", "true", null), + ["input.gamepad"] = new("input.gamepad", "true", null), + ["lang"] = new("lang", "en", null), + ["livetraction"] = new("livetraction", "true", null), + ["maxcabtexturesize"] = new("maxcabtexturesize", "4096", null), + ["maxtexturesize"] = new("maxtexturesize", "4096", null), + ["multiplelogs"] = new("multiplelogs", "false", null), + ["multisampling"] = new("multisampling", "2", "0-3"), + ["pause"] = new("pause", "false", null), + ["physicslog"] = new("physicslog", "false", null), + ["python.enabled"] = new("python.enabled", "true", null), + ["python.mipmaps"] = new("python.mipmaps", "true", null), + ["python.threadedupload"] = new("python.threadedupload", "true", null), + ["python.updatetime"] = new("python.updatetime", "200", null), + ["scalespeculars"] = new("scalespeculars", "true", null), + ["shadows"] = new("shadows", "true", null), + ["sound.volume"] = new("sound.volume", "1.0", null), + ["sound.volume.ambient"] = new("sound.volume.ambient", "1.0", null), + ["sound.volume.positional"] = new("sound.volume.positional", "1.0", null), + ["sound.volume.radio"] = new("sound.volume.radio", "0.75", null), + ["sound.volume.vehicle"] = new("sound.volume.vehicle", "1.0", null), + ["soundenabled"] = new("soundenabled", "true", null), + ["splinefidelity"] = new("splinefidelity", "1", "1-4"), + ["starter.autoclose"] = new("starter.autoclose", "false", null), + ["starter.batterydefault"] = new("starter.batterydefault", "0", "0-2"), + ["starter.bufferscale"] = new("starter.bufferscale", "100", "50-200"), + ["starter.drivableonly"] = new("starter.drivableonly", "true", null), + ["starter.exe.auto"] = new("starter.exe.auto", "true", null), + ["starter.exe.path"] = new("starter.exe.path", "eu07.exe", null), + ["starter.expandtree"] = new("starter.expandtree", "false", null), + ["starter.hide.archivalvehicles"] = new("starter.hide.archivalvehicles", "true", null), + ["starter.ignoreirrelevant"] = new("starter.ignoreirrelevant", "false", null), + ["starter.largethumbnails"] = new("starter.largethumbnails", "false", null), + ["starter.last.scenery"] = new("starter.last.scenery", null, null), + ["starter.logmissingvehicles"] = new("starter.logmissingvehicles", "false", null), + ["starter.show.ai"] = new("starter.show.ai", "true", null), + ["starter.show.archival"] = new("starter.show.archival", "true", null), + ["starter.window.maximized"] = new("starter.window.maximized", "false", null), + ["uart"] = new("uart", null, null), + ["uartdebug"] = new("uartdebug", "false", null), + ["uarttune"] = new("uarttune", null, null), + ["usevbo"] = new("usevbo", "true", null), + ["vsync"] = new("vsync", "false", null), + ["width"] = new("width", "1280", null), + }; + + /// + /// The key annotation for a settings row: the key, its default and its range, + /// e.g. "dynamiclights = 7 (0-7)". Empty for a key we know nothing about, and + /// for the composite keys a row only partly owns. + /// + public static string Annotate(string? key) + { + if (string.IsNullOrEmpty(key) || !Entries.TryGetValue(key, out var info)) + return key ?? ""; + + string text = info.Default is null ? info.Key : $"{info.Key} = {info.Default}"; + return info.Range is null ? text : $"{text} ({info.Range})"; + } +} diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 49615b5..4c0be03 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -53,13 +53,25 @@ VerticalAlignment="Center" Margin="12,0,0,0" /> - + + + + + @@ -67,6 +79,9 @@ + diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index 49cbaa2..d452453 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -79,29 +79,29 @@ Text="{Binding [General], Source={x:Static starterNg:App.Loc}}" /> - + - + - + - + - - + - + @@ -109,16 +109,16 @@ - + - + - + - @@ -131,16 +131,16 @@ - + - + - + - + @@ -153,7 +153,7 @@ Text="{Binding [Graphic], Source={x:Static starterNg:App.Loc}}" /> - + 3840x2160 2560x1440 @@ -163,49 +163,49 @@ 1280x720 - + - - - - - - - - @@ -228,42 +228,42 @@ - + - + - + - + - + - + - + - - + - + - - - - + - - - - @@ -336,10 +336,10 @@ - + - @@ -347,7 +347,7 @@ - - + - + - @@ -387,27 +387,27 @@ Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" /> - - - - - @@ -422,45 +422,45 @@ Text="{Binding [Physics], Source={x:Static starterNg:App.Loc}}" /> - + - + - + - + - + - + - + - + - + - - - @@ -475,10 +475,10 @@ Text="{Binding [Communication], Source={x:Static starterNg:App.Loc}}" /> - + - @@ -489,13 +489,13 @@ - - +