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 @@
-
-
+