From c4147a2f66b464004ae5e4313cfaaec0403266ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20W=C5=82odarczyk?= Date: Sun, 5 Jul 2026 18:04:07 +0200 Subject: [PATCH] Add settings profiles and expand exposed eu07.ini options Adds save/apply named settings profiles (SettingsProfileStore), a battery-default export option that rewrites trainset velocity on launch, and an option to omit decorative trainsets from the exported scenery. Also surfaces many previously-unmapped eu07.ini keys in the Settings UI (UART/feedback port, FPS limit, shadow angle, fullscreen windowed, ANGLE Vulkan, friction/brake sensitivity, and an Advanced section for texture/GLES/threading tunables). --- StarterNG/Classes/ConfigFile.cs | 22 +++ StarterNG/Classes/Scenery.cs | 9 +- StarterNG/Classes/Settings.cs | 152 ++++++++++++++++++-- StarterNG/Classes/SettingsProfileStore.cs | 34 +++++ StarterNG/Classes/Trainset.cs | 2 + StarterNG/MainWindow.axaml.cs | 12 +- StarterNG/Views/Settings.axaml | 96 ++++++++++++- StarterNG/Views/Settings.axaml.cs | 163 +++++++++++++++++++++- StarterNG/startercfg/lang/en.xml | 42 ++++++ StarterNG/startercfg/lang/pl.xml | 42 ++++++ build.sh | 2 + 11 files changed, 557 insertions(+), 19 deletions(-) create mode 100644 StarterNG/Classes/SettingsProfileStore.cs create mode 100755 build.sh diff --git a/StarterNG/Classes/ConfigFile.cs b/StarterNG/Classes/ConfigFile.cs index 95e645f..f38895d 100644 --- a/StarterNG/Classes/ConfigFile.cs +++ b/StarterNG/Classes/ConfigFile.cs @@ -125,6 +125,28 @@ public sealed class ConfigFile } } + /// + /// Deactivates by turning its line into a comment, + /// preserving indentation, the key/value that was there and any trailing + /// comment. No-op if the key is not currently active. + /// + public void Remove(string key) + { + if (!_index.TryGetValue(key, out int i)) + return; + + SplitLine(_lines[i], out string indent, out _, out string value, out string comment); + var sb = new StringBuilder(); + sb.Append(indent).Append("// ").Append(key); + if (value.Length > 0) + sb.Append(' ').Append(value); + if (comment.Length > 0) + sb.Append('\t').Append(comment); + _lines[i] = sb.ToString(); + + _index.Remove(key); + } + 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)); diff --git a/StarterNG/Classes/Scenery.cs b/StarterNG/Classes/Scenery.cs index e4502c7..bcf3583 100644 --- a/StarterNG/Classes/Scenery.cs +++ b/StarterNG/Classes/Scenery.cs @@ -94,7 +94,7 @@ public class Scenery /// Rebuilds the full .scn content with the (possibly modified) trainsets /// substituted back into their original positions. /// - public string BuildExportContent() + public string BuildExportContent(bool skipDecorTrainsets = false) { string result = _template; @@ -105,7 +105,12 @@ public class Scenery result = RewriteWeather(result); for (int i = 0; i < Trainsets.Count; i++) - result = result.Replace("{{" + i + "}}", Trainsets[i].ToSceneryEntry()); + { + string entry = skipDecorTrainsets && Trainsets[i].Decor + ? string.Empty + : Trainsets[i].ToSceneryEntry(); + result = result.Replace("{{" + i + "}}", entry); + } return result; } diff --git a/StarterNG/Classes/Settings.cs b/StarterNG/Classes/Settings.cs index 1624669..44baa00 100644 --- a/StarterNG/Classes/Settings.cs +++ b/StarterNG/Classes/Settings.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; namespace StarterNG.Classes; +public enum BatteryDefault { Default = 0, AlwaysOff = 1, AlwaysOn = 2 } + /// /// Application settings backing the Settings view, persisted to the simulator's /// eu07.ini. @@ -49,12 +53,19 @@ public sealed class Settings // ── Communication ───────────────────────────────────────────────────── public bool IgnoreGamepad; // input.gamepad (inverted) public int FeedbackMode; // feedbackmode (0-5) + public int FeedbackPort = 888; // feedbackport + public bool UartEnabled; // uart presence + public string UartPort = ""; // uart + public string UartTune = ""; // uarttune + public bool UartDebug; // uartdebug + public bool UartMain, UartScnd, UartTrain, UartLocal, UartRadioVolume, UartRadioChannel; // uartfeature tokens // ── 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 + public bool LogMissingVehicleFiles; // starter.logmissingvehicles (launcher-only; not yet wired to any load/export codepath) // ── Graphics ────────────────────────────────────────────────────────── public int RenderEngine; // gfxrenderer (index, see RenderEngines) @@ -87,6 +98,11 @@ public sealed class Settings public bool PythonScreens = true; // python.enabled public bool PythonThreadedUpload = true; // python.threadedupload public int ScreenRendererPriority = 4; // pyscreenrendererpriority (1-5, see ScreenPriorities) + public bool FpsLimitEnabled; // fpslimit (presence) + public int FpsLimit = 30; // fpslimit (1-100) + public double ShadowAngleLimit = 0.2; // gfx.shadow.angle.min (0.2-1.0) + public bool FullscreenWindowed; // fullscreenwindowed + public bool RenderAngleVulkan; // gfx.angleplatform ("vulkan") // ── Physics ─────────────────────────────────────────────────────────── public int SplineFidelity = 1; // splinefidelity (1-4) @@ -98,6 +114,9 @@ public sealed class Settings public bool MultipleLogs; // multiplelogs public bool DisplaySimulation = true; // gfx.skiprendering (inverted) public bool CrashDamage = true; // crashdamage + public double Friction = 1.0; // friction (0.1-3.0) + public double BrakeStep = 1.0; // brakestep (0.4-3.0) + public double BrakeSpeed = 1.0; // brakespeed (0.4-3.0) // ── Sound ───────────────────────────────────────────────────────────── public bool SoundEnabled = true; // soundenabled @@ -108,10 +127,23 @@ public sealed class Settings public int AmbientVolume = 80; // sound.volume.ambient public int PausedVolume; // sound.volume.paused + // ── Advanced ────────────────────────────────────────────────────────── + public bool CompressTextures = true; // compresstex + public bool ScaleSpeculars = true; // scalespeculars + public bool UseGLES; // gfx.usegles + public bool ShaderGamma; // gfx.shadergamma + public bool ScreenMipmaps = true; // python.mipmaps + public bool ExtendedModelConversion; // convertmodels ("143"/"0") + public bool GfxResourceMove; // gfx.resource.move + public bool GfxResourceSweep; // gfx.resource.sweep + public int TrainPhysicsThreads; // async.trainThreads + public bool IgnoreIrrelevantTrains; // starter.ignoreirrelevant (launcher-only) + // ── Starter (launcher-only, stored under starter.* keys) ─────────────── public bool AutoCloseStarter; // starter.autoclose public bool LargeThumbnails; // starter.largethumbnails public bool AutoExpandSceneryTree; // starter.expandtree + public BatteryDefault BatteryDefault = BatteryDefault.Default; // starter.batterydefault (launcher-only) // List filters (launcher-only). Default on. public bool ShowAiVehicles = true; // starter.show.ai (show //$o "-" consists) @@ -165,6 +197,10 @@ public sealed class Settings private static string DefaultConfigPath() => Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini"); + /// Directory holding the per-user eu07.ini, for locating launcher-only data alongside it. + public static string UserConfigDirectory() => + Path.GetDirectoryName(UserConfigPath()) ?? AppContext.BaseDirectory; + /// /// The simulator executable to launch. With auto-selection on, the working /// directory is scanned for an eu07* binary - eu07 on Linux/macOS, @@ -198,6 +234,25 @@ public sealed class Settings return canonical; } + /// + /// Scans (defaults to the working directory) for + /// plausible eu07* launcher binaries, newest first, for populating the + /// manual-selection dropdown. Does not affect . + /// + public static List ListCandidateExecutables(string? directory = null) + { + directory ??= Directory.GetCurrentDirectory(); + try + { + return Directory.GetFiles(directory, "eu07*") + .Where(IsExecutableCandidate) + .OrderByDescending(File.GetLastWriteTimeUtc) + .Select(Path.GetFileName) + .ToList()!; + } + catch { return new List(); } + } + // Filters the eu07* matches down to plausible launcher binaries, skipping data // and config files (eu07.ini, eu07.log, libraries, …). private static bool IsExecutableCandidate(string path) @@ -233,11 +288,23 @@ public sealed class Settings { _savePath = UserConfigPath(); string source = File.Exists(_savePath) ? _savePath : DefaultConfigPath(); + LoadFrom(source); + } + /// Captures the latest UI values (if the view is open) and saves. + public void CaptureAndSave() + { + CaptureFromUi?.Invoke(); + Save(); + } + + /// Loads settings from an arbitrary path without changing where writes. + public void LoadFrom(string path) + { try { - _config = File.Exists(source) - ? ConfigFile.Parse(File.ReadAllText(source, FileEncoding)) + _config = File.Exists(path) + ? ConfigFile.Parse(File.ReadAllText(path, FileEncoding)) : new ConfigFile(); } catch @@ -248,27 +315,26 @@ public sealed class Settings ReadFromConfig(); } - /// 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() { if (string.IsNullOrEmpty(_savePath)) _savePath = UserConfigPath(); + SaveTo(_savePath); + } + + /// Writes settings to an arbitrary path without changing where writes. + public void SaveTo(string path) + { WriteToConfig(); try { - string? dir = Path.GetDirectoryName(_savePath); + string? dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - File.WriteAllText(_savePath, _config.ToText(), FileEncoding); + File.WriteAllText(path, _config.ToText(), FileEncoding); } catch { @@ -303,12 +369,22 @@ public sealed class Settings // Communication IgnoreGamepad = !c.GetBool("input.gamepad", true); FeedbackMode = Clamp(c.GetInt("feedbackmode", 0), 0, 5); + FeedbackPort = c.GetInt("feedbackport", 888); + UartEnabled = c.Has("uart"); + UartPort = c.GetString("uart", ""); + UartTune = c.GetString("uarttune", ""); + UartDebug = c.GetBool("uartdebug", false); + var uartTokens = (c.GetRaw("uartfeature") ?? "").Split('|', StringSplitOptions.RemoveEmptyEntries); + var uf = new HashSet(uartTokens, StringComparer.OrdinalIgnoreCase); + UartMain = uf.Contains("main"); UartScnd = uf.Contains("scnd"); UartTrain = uf.Contains("train"); + UartLocal = uf.Contains("local"); UartRadioVolume = uf.Contains("radiovolume"); UartRadioChannel = uf.Contains("radiochannel"); // 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); + LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false); // Graphics RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0); @@ -345,6 +421,11 @@ public sealed class Settings PythonScreens = c.GetBool("python.enabled", true); PythonThreadedUpload = c.GetBool("python.threadedupload", true); ScreenRendererPriority = IndexOf(ScreenPriorities, c.GetString("pyscreenrendererpriority", "lower"), 3) + 1; + FpsLimitEnabled = c.Has("fpslimit"); + FpsLimit = Clamp(c.GetInt("fpslimit", 30), 1, 100); + ShadowAngleLimit = c.GetDouble("gfx.shadow.angle.min", 0.2); + FullscreenWindowed = c.GetBool("fullscreenwindowed", false); + RenderAngleVulkan = string.Equals(c.GetString("gfx.angleplatform", ""), "vulkan", StringComparison.OrdinalIgnoreCase); // Physics SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4); @@ -356,6 +437,9 @@ public sealed class Settings MultipleLogs = c.GetBool("multiplelogs", false); DisplaySimulation = !c.GetBool("gfx.skiprendering", false); CrashDamage = c.GetBool("crashdamage", true); + Friction = c.GetDouble("friction", 1.0); + BrakeStep = c.GetDouble("brakestep", 1.0); + BrakeSpeed = c.GetDouble("brakespeed", 1.0); // Sound SoundEnabled = c.GetBool("soundenabled", true); @@ -366,10 +450,23 @@ public sealed class Settings 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); + // Advanced + CompressTextures = c.GetBool("compresstex", true); + ScaleSpeculars = c.GetBool("scalespeculars", true); + UseGLES = c.GetBool("gfx.usegles", false); + ShaderGamma = c.GetBool("gfx.shadergamma", false); + ScreenMipmaps = c.GetBool("python.mipmaps", true); + ExtendedModelConversion = c.GetInt("convertmodels", 0) != 0; + GfxResourceMove = c.GetBool("gfx.resource.move", false); + GfxResourceSweep = c.GetBool("gfx.resource.sweep", false); + TrainPhysicsThreads = Clamp(c.GetInt("async.trainThreads", 0), 0, Environment.ProcessorCount); + IgnoreIrrelevantTrains = c.GetBool("starter.ignoreirrelevant", false); + // Starter AutoCloseStarter = c.GetBool("starter.autoclose", false); LargeThumbnails = c.GetBool("starter.largethumbnails", false); AutoExpandSceneryTree = c.GetBool("starter.expandtree", false); + BatteryDefault = (BatteryDefault)Clamp(c.GetInt("starter.batterydefault", 0), 0, 2); ShowAiVehicles = c.GetBool("starter.show.ai", true); DrivableOnly = c.GetBool("starter.drivableonly", true); ShowArchivalSceneries = c.GetBool("starter.show.archival", true); @@ -395,12 +492,25 @@ public sealed class Settings // Communication c.SetBool("input.gamepad", !IgnoreGamepad); c.SetInt("feedbackmode", FeedbackMode); + c.SetInt("feedbackport", FeedbackPort); + if (UartEnabled && !string.IsNullOrWhiteSpace(UartPort)) c.Set("uart", UartPort); else c.Remove("uart"); + c.Set("uarttune", UartTune); + c.SetBool("uartdebug", UartDebug); + var sb = new StringBuilder(); + if (UartMain) sb.Append("main|"); + if (UartScnd) sb.Append("scnd|"); + if (UartTrain) sb.Append("train|"); + if (UartLocal) sb.Append("local|"); + if (UartRadioVolume) sb.Append("radiovolume|"); + if (UartRadioChannel) sb.Append("radiochannel|"); + c.Set("uartfeature", sb.ToString()); // Other c.SetBool("starter.exe.auto", SelectExeAutomatically); c.Set("starter.exe.path", ExecutablePath); c.SetBool("debugmode", DebugMode); c.SetBool("ai.trainman", VirtualShunting); + c.SetBool("starter.logmissingvehicles", LogMissingVehicleFiles); // Graphics c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]); @@ -436,6 +546,10 @@ public sealed class Settings c.SetBool("python.threadedupload", PythonThreadedUpload); c.Set("pyscreenrendererpriority", ScreenPriorities[Clamp(ScreenRendererPriority - 1, 0, ScreenPriorities.Length - 1)]); + if (FpsLimitEnabled) c.SetInt("fpslimit", FpsLimit); else c.Remove("fpslimit"); + c.SetDouble("gfx.shadow.angle.min", ShadowAngleLimit); + c.SetBool("fullscreenwindowed", FullscreenWindowed); + if (RenderAngleVulkan) c.Set("gfx.angleplatform", "vulkan"); else c.Remove("gfx.angleplatform"); // Physics c.SetInt("splinefidelity", SplineFidelity); @@ -447,6 +561,9 @@ public sealed class Settings c.SetBool("multiplelogs", MultipleLogs); c.SetBool("gfx.skiprendering", !DisplaySimulation); c.SetBool("crashdamage", CrashDamage); + c.SetDouble("friction", Friction); + c.SetDouble("brakestep", BrakeStep); + c.SetDouble("brakespeed", BrakeSpeed); // Sound c.SetBool("soundenabled", SoundEnabled); @@ -457,10 +574,23 @@ public sealed class Settings c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0); c.SetDouble("sound.volume.paused", PausedVolume / 100.0); + // Advanced + c.SetBool("compresstex", CompressTextures); + c.SetBool("scalespeculars", ScaleSpeculars); + c.SetBool("gfx.usegles", UseGLES); + c.SetBool("gfx.shadergamma", ShaderGamma); + c.SetBool("python.mipmaps", ScreenMipmaps); + c.Set("convertmodels", ExtendedModelConversion ? "143" : "0"); + c.SetBool("gfx.resource.move", GfxResourceMove); + c.SetBool("gfx.resource.sweep", GfxResourceSweep); + c.SetInt("async.trainThreads", TrainPhysicsThreads); + c.SetBool("starter.ignoreirrelevant", IgnoreIrrelevantTrains); + // Starter c.SetBool("starter.autoclose", AutoCloseStarter); c.SetBool("starter.largethumbnails", LargeThumbnails); c.SetBool("starter.expandtree", AutoExpandSceneryTree); + c.SetInt("starter.batterydefault", (int)BatteryDefault); c.SetBool("starter.show.ai", ShowAiVehicles); c.SetBool("starter.drivableonly", DrivableOnly); c.SetBool("starter.show.archival", ShowArchivalSceneries); diff --git a/StarterNG/Classes/SettingsProfileStore.cs b/StarterNG/Classes/SettingsProfileStore.cs new file mode 100644 index 0000000..78858f2 --- /dev/null +++ b/StarterNG/Classes/SettingsProfileStore.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace StarterNG.Classes; + +public static class SettingsProfileStore +{ + public static string DirectoryPath { get; } = + Path.Combine(Settings.UserConfigDirectory(), "starter", "profiles"); + + public static IReadOnlyList ListProfiles() + { + try + { + if (!Directory.Exists(DirectoryPath)) return Array.Empty(); + return Directory.GetFiles(DirectoryPath, "*.ini") + .Select(Path.GetFileNameWithoutExtension) + .Where(n => !string.IsNullOrEmpty(n)) + .OrderBy(n => n, StringComparer.OrdinalIgnoreCase) + .ToList()!; + } + catch { return Array.Empty(); } + } + + public static string PathFor(string name) + { + string safe = name.Trim(); + foreach (char c in Path.GetInvalidFileNameChars()) + safe = safe.Replace(c, '_'); + return Path.Combine(DirectoryPath, safe + ".ini"); + } +} diff --git a/StarterNG/Classes/Trainset.cs b/StarterNG/Classes/Trainset.cs index c1554d5..f857168 100644 --- a/StarterNG/Classes/Trainset.cs +++ b/StarterNG/Classes/Trainset.cs @@ -21,6 +21,7 @@ public class Trainset public string Track; public float Offset; public float Velocity; + public float OriginalVelocity; public string Description; public List Vehicles; @@ -82,6 +83,7 @@ public class Trainset this.Track = tokens[++i]; this.Offset = float.Parse(tokens[++i], CultureInfo.InvariantCulture); this.Velocity = float.Parse(tokens[++i], CultureInfo.InvariantCulture); + this.OriginalVelocity = this.Velocity; continue; } diff --git a/StarterNG/MainWindow.axaml.cs b/StarterNG/MainWindow.axaml.cs index 7e92cfe..8293ba3 100644 --- a/StarterNG/MainWindow.axaml.cs +++ b/StarterNG/MainWindow.axaml.cs @@ -100,13 +100,23 @@ public partial class MainWindow : Window return; var trainset = AppState.Instance.CurrentTrainset; + if (trainset is { Vehicles.Count: > 0 }) + { + trainset.Velocity = Settings.Instance.BatteryDefault switch + { + BatteryDefault.AlwaysOff => 0f, + BatteryDefault.AlwaysOn => trainset.OriginalVelocity < 0.2f ? 0.1f : trainset.OriginalVelocity, + _ => trainset.OriginalVelocity + }; + } + // write scenery/$.scn with the replaced trainsets string dir = Path.GetDirectoryName(scenery.Path) ?? "scenery"; string exportName = "$" + Path.GetFileName(scenery.Path); string exportPath = Path.Combine(dir, exportName); try { - File.WriteAllText(exportPath, scenery.BuildExportContent(), Encoding.GetEncoding(1250)); + File.WriteAllText(exportPath, scenery.BuildExportContent(Settings.Instance.IgnoreIrrelevantTrains), Encoding.GetEncoding(1250)); } catch { diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index ed46966..0b05fb6 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -64,6 +64,30 @@ + + + + + + + + + + + + + + + + + + @@ -82,13 +106,11 @@ + @@ -159,6 +181,18 @@ + + + + + + + + + + @@ -254,6 +288,21 @@ + + + + + + + + + + + + @@ -297,13 +346,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + +