diff --git a/StarterNG.sln.DotSettings.user b/StarterNG.sln.DotSettings.user
index 680ff6c..e68f177 100644
--- a/StarterNG.sln.DotSettings.user
+++ b/StarterNG.sln.DotSettings.user
@@ -8,6 +8,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/StarterNG/App.axaml.cs b/StarterNG/App.axaml.cs
index 5485232..53bdb39 100644
--- a/StarterNG/App.axaml.cs
+++ b/StarterNG/App.axaml.cs
@@ -22,13 +22,20 @@ public partial class App : Application
{
AvaloniaXamlLoader.Load(this);
+ // Load the persisted configuration up front so the saved language wins;
+ // fall back to the system culture only when the file has no lang entry.
+ Settings.Instance.Load();
+
CultureInfo ci = CultureInfo.InstalledUICulture ;
-
- var langName = ci.Name switch
- {
- "pl-PL" => "Polski",
- _ => "English"
- };
+
+ var langName = Settings.Instance.LanguageWasSet
+ ? Settings.Instance.Language
+ : ci.Name switch
+ {
+ "pl-PL" => "Polski",
+ _ => "English"
+ };
+ Settings.Instance.Language = langName;
var langDict = new ResourceInclude(new Uri("avares://StarterNG/"))
{
@@ -61,6 +68,9 @@ public partial class App : Application
// Code page 1250 is needed to parse scenery files (done during load).
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ // Persist settings when the launcher is closing.
+ desktop.ShutdownRequested += (_, _) => Settings.Instance.CaptureAndSave();
+
var splash = new SplashWindow();
desktop.MainWindow = splash;
splash.Show();
diff --git a/StarterNG/Assets/Langs/English.axaml b/StarterNG/Assets/Langs/English.axaml
index e3ce747..3fe5713 100644
--- a/StarterNG/Assets/Langs/English.axaml
+++ b/StarterNG/Assets/Langs/English.axaml
@@ -171,5 +171,10 @@
Automatically close starter
Large thumbnails
Automatically expand scenery tree
-
+
+
+ Save
+ Reset
+ Settings saved
+
diff --git a/StarterNG/Assets/Langs/Polski.axaml b/StarterNG/Assets/Langs/Polski.axaml
index 322de8e..28545e2 100644
--- a/StarterNG/Assets/Langs/Polski.axaml
+++ b/StarterNG/Assets/Langs/Polski.axaml
@@ -171,8 +171,10 @@
Zamknij starter automatycznie
Duże miniaturki
Automatycznie rozwijanie drzewka scenerii
-
-
-
-
+
+
+ Zapisz
+ Przywróć
+ Zapisano ustawienia
+
diff --git a/StarterNG/Classes/ConfigFile.cs b/StarterNG/Classes/ConfigFile.cs
new file mode 100644
index 0000000..95e645f
--- /dev/null
+++ b/StarterNG/Classes/ConfigFile.cs
@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace StarterNG.Classes;
+
+///
+/// A line-oriented reader/writer for the EU07 eu07.ini configuration
+/// file.
+///
+/// The file is not a classic INI: it is a flat list of key value [value…]
+/// entries, one per line, with // comments (either trailing a line or
+/// commenting the whole line out). The simulator parses it as a stream of
+/// whitespace-separated tokens.
+///
+/// This class keeps every line verbatim and only rewrites the value of a key
+/// when is called. Comments, blank lines, commented-out
+/// entries and — crucially — any keys the launcher does not understand are
+/// preserved on save, so editing a handful of settings never drops the rest of
+/// the user's configuration.
+///
+public sealed class ConfigFile
+{
+ private readonly List _lines = new();
+
+ // Active (uncommented) setting key -> index into _lines. Last occurrence wins,
+ // matching the simulator which lets later entries override earlier ones.
+ private readonly Dictionary _index =
+ new(StringComparer.OrdinalIgnoreCase);
+
+ /// Parses raw file text, preserving line breaks and content.
+ public static ConfigFile Parse(string text)
+ {
+ var cfg = new ConfigFile();
+ // Normalise CRLF/CR to LF for splitting; output uses Environment.NewLine.
+ text = text.Replace("\r\n", "\n").Replace('\r', '\n');
+ foreach (var line in text.Split('\n'))
+ cfg.AddLine(line);
+ return cfg;
+ }
+
+ private void AddLine(string raw)
+ {
+ int idx = _lines.Count;
+ _lines.Add(raw);
+ if (TryParseKey(raw, out string key))
+ _index[key] = idx; // later duplicates override earlier ones
+ }
+
+ /// True if the key is present as an active (uncommented) entry.
+ public bool Has(string key) => _index.ContainsKey(key);
+
+ /// Raw value (all tokens after the key, comment stripped), or null.
+ public string? GetRaw(string key)
+ {
+ if (!_index.TryGetValue(key, out int i))
+ return null;
+ SplitLine(_lines[i], out _, out _, out string value, out _);
+ return value;
+ }
+
+ public string GetString(string key, string fallback) =>
+ GetRaw(key) is { Length: > 0 } v ? v : fallback;
+
+ public bool GetBool(string key, bool fallback)
+ {
+ var v = FirstToken(GetRaw(key));
+ if (v is null) return fallback;
+ return v.ToLowerInvariant() switch
+ {
+ "yes" or "true" or "1" or "tak" => true,
+ "no" or "false" or "0" or "nie" => false,
+ _ => fallback
+ };
+ }
+
+ public int GetInt(string key, int fallback)
+ {
+ var v = FirstToken(GetRaw(key));
+ return v != null && int.TryParse(v, NumberStyles.Integer,
+ CultureInfo.InvariantCulture, out int n) ? n : fallback;
+ }
+
+ public double GetDouble(string key, double fallback)
+ {
+ var v = FirstToken(GetRaw(key));
+ return v != null && double.TryParse(v, NumberStyles.Float,
+ CultureInfo.InvariantCulture, out double d) ? d : fallback;
+ }
+
+ /// All whitespace-separated value tokens for a key (never null).
+ public string[] GetTokens(string key)
+ {
+ var v = GetRaw(key);
+ return string.IsNullOrEmpty(v)
+ ? Array.Empty()
+ : v.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ ///
+ /// Sets the value of . If the key already exists its
+ /// line is rewritten in place, preserving original indentation and any
+ /// trailing comment. Otherwise a new line is appended.
+ ///
+ public void Set(string key, string value)
+ {
+ value ??= string.Empty;
+ if (_index.TryGetValue(key, out int i))
+ {
+ SplitLine(_lines[i], out string indent, out _, out _, out string comment);
+ var sb = new StringBuilder();
+ sb.Append(indent).Append(key);
+ if (value.Length > 0)
+ sb.Append(' ').Append(value);
+ if (comment.Length > 0)
+ sb.Append('\t').Append(comment);
+ _lines[i] = sb.ToString();
+ }
+ else
+ {
+ int idx = _lines.Count;
+ _lines.Add(value.Length > 0 ? $"{key} {value}" : key);
+ _index[key] = idx;
+ }
+ }
+
+ public void SetBool(string key, bool value) => Set(key, value ? "yes" : "no");
+ public void SetInt(string key, int value) =>
+ Set(key, value.ToString(CultureInfo.InvariantCulture));
+ public void SetDouble(string key, double value) =>
+ Set(key, value.ToString("0.###", CultureInfo.InvariantCulture));
+
+ /// Serialises back to text using the platform newline.
+ public string ToText() => string.Join(Environment.NewLine, _lines);
+
+ // --- line parsing helpers -------------------------------------------------
+
+ private static bool TryParseKey(string raw, out string key)
+ {
+ SplitLine(raw, out _, out key, out _, out _);
+ return key.Length > 0;
+ }
+
+ ///
+ /// Decomposes a raw line into leading whitespace, key, value (tokens after
+ /// the key) and trailing comment (including the //). For blank or
+ /// fully commented-out lines, key is empty.
+ ///
+ private static void SplitLine(string raw, out string indent, out string key,
+ out string value, out string comment)
+ {
+ indent = key = value = comment = string.Empty;
+
+ // leading whitespace
+ int start = 0;
+ while (start < raw.Length && char.IsWhiteSpace(raw[start]))
+ start++;
+ indent = raw.Substring(0, start);
+
+ string body = raw.Substring(start);
+
+ // A line whose first non-space content is "//" is a pure comment.
+ if (body.StartsWith("//"))
+ {
+ comment = body;
+ return;
+ }
+ if (body.Length == 0)
+ return;
+
+ // Split off a trailing comment: the first "//" preceded by whitespace
+ // (so URLs such as tcp://… inside a value are not mistaken for one).
+ int commentAt = FindCommentStart(body);
+ string content = commentAt >= 0 ? body.Substring(0, commentAt) : body;
+ if (commentAt >= 0)
+ comment = body.Substring(commentAt).TrimEnd();
+
+ content = content.TrimEnd();
+ if (content.Length == 0)
+ return;
+
+ int sp = IndexOfWhitespace(content);
+ if (sp < 0)
+ {
+ key = content;
+ }
+ else
+ {
+ key = content.Substring(0, sp);
+ value = content.Substring(sp).Trim();
+ }
+ }
+
+ private static int FindCommentStart(string body)
+ {
+ for (int i = 0; i + 1 < body.Length; i++)
+ {
+ if (body[i] == '/' && body[i + 1] == '/' &&
+ (i == 0 || char.IsWhiteSpace(body[i - 1])))
+ return i;
+ }
+ return -1;
+ }
+
+ private static int IndexOfWhitespace(string s)
+ {
+ for (int i = 0; i < s.Length; i++)
+ if (char.IsWhiteSpace(s[i]))
+ return i;
+ return -1;
+ }
+
+ private static string? FirstToken(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return null;
+ int sp = IndexOfWhitespace(value);
+ return sp < 0 ? value : value.Substring(0, sp);
+ }
+}
diff --git a/StarterNG/Classes/Settings.cs b/StarterNG/Classes/Settings.cs
index 38cb906..b2c79be 100644
--- a/StarterNG/Classes/Settings.cs
+++ b/StarterNG/Classes/Settings.cs
@@ -1,51 +1,415 @@
-namespace StarterNG.Classes;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+namespace StarterNG.Classes;
///
-/// Application settings backing the Settings view. Load/Save are stubs for now;
-/// the real persistence will be implemented later.
+/// Application settings backing the Settings view, persisted to the simulator's
+/// eu07.ini.
+///
+/// Where it loads/saves. The active file lives next to the
+/// simulator's own config: %APPDATA%\MaSzyna\eu07.ini on Windows or
+/// ~/.config/MaSzyna/eu07.ini elsewhere. If that file does not exist yet,
+/// defaults are read from an eu07.ini in the launcher's working directory
+/// (the same fallback the simulator uses). Saving always writes to the per-user
+/// path.
+///
+/// Unknown keys are kept. The whole file is round-tripped through
+/// ; only the keys the UI actually edits are rewritten,
+/// so any extra settings the launcher does not model survive a save.
+///
+/// Some mappings are best-effort where the wiki does not document an exact
+/// key; those are marked with NOTE and can be adjusted against the running exe.
///
public sealed class Settings
{
public static Settings Instance { get; } = new();
- // General
- public string Language = "English";
- public bool Fullscreen;
- public bool PauseWhenInactive;
- public bool PauseOnStart;
- public int CursorSensitivity = 3;
- public bool InvertMouseHorizontal;
- public bool InvertMouseVertical;
+ ///
+ /// Pulls the current values out of the Settings view into this instance.
+ /// The view assigns this when it is constructed; null if the view was never
+ /// opened (in which case Save just re-writes the loaded values).
+ ///
+ public Action? CaptureFromUi { get; set; }
- // Other
- public string ExecutablePath = "eu07.exe"; // game executable used to launch
- public bool SelectExeAutomatically = true;
- public bool DebugMode;
- public bool VirtualShunting;
+ /// True if the loaded config explicitly contained a lang entry.
+ public bool LanguageWasSet { get; private set; }
- // Graphics (subset)
- public string Resolution = "1280x720";
- public bool VSync = true;
- public bool RenderShadows = true;
+ // ── General ───────────────────────────────────────────────────────────
+ public string Language = "English"; // lang (pl/en)
+ public bool Fullscreen; // fullscreen
+ public bool PauseWhenInactive = true; // inactivepause
+ public bool PauseOnStart; // pause
+ public int CursorSensitivity = 3; // |mousescale x|
+ public bool InvertMouseHorizontal; // mousescale x < 0
+ public bool InvertMouseVertical; // mousescale y < 0
- // Sound
- public bool SoundEnabled = true;
- public int Volume = 100;
+ // ── Communication ─────────────────────────────────────────────────────
+ public bool IgnoreGamepad; // input.gamepad (inverted)
+ public int FeedbackMode; // feedbackmode (0-5)
- // Starter
- public bool AutoCloseStarter;
- public bool LargeThumbnails;
- public bool AutoExpandSceneryTree;
+ // ── Other ─────────────────────────────────────────────────────────────
+ public bool SelectExeAutomatically = true; // starter.exe.auto (launcher-only)
+ public string ExecutablePath = "eu07.exe"; // starter.exe.path (launcher-only)
+ public bool DebugMode; // debugmode
+ public bool VirtualShunting; // ai.trainman
- /// Loads settings from disk. TODO: implement persistence.
+ // ── Graphics ──────────────────────────────────────────────────────────
+ public int RenderEngine; // gfxrenderer (index, see RenderEngines)
+ public int Width = 1280; // width
+ public int Height = 720; // height
+ public int BufferScalePercent = 100; // starter.bufferscale (NOTE: launcher-only)
+ public int MaxTextureSize = 4096; // maxtexturesize
+ public int MaxCabTextureSize = 4096; // maxcabtexturesize
+ public int TextureFiltering = 8; // anisotropicfiltering (1/2/4/8/16)
+ public int Multisampling = 2; // multisampling (0-3)
+ public double DrawRangeFactor = 1.0; // gfx.drawrange.factor.max
+ public bool VSync = true; // vsync
+ public bool Smoke = true; // gfx.smoke
+ public int SmokeFidelity = 1; // gfx.smoke.fidelity (1-4)
+ public int Tonemapping; // gfx.postfx.tonemapping (NOTE: 0=reinhard,1=aces)
+ public bool ChromaticAberration; // gfx.postfx.chromaticaberration.enabled
+ public bool MotionBlur = true; // gfx.postfx.motionblur.enabled
+ public bool ExtraEffects = true; // gfx.extraeffects
+ public bool EnvMap = true; // gfx.envmap.enabled
+ public bool UseVbo = true; // usevbo
+ public bool RenderShadows = true; // shadows + gfx.shadowmap.enabled
+ public int ReflectionsFramerate = 60; // gfx.reflections.framerate
+ public int ShadowMapResolution = 4096; // shadowtune[0]
+ public int ShadowProjectionRange = 250; // shadowtune[1]
+ public int CabShadowsRange = 30; // gfx.shadows.cab.range
+ public int ShadowRankCutoff = 3; // gfx.shadow.rank.cutoff (NOTE)
+ public int ReflectionsFidelity = 2; // gfx.reflections.fidelity (0-2)
+ public int FieldOfView = 45; // fieldofview (15-75) NOTE: 'fovSlider' in XAML
+ public bool PythonScreens = true; // python.enabled
+ public bool PythonThreadedUpload = true; // python.threadedupload
+ public int ScreenRendererPriority = 4; // pyscreenrendererpriority (1-5, see ScreenPriorities)
+
+ // ── Physics ───────────────────────────────────────────────────────────
+ public int SplineFidelity = 1; // splinefidelity (1-4)
+ public bool FullPhysics = true; // fullphysics
+ public bool EnableTraction = true; // enabletraction (pantograph can break)
+ public bool LiveTraction; // livetraction
+ public bool PhysicsLog; // physicslog (speedometer tapes)
+ public bool DebugLog = true; // debuglog (3 / 0)
+ public bool MultipleLogs; // multiplelogs
+ public bool DisplaySimulation = true; // gfx.skiprendering (inverted)
+ public bool CrashDamage = true; // crashdamage
+
+ // ── Sound ─────────────────────────────────────────────────────────────
+ public bool SoundEnabled = true; // soundenabled
+ public int Volume = 100; // sound.volume (0-2 → 0-100)
+ public int RadioVolume = 80; // sound.volume.radio (0-1 → 0-100)
+ public int VehiclesVolume = 100; // sound.volume.vehicle
+ public int PositionalVolume = 100; // sound.volume.positional
+ public int AmbientVolume = 80; // sound.volume.ambient
+ public int PausedVolume; // sound.volume.paused
+
+ // ── Starter (launcher-only, stored under starter.* keys) ───────────────
+ public bool AutoCloseStarter; // starter.autoclose
+ public bool LargeThumbnails; // starter.largethumbnails
+ public bool AutoExpandSceneryTree; // starter.expandtree
+
+ /// gfxrenderer tokens in the order shown by the render-engine combo.
+ public static readonly string[] RenderEngines =
+ { "full", "legacy", "simpleshader", "simple", "off", "experimental" };
+
+ /// Anisotropic-filtering steps for the texture-quality slider (1-5).
+ public static readonly int[] AnisotropySteps = { 1, 2, 4, 8, 16 };
+
+ /// pyscreenrendererpriority tokens indexed by the slider value (1-5).
+ public static readonly string[] ScreenPriorities =
+ { "off", "idle", "lowest", "lower", "normal" };
+
+ // Tokens of mousescale / shadowtune that the UI does not edit but must keep.
+ private double _mouseScaleYMagnitude = 0.5;
+ private string[] _shadowTuneExtra = { "400", "300" }; // tokens [2], [3]
+
+ private ConfigFile _config = new();
+ private string _savePath = string.Empty;
+
+ /// Resolves the per-user eu07.ini path for the current OS.
+ public static string UserConfigPath()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ string? appData = Environment.GetEnvironmentVariable("APPDATA");
+ if (!string.IsNullOrEmpty(appData))
+ return Path.Combine(appData, "MaSzyna", "eu07.ini");
+ }
+ else
+ {
+ string? home = Environment.GetEnvironmentVariable("HOME");
+ if (!string.IsNullOrEmpty(home))
+ return Path.Combine(home, ".config", "MaSzyna", "eu07.ini");
+ }
+ // Last resort: alongside the launcher.
+ return Path.Combine(AppContext.BaseDirectory, "eu07.ini");
+ }
+
+ /// Default config shipped in the launcher's working directory.
+ private static string DefaultConfigPath() =>
+ Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini");
+
+ private static readonly Encoding FileEncoding = ResolveEncoding();
+
+ private static Encoding ResolveEncoding()
+ {
+ // eu07.ini comments use Polish diacritics in Windows-1250; preserve them.
+ try
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ return Encoding.GetEncoding(1250);
+ }
+ catch
+ {
+ return Encoding.UTF8;
+ }
+ }
+
+ /// Loads settings from the per-user file, or the working-dir default.
public void Load()
{
- // intentionally empty for now
+ _savePath = UserConfigPath();
+ string source = File.Exists(_savePath) ? _savePath : DefaultConfigPath();
+
+ try
+ {
+ _config = File.Exists(source)
+ ? ConfigFile.Parse(File.ReadAllText(source, FileEncoding))
+ : new ConfigFile();
+ }
+ catch
+ {
+ _config = new ConfigFile();
+ }
+
+ ReadFromConfig();
}
- /// Persists settings to disk. TODO: implement persistence.
+ /// Captures the latest UI values (if the view is open) and saves.
+ public void CaptureAndSave()
+ {
+ CaptureFromUi?.Invoke();
+ Save();
+ }
+
+ /// Persists settings to the per-user eu07.ini, keeping unknown keys.
public void Save()
{
- // intentionally empty for now
+ if (string.IsNullOrEmpty(_savePath))
+ _savePath = UserConfigPath();
+
+ WriteToConfig();
+
+ try
+ {
+ string? dir = Path.GetDirectoryName(_savePath);
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
+ File.WriteAllText(_savePath, _config.ToText(), FileEncoding);
+ }
+ catch
+ {
+ // Could not write (permissions / path). Leave values in memory.
+ }
+ }
+
+ // ── config → fields ───────────────────────────────────────────────────
+ private void ReadFromConfig()
+ {
+ var c = _config;
+
+ // General
+ LanguageWasSet = c.Has("lang");
+ Language = c.GetString("lang", "en").ToLowerInvariant() == "pl" ? "Polski" : "English";
+ Fullscreen = c.GetBool("fullscreen", false);
+ PauseWhenInactive = c.GetBool("inactivepause", true);
+ PauseOnStart = c.GetBool("pause", false);
+
+ var mouse = c.GetTokens("mousescale");
+ if (mouse.Length >= 1 && double.TryParse(mouse[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double mx))
+ {
+ InvertMouseHorizontal = mx < 0;
+ CursorSensitivity = Clamp((int)Math.Round(Math.Abs(mx)), 1, 5);
+ }
+ if (mouse.Length >= 2 && double.TryParse(mouse[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double my))
+ {
+ InvertMouseVertical = my < 0;
+ _mouseScaleYMagnitude = Math.Abs(my);
+ }
+
+ // Communication
+ IgnoreGamepad = !c.GetBool("input.gamepad", true);
+ FeedbackMode = Clamp(c.GetInt("feedbackmode", 0), 0, 5);
+
+ // Other
+ SelectExeAutomatically = c.GetBool("starter.exe.auto", true);
+ ExecutablePath = c.GetString("starter.exe.path", "eu07.exe");
+ DebugMode = c.GetBool("debugmode", false);
+ VirtualShunting = c.GetBool("ai.trainman", false);
+
+ // Graphics
+ RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0);
+ Width = c.GetInt("width", 1280);
+ Height = c.GetInt("height", 720);
+ BufferScalePercent = Clamp(c.GetInt("starter.bufferscale", 100), 50, 200);
+ MaxTextureSize = c.GetInt("maxtexturesize", 4096);
+ MaxCabTextureSize = c.GetInt("maxcabtexturesize", 4096);
+ TextureFiltering = c.GetInt("anisotropicfiltering", 8);
+ Multisampling = Clamp(c.GetInt("multisampling", 2), 0, 3);
+ DrawRangeFactor = c.GetDouble("gfx.drawrange.factor.max", 1.0);
+ VSync = c.GetBool("vsync", true);
+ Smoke = c.GetBool("gfx.smoke", true);
+ SmokeFidelity = Clamp(c.GetInt("gfx.smoke.fidelity", 1), 1, 4);
+ Tonemapping = c.GetString("gfx.postfx.tonemapping", "reinhard").ToLowerInvariant() == "aces" ? 1 : 0;
+ ChromaticAberration = c.GetBool("gfx.postfx.chromaticaberration.enabled", false);
+ MotionBlur = c.GetBool("gfx.postfx.motionblur.enabled", true);
+ ExtraEffects = c.GetBool("gfx.extraeffects", true);
+ EnvMap = c.GetBool("gfx.envmap.enabled", true);
+ UseVbo = c.GetBool("usevbo", true);
+ RenderShadows = c.GetBool("shadows", true);
+ ReflectionsFramerate = c.GetInt("gfx.reflections.framerate", 60);
+
+ var shadowTune = c.GetTokens("shadowtune");
+ if (shadowTune.Length >= 1) ShadowMapResolution = ParseInt(shadowTune[0], 4096);
+ if (shadowTune.Length >= 2) ShadowProjectionRange = ParseInt(shadowTune[1], 250);
+ if (shadowTune.Length >= 4) _shadowTuneExtra = new[] { shadowTune[2], shadowTune[3] };
+
+ CabShadowsRange = (int)Math.Round(c.GetDouble("gfx.shadows.cab.range", 30));
+ ShadowRankCutoff = c.GetInt("gfx.shadow.rank.cutoff", 3);
+ ReflectionsFidelity = Clamp(c.GetInt("gfx.reflections.fidelity", 2), 0, 2);
+ FieldOfView = Clamp((int)Math.Round(c.GetDouble("fieldofview", 45)), 15, 75);
+ PythonScreens = c.GetBool("python.enabled", true);
+ PythonThreadedUpload = c.GetBool("python.threadedupload", true);
+ ScreenRendererPriority = IndexOf(ScreenPriorities, c.GetString("pyscreenrendererpriority", "lower"), 3) + 1;
+
+ // Physics
+ SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4);
+ FullPhysics = c.GetBool("fullphysics", true);
+ EnableTraction = c.GetBool("enabletraction", true);
+ LiveTraction = c.GetBool("livetraction", false);
+ PhysicsLog = c.GetBool("physicslog", false);
+ DebugLog = c.GetInt("debuglog", 3) != 0;
+ MultipleLogs = c.GetBool("multiplelogs", false);
+ DisplaySimulation = !c.GetBool("gfx.skiprendering", false);
+ CrashDamage = c.GetBool("crashdamage", true);
+
+ // Sound
+ SoundEnabled = c.GetBool("soundenabled", true);
+ Volume = Clamp((int)Math.Round(c.GetDouble("sound.volume", 1.5) * 50), 1, 100);
+ RadioVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.radio", 0.8) * 100), 1, 100);
+ VehiclesVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.vehicle", 1.0) * 100), 1, 100);
+ PositionalVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.positional", 1.0) * 100), 1, 100);
+ AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 0.8) * 100), 1, 100);
+ PausedVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.paused", 0.0) * 100), 0, 100);
+
+ // Starter
+ AutoCloseStarter = c.GetBool("starter.autoclose", false);
+ LargeThumbnails = c.GetBool("starter.largethumbnails", false);
+ AutoExpandSceneryTree = c.GetBool("starter.expandtree", false);
+ }
+
+ // ── fields → config ───────────────────────────────────────────────────
+ private void WriteToConfig()
+ {
+ var c = _config;
+
+ // General
+ c.Set("lang", Language == "Polski" ? "pl" : "en");
+ c.SetBool("fullscreen", Fullscreen);
+ c.SetBool("inactivepause", PauseWhenInactive);
+ c.SetBool("pause", PauseOnStart);
+
+ double mx = CursorSensitivity * (InvertMouseHorizontal ? -1 : 1);
+ double my = _mouseScaleYMagnitude * (InvertMouseVertical ? -1 : 1);
+ c.Set("mousescale",
+ $"{mx.ToString("0.###", CultureInfo.InvariantCulture)} " +
+ $"{my.ToString("0.###", CultureInfo.InvariantCulture)}");
+
+ // Communication
+ c.SetBool("input.gamepad", !IgnoreGamepad);
+ c.SetInt("feedbackmode", FeedbackMode);
+
+ // Other
+ c.SetBool("starter.exe.auto", SelectExeAutomatically);
+ c.Set("starter.exe.path", ExecutablePath);
+ c.SetBool("debugmode", DebugMode);
+ c.SetBool("ai.trainman", VirtualShunting);
+
+ // Graphics
+ c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]);
+ c.SetInt("width", Width);
+ c.SetInt("height", Height);
+ c.SetInt("starter.bufferscale", BufferScalePercent);
+ c.SetInt("maxtexturesize", MaxTextureSize);
+ c.SetInt("maxcabtexturesize", MaxCabTextureSize);
+ c.SetInt("anisotropicfiltering", TextureFiltering);
+ c.SetInt("multisampling", Multisampling);
+ c.SetDouble("gfx.drawrange.factor.max", DrawRangeFactor);
+ c.SetBool("vsync", VSync);
+ c.SetBool("gfx.smoke", Smoke);
+ c.SetInt("gfx.smoke.fidelity", SmokeFidelity);
+ c.Set("gfx.postfx.tonemapping", Tonemapping == 1 ? "aces" : "reinhard");
+ c.SetBool("gfx.postfx.chromaticaberration.enabled", ChromaticAberration);
+ c.SetBool("gfx.postfx.motionblur.enabled", MotionBlur);
+ c.SetBool("gfx.extraeffects", ExtraEffects);
+ c.SetBool("gfx.envmap.enabled", EnvMap);
+ c.SetBool("usevbo", UseVbo);
+ c.SetBool("shadows", RenderShadows);
+ c.SetBool("gfx.shadowmap.enabled", RenderShadows);
+ c.SetInt("gfx.reflections.framerate", ReflectionsFramerate);
+ c.Set("shadowtune",
+ $"{ShadowMapResolution} {ShadowProjectionRange} " +
+ $"{_shadowTuneExtra[0]} {_shadowTuneExtra[1]}");
+ c.SetInt("gfx.shadows.cab.range", CabShadowsRange);
+ c.SetInt("gfx.shadow.rank.cutoff", ShadowRankCutoff);
+ c.SetInt("gfx.reflections.fidelity", ReflectionsFidelity);
+ c.SetInt("fieldofview", FieldOfView);
+ c.SetBool("python.enabled", PythonScreens);
+ c.SetBool("python.threadedupload", PythonThreadedUpload);
+ c.Set("pyscreenrendererpriority",
+ ScreenPriorities[Clamp(ScreenRendererPriority - 1, 0, ScreenPriorities.Length - 1)]);
+
+ // Physics
+ c.SetInt("splinefidelity", SplineFidelity);
+ c.SetBool("fullphysics", FullPhysics);
+ c.SetBool("enabletraction", EnableTraction);
+ c.SetBool("livetraction", LiveTraction);
+ c.SetBool("physicslog", PhysicsLog);
+ c.SetInt("debuglog", DebugLog ? 3 : 0);
+ c.SetBool("multiplelogs", MultipleLogs);
+ c.SetBool("gfx.skiprendering", !DisplaySimulation);
+ c.SetBool("crashdamage", CrashDamage);
+
+ // Sound
+ c.SetBool("soundenabled", SoundEnabled);
+ c.SetDouble("sound.volume", Volume / 50.0);
+ c.SetDouble("sound.volume.radio", RadioVolume / 100.0);
+ c.SetDouble("sound.volume.vehicle", VehiclesVolume / 100.0);
+ c.SetDouble("sound.volume.positional", PositionalVolume / 100.0);
+ c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0);
+ c.SetDouble("sound.volume.paused", PausedVolume / 100.0);
+
+ // Starter
+ c.SetBool("starter.autoclose", AutoCloseStarter);
+ c.SetBool("starter.largethumbnails", LargeThumbnails);
+ c.SetBool("starter.expandtree", AutoExpandSceneryTree);
+ }
+
+ // ── small helpers ──────────────────────────────────────────────────────
+ private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
+
+ private static int ParseInt(string s, int fallback) =>
+ int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) ? n : fallback;
+
+ private static int IndexOf(string[] values, string token, int fallback)
+ {
+ for (int i = 0; i < values.Length; i++)
+ if (string.Equals(values[i], token, StringComparison.OrdinalIgnoreCase))
+ return i;
+ return fallback;
}
}
diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs
index 910a806..8445082 100644
--- a/StarterNG/Views/Scenarios.axaml.cs
+++ b/StarterNG/Views/Scenarios.axaml.cs
@@ -60,6 +60,10 @@ public partial class Scenarios : UserControl
Tag = i
});
}
+
+ // refresh the consist preview when the view is shown again (e.g. after
+ // editing the consist in the depot)
+ AttachedToVisualTree += (_, _) => RefreshSelectedConsist();
}
private void SceneryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
@@ -89,16 +93,14 @@ public partial class Scenarios : UserControl
private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
- consistStack.Children.Clear();
-
// get selected scenery
var tItem = sceneryList.SelectedItem as TreeViewItem;
if (tItem?.Tag is not int tTag)
return;
Scenery selectedScn = sceneries[tTag];
-
+
// get selected trainset
- ListBoxItem vItem = vehicleList.SelectedItem as ListBoxItem;
+ ListBoxItem? vItem = vehicleList.SelectedItem as ListBoxItem;
if (vItem?.Tag is not int vTag)
return;
Trainset selectedTrainset = selectedScn.Trainsets[vTag];
@@ -107,29 +109,41 @@ public partial class Scenarios : UserControl
AppState.Instance.CurrentScenery = selectedScn;
AppState.Instance.CurrentTrainset = selectedTrainset;
+ ShowConsist(selectedTrainset);
+ }
+
+ // Renders the consist preview for a trainset (re-reads its current vehicles
+ // so depot edits are reflected).
+ private void ShowConsist(Trainset trainset)
+ {
+ consistStack.Children.Clear();
+
var db = GameData.Instance.Vehicles;
- foreach (var train in selectedTrainset.Vehicles)
+ foreach (var train in trainset.Vehicles)
{
// thumbnail name comes from the matching texture's texture_mini
string miniName = db.MiniForSkin(train.SkinFile) ?? train.SkinFile;
string? path = VehicleDatabase.MiniPath(miniName);
if (path is null)
- {
- // fallback
- continue;
- }
+ continue; // fallback: no mini
- var bitmap = new Bitmap(path);
- var image = new Image
+ consistStack.Children.Add(new Image
{
- Source = bitmap,
+ Source = new Bitmap(path),
Height = 45,
Stretch = Avalonia.Media.Stretch.Uniform,
Margin = new Thickness(0)
- };
- consistStack.Children.Add(image);
+ });
}
- missionDescription.Text = selectedTrainset.Description;
+ missionDescription.Text = trainset.Description;
+ }
+
+ // When returning to this view, refresh the preview of the selected consist
+ // in case it was modified in the depot.
+ private void RefreshSelectedConsist()
+ {
+ if (AppState.Instance.CurrentTrainset is { } trainset)
+ ShowConsist(trainset);
}
// Exports the (possibly depot-modified) scenery to a $-prefixed copy and
@@ -159,6 +173,9 @@ public partial class Scenarios : UserControl
.FirstOrDefault(v => v.DriverType is eDriverType.Headdriver or eDriverType.Reardriver)?.Name
?? trainset?.Vehicles.FirstOrDefault()?.Name;
+ // make sure the latest settings are on disk before the sim reads them
+ StarterNG.Classes.Settings.Instance.CaptureAndSave();
+
// launch the game: -s $.scn -v
try
{
diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml
index 70fc150..0327740 100644
--- a/StarterNG/Views/Settings.axaml
+++ b/StarterNG/Views/Settings.axaml
@@ -1,4 +1,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Polski
- English
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Polski
+ English
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ eu07.exe
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3840x2160
+ 2560x1440
+ 1920x1080
+ 1600x900
+ 1366x768
+ 1280x720
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- eu07.exe
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- 2560x1440
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs
index 08b90f0..0c6f40e 100644
--- a/StarterNG/Views/Settings.axaml.cs
+++ b/StarterNG/Views/Settings.axaml.cs
@@ -1,30 +1,259 @@
-using System;
+using System;
+using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml.Styling;
+using StarterNG.Classes;
namespace StarterNG.Views;
public partial class Settings : UserControl
{
+ private bool _loading;
+
public Settings()
{
InitializeComponent();
+
this.AttachedToVisualTree += (_, _) =>
{
TextureResolutionSlider_OnValueChanged(null, null);
CabTextureResolutionSlider_OnValueChanged(null, null);
shaderResolutionSlider_OnValueChanged(null, null);
};
-
+
ChangeLanguageCb.SelectedIndex = App.Loc.CurrentLanguage switch
{
"Polski" => 0,
_ => 1
};
+
+ // The settings instance pulls live values from here whenever the app
+ // closes or the game is launched.
+ StarterNG.Classes.Settings.Instance.CaptureFromUi = ReadFromUi;
+
+ ApplyToUi();
}
+ // ── Settings instance → controls ──────────────────────────────────────
+ private void ApplyToUi()
+ {
+ var s = StarterNG.Classes.Settings.Instance;
+ _loading = true;
+ try
+ {
+ // General
+ ChangeLanguageCb.SelectedIndex = s.Language == "Polski" ? 0 : 1;
+ FullscreenCb.IsChecked = s.Fullscreen;
+ PauseInactiveCb.IsChecked = s.PauseWhenInactive;
+ PauseStartCb.IsChecked = s.PauseOnStart;
+ CursorSensitivitySlider.Value = s.CursorSensitivity;
+ MouseHorInvertCb.IsChecked = s.InvertMouseHorizontal;
+ MouseVertInvertCb.IsChecked = s.InvertMouseVertical;
+
+ // Communication
+ GamepadIgnoreCb.IsChecked = s.IgnoreGamepad;
+ FeedbackCb.SelectedIndex = s.FeedbackMode;
+
+ // Other
+ SelectExeCb.SelectedIndex = s.SelectExeAutomatically ? 0 : 1;
+ DebugModeCb.IsChecked = s.DebugMode;
+ VirtualShuntingCb.IsChecked = s.VirtualShunting;
+
+ // Graphics
+ RenderEngineCb.SelectedIndex = s.RenderEngine;
+ SelectResolution(s.Width, s.Height);
+ bufferScale.Value = s.BufferScalePercent;
+ textureResolutionSlider.Value = Log2(s.MaxTextureSize, 12);
+ cabTextureResolutionSlider.Value = Log2(s.MaxCabTextureSize, 12);
+ TexFilteringSlider.Value = AnisotropyToSlider(s.TextureFiltering);
+ MultisamplingSlider.Value = s.Multisampling + 1;
+ RenderRangeSlider.Value = (int)Math.Round(s.DrawRangeFactor);
+ VSyncCb.IsChecked = s.VSync;
+ SmokeDisplayCb.IsChecked = s.Smoke;
+ SmokeParticlesSlider.Value = s.SmokeFidelity;
+ PostprocessingCb.SelectedIndex = s.Tonemapping;
+ ChromaticAberrationCb.IsChecked = s.ChromaticAberration;
+ MotionBlurCb.IsChecked = s.MotionBlur;
+ AdditionalShadersCb.IsChecked = s.ExtraEffects;
+ ReflectionsCubeMapCb.IsChecked = s.EnvMap;
+ RenderVBOCb.IsChecked = s.UseVbo;
+ RenderShadowsCb.IsChecked = s.RenderShadows;
+ reflectionsFramerate.Value = s.ReflectionsFramerate;
+ shaderResolutionSlider.Value = Log2(s.ShadowMapResolution, 12);
+ shaderRange.Value = s.ShadowProjectionRange;
+ cabShaderSourceRange.Value = s.CabShadowsRange;
+ ShadowDisplayCb.SelectedIndex = Clamp(s.ShadowRankCutoff - 1, 0, 2);
+ ReflectionsDetailsCb.SelectedIndex = s.ReflectionsFidelity;
+ fovSlider.Value = s.FieldOfView;
+ RenderScreensCb.IsChecked = s.PythonScreens;
+ RenderScreensThreadCb.IsChecked = s.PythonThreadedUpload;
+ RenderScreensFramerateSlider.Value = s.ScreenRendererPriority;
+
+ // Physics
+ TrackCurvesSlider.Value = s.SplineFidelity;
+ PhysicsAccuracyCb.IsChecked = s.FullPhysics;
+ PantographBreakCb.IsChecked = s.EnableTraction;
+ OverheadOnlyCb.IsChecked = s.LiveTraction;
+ SpeedometerTapesCb.IsChecked = s.PhysicsLog;
+ SimLogsCb.IsChecked = s.DebugLog;
+ KeepLogsCb.IsChecked = s.MultipleLogs;
+ DisplaySimulationCb.IsChecked = s.DisplaySimulation;
+ CrashDamageCb.IsChecked = s.CrashDamage;
+
+ // Sound
+ EnableSoundsCb.IsChecked = s.SoundEnabled;
+ VolumeSlider.Value = s.Volume;
+ RadioVolumeSlider.Value = s.RadioVolume;
+ VehiclesVolumeSlider.Value = s.VehiclesVolume;
+ PositionalVolumeSlider.Value = s.PositionalVolume;
+ AmbientVolumeSlider.Value = s.AmbientVolume;
+ PauseVolumeSlider.Value = s.PausedVolume;
+
+ // Starter
+ AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
+ LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
+ AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree;
+ }
+ finally
+ {
+ _loading = false;
+ }
+ }
+
+ // ── controls → Settings instance ──────────────────────────────────────
+ private void ReadFromUi()
+ {
+ var s = StarterNG.Classes.Settings.Instance;
+
+ // General
+ s.Language = ChangeLanguageCb.SelectedIndex == 0 ? "Polski" : "English";
+ s.Fullscreen = IsChecked(FullscreenCb);
+ s.PauseWhenInactive = IsChecked(PauseInactiveCb);
+ s.PauseOnStart = IsChecked(PauseStartCb);
+ s.CursorSensitivity = (int)CursorSensitivitySlider.Value;
+ s.InvertMouseHorizontal = IsChecked(MouseHorInvertCb);
+ s.InvertMouseVertical = IsChecked(MouseVertInvertCb);
+
+ // Communication
+ s.IgnoreGamepad = IsChecked(GamepadIgnoreCb);
+ s.FeedbackMode = Math.Max(0, FeedbackCb.SelectedIndex);
+
+ // Other
+ s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0;
+ s.ExecutablePath = s.SelectExeAutomatically ? "eu07.exe"
+ : (SelectExeCb.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "eu07.exe";
+ s.DebugMode = IsChecked(DebugModeCb);
+ s.VirtualShunting = IsChecked(VirtualShuntingCb);
+
+ // Graphics
+ s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex);
+ ReadResolution(s);
+ s.BufferScalePercent = (int)bufferScale.Value;
+ s.MaxTextureSize = 1 << (int)textureResolutionSlider.Value;
+ s.MaxCabTextureSize = 1 << (int)cabTextureResolutionSlider.Value;
+ s.TextureFiltering = StarterNG.Classes.Settings.AnisotropySteps[
+ Clamp((int)TexFilteringSlider.Value - 1, 0, StarterNG.Classes.Settings.AnisotropySteps.Length - 1)];
+ s.Multisampling = Clamp((int)MultisamplingSlider.Value - 1, 0, 3);
+ s.DrawRangeFactor = RenderRangeSlider.Value;
+ s.VSync = IsChecked(VSyncCb);
+ s.Smoke = IsChecked(SmokeDisplayCb);
+ s.SmokeFidelity = (int)SmokeParticlesSlider.Value;
+ s.Tonemapping = Math.Max(0, PostprocessingCb.SelectedIndex);
+ s.ChromaticAberration = IsChecked(ChromaticAberrationCb);
+ s.MotionBlur = IsChecked(MotionBlurCb);
+ s.ExtraEffects = IsChecked(AdditionalShadersCb);
+ s.EnvMap = IsChecked(ReflectionsCubeMapCb);
+ s.UseVbo = IsChecked(RenderVBOCb);
+ s.RenderShadows = IsChecked(RenderShadowsCb);
+ s.ReflectionsFramerate = (int)reflectionsFramerate.Value;
+ s.ShadowMapResolution = 1 << (int)shaderResolutionSlider.Value;
+ s.ShadowProjectionRange = (int)shaderRange.Value;
+ s.CabShadowsRange = (int)cabShaderSourceRange.Value;
+ s.ShadowRankCutoff = Math.Max(0, ShadowDisplayCb.SelectedIndex) + 1;
+ s.ReflectionsFidelity = Math.Max(0, ReflectionsDetailsCb.SelectedIndex);
+ s.FieldOfView = Clamp((int)fovSlider.Value, 15, 75);
+ s.PythonScreens = IsChecked(RenderScreensCb);
+ s.PythonThreadedUpload = IsChecked(RenderScreensThreadCb);
+ s.ScreenRendererPriority = (int)RenderScreensFramerateSlider.Value;
+
+ // Physics
+ s.SplineFidelity = (int)TrackCurvesSlider.Value;
+ s.FullPhysics = IsChecked(PhysicsAccuracyCb);
+ s.EnableTraction = IsChecked(PantographBreakCb);
+ s.LiveTraction = IsChecked(OverheadOnlyCb);
+ s.PhysicsLog = IsChecked(SpeedometerTapesCb);
+ s.DebugLog = IsChecked(SimLogsCb);
+ s.MultipleLogs = IsChecked(KeepLogsCb);
+ s.DisplaySimulation = IsChecked(DisplaySimulationCb);
+ s.CrashDamage = IsChecked(CrashDamageCb);
+
+ // Sound
+ s.SoundEnabled = IsChecked(EnableSoundsCb);
+ s.Volume = (int)VolumeSlider.Value;
+ s.RadioVolume = (int)RadioVolumeSlider.Value;
+ s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
+ s.PositionalVolume = (int)PositionalVolumeSlider.Value;
+ s.AmbientVolume = (int)AmbientVolumeSlider.Value;
+ s.PausedVolume = (int)PauseVolumeSlider.Value;
+
+ // Starter
+ s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
+ s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
+ s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb);
+ }
+
+ private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ ReadFromUi();
+ StarterNG.Classes.Settings.Instance.Save();
+ if (SaveStatus is not null)
+ SaveStatus.Text = App.Loc["SettingsSaved"];
+ }
+
+ private void ResetButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ StarterNG.Classes.Settings.Instance.Load();
+ ApplyToUi();
+ if (SaveStatus is not null)
+ SaveStatus.Text = string.Empty;
+ }
+
+ // ── resolution combo helpers ──────────────────────────────────────────
+ private void SelectResolution(int width, int height)
+ {
+ string target = $"{width}x{height}";
+ foreach (var obj in ResolutionCb.Items)
+ {
+ if (obj is ComboBoxItem item && item.Content?.ToString() == target)
+ {
+ ResolutionCb.SelectedItem = item;
+ return;
+ }
+ }
+ // Not in the predefined list: add and select it.
+ var added = new ComboBoxItem { Content = target };
+ ResolutionCb.Items.Add(added);
+ ResolutionCb.SelectedItem = added;
+ }
+
+ private void ReadResolution(Classes.Settings s)
+ {
+ var text = (ResolutionCb.SelectedItem as ComboBoxItem)?.Content?.ToString();
+ if (string.IsNullOrEmpty(text))
+ return;
+ var parts = text.Split('x');
+ if (parts.Length == 2 &&
+ int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
+ {
+ s.Width = w;
+ s.Height = h;
+ }
+ }
+
+ // ── existing slider readouts ──────────────────────────────────────────
private void TextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
if (texResolution is null || textureResolutionSlider is null)
@@ -54,12 +283,11 @@ public partial class Settings : UserControl
private void LanguageComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
-
if (sender is not ComboBox { SelectedItem: ComboBoxItem item }) return;
- if (!item.IsKeyboardFocusWithin) return;
+ if (!item.IsKeyboardFocusWithin) return; // ignore programmatic (ApplyToUi) changes
var lang = item.Content?.ToString();
if (string.IsNullOrEmpty(lang)) return;
-
+
var langDict = new ResourceInclude(new Uri("avares://StarterNG/"))
{
Source = new Uri($"Assets/Langs/{lang}.axaml", UriKind.Relative)
@@ -77,4 +305,25 @@ public partial class Settings : UserControl
App.Loc.SetLanguage(langDict, lang);
}
-}
\ No newline at end of file
+
+ // ── tiny helpers ──────────────────────────────────────────────────────
+ private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
+
+ private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
+
+ private static int Log2(int value, int fallback)
+ {
+ if (value <= 0) return fallback;
+ int log = (int)Math.Round(Math.Log2(value));
+ return log;
+ }
+
+ private static int AnisotropyToSlider(int anisotropy)
+ {
+ var steps = StarterNG.Classes.Settings.AnisotropySteps;
+ for (int i = 0; i < steps.Length; i++)
+ if (steps[i] == anisotropy)
+ return i + 1;
+ return 4; // default → 8x
+ }
+}