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/KeyboardConfig.cs b/StarterNG/Classes/KeyboardConfig.cs
index f6a8c5e..840f7b5 100644
--- a/StarterNG/Classes/KeyboardConfig.cs
+++ b/StarterNG/Classes/KeyboardConfig.cs
@@ -88,6 +88,12 @@ public sealed class KeyboardConfig
if (!string.IsNullOrEmpty(appData))
return Path.Combine(appData, "MaSzyna", "eu07_input-keyboard.ini");
}
+ else if (OperatingSystem.IsMacOS())
+ {
+ string? home = Environment.GetEnvironmentVariable("HOME");
+ if (!string.IsNullOrEmpty(home))
+ return Path.Combine(home, "Library", "Application Support", "MaSzyna", "eu07_input-keyboard.ini");
+ }
else
{
string? home = Environment.GetEnvironmentVariable("HOME");
diff --git a/StarterNG/Classes/PresetStore.cs b/StarterNG/Classes/PresetStore.cs
index 629905c..9d9ce65 100644
--- a/StarterNG/Classes/PresetStore.cs
+++ b/StarterNG/Classes/PresetStore.cs
@@ -27,16 +27,32 @@ public sealed class PresetCollection
///
/// The user's vehicle/consist "warehouse": named trainset presets persisted as JSON.
/// Stored under the per-user config directory so it survives reinstalls:
-/// Windows: %appdata%\MaSzyna\starter\userpresets.json
-/// Unix: ~/.config/MaSzyna/starter/userpresets.json
-/// (both resolved via ).
+/// Windows: %APPDATA%\MaSzyna\starter\userpresets.json
+/// macOS: ~/Library/Application Support/MaSzyna/starter/userpresets.json
+/// Linux: ~/.config/MaSzyna/starter/userpresets.json
/// All operations are best-effort and never throw.
///
public static class PresetStore
{
- public static string FilePath { get; } = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "MaSzyna", "starter", "userpresets.json");
+ private static string ResolvePath()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ return Path.Combine(appData, "MaSzyna", "starter", "userpresets.json");
+ }
+ if (OperatingSystem.IsMacOS())
+ {
+ string home = Environment.GetEnvironmentVariable("HOME") ?? AppContext.BaseDirectory;
+ return Path.Combine(home, "Library", "Application Support", "MaSzyna", "starter", "userpresets.json");
+ }
+ {
+ string home = Environment.GetEnvironmentVariable("HOME") ?? AppContext.BaseDirectory;
+ return Path.Combine(home, ".config", "MaSzyna", "starter", "userpresets.json");
+ }
+ }
+
+ public static string FilePath { get; } = ResolvePath();
// Source-generated metadata keeps (de)serialisation AOT/trim-safe, matching the
// approach used for the vehicle database.
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 4f57612..1275cb9 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.
@@ -42,19 +46,26 @@ public sealed class Settings
public bool Fullscreen; // fullscreen
public bool PauseWhenInactive = true; // inactivepause
public bool PauseOnStart; // pause
- public int CursorSensitivity = 3; // |mousescale x|
+ public int CursorSensitivity = 2; // |mousescale x|
public bool InvertMouseHorizontal; // mousescale x < 0
public bool InvertMouseVertical; // mousescale y < 0
// ── Communication ─────────────────────────────────────────────────────
public bool IgnoreGamepad; // input.gamepad (inverted)
- public int FeedbackMode; // feedbackmode (0-5)
+ public int FeedbackMode = 1; // 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 VirtualShunting = true; // 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)
@@ -65,52 +76,71 @@ public sealed class Settings
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 int DynamicLights = 7; // dynamiclights (0-7)
+ public double DrawRangeFactor = 3.0; // gfx.drawrange.factor.max
+ public bool VSync; // 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 ChromaticAberration = true; // 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 ShadowMapResolution = 2048; // shadowtune[0]
public int ShadowProjectionRange = 250; // shadowtune[1]
- public int CabShadowsRange = 30; // gfx.shadows.cab.range
+ public int CabShadowsRange; // gfx.shadows.cab.range
public int ShadowRankCutoff = 3; // gfx.shadow.rank.cutoff (NOTE)
- public int ReflectionsFidelity = 2; // gfx.reflections.fidelity (0-2)
+ public int ReflectionsFidelity; // 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)
+ 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)
public bool FullPhysics = true; // fullphysics
public bool EnableTraction = true; // enabletraction (pantograph can break)
- public bool LiveTraction; // livetraction
+ public bool LiveTraction = true; // 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
+ public double Friction = 1.0; // friction (0.1-3.0)
+ public double BrakeStep = 1.0; // brakestep (0.4-3.0)
+ public double BrakeSpeed = 3.0; // brakespeed (0.4-3.0)
// ── 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 Volume = 50; // sound.volume (0-2 → 0-100)
+ public int RadioVolume = 75; // 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
+ public int AmbientVolume = 100; // sound.volume.ambient
+
+ // ── 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 = true; // 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)
@@ -124,12 +154,8 @@ public sealed class Settings
/// 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 double _mouseScaleYMagnitude = 0.2;
private string[] _shadowTuneExtra = { "400", "300" }; // tokens [2], [3]
private ConfigFile _config = new();
@@ -144,6 +170,12 @@ public sealed class Settings
if (!string.IsNullOrEmpty(appData))
return Path.Combine(appData, "MaSzyna", "eu07.ini");
}
+ else if (OperatingSystem.IsMacOS())
+ {
+ string? home = Environment.GetEnvironmentVariable("HOME");
+ if (!string.IsNullOrEmpty(home))
+ return Path.Combine(home, "Library", "Application Support", "MaSzyna", "eu07.ini");
+ }
else
{
string? home = Environment.GetEnvironmentVariable("HOME");
@@ -158,6 +190,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,
@@ -191,6 +227,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)
@@ -201,8 +256,10 @@ public sealed class Settings
return false;
if (OperatingSystem.IsWindows())
return ext == ".exe";
- // Linux / macOS: no extension (eu07) or a known binary suffix
- return ext.Length == 0 || ext is ".x86_64" or ".run" or ".appimage";
+ // Linux / macOS: no extension (eu07), a known binary suffix, or .exe
+ // (some builds - notably this project's own build-macos.sh - ship the
+ // macOS binary under the historical "eu07.exe" name).
+ return ext.Length == 0 || ext is ".exe" or ".x86_64" or ".run" or ".appimage";
}
private static readonly Encoding FileEncoding = ResolveEncoding();
@@ -226,11 +283,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
@@ -241,27 +310,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
{
@@ -295,13 +363,31 @@ public sealed class Settings
// Communication
IgnoreGamepad = !c.GetBool("input.gamepad", true);
- FeedbackMode = Clamp(c.GetInt("feedbackmode", 0), 0, 5);
+ FeedbackMode = Clamp(c.GetInt("feedbackmode", 1), 0, 5);
+ FeedbackPort = c.GetInt("feedbackport", 888);
+ UartEnabled = c.Has("uart");
+ UartPort = c.GetString("uart", "");
+ UartTune = c.GetString("uarttune", "");
+ UartDebug = c.GetBool("uartdebug", false);
+ if (c.Has("uartfeature"))
+ {
+ 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");
+ }
+ else
+ {
+ UartMain = UartScnd = UartTrain = UartLocal = true;
+ UartRadioVolume = UartRadioChannel = false;
+ }
// 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);
+ VirtualShunting = c.GetBool("ai.trainman", true);
+ LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false);
// Graphics
RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0);
@@ -312,12 +398,12 @@ public sealed class Settings
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);
+ DynamicLights = Clamp(c.GetInt("dynamiclights", 7), 0, 7);
+ DrawRangeFactor = c.GetDouble("gfx.drawrange.factor.max", 3.0);
+ VSync = c.GetBool("vsync", false);
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);
+ ChromaticAberration = c.GetBool("gfx.postfx.chromaticaberration.enabled", true);
MotionBlur = c.GetBool("gfx.postfx.motionblur.enabled", true);
ExtraEffects = c.GetBool("gfx.extraeffects", true);
EnvMap = c.GetBool("gfx.envmap.enabled", true);
@@ -326,42 +412,61 @@ public sealed class Settings
ReflectionsFramerate = c.GetInt("gfx.reflections.framerate", 60);
var shadowTune = c.GetTokens("shadowtune");
- if (shadowTune.Length >= 1) ShadowMapResolution = ParseInt(shadowTune[0], 4096);
+ if (shadowTune.Length >= 1) ShadowMapResolution = ParseInt(shadowTune[0], 2048);
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));
+ CabShadowsRange = (int)Math.Round(c.GetDouble("gfx.shadows.cab.range", 0));
ShadowRankCutoff = c.GetInt("gfx.shadow.rank.cutoff", 3);
- ReflectionsFidelity = Clamp(c.GetInt("gfx.reflections.fidelity", 2), 0, 2);
+ ReflectionsFidelity = Clamp(c.GetInt("gfx.reflections.fidelity", 0), 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;
+ 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);
FullPhysics = c.GetBool("fullphysics", true);
EnableTraction = c.GetBool("enabletraction", true);
- LiveTraction = c.GetBool("livetraction", false);
+ LiveTraction = c.GetBool("livetraction", true);
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);
+ Friction = c.GetDouble("friction", 1.0);
+ BrakeStep = c.GetDouble("brakestep", 1.0);
+ BrakeSpeed = c.GetDouble("brakespeed", 3.0);
// 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);
+ Volume = Clamp((int)Math.Round(c.GetDouble("sound.volume", 1.0) * 50), 1, 100);
+ RadioVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.radio", 0.75) * 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);
+ AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 1.0) * 100), 1, 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", true);
+ 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);
@@ -387,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)]);
@@ -403,11 +521,11 @@ public sealed class Settings
c.SetInt("maxcabtexturesize", MaxCabTextureSize);
c.SetInt("anisotropicfiltering", TextureFiltering);
c.SetInt("multisampling", Multisampling);
+ c.SetInt("dynamiclights", DynamicLights);
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);
@@ -425,8 +543,10 @@ public sealed class Settings
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)]);
+ 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);
@@ -438,6 +558,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);
@@ -446,12 +569,24 @@ public sealed class Settings
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);
+
+ // 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..69c9c0e 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 => MathF.Abs(trainset.OriginalVelocity) < 0.01f ? 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 46b8daf..38d51fc 100644
--- a/StarterNG/Views/Settings.axaml
+++ b/StarterNG/Views/Settings.axaml
@@ -64,6 +64,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -82,13 +106,11 @@
-
-
- eu07.exe
-
+
+
@@ -149,11 +171,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -161,13 +200,6 @@
-
-
-
-
-
-
-
@@ -223,11 +255,6 @@
-
-
-
-
@@ -249,6 +276,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -283,11 +325,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
@@ -296,9 +356,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs
index e30aae5..a028a32 100644
--- a/StarterNG/Views/Settings.axaml.cs
+++ b/StarterNG/Views/Settings.axaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
@@ -24,6 +25,8 @@ public partial class Settings : UserControl
{
InitializeComponent();
+ TrainPhysicsThreadsSlider.Maximum = Environment.ProcessorCount;
+
this.AttachedToVisualTree += (_, _) =>
{
TextureResolutionSlider_OnValueChanged(null, null);
@@ -37,10 +40,14 @@ public partial class Settings : UserControl
_ => 1
};
+ FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
+ FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb);
+
// The settings instance pulls live values from here whenever the app
// closes or the game is launched.
StarterNG.Classes.Settings.Instance.CaptureFromUi = ReadFromUi;
+ PopulateProfileCombo();
ApplyToUi();
// Controls tab: load the key bindings and build the editor + on-screen
@@ -51,6 +58,78 @@ public partial class Settings : UserControl
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
}
+ private static int FindComboIndexByContent(ComboBox cb, string content)
+ {
+ for (int i = 0; i < cb.Items.Count; i++)
+ if (cb.Items[i] is ComboBoxItem item &&
+ string.Equals(item.Content?.ToString(), content, StringComparison.OrdinalIgnoreCase))
+ return i;
+ return -1;
+ }
+
+ private void PopulateProfileCombo()
+ {
+ string? current = (ProfileCb.SelectedItem as ComboBoxItem)?.Content?.ToString();
+ ProfileCb.Items.Clear();
+ foreach (string name in SettingsProfileStore.ListProfiles())
+ ProfileCb.Items.Add(new ComboBoxItem { Content = name });
+ if (current is not null)
+ {
+ int idx = FindComboIndexByContent(ProfileCb, current);
+ if (idx >= 0) ProfileCb.SelectedIndex = idx;
+ }
+ }
+
+ private void ProfileApplyButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ if (ProfileCb.SelectedItem is not ComboBoxItem { Content: string name } || string.IsNullOrWhiteSpace(name))
+ return;
+ string path = SettingsProfileStore.PathFor(name);
+ if (!File.Exists(path)) return;
+ StarterNG.Classes.Settings.Instance.LoadFrom(path);
+ ApplyToUi();
+ }
+
+ private void ProfileSaveAsButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ var nameBox = new TextBox { PlaceholderText = App.Loc["ProfileNamePrompt"], MinWidth = 220 };
+ var ok = new Button { Content = App.Loc["ProfileSaveAs"] };
+ var panel = new StackPanel { Spacing = 6, Margin = new Thickness(8), MinWidth = 240 };
+ panel.Children.Add(new TextBlock { Text = App.Loc["ProfileNamePrompt"], FontWeight = FontWeight.Bold, FontSize = 12 });
+ panel.Children.Add(nameBox);
+ panel.Children.Add(ok);
+ var flyout = new Flyout { Content = panel };
+
+ void Commit()
+ {
+ if (string.IsNullOrWhiteSpace(nameBox.Text)) return;
+ ReadFromUi();
+ StarterNG.Classes.Settings.Instance.SaveTo(SettingsProfileStore.PathFor(nameBox.Text));
+ PopulateProfileCombo();
+ int idx = FindComboIndexByContent(ProfileCb, nameBox.Text.Trim());
+ if (idx >= 0) ProfileCb.SelectedIndex = idx;
+ flyout.Hide();
+ }
+ ok.Click += (_, _) => Commit();
+ nameBox.KeyDown += (_, ev) => { if (ev.Key == Key.Enter) { ev.Handled = true; Commit(); } };
+ flyout.ShowAt(ProfileSaveAsBtn, showAtPointer: false);
+ nameBox.Focus(); nameBox.SelectAll();
+ }
+
+ private void PopulateExeCombo()
+ {
+ string? current = (SelectExeCb.SelectedItem as ComboBoxItem)?.Content?.ToString();
+ SelectExeCb.Items.Clear();
+ SelectExeCb.Items.Add(new ComboBoxItem { Content = App.Loc["SelectEXEAuto"] });
+ foreach (string exe in StarterNG.Classes.Settings.ListCandidateExecutables())
+ SelectExeCb.Items.Add(new ComboBoxItem { Content = exe });
+ if (current is not null)
+ {
+ int idx = FindComboIndexByContent(SelectExeCb, current);
+ SelectExeCb.SelectedIndex = idx >= 0 ? idx : 0;
+ }
+ }
+
// ── Settings instance → controls ──────────────────────────────────────
private void ApplyToUi()
{
@@ -58,6 +137,9 @@ public partial class Settings : UserControl
_loading = true;
try
{
+ PopulateProfileCombo();
+ PopulateExeCombo();
+
// General
ChangeLanguageCb.SelectedIndex = s.Language == "Polski" ? 0 : 1;
FullscreenCb.IsChecked = s.Fullscreen;
@@ -70,11 +152,26 @@ public partial class Settings : UserControl
// Communication
GamepadIgnoreCb.IsChecked = s.IgnoreGamepad;
FeedbackCb.SelectedIndex = s.FeedbackMode;
+ FeedbackPortNud.Value = (decimal)s.FeedbackPort;
+ UartEnableCb.IsChecked = s.UartEnabled;
+ UartPortTb.Text = s.UartPort;
+ UartTuneTb.Text = s.UartTune;
+ UartDebugCb.IsChecked = s.UartDebug;
+ UartMainCb.IsChecked = s.UartMain;
+ UartScndCb.IsChecked = s.UartScnd;
+ UartTrainCb.IsChecked = s.UartTrain;
+ UartLocalCb.IsChecked = s.UartLocal;
+ UartRadioVolumeCb.IsChecked = s.UartRadioVolume;
+ UartRadioChannelCb.IsChecked = s.UartRadioChannel;
+ UpdateFeedbackDetailVisibility();
// Other
- SelectExeCb.SelectedIndex = s.SelectExeAutomatically ? 0 : 1;
+ SelectExeCb.SelectedIndex = s.SelectExeAutomatically
+ ? 0
+ : Math.Max(0, FindComboIndexByContent(SelectExeCb, s.ExecutablePath));
DebugModeCb.IsChecked = s.DebugMode;
VirtualShuntingCb.IsChecked = s.VirtualShunting;
+ LogMissingVehicleFilesCb.IsChecked = s.LogMissingVehicleFiles;
// Graphics
RenderEngineCb.SelectedIndex = s.RenderEngine;
@@ -84,11 +181,11 @@ public partial class Settings : UserControl
cabTextureResolutionSlider.Value = Log2(s.MaxCabTextureSize, 12);
TexFilteringSlider.Value = AnisotropyToSlider(s.TextureFiltering);
MultisamplingSlider.Value = s.Multisampling + 1;
+ DynamicLightsSlider.Value = s.DynamicLights;
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;
@@ -104,7 +201,12 @@ public partial class Settings : UserControl
fovSlider.Value = s.FieldOfView;
RenderScreensCb.IsChecked = s.PythonScreens;
RenderScreensThreadCb.IsChecked = s.PythonThreadedUpload;
- RenderScreensFramerateSlider.Value = s.ScreenRendererPriority;
+ FpsLimitEnableCb.IsChecked = s.FpsLimitEnabled;
+ FpsLimitSlider.Value = s.FpsLimit;
+ FpsLimitSlider.IsEnabled = s.FpsLimitEnabled;
+ ShadowAngleLimitSlider.Value = s.ShadowAngleLimit;
+ FullscreenWindowedCb.IsChecked = s.FullscreenWindowed;
+ RenderAngleVulkanCb.IsChecked = s.RenderAngleVulkan;
// Physics
TrackCurvesSlider.Value = s.SplineFidelity;
@@ -116,6 +218,9 @@ public partial class Settings : UserControl
KeepLogsCb.IsChecked = s.MultipleLogs;
DisplaySimulationCb.IsChecked = s.DisplaySimulation;
CrashDamageCb.IsChecked = s.CrashDamage;
+ FrictionSlider.Value = s.Friction;
+ BrakeStepSlider.Value = s.BrakeStep;
+ BrakeSpeedSlider.Value = s.BrakeSpeed;
// Sound
EnableSoundsCb.IsChecked = s.SoundEnabled;
@@ -124,12 +229,24 @@ public partial class Settings : UserControl
VehiclesVolumeSlider.Value = s.VehiclesVolume;
PositionalVolumeSlider.Value = s.PositionalVolume;
AmbientVolumeSlider.Value = s.AmbientVolume;
- PauseVolumeSlider.Value = s.PausedVolume;
+
+ // Advanced
+ CompressTexturesCb.IsChecked = s.CompressTextures;
+ ScaleSpecularsCb.IsChecked = s.ScaleSpeculars;
+ UseGLESCb.IsChecked = s.UseGLES;
+ ShaderGammaCb.IsChecked = s.ShaderGamma;
+ ScreenMipmapsCb.IsChecked = s.ScreenMipmaps;
+ ExtendedModelConversionCb.IsChecked = s.ExtendedModelConversion;
+ GfxResourceMoveCb.IsChecked = s.GfxResourceMove;
+ GfxResourceSweepCb.IsChecked = s.GfxResourceSweep;
+ TrainPhysicsThreadsSlider.Value = s.TrainPhysicsThreads;
+ IgnoreIrrelevantTrainsCb.IsChecked = s.IgnoreIrrelevantTrains;
// Starter
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree;
+ BatteryDefaultCb.SelectedIndex = (int)s.BatteryDefault;
}
finally
{
@@ -154,6 +271,17 @@ public partial class Settings : UserControl
// Communication
s.IgnoreGamepad = IsChecked(GamepadIgnoreCb);
s.FeedbackMode = Math.Max(0, FeedbackCb.SelectedIndex);
+ s.FeedbackPort = (int)(FeedbackPortNud.Value ?? 888);
+ s.UartEnabled = IsChecked(UartEnableCb);
+ s.UartPort = UartPortTb.Text ?? "";
+ s.UartTune = UartTuneTb.Text ?? "";
+ s.UartDebug = IsChecked(UartDebugCb);
+ s.UartMain = IsChecked(UartMainCb);
+ s.UartScnd = IsChecked(UartScndCb);
+ s.UartTrain = IsChecked(UartTrainCb);
+ s.UartLocal = IsChecked(UartLocalCb);
+ s.UartRadioVolume = IsChecked(UartRadioVolumeCb);
+ s.UartRadioChannel = IsChecked(UartRadioChannelCb);
// Other
s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0;
@@ -161,6 +289,7 @@ public partial class Settings : UserControl
: (SelectExeCb.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "eu07.exe";
s.DebugMode = IsChecked(DebugModeCb);
s.VirtualShunting = IsChecked(VirtualShuntingCb);
+ s.LogMissingVehicleFiles = IsChecked(LogMissingVehicleFilesCb);
// Graphics
s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex);
@@ -171,11 +300,11 @@ public partial class Settings : UserControl
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.DynamicLights = (int)DynamicLightsSlider.Value;
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);
@@ -191,7 +320,11 @@ public partial class Settings : UserControl
s.FieldOfView = Clamp((int)fovSlider.Value, 15, 75);
s.PythonScreens = IsChecked(RenderScreensCb);
s.PythonThreadedUpload = IsChecked(RenderScreensThreadCb);
- s.ScreenRendererPriority = (int)RenderScreensFramerateSlider.Value;
+ s.FpsLimitEnabled = IsChecked(FpsLimitEnableCb);
+ s.FpsLimit = (int)FpsLimitSlider.Value;
+ s.ShadowAngleLimit = ShadowAngleLimitSlider.Value;
+ s.FullscreenWindowed = IsChecked(FullscreenWindowedCb);
+ s.RenderAngleVulkan = IsChecked(RenderAngleVulkanCb);
// Physics
s.SplineFidelity = (int)TrackCurvesSlider.Value;
@@ -203,6 +336,9 @@ public partial class Settings : UserControl
s.MultipleLogs = IsChecked(KeepLogsCb);
s.DisplaySimulation = IsChecked(DisplaySimulationCb);
s.CrashDamage = IsChecked(CrashDamageCb);
+ s.Friction = FrictionSlider.Value;
+ s.BrakeStep = BrakeStepSlider.Value;
+ s.BrakeSpeed = BrakeSpeedSlider.Value;
// Sound
s.SoundEnabled = IsChecked(EnableSoundsCb);
@@ -211,12 +347,24 @@ public partial class Settings : UserControl
s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
s.PositionalVolume = (int)PositionalVolumeSlider.Value;
s.AmbientVolume = (int)AmbientVolumeSlider.Value;
- s.PausedVolume = (int)PauseVolumeSlider.Value;
+
+ // Advanced
+ s.CompressTextures = IsChecked(CompressTexturesCb);
+ s.ScaleSpeculars = IsChecked(ScaleSpecularsCb);
+ s.UseGLES = IsChecked(UseGLESCb);
+ s.ShaderGamma = IsChecked(ShaderGammaCb);
+ s.ScreenMipmaps = IsChecked(ScreenMipmapsCb);
+ s.ExtendedModelConversion = IsChecked(ExtendedModelConversionCb);
+ s.GfxResourceMove = IsChecked(GfxResourceMoveCb);
+ s.GfxResourceSweep = IsChecked(GfxResourceSweepCb);
+ s.TrainPhysicsThreads = (int)TrainPhysicsThreadsSlider.Value;
+ s.IgnoreIrrelevantTrains = IsChecked(IgnoreIrrelevantTrainsCb);
// Starter
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb);
+ s.BatteryDefault = (BatteryDefault)Math.Max(0, BatteryDefaultCb.SelectedIndex);
}
private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
@@ -310,6 +458,15 @@ public partial class Settings : UserControl
App.ApplyLanguage(lang);
}
+ private void FeedbackCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) =>
+ UpdateFeedbackDetailVisibility();
+
+ private void UpdateFeedbackDetailVisibility()
+ {
+ FeedbackPortRow.IsVisible = FeedbackCb.SelectedIndex == 3;
+ UartExpander.IsVisible = FeedbackCb.SelectedIndex == 5;
+ }
+
// ── tiny helpers ──────────────────────────────────────────────────────
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml
index e2514bc..ea6acdf 100644
--- a/StarterNG/startercfg/lang/en.xml
+++ b/StarterNG/startercfg/lang/en.xml
@@ -222,11 +222,26 @@
PoKeys55
Serial port (COM)
+ LPT port address
+
+ UART configuration
+ Enable UART communication
+ Serial port
+ Tuning parameter
+ Print debug data to the console
+ Master controller
+ Secondary/shunting controller
+ Train brake
+ Locomotive brake
+ Radio volume
+ Radio channel
+
Other
Select exe:
Select automatically
Debug mode (test mode)
Virtual shunting
+ Log missing vehicle files during export
Graphics
Render engine
@@ -243,15 +258,16 @@
Maximum cabin texture resolution:
Texture filtering quality:
Multisampling:
+ Dynamic light sources:
Rendering range:
+ Limit frame rate
+ Shadow length limit
+ Automatic resolution (borderless fullscreen)
+ Use ANGLE Vulkan renderer
VSync
Smoke display
Smoke particles multiplier
- Post-processing
- Reinhard
- ACES
-
Chromatic aberration
Motion blur
Additional shader effects
@@ -276,7 +292,6 @@
View angle
Render onboard computer screens
Render screens in a separate thread
- Onboard screens refresh rate
Physics
@@ -289,6 +304,9 @@
Keep previous logs
Display simulation
Collision damage
+ Friction multiplier
+ Brake valve sensitivity
+ FV4a valve sensitivity
Sound
Enable sound
@@ -297,12 +315,31 @@
Vehicles volume
Positional sounds volume
Ambient volume
- Volume during pause
+
+ Advanced
+ Compress TGA textures
+ Scale specular highlights (old-model compatibility)
+ Use OpenGL ES
+ Gamma correction in shaders
+ Generate mipmaps for in-cab (Python) screen textures
+ Extended E3D model conversion (experimental)
+ Conservative texture memory management
+ Purge unused textures from GPU memory
+ Vehicle physics threads (0 = main thread)
+ Skip decorative consists when starting
Starter
+ Settings profile:
+ Apply
+ Save as…
+ Profile name
Automatically close starter
Large thumbnails
Automatically expand scenery tree
+ Battery on launch:
+ Scenario default
+ Always off
+ Always on
Controls
diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml
index de2087f..afc328a 100644
--- a/StarterNG/startercfg/lang/pl.xml
+++ b/StarterNG/startercfg/lang/pl.xml
@@ -222,11 +222,26 @@
PoKeys55
Serialport (COM)
+ Adres portu LPT
+
+ Konfiguracja UART
+ Włącz komunikację UART
+ Port szeregowy
+ Parametr strojenia
+ Wypisuj dane debugowania w konsoli
+ Nastawnik główny
+ Nastawnik manewrowy/pomocniczy
+ Hamulec zespolony
+ Hamulec lokomotywy
+ Głośność radiotelefonu
+ Kanał radiotelefonu
+
Inne
Wybór exe:
Wybierz automatycznie
Tryb testowy (debugmode)
Wirtualny manewrowy
+ Zapisuj w logu brakujące pliki pojazdów podczas eksportu
Grafika
Silnik renderowania
@@ -243,15 +258,16 @@
Maksymalna rozdzielczość tekstur kabiny:
Jakość filtrowania tekstur:
Multisampling:
+ Dynamiczne źródła światła:
Zasięg renderowania:
+ Ogranicz liczbę klatek na sekundę
+ Limit długości cienia
+ Automatyczna rozdzielczość (pełny ekran bez obramowania)
+ Użyj renderera ANGLE Vulkan
VSync
Wyświetlanie dymu
Mnożnik ilości cząsteczek dymu
- Postprocessing
- Reinhard
- ACES
-
Aberracja chromatyczna
Rozmycie podczas ruchu
Dodatkowe efekty shaderów
@@ -276,7 +292,6 @@
Kąt widzenia
Renderowanie ekranów komputerów pokładowych
Renderowanie ekranów w osobnym wątku
- Częstotliwość odświeżania ekranów komputerów pokładowych
Fizyka
@@ -289,6 +304,9 @@
Zachowuj poprzednie logi
Wyświetlaj symulację
Uszkodzenia przy zderzeniu
+ Mnożnik tarcia
+ Czułość zaworu hamulcowego
+ Czułość zaworu FV4a
Dżwięk
Włącz dźwięk
@@ -297,12 +315,31 @@
Głośność pojazdów
Głośność dżwięków pozycjonowanych
Głośność otoczenia
- Głośność podczas pauzy
+
+ Zaawansowane
+ Kompresuj tekstury TGA
+ Skaluj odbicia specularne (kompatybilność ze starymi modelami)
+ Używaj OpenGL ES
+ Korekcja gamma w shaderach
+ Generuj mipmapy dla tekstur ekranów kabinowych (Python)
+ Rozszerzona konwersja modeli E3D (eksperymentalne)
+ Ostrożne zarządzanie pamięcią tekstur
+ Usuwaj nieużywane tekstury z pamięci GPU
+ Wątki fizyki pojazdów (0 = wątek główny)
+ Pomijaj składy dekoracyjne przy starcie
Starter
+ Profil ustawień:
+ Zastosuj
+ Zapisz jako…
+ Nazwa profilu
Zamknij starter automatycznie
Duże miniaturki
Automatycznie rozwijanie drzewka scenerii
+ Akumulator przy starcie:
+ Domyślnie ze scenerii
+ Zawsze wyłączony
+ Zawsze włączony
Sterowanie
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..1b773f5
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+dotnet build StarterNG/StarterNG.csproj