Show the eu07.ini key on every settings row

This commit is contained in:
maj00r
2026-09-04 21:21:14 +02:00
parent 964884bd56
commit e969137824
6 changed files with 284 additions and 78 deletions

View File

@@ -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"));
}
}

View File

@@ -5,7 +5,8 @@ namespace StarterNG.Controls;
/// <summary>
/// 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.
/// </summary>
@@ -17,6 +18,15 @@ public class SettingRow : ContentControl
public static readonly StyledProperty<string?> DescriptionProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(Description));
/// <summary>
/// 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.
/// </summary>
public static readonly StyledProperty<string?> ConfigKeyProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(ConfigKey));
public static readonly StyledProperty<string?> ValueTextProperty =
AvaloniaProperty.Register<SettingRow, string?>(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

View File

@@ -0,0 +1,121 @@
using System.Collections.Generic;
namespace StarterNG.Infrastructure.Settings;
/// <summary>What the settings screen can state about a key without describing it.</summary>
/// <param name="Key">The eu07.ini key itself.</param>
/// <param name="Default">The value the simulator assumes when the key is absent.</param>
/// <param name="Range">"lo-hi" when the starter clamps the value, otherwise null.</param>
public readonly record struct SettingKeyInfo(string Key, string? Default, string? Range);
/// <summary>
/// The default and the accepted range of every eu07.ini key the starter writes.
/// </summary>
/// <remarks>
/// Read straight off <see cref="SettingsSerializer"/> - 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.
/// </remarks>
public static class SettingKeyCatalog
{
private static readonly Dictionary<string, SettingKeyInfo> 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),
};
/// <summary>
/// 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.
/// </summary>
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})";
}
}

View File

@@ -53,13 +53,25 @@
VerticalAlignment="Center"
Margin="12,0,0,0" />
<TextBlock Name="PART_Description"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
Text="{TemplateBinding Description}"
FontSize="12"
Foreground="{StaticResource CarbonFgDim}"
TextWrapping="Wrap"
Margin="0,3,0,0" />
<!-- Second line: the eu07.ini key this row writes, then the
explanation. The key is monospaced and selectable so it
can be matched against the file itself. -->
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
Orientation="Horizontal" Spacing="8" Margin="0,3,0,0">
<SelectableTextBlock Name="PART_ConfigKey"
Text="{TemplateBinding ConfigKey}"
FontFamily="Consolas, DejaVu Sans Mono, Liberation Mono, Menlo, Courier New"
FontSize="11"
Foreground="{StaticResource CarbonAccentBright}"
Opacity="0.75"
VerticalAlignment="Top" />
<TextBlock Name="PART_Description"
Text="{TemplateBinding Description}"
FontSize="12"
Foreground="{StaticResource CarbonFgDim}"
TextWrapping="Wrap"
VerticalAlignment="Top" />
</StackPanel>
</Grid>
</ControlTemplate>
</Setter>
@@ -67,6 +79,9 @@
<Style Selector="^:no-description /template/ TextBlock#PART_Description">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:no-key /template/ SelectableTextBlock#PART_ConfigKey">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:no-value /template/ TextBlock#PART_Value">
<Setter Property="IsVisible" Value="False" />
</Style>

View File

@@ -79,29 +79,29 @@
Text="{Binding [General], Source={x:Static starterNg:App.Loc}}" />
<ctl:SettingsSection Header="{Binding [General], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Label="{Binding [Language], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="lang" Label="{Binding [Language], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="ChangeLanguageCb"
SelectionChanged="LanguageComboBox_OnSelectionChanged" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="fullscreen">
<CheckBox x:Name="FullscreenCb" Content="{Binding [Fullscreen], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="inactivepause">
<CheckBox x:Name="PauseInactiveCb" Content="{Binding [PauseSimInactive], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="pause">
<CheckBox x:Name="PauseStartCb" Content="{Binding [PauseSimStart], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="CursorSensitivityRow"
<ctl:SettingRow ConfigKey="mousescale" x:Name="CursorSensitivityRow"
Label="{Binding [CursorSensitivity], Source={x:Static starterNg:App.Loc}}">
<Slider x:Name="CursorSensitivitySlider" Minimum="1" Maximum="5" TickFrequency="1"
IsSnapToTickEnabled="True" Value="3"
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="mousescale">
<CheckBox x:Name="MouseHorInvertCb" Content="{Binding [MouseHorInvert], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="mousescale">
<CheckBox x:Name="MouseVertInvertCb" Content="{Binding [MouseVertInvert], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
</StackPanel>
@@ -109,16 +109,16 @@
<ctl:SettingsSection Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="starter.autoclose">
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="starter.largethumbnails">
<CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="starter.expandtree">
<CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="starter.batterydefault" Label="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [BatteryDefaultDesc], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="BatteryDefaultCb" SelectedIndex="0">
<ComboBoxItem Content="{Binding [BatteryDefaultScenario], Source={x:Static starterNg:App.Loc}}" />
@@ -131,16 +131,16 @@
<ctl:SettingsSection Header="{Binding [Others], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Label="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="starter.exe.path" Label="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="SelectExeCb" SelectedIndex="0" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="debugmode">
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [VirtualShuntingDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="ai.trainman" Description="{Binding [VirtualShuntingDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [LogMissingVehicleFilesDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="starter.logmissingvehicles" Description="{Binding [LogMissingVehicleFilesDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
</StackPanel>
@@ -153,7 +153,7 @@
Text="{Binding [Graphic], Source={x:Static starterNg:App.Loc}}" />
<ctl:SettingsSection Header="{Binding [GfxColResolution], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Label="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="width, height" Label="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="ResolutionCb" SelectedIndex="0">
<ComboBoxItem>3840x2160</ComboBoxItem>
<ComboBoxItem>2560x1440</ComboBoxItem>
@@ -163,49 +163,49 @@
<ComboBoxItem>1280x720</ComboBoxItem>
</ComboBox>
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="fullscreenwindowed">
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [BufforResolution], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="starter.bufferscale" Label="{Binding [BufforResolution], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [BufforResolutionDesc], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #bufferScale.Value, StringFormat='{}{0:0}%'}">
<Slider Name="bufferScale" Minimum="50" Maximum="200" TickFrequency="1"
IsSnapToTickEnabled="True" Value="100" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="TexResolutionRow"
<ctl:SettingRow ConfigKey="maxtexturesize" x:Name="TexResolutionRow"
Label="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}">
<Slider Name="textureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1"
IsSnapToTickEnabled="True" Value="12"
ValueChanged="TextureResolutionSlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="CabTexResolutionRow"
<ctl:SettingRow ConfigKey="maxcabtexturesize" x:Name="CabTexResolutionRow"
Label="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}">
<Slider Name="cabTextureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1"
IsSnapToTickEnabled="True" Value="12"
ValueChanged="CabTextureResolutionSlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="TexFilteringRow"
<ctl:SettingRow ConfigKey="anisotropicfiltering" x:Name="TexFilteringRow"
Label="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [TexFilteringQualityDesc], Source={x:Static starterNg:App.Loc}}">
<Slider x:Name="TexFilteringSlider" Minimum="1" Maximum="5" TickFrequency="1"
IsSnapToTickEnabled="True" Value="4"
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="MultisamplingRow"
<ctl:SettingRow ConfigKey="multisampling" x:Name="MultisamplingRow"
Label="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [MultisamplingDesc], Source={x:Static starterNg:App.Loc}}">
<Slider x:Name="MultisamplingSlider" Minimum="1" Maximum="4" TickFrequency="1"
IsSnapToTickEnabled="True" Value="3"
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="TrackCurvesRow"
<ctl:SettingRow ConfigKey="splinefidelity" x:Name="TrackCurvesRow"
Label="{Binding [TrackCurvesQuality], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [TrackCurvesQualityDesc], Source={x:Static starterNg:App.Loc}}">
<Slider x:Name="TrackCurvesSlider" Minimum="1" Maximum="4" TickFrequency="1"
IsSnapToTickEnabled="True" Value="4"
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="RenderRangeRow"
<ctl:SettingRow ConfigKey="gfx.drawrange.factor.max" x:Name="RenderRangeRow"
Label="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [RenderRangeDesc], Source={x:Static starterNg:App.Loc}}">
<Slider x:Name="RenderRangeSlider" Minimum="1" Maximum="3" TickFrequency="1"
@@ -217,7 +217,7 @@
<ctl:SettingsSection Header="{Binding [GfxColRendering], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Label="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="gfxrenderer" Label="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [RenderEngineDesc], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="RenderEngineCb" SelectedIndex="0">
<ComboBoxItem Content="{Binding [REFull], Source={x:Static starterNg:App.Loc}}" />
@@ -228,42 +228,42 @@
<ComboBoxItem Content="{Binding [REExperimental], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [RenderAngleVulkanDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="gfx.angleplatform" Description="{Binding [RenderAngleVulkanDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="gfx.extraeffects">
<CheckBox x:Name="AdditionalShadersCb" Content="{Binding [AdditionalShadersEffects], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="gfx.postfx.chromaticaberration.enabled">
<CheckBox x:Name="ChromaticAberrationCb" Content="{Binding [ChromaticAberration], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="gfx.postfx.motionblur.enabled">
<CheckBox x:Name="MotionBlurCb" Content="{Binding [MotionBlur], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [SkipPipelineDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="gfx.skippipeline" Description="{Binding [SkipPipelineDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="SkipPipelineCb" Content="{Binding [SkipPipeline], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [RenderVBODesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="usevbo" Description="{Binding [RenderVBODesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [VSyncDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="vsync" Description="{Binding [VSyncDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Margin="0,0,0,4"
<ctl:SettingRow ConfigKey="fpslimit" Margin="0,0,0,4"
ValueText="{Binding #FpsLimitSlider.Value, StringFormat='{}{0:0} FPS'}">
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="fpslimit">
<Slider x:Name="FpsLimitSlider" Minimum="1" Maximum="100" TickFrequency="1"
IsSnapToTickEnabled="True" Value="30"
IsEnabled="{Binding #FpsLimitEnableCb.IsChecked}" />
</ctl:SettingRow>
<ctl:SettingRow Margin="0,0,0,4">
<ctl:SettingRow ConfigKey="gfx.smoke" Margin="0,0,0,4">
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="SmokeParticlesRow"
<ctl:SettingRow ConfigKey="gfx.smoke.fidelity" x:Name="SmokeParticlesRow"
Label="{Binding [SmokeParticlesMultiplier], Source={x:Static starterNg:App.Loc}}"
IsEnabled="{Binding #SmokeDisplayCb.IsChecked}">
<Slider x:Name="SmokeParticlesSlider" Minimum="1" Maximum="4" TickFrequency="1"
@@ -271,12 +271,12 @@
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="dynamiclights" Label="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #DynamicLightsSlider.Value, StringFormat='{}{0:0}'}">
<Slider x:Name="DynamicLightsSlider" Minimum="0" Maximum="7" TickFrequency="1"
IsSnapToTickEnabled="True" Value="7" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="fieldofview" Label="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [FieldOfViewDesc], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #fovSlider.Value, StringFormat='{}{0:0}°'}">
<Slider Name="fovSlider" Minimum="15" Maximum="75" TickFrequency="5"
@@ -291,33 +291,33 @@
Content="{Binding [RenderShadows], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingsSection.Toggle>
<StackPanel IsEnabled="{Binding #RenderShadowsCb.IsChecked}">
<ctl:SettingRow Label="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="gfx.shadow.rank.cutoff" Label="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="ShadowDisplayCb" SelectedIndex="0">
<ComboBoxItem Content="{Binding [CSROnlyImportant], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRLimited], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRAll], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</ctl:SettingRow>
<ctl:SettingRow x:Name="ShadowResolutionRow"
<ctl:SettingRow ConfigKey="shadowtune" x:Name="ShadowResolutionRow"
Label="{Binding [ShadowsResolution], Source={x:Static starterNg:App.Loc}}">
<Slider Name="shaderResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1"
IsSnapToTickEnabled="True" Value="12"
ValueChanged="shaderResolutionSlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="ShadowRangeRow"
<ctl:SettingRow ConfigKey="shadowtune" x:Name="ShadowRangeRow"
Label="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}">
<Slider Name="shaderRange" Minimum="25" Maximum="400" TickFrequency="5"
IsSnapToTickEnabled="True" Value="150"
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="CabShadowRangeRow"
<ctl:SettingRow ConfigKey="gfx.shadows.cab.range" x:Name="CabShadowRangeRow"
Label="{Binding [CabShadowsRange], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [CabShadowsRangeDesc], Source={x:Static starterNg:App.Loc}}">
<Slider Name="cabShaderSourceRange" Minimum="0" Maximum="100" TickFrequency="10"
IsSnapToTickEnabled="True" Value="30"
ValueChanged="QualitySlider_OnValueChanged" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="gfx.shadow.angle.min" Label="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [ShadowAngleLimitDesc], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #ShadowAngleLimitSlider.Value, StringFormat='{}{0:0.0}'}">
<StackPanel>
@@ -336,10 +336,10 @@
<ctl:SettingsSection Header="{Binding [GfxColReflections], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Description="{Binding [ReflectionsCubeMapDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="gfx.envmap.enabled" Description="{Binding [ReflectionsCubeMapDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="ReflectionsCubeMapCb" Content="{Binding [ReflectionsCubeMap], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="gfx.reflections.fidelity" Label="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [ReflectionsDetailsDesc], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="ReflectionsDetailsCb" SelectedIndex="0">
<ComboBoxItem Content="{Binding [RDTerrain], Source={x:Static starterNg:App.Loc}}" />
@@ -347,7 +347,7 @@
<ComboBoxItem Content="{Binding [RDTerrainModelsVehicles], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</ctl:SettingRow>
<ctl:SettingRow x:Name="ReflectionsFramerateRow"
<ctl:SettingRow ConfigKey="gfx.reflections.framerate" x:Name="ReflectionsFramerateRow"
Label="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}">
<Slider Name="reflectionsFramerate" Minimum="5" Maximum="120" TickFrequency="5"
IsSnapToTickEnabled="True" Value="30"
@@ -358,13 +358,13 @@
<ctl:SettingsSection Header="{Binding [GfxColScripts], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Description="{Binding [RenderScreensDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="python.enabled" Description="{Binding [RenderScreensDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="RenderScreensCb" Content="{Binding [RenderScreens], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="python.threadedupload">
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [PyScreenPriority], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="python.updatetime" Label="{Binding [PyScreenPriority], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [PyScreenPriorityDesc], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="PyScreenPriorityCb">
<ComboBoxItem Tag="200" Content="{Binding [PyPriorityNormal], Source={x:Static starterNg:App.Loc}}" />
@@ -387,27 +387,27 @@
Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingsSection.Toggle>
<StackPanel IsEnabled="{Binding #EnableSoundsCb.IsChecked}">
<ctl:SettingRow Label="{Binding [Volume], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="sound.volume" Label="{Binding [Volume], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #VolumeSlider.Value, StringFormat='{}{0:0}%'}">
<Slider x:Name="VolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
IsSnapToTickEnabled="True" Value="100" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="sound.volume.radio" Label="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #RadioVolumeSlider.Value, StringFormat='{}{0:0}%'}">
<Slider x:Name="RadioVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
IsSnapToTickEnabled="True" Value="100" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="sound.volume.vehicle" Label="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #VehiclesVolumeSlider.Value, StringFormat='{}{0:0}%'}">
<Slider x:Name="VehiclesVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
IsSnapToTickEnabled="True" Value="100" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="sound.volume.positional" Label="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #PositionalVolumeSlider.Value, StringFormat='{}{0:0}%'}">
<Slider x:Name="PositionalVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
IsSnapToTickEnabled="True" Value="100" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="sound.volume.ambient" Label="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #AmbientVolumeSlider.Value, StringFormat='{}{0:0}%'}">
<Slider x:Name="AmbientVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
IsSnapToTickEnabled="True" Value="100" />
@@ -422,45 +422,45 @@
Text="{Binding [Physics], Source={x:Static starterNg:App.Loc}}" />
<ctl:SettingsSection Header="{Binding [Physics], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Description="{Binding [PhysicsAccuracyIncreasedDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="fullphysics" Description="{Binding [PhysicsAccuracyIncreasedDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="PhysicsAccuracyCb" Content="{Binding [PhysicsAccuracyIncreased], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [BreakingPantographPossibilityDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="enabletraction" Description="{Binding [BreakingPantographPossibilityDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="PantographBreakCb" Content="{Binding [BreakingPantographPossibility], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Description="{Binding [PowerOverheadLineOnlyDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="livetraction" Description="{Binding [PowerOverheadLineOnlyDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="OverheadOnlyCb" Content="{Binding [PowerOverheadLineOnly], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="physicslog">
<CheckBox x:Name="SpeedometerTapesCb" Content="{Binding [SaveSpeedometerTapes], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="debuglog">
<CheckBox x:Name="SimLogsCb" Content="{Binding [SaveSimLogs], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="multiplelogs">
<CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="gfx.skiprendering">
<CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="debuglog">
<CheckBox x:Name="DebugLogVisibleCb" Content="{Binding [DebugLogVisible], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow>
<ctl:SettingRow ConfigKey="crashdamage">
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [Friction], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="friction" Label="{Binding [Friction], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [FrictionDesc], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #FrictionSlider.Value, StringFormat='{}{0:0.00}'}">
<Slider x:Name="FrictionSlider" Minimum="0.1" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="brakestep" Label="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [BrakeStepDesc], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #BrakeStepSlider.Value, StringFormat='{}{0:0.00}'}">
<Slider x:Name="BrakeStepSlider" Minimum="0.4" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="brakespeed" Label="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [BrakeSpeedDesc], Source={x:Static starterNg:App.Loc}}"
ValueText="{Binding #BrakeSpeedSlider.Value, StringFormat='{}{0:0.00}'}">
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
@@ -475,10 +475,10 @@
Text="{Binding [Communication], Source={x:Static starterNg:App.Loc}}" />
<ctl:SettingsSection Header="{Binding [Communication], Source={x:Static starterNg:App.Loc}}">
<StackPanel>
<ctl:SettingRow Description="{Binding [GamepadSignalIgnoreDesc], Source={x:Static starterNg:App.Loc}}">
<ctl:SettingRow ConfigKey="input.gamepad" Description="{Binding [GamepadSignalIgnoreDesc], Source={x:Static starterNg:App.Loc}}">
<CheckBox x:Name="GamepadIgnoreCb" Content="{Binding [GamepadSignalIgnore], Source={x:Static starterNg:App.Loc}}" />
</ctl:SettingRow>
<ctl:SettingRow Label="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}"
<ctl:SettingRow ConfigKey="feedbackmode" Label="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [FeedbackDesc], Source={x:Static starterNg:App.Loc}}">
<ComboBox x:Name="FeedbackCb" SelectedIndex="0">
<ComboBoxItem Tag="0" Content="{Binding [FeedbackOff], Source={x:Static starterNg:App.Loc}}" />
@@ -489,13 +489,13 @@
<ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</ctl:SettingRow>
<ctl:SettingRow x:Name="FeedbackPortRow" IsVisible="False"
<ctl:SettingRow ConfigKey="feedbackport" x:Name="FeedbackPortRow" IsVisible="False"
Label="{Binding [FeedbackPort], Source={x:Static starterNg:App.Loc}}"
Description="{Binding [FeedbackPortDesc], Source={x:Static starterNg:App.Loc}}">
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888"
Increment="1" FormatString="0" />
</ctl:SettingRow>
<ctl:SettingRow x:Name="ComButtonRow" IsVisible="False">
<ctl:SettingRow ConfigKey="uart" x:Name="ComButtonRow" IsVisible="False">
<Button x:Name="ComButton" Classes="Flat" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="ComButton_OnClick"

View File

@@ -15,6 +15,7 @@ using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media;
using StarterNG.Classes;
using StarterNG.Controls;
using StarterNG.Infrastructure.Settings;
using StarterNG.Infrastructure;
using StarterNG.Services;
@@ -64,6 +65,7 @@ public partial class Settings : UserControl, ISettingsCapture
AppServices.Current.SettingsStore.RegisterCapture(this);
AnnotateConfigKeys();
PopulateProfileCombo();
ApplyToUi();
@@ -160,6 +162,19 @@ public partial class Settings : UserControl, ISettingsCapture
public void ReloadFromSettings() => ApplyToUi();
/// <summary>
/// Expands each row's eu07.ini key into "key = default (range)", so the line
/// under a setting says what the file holds rather than paraphrasing what the
/// setting does.
/// </summary>
private void AnnotateConfigKeys()
{
// Logical, not visual: the rows exist as soon as the XAML is loaded,
// while categories that are not on screen yet have no visual tree.
foreach (var row in this.GetLogicalDescendants().OfType<SettingRow>())
row.ConfigKey = SettingKeyCatalog.Annotate(row.ConfigKey);
}
private void ApplyToUi()
{
var s = AppServices.Current.Settings;