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).
This commit is contained in:
Mateusz Włodarczyk
2026-07-05 18:04:07 +02:00
parent 211863c921
commit c4147a2f66
11 changed files with 557 additions and 19 deletions

View File

@@ -125,6 +125,28 @@ public sealed class ConfigFile
} }
} }
/// <summary>
/// Deactivates <paramref name="key"/> 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.
/// </summary>
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 SetBool(string key, bool value) => Set(key, value ? "yes" : "no");
public void SetInt(string key, int value) => public void SetInt(string key, int value) =>
Set(key, value.ToString(CultureInfo.InvariantCulture)); Set(key, value.ToString(CultureInfo.InvariantCulture));

View File

@@ -94,7 +94,7 @@ public class Scenery
/// Rebuilds the full .scn content with the (possibly modified) trainsets /// Rebuilds the full .scn content with the (possibly modified) trainsets
/// substituted back into their original positions. /// substituted back into their original positions.
/// </summary> /// </summary>
public string BuildExportContent() public string BuildExportContent(bool skipDecorTrainsets = false)
{ {
string result = _template; string result = _template;
@@ -105,7 +105,12 @@ public class Scenery
result = RewriteWeather(result); result = RewriteWeather(result);
for (int i = 0; i < Trainsets.Count; i++) 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; return result;
} }

View File

@@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
namespace StarterNG.Classes; namespace StarterNG.Classes;
public enum BatteryDefault { Default = 0, AlwaysOff = 1, AlwaysOn = 2 }
/// <summary> /// <summary>
/// Application settings backing the Settings view, persisted to the simulator's /// Application settings backing the Settings view, persisted to the simulator's
/// <c>eu07.ini</c>. /// <c>eu07.ini</c>.
@@ -49,12 +53,19 @@ public sealed class Settings
// ── Communication ───────────────────────────────────────────────────── // ── Communication ─────────────────────────────────────────────────────
public bool IgnoreGamepad; // input.gamepad (inverted) public bool IgnoreGamepad; // input.gamepad (inverted)
public int FeedbackMode; // feedbackmode (0-5) 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 ───────────────────────────────────────────────────────────── // ── Other ─────────────────────────────────────────────────────────────
public bool SelectExeAutomatically = true; // starter.exe.auto (launcher-only) public bool SelectExeAutomatically = true; // starter.exe.auto (launcher-only)
public string ExecutablePath = "eu07.exe"; // starter.exe.path (launcher-only) public string ExecutablePath = "eu07.exe"; // starter.exe.path (launcher-only)
public bool DebugMode; // debugmode public bool DebugMode; // debugmode
public bool VirtualShunting; // ai.trainman public bool VirtualShunting; // ai.trainman
public bool LogMissingVehicleFiles; // starter.logmissingvehicles (launcher-only; not yet wired to any load/export codepath)
// ── Graphics ────────────────────────────────────────────────────────── // ── Graphics ──────────────────────────────────────────────────────────
public int RenderEngine; // gfxrenderer (index, see RenderEngines) public int RenderEngine; // gfxrenderer (index, see RenderEngines)
@@ -87,6 +98,11 @@ public sealed class Settings
public bool PythonScreens = true; // python.enabled public bool PythonScreens = true; // python.enabled
public bool PythonThreadedUpload = true; // python.threadedupload public bool PythonThreadedUpload = true; // python.threadedupload
public int ScreenRendererPriority = 4; // pyscreenrendererpriority (1-5, see ScreenPriorities) 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 ─────────────────────────────────────────────────────────── // ── Physics ───────────────────────────────────────────────────────────
public int SplineFidelity = 1; // splinefidelity (1-4) public int SplineFidelity = 1; // splinefidelity (1-4)
@@ -98,6 +114,9 @@ public sealed class Settings
public bool MultipleLogs; // multiplelogs public bool MultipleLogs; // multiplelogs
public bool DisplaySimulation = true; // gfx.skiprendering (inverted) public bool DisplaySimulation = true; // gfx.skiprendering (inverted)
public bool CrashDamage = true; // crashdamage 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 ───────────────────────────────────────────────────────────── // ── Sound ─────────────────────────────────────────────────────────────
public bool SoundEnabled = true; // soundenabled public bool SoundEnabled = true; // soundenabled
@@ -108,10 +127,23 @@ public sealed class Settings
public int AmbientVolume = 80; // sound.volume.ambient public int AmbientVolume = 80; // sound.volume.ambient
public int PausedVolume; // sound.volume.paused 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) ─────────────── // ── Starter (launcher-only, stored under starter.* keys) ───────────────
public bool AutoCloseStarter; // starter.autoclose public bool AutoCloseStarter; // starter.autoclose
public bool LargeThumbnails; // starter.largethumbnails public bool LargeThumbnails; // starter.largethumbnails
public bool AutoExpandSceneryTree; // starter.expandtree public bool AutoExpandSceneryTree; // starter.expandtree
public BatteryDefault BatteryDefault = BatteryDefault.Default; // starter.batterydefault (launcher-only)
// List filters (launcher-only). Default on. // List filters (launcher-only). Default on.
public bool ShowAiVehicles = true; // starter.show.ai (show //$o "-" consists) public bool ShowAiVehicles = true; // starter.show.ai (show //$o "-" consists)
@@ -165,6 +197,10 @@ public sealed class Settings
private static string DefaultConfigPath() => private static string DefaultConfigPath() =>
Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini"); Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini");
/// <summary>Directory holding the per-user eu07.ini, for locating launcher-only data alongside it.</summary>
public static string UserConfigDirectory() =>
Path.GetDirectoryName(UserConfigPath()) ?? AppContext.BaseDirectory;
/// <summary> /// <summary>
/// The simulator executable to launch. With auto-selection on, the working /// The simulator executable to launch. With auto-selection on, the working
/// directory is scanned for an <c>eu07*</c> binary - <c>eu07</c> on Linux/macOS, /// directory is scanned for an <c>eu07*</c> binary - <c>eu07</c> on Linux/macOS,
@@ -198,6 +234,25 @@ public sealed class Settings
return canonical; return canonical;
} }
/// <summary>
/// Scans <paramref name="directory"/> (defaults to the working directory) for
/// plausible <c>eu07*</c> launcher binaries, newest first, for populating the
/// manual-selection dropdown. Does not affect <see cref="ResolveExecutable"/>.
/// </summary>
public static List<string> 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<string>(); }
}
// Filters the eu07* matches down to plausible launcher binaries, skipping data // Filters the eu07* matches down to plausible launcher binaries, skipping data
// and config files (eu07.ini, eu07.log, libraries, …). // and config files (eu07.ini, eu07.log, libraries, …).
private static bool IsExecutableCandidate(string path) private static bool IsExecutableCandidate(string path)
@@ -233,11 +288,23 @@ public sealed class Settings
{ {
_savePath = UserConfigPath(); _savePath = UserConfigPath();
string source = File.Exists(_savePath) ? _savePath : DefaultConfigPath(); string source = File.Exists(_savePath) ? _savePath : DefaultConfigPath();
LoadFrom(source);
}
/// <summary>Captures the latest UI values (if the view is open) and saves.</summary>
public void CaptureAndSave()
{
CaptureFromUi?.Invoke();
Save();
}
/// <summary>Loads settings from an arbitrary path without changing where <see cref="Save"/> writes.</summary>
public void LoadFrom(string path)
{
try try
{ {
_config = File.Exists(source) _config = File.Exists(path)
? ConfigFile.Parse(File.ReadAllText(source, FileEncoding)) ? ConfigFile.Parse(File.ReadAllText(path, FileEncoding))
: new ConfigFile(); : new ConfigFile();
} }
catch catch
@@ -248,27 +315,26 @@ public sealed class Settings
ReadFromConfig(); ReadFromConfig();
} }
/// <summary>Captures the latest UI values (if the view is open) and saves.</summary>
public void CaptureAndSave()
{
CaptureFromUi?.Invoke();
Save();
}
/// <summary>Persists settings to the per-user eu07.ini, keeping unknown keys.</summary> /// <summary>Persists settings to the per-user eu07.ini, keeping unknown keys.</summary>
public void Save() public void Save()
{ {
if (string.IsNullOrEmpty(_savePath)) if (string.IsNullOrEmpty(_savePath))
_savePath = UserConfigPath(); _savePath = UserConfigPath();
SaveTo(_savePath);
}
/// <summary>Writes settings to an arbitrary path without changing where <see cref="Save"/> writes.</summary>
public void SaveTo(string path)
{
WriteToConfig(); WriteToConfig();
try try
{ {
string? dir = Path.GetDirectoryName(_savePath); string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir)) if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
File.WriteAllText(_savePath, _config.ToText(), FileEncoding); File.WriteAllText(path, _config.ToText(), FileEncoding);
} }
catch catch
{ {
@@ -303,12 +369,22 @@ public sealed class Settings
// Communication // Communication
IgnoreGamepad = !c.GetBool("input.gamepad", true); IgnoreGamepad = !c.GetBool("input.gamepad", true);
FeedbackMode = Clamp(c.GetInt("feedbackmode", 0), 0, 5); 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<string>(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 // Other
SelectExeAutomatically = c.GetBool("starter.exe.auto", true); SelectExeAutomatically = c.GetBool("starter.exe.auto", true);
ExecutablePath = c.GetString("starter.exe.path", "eu07.exe"); ExecutablePath = c.GetString("starter.exe.path", "eu07.exe");
DebugMode = c.GetBool("debugmode", false); DebugMode = c.GetBool("debugmode", false);
VirtualShunting = c.GetBool("ai.trainman", false); VirtualShunting = c.GetBool("ai.trainman", false);
LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false);
// Graphics // Graphics
RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0); RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0);
@@ -345,6 +421,11 @@ public sealed class Settings
PythonScreens = c.GetBool("python.enabled", true); PythonScreens = c.GetBool("python.enabled", true);
PythonThreadedUpload = c.GetBool("python.threadedupload", true); PythonThreadedUpload = c.GetBool("python.threadedupload", true);
ScreenRendererPriority = IndexOf(ScreenPriorities, c.GetString("pyscreenrendererpriority", "lower"), 3) + 1; 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 // Physics
SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4); SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4);
@@ -356,6 +437,9 @@ public sealed class Settings
MultipleLogs = c.GetBool("multiplelogs", false); MultipleLogs = c.GetBool("multiplelogs", false);
DisplaySimulation = !c.GetBool("gfx.skiprendering", false); DisplaySimulation = !c.GetBool("gfx.skiprendering", false);
CrashDamage = c.GetBool("crashdamage", true); CrashDamage = c.GetBool("crashdamage", true);
Friction = c.GetDouble("friction", 1.0);
BrakeStep = c.GetDouble("brakestep", 1.0);
BrakeSpeed = c.GetDouble("brakespeed", 1.0);
// Sound // Sound
SoundEnabled = c.GetBool("soundenabled", true); 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); 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); 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 // Starter
AutoCloseStarter = c.GetBool("starter.autoclose", false); AutoCloseStarter = c.GetBool("starter.autoclose", false);
LargeThumbnails = c.GetBool("starter.largethumbnails", false); LargeThumbnails = c.GetBool("starter.largethumbnails", false);
AutoExpandSceneryTree = c.GetBool("starter.expandtree", false); AutoExpandSceneryTree = c.GetBool("starter.expandtree", false);
BatteryDefault = (BatteryDefault)Clamp(c.GetInt("starter.batterydefault", 0), 0, 2);
ShowAiVehicles = c.GetBool("starter.show.ai", true); ShowAiVehicles = c.GetBool("starter.show.ai", true);
DrivableOnly = c.GetBool("starter.drivableonly", true); DrivableOnly = c.GetBool("starter.drivableonly", true);
ShowArchivalSceneries = c.GetBool("starter.show.archival", true); ShowArchivalSceneries = c.GetBool("starter.show.archival", true);
@@ -395,12 +492,25 @@ public sealed class Settings
// Communication // Communication
c.SetBool("input.gamepad", !IgnoreGamepad); c.SetBool("input.gamepad", !IgnoreGamepad);
c.SetInt("feedbackmode", FeedbackMode); 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 // Other
c.SetBool("starter.exe.auto", SelectExeAutomatically); c.SetBool("starter.exe.auto", SelectExeAutomatically);
c.Set("starter.exe.path", ExecutablePath); c.Set("starter.exe.path", ExecutablePath);
c.SetBool("debugmode", DebugMode); c.SetBool("debugmode", DebugMode);
c.SetBool("ai.trainman", VirtualShunting); c.SetBool("ai.trainman", VirtualShunting);
c.SetBool("starter.logmissingvehicles", LogMissingVehicleFiles);
// Graphics // Graphics
c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]); c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]);
@@ -436,6 +546,10 @@ public sealed class Settings
c.SetBool("python.threadedupload", PythonThreadedUpload); c.SetBool("python.threadedupload", PythonThreadedUpload);
c.Set("pyscreenrendererpriority", c.Set("pyscreenrendererpriority",
ScreenPriorities[Clamp(ScreenRendererPriority - 1, 0, ScreenPriorities.Length - 1)]); 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 // Physics
c.SetInt("splinefidelity", SplineFidelity); c.SetInt("splinefidelity", SplineFidelity);
@@ -447,6 +561,9 @@ public sealed class Settings
c.SetBool("multiplelogs", MultipleLogs); c.SetBool("multiplelogs", MultipleLogs);
c.SetBool("gfx.skiprendering", !DisplaySimulation); c.SetBool("gfx.skiprendering", !DisplaySimulation);
c.SetBool("crashdamage", CrashDamage); c.SetBool("crashdamage", CrashDamage);
c.SetDouble("friction", Friction);
c.SetDouble("brakestep", BrakeStep);
c.SetDouble("brakespeed", BrakeSpeed);
// Sound // Sound
c.SetBool("soundenabled", SoundEnabled); c.SetBool("soundenabled", SoundEnabled);
@@ -457,10 +574,23 @@ public sealed class Settings
c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0); c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0);
c.SetDouble("sound.volume.paused", PausedVolume / 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 // Starter
c.SetBool("starter.autoclose", AutoCloseStarter); c.SetBool("starter.autoclose", AutoCloseStarter);
c.SetBool("starter.largethumbnails", LargeThumbnails); c.SetBool("starter.largethumbnails", LargeThumbnails);
c.SetBool("starter.expandtree", AutoExpandSceneryTree); c.SetBool("starter.expandtree", AutoExpandSceneryTree);
c.SetInt("starter.batterydefault", (int)BatteryDefault);
c.SetBool("starter.show.ai", ShowAiVehicles); c.SetBool("starter.show.ai", ShowAiVehicles);
c.SetBool("starter.drivableonly", DrivableOnly); c.SetBool("starter.drivableonly", DrivableOnly);
c.SetBool("starter.show.archival", ShowArchivalSceneries); c.SetBool("starter.show.archival", ShowArchivalSceneries);

View File

@@ -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<string> ListProfiles()
{
try
{
if (!Directory.Exists(DirectoryPath)) return Array.Empty<string>();
return Directory.GetFiles(DirectoryPath, "*.ini")
.Select(Path.GetFileNameWithoutExtension)
.Where(n => !string.IsNullOrEmpty(n))
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList()!;
}
catch { return Array.Empty<string>(); }
}
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");
}
}

View File

@@ -21,6 +21,7 @@ public class Trainset
public string Track; public string Track;
public float Offset; public float Offset;
public float Velocity; public float Velocity;
public float OriginalVelocity;
public string Description; public string Description;
public List<Dynamic> Vehicles; public List<Dynamic> Vehicles;
@@ -82,6 +83,7 @@ public class Trainset
this.Track = tokens[++i]; this.Track = tokens[++i];
this.Offset = float.Parse(tokens[++i], CultureInfo.InvariantCulture); this.Offset = float.Parse(tokens[++i], CultureInfo.InvariantCulture);
this.Velocity = float.Parse(tokens[++i], CultureInfo.InvariantCulture); this.Velocity = float.Parse(tokens[++i], CultureInfo.InvariantCulture);
this.OriginalVelocity = this.Velocity;
continue; continue;
} }

View File

@@ -100,13 +100,23 @@ public partial class MainWindow : Window
return; return;
var trainset = AppState.Instance.CurrentTrainset; 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/$<name>.scn with the replaced trainsets // write scenery/$<name>.scn with the replaced trainsets
string dir = Path.GetDirectoryName(scenery.Path) ?? "scenery"; string dir = Path.GetDirectoryName(scenery.Path) ?? "scenery";
string exportName = "$" + Path.GetFileName(scenery.Path); string exportName = "$" + Path.GetFileName(scenery.Path);
string exportPath = Path.Combine(dir, exportName); string exportPath = Path.Combine(dir, exportName);
try try
{ {
File.WriteAllText(exportPath, scenery.BuildExportContent(), Encoding.GetEncoding(1250)); File.WriteAllText(exportPath, scenery.BuildExportContent(Settings.Instance.IgnoreIrrelevantTrains), Encoding.GetEncoding(1250));
} }
catch catch
{ {

View File

@@ -64,6 +64,30 @@
<ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" /> <ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" />
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
<StackPanel x:Name="FeedbackPortRow" Spacing="8" Orientation="Horizontal" IsVisible="False">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [FeedbackPort], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888" Width="120" />
</StackPanel>
<Expander x:Name="UartExpander" IsVisible="False" Header="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="UartEnableCb" Content="{Binding [UartEnable], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [UartPort], Source={x:Static starterNg:App.Loc}}" />
<TextBox x:Name="UartPortTb" MinWidth="150" PlaceholderText="COM3" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [UartTune], Source={x:Static starterNg:App.Loc}}" />
<TextBox x:Name="UartTuneTb" MinWidth="150" />
</StackPanel>
<CheckBox x:Name="UartDebugCb" Content="{Binding [UartDebug], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartMainCb" Content="{Binding [UartMain], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartScndCb" Content="{Binding [UartScnd], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartTrainCb" Content="{Binding [UartTrain], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartLocalCb" Content="{Binding [UartLocal], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartRadioVolumeCb" Content="{Binding [UartRadioVolume], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartRadioChannelCb" Content="{Binding [UartRadioChannel], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</Expander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@@ -82,13 +106,11 @@
<StackPanel Spacing="8" Orientation="Vertical" Margin="12"> <StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal"> <StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}" /> <Label VerticalAlignment="Center" FontSize="14" Content="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="SelectExeCb" MinWidth="350" MaxWidth="350" SelectedIndex="0"> <ComboBox x:Name="SelectExeCb" MinWidth="350" MaxWidth="350" SelectedIndex="0" />
<ComboBoxItem Content="{Binding [SelectEXEAuto], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem>eu07.exe</ComboBoxItem>
</ComboBox>
</StackPanel> </StackPanel>
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@@ -159,6 +181,18 @@
<Slider x:Name="RenderRangeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left" <Slider x:Name="RenderRangeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3" Value="1" /> MinWidth="250" Maximum="3" Value="1" />
</StackPanel> </StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="FpsLimitSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="1" IsSnapToTickEnabled="True" Value="30" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="ShadowAngleLimitSlider" Minimum="0.2" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="1.0" TickFrequency="0.1" Value="0.2" />
</StackPanel>
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal"> <StackPanel Spacing="8" Orientation="Horizontal">
@@ -254,6 +288,21 @@
<CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Friction], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="FrictionSlider" Minimum="0.1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="BrakeStepSlider" Minimum="0.4" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@@ -297,13 +346,52 @@
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<!-- Advanced -->
<TabItem Header="{Binding [Advanced], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="CompressTexturesCb" Content="{Binding [CompressTextures], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ScaleSpecularsCb" Content="{Binding [ScaleSpeculars], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UseGLESCb" Content="{Binding [UseGLES], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ShaderGammaCb" Content="{Binding [ShaderGamma], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ScreenMipmapsCb" Content="{Binding [ScreenMipmaps], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ExtendedModelConversionCb" Content="{Binding [ExtendedModelConversion], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="GfxResourceMoveCb" Content="{Binding [GfxResourceMove], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="GfxResourceSweepCb" Content="{Binding [GfxResourceSweep], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TrainPhysicsThreads], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TrainPhysicsThreadsSlider" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="0" TickFrequency="1" IsSnapToTickEnabled="True" Value="0" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #TrainPhysicsThreadsSlider.Value}" />
</StackPanel>
<CheckBox x:Name="IgnoreIrrelevantTrainsCb" Content="{Binding [IgnoreIrrelevantTrains], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Starter --> <!-- Starter -->
<TabItem Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}"> <TabItem Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12"> <StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding [SettingsProfile], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ProfileCb" MinWidth="220" />
<Button x:Name="ProfileApplyBtn" Content="{Binding [ProfileApply], Source={x:Static starterNg:App.Loc}}"
Click="ProfileApplyButton_OnClick" />
<Button x:Name="ProfileSaveAsBtn" Content="{Binding [ProfileSaveAs], Source={x:Static starterNg:App.Loc}}"
Click="ProfileSaveAsButton_OnClick" />
</StackPanel>
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" /> <CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="BatteryDefaultCb" MinWidth="220" SelectedIndex="0">
<ComboBoxItem Content="{Binding [BatteryDefaultScenario], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOn], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@@ -24,6 +25,8 @@ public partial class Settings : UserControl
{ {
InitializeComponent(); InitializeComponent();
TrainPhysicsThreadsSlider.Maximum = Environment.ProcessorCount;
this.AttachedToVisualTree += (_, _) => this.AttachedToVisualTree += (_, _) =>
{ {
TextureResolutionSlider_OnValueChanged(null, null); TextureResolutionSlider_OnValueChanged(null, null);
@@ -37,10 +40,14 @@ public partial class Settings : UserControl
_ => 1 _ => 1
}; };
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb);
// The settings instance pulls live values from here whenever the app // The settings instance pulls live values from here whenever the app
// closes or the game is launched. // closes or the game is launched.
StarterNG.Classes.Settings.Instance.CaptureFromUi = ReadFromUi; StarterNG.Classes.Settings.Instance.CaptureFromUi = ReadFromUi;
PopulateProfileCombo();
ApplyToUi(); ApplyToUi();
// Controls tab: load the key bindings and build the editor + on-screen // 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); 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 ────────────────────────────────────── // ── Settings instance → controls ──────────────────────────────────────
private void ApplyToUi() private void ApplyToUi()
{ {
@@ -58,6 +137,9 @@ public partial class Settings : UserControl
_loading = true; _loading = true;
try try
{ {
PopulateProfileCombo();
PopulateExeCombo();
// General // General
ChangeLanguageCb.SelectedIndex = s.Language == "Polski" ? 0 : 1; ChangeLanguageCb.SelectedIndex = s.Language == "Polski" ? 0 : 1;
FullscreenCb.IsChecked = s.Fullscreen; FullscreenCb.IsChecked = s.Fullscreen;
@@ -70,11 +152,26 @@ public partial class Settings : UserControl
// Communication // Communication
GamepadIgnoreCb.IsChecked = s.IgnoreGamepad; GamepadIgnoreCb.IsChecked = s.IgnoreGamepad;
FeedbackCb.SelectedIndex = s.FeedbackMode; 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 // Other
SelectExeCb.SelectedIndex = s.SelectExeAutomatically ? 0 : 1; SelectExeCb.SelectedIndex = s.SelectExeAutomatically
? 0
: Math.Max(0, FindComboIndexByContent(SelectExeCb, s.ExecutablePath));
DebugModeCb.IsChecked = s.DebugMode; DebugModeCb.IsChecked = s.DebugMode;
VirtualShuntingCb.IsChecked = s.VirtualShunting; VirtualShuntingCb.IsChecked = s.VirtualShunting;
LogMissingVehicleFilesCb.IsChecked = s.LogMissingVehicleFiles;
// Graphics // Graphics
RenderEngineCb.SelectedIndex = s.RenderEngine; RenderEngineCb.SelectedIndex = s.RenderEngine;
@@ -106,6 +203,12 @@ public partial class Settings : UserControl
RenderScreensCb.IsChecked = s.PythonScreens; RenderScreensCb.IsChecked = s.PythonScreens;
RenderScreensThreadCb.IsChecked = s.PythonThreadedUpload; RenderScreensThreadCb.IsChecked = s.PythonThreadedUpload;
RenderScreensFramerateSlider.Value = s.ScreenRendererPriority; 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 // Physics
TrackCurvesSlider.Value = s.SplineFidelity; TrackCurvesSlider.Value = s.SplineFidelity;
@@ -117,6 +220,9 @@ public partial class Settings : UserControl
KeepLogsCb.IsChecked = s.MultipleLogs; KeepLogsCb.IsChecked = s.MultipleLogs;
DisplaySimulationCb.IsChecked = s.DisplaySimulation; DisplaySimulationCb.IsChecked = s.DisplaySimulation;
CrashDamageCb.IsChecked = s.CrashDamage; CrashDamageCb.IsChecked = s.CrashDamage;
FrictionSlider.Value = s.Friction;
BrakeStepSlider.Value = s.BrakeStep;
BrakeSpeedSlider.Value = s.BrakeSpeed;
// Sound // Sound
EnableSoundsCb.IsChecked = s.SoundEnabled; EnableSoundsCb.IsChecked = s.SoundEnabled;
@@ -127,10 +233,23 @@ public partial class Settings : UserControl
AmbientVolumeSlider.Value = s.AmbientVolume; AmbientVolumeSlider.Value = s.AmbientVolume;
PauseVolumeSlider.Value = s.PausedVolume; 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 // Starter
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter; AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
LargeThumbnailsCb.IsChecked = s.LargeThumbnails; LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree; AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree;
BatteryDefaultCb.SelectedIndex = (int)s.BatteryDefault;
} }
finally finally
{ {
@@ -155,6 +274,17 @@ public partial class Settings : UserControl
// Communication // Communication
s.IgnoreGamepad = IsChecked(GamepadIgnoreCb); s.IgnoreGamepad = IsChecked(GamepadIgnoreCb);
s.FeedbackMode = Math.Max(0, FeedbackCb.SelectedIndex); 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 // Other
s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0; s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0;
@@ -162,6 +292,7 @@ public partial class Settings : UserControl
: (SelectExeCb.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "eu07.exe"; : (SelectExeCb.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "eu07.exe";
s.DebugMode = IsChecked(DebugModeCb); s.DebugMode = IsChecked(DebugModeCb);
s.VirtualShunting = IsChecked(VirtualShuntingCb); s.VirtualShunting = IsChecked(VirtualShuntingCb);
s.LogMissingVehicleFiles = IsChecked(LogMissingVehicleFilesCb);
// Graphics // Graphics
s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex); s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex);
@@ -194,6 +325,11 @@ public partial class Settings : UserControl
s.PythonScreens = IsChecked(RenderScreensCb); s.PythonScreens = IsChecked(RenderScreensCb);
s.PythonThreadedUpload = IsChecked(RenderScreensThreadCb); s.PythonThreadedUpload = IsChecked(RenderScreensThreadCb);
s.ScreenRendererPriority = (int)RenderScreensFramerateSlider.Value; 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 // Physics
s.SplineFidelity = (int)TrackCurvesSlider.Value; s.SplineFidelity = (int)TrackCurvesSlider.Value;
@@ -205,6 +341,9 @@ public partial class Settings : UserControl
s.MultipleLogs = IsChecked(KeepLogsCb); s.MultipleLogs = IsChecked(KeepLogsCb);
s.DisplaySimulation = IsChecked(DisplaySimulationCb); s.DisplaySimulation = IsChecked(DisplaySimulationCb);
s.CrashDamage = IsChecked(CrashDamageCb); s.CrashDamage = IsChecked(CrashDamageCb);
s.Friction = FrictionSlider.Value;
s.BrakeStep = BrakeStepSlider.Value;
s.BrakeSpeed = BrakeSpeedSlider.Value;
// Sound // Sound
s.SoundEnabled = IsChecked(EnableSoundsCb); s.SoundEnabled = IsChecked(EnableSoundsCb);
@@ -215,10 +354,23 @@ public partial class Settings : UserControl
s.AmbientVolume = (int)AmbientVolumeSlider.Value; s.AmbientVolume = (int)AmbientVolumeSlider.Value;
s.PausedVolume = (int)PauseVolumeSlider.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 // Starter
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb); s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
s.LargeThumbnails = IsChecked(LargeThumbnailsCb); s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb); s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb);
s.BatteryDefault = (BatteryDefault)Math.Max(0, BatteryDefaultCb.SelectedIndex);
} }
private void SaveButton_OnClick(object? sender, RoutedEventArgs e) private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
@@ -312,6 +464,15 @@ public partial class Settings : UserControl
App.ApplyLanguage(lang); 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 ────────────────────────────────────────────────────── // ── tiny helpers ──────────────────────────────────────────────────────
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true; private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;

View File

@@ -222,11 +222,26 @@
<String key="FeedbackPoKeys">PoKeys55</String> <String key="FeedbackPoKeys">PoKeys55</String>
<String key="FeedbackSP">Serial port (COM)</String> <String key="FeedbackSP">Serial port (COM)</String>
<String key="FeedbackPort">LPT port address</String>
<String key="UartSection">UART configuration</String>
<String key="UartEnable">Enable UART communication</String>
<String key="UartPort">Serial port</String>
<String key="UartTune">Tuning parameter</String>
<String key="UartDebug">Print debug data to the console</String>
<String key="UartMain">Master controller</String>
<String key="UartScnd">Secondary/shunting controller</String>
<String key="UartTrain">Train brake</String>
<String key="UartLocal">Locomotive brake</String>
<String key="UartRadioVolume">Radio volume</String>
<String key="UartRadioChannel">Radio channel</String>
<String key="Others">Other</String> <String key="Others">Other</String>
<String key="SelectEXE">Select exe:</String> <String key="SelectEXE">Select exe:</String>
<String key="SelectEXEAuto">Select automatically</String> <String key="SelectEXEAuto">Select automatically</String>
<String key="DebugMode">Debug mode (test mode)</String> <String key="DebugMode">Debug mode (test mode)</String>
<String key="VirtualShunting">Virtual shunting</String> <String key="VirtualShunting">Virtual shunting</String>
<String key="LogMissingVehicleFiles">Log missing vehicle files during export</String>
<String key="Graphic">Graphics</String> <String key="Graphic">Graphics</String>
<String key="RenderEngine">Render engine</String> <String key="RenderEngine">Render engine</String>
@@ -245,6 +260,10 @@
<String key="Multisampling">Multisampling:</String> <String key="Multisampling">Multisampling:</String>
<String key="DynamicLights">Dynamic light sources:</String> <String key="DynamicLights">Dynamic light sources:</String>
<String key="RenderRange">Rendering range:</String> <String key="RenderRange">Rendering range:</String>
<String key="FpsLimitEnable">Limit frame rate</String>
<String key="ShadowAngleLimit">Shadow length limit</String>
<String key="FullscreenWindowed">Automatic resolution (borderless fullscreen)</String>
<String key="RenderAngleVulkan">Use ANGLE Vulkan renderer</String>
<String key="VSync">VSync</String> <String key="VSync">VSync</String>
<String key="SmokeDisplay">Smoke display</String> <String key="SmokeDisplay">Smoke display</String>
<String key="SmokeParticlesMultiplier">Smoke particles multiplier</String> <String key="SmokeParticlesMultiplier">Smoke particles multiplier</String>
@@ -290,6 +309,9 @@
<String key="KeepPrevLogs">Keep previous logs</String> <String key="KeepPrevLogs">Keep previous logs</String>
<String key="DisplaySimulation">Display simulation</String> <String key="DisplaySimulation">Display simulation</String>
<String key="CrashDamage">Collision damage</String> <String key="CrashDamage">Collision damage</String>
<String key="Friction">Friction multiplier</String>
<String key="BrakeStep">Brake valve sensitivity</String>
<String key="BrakeSpeed">FV4a valve sensitivity</String>
<String key="Sounds">Sound</String> <String key="Sounds">Sound</String>
<String key="EnableSounds">Enable sound</String> <String key="EnableSounds">Enable sound</String>
@@ -300,10 +322,30 @@
<String key="AmbientVolume">Ambient volume</String> <String key="AmbientVolume">Ambient volume</String>
<String key="VolumeDuringPause">Volume during pause</String> <String key="VolumeDuringPause">Volume during pause</String>
<String key="Advanced">Advanced</String>
<String key="CompressTextures">Compress TGA textures</String>
<String key="ScaleSpeculars">Scale specular highlights (old-model compatibility)</String>
<String key="UseGLES">Use OpenGL ES</String>
<String key="ShaderGamma">Gamma correction in shaders</String>
<String key="ScreenMipmaps">Generate mipmaps for in-cab (Python) screen textures</String>
<String key="ExtendedModelConversion">Extended E3D model conversion (experimental)</String>
<String key="GfxResourceMove">Conservative texture memory management</String>
<String key="GfxResourceSweep">Purge unused textures from GPU memory</String>
<String key="TrainPhysicsThreads">Vehicle physics threads (0 = main thread)</String>
<String key="IgnoreIrrelevantTrains">Skip decorative consists when starting</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Settings profile:</String>
<String key="ProfileApply">Apply</String>
<String key="ProfileSaveAs">Save as…</String>
<String key="ProfileNamePrompt">Profile name</String>
<String key="AutoCloseStarter">Automatically close starter</String> <String key="AutoCloseStarter">Automatically close starter</String>
<String key="LargeThumbnails">Large thumbnails</String> <String key="LargeThumbnails">Large thumbnails</String>
<String key="AutoExpandSceneryTree">Automatically expand scenery tree</String> <String key="AutoExpandSceneryTree">Automatically expand scenery tree</String>
<String key="BatteryDefault">Battery on launch:</String>
<String key="BatteryDefaultScenario">Scenario default</String>
<String key="BatteryDefaultOff">Always off</String>
<String key="BatteryDefaultOn">Always on</String>
<!-- Controls / key bindings --> <!-- Controls / key bindings -->
<String key="Controls">Controls</String> <String key="Controls">Controls</String>

View File

@@ -222,11 +222,26 @@
<String key="FeedbackPoKeys">PoKeys55</String> <String key="FeedbackPoKeys">PoKeys55</String>
<String key="FeedbackSP">Serialport (COM)</String> <String key="FeedbackSP">Serialport (COM)</String>
<String key="FeedbackPort">Adres portu LPT</String>
<String key="UartSection">Konfiguracja UART</String>
<String key="UartEnable">Włącz komunikację UART</String>
<String key="UartPort">Port szeregowy</String>
<String key="UartTune">Parametr strojenia</String>
<String key="UartDebug">Wypisuj dane debugowania w konsoli</String>
<String key="UartMain">Nastawnik główny</String>
<String key="UartScnd">Nastawnik manewrowy/pomocniczy</String>
<String key="UartTrain">Hamulec zespolony</String>
<String key="UartLocal">Hamulec lokomotywy</String>
<String key="UartRadioVolume">Głośność radiotelefonu</String>
<String key="UartRadioChannel">Kanał radiotelefonu</String>
<String key="Others">Inne</String> <String key="Others">Inne</String>
<String key="SelectEXE">Wybór exe:</String> <String key="SelectEXE">Wybór exe:</String>
<String key="SelectEXEAuto">Wybierz automatycznie</String> <String key="SelectEXEAuto">Wybierz automatycznie</String>
<String key="DebugMode">Tryb testowy (debugmode)</String> <String key="DebugMode">Tryb testowy (debugmode)</String>
<String key="VirtualShunting">Wirtualny manewrowy</String> <String key="VirtualShunting">Wirtualny manewrowy</String>
<String key="LogMissingVehicleFiles">Zapisuj w logu brakujące pliki pojazdów podczas eksportu</String>
<String key="Graphic">Grafika</String> <String key="Graphic">Grafika</String>
<String key="RenderEngine">Silnik renderowania</String> <String key="RenderEngine">Silnik renderowania</String>
@@ -245,6 +260,10 @@
<String key="Multisampling">Multisampling:</String> <String key="Multisampling">Multisampling:</String>
<String key="DynamicLights">Dynamiczne źródła światła:</String> <String key="DynamicLights">Dynamiczne źródła światła:</String>
<String key="RenderRange">Zasięg renderowania:</String> <String key="RenderRange">Zasięg renderowania:</String>
<String key="FpsLimitEnable">Ogranicz liczbę klatek na sekundę</String>
<String key="ShadowAngleLimit">Limit długości cienia</String>
<String key="FullscreenWindowed">Automatyczna rozdzielczość (pełny ekran bez obramowania)</String>
<String key="RenderAngleVulkan">Użyj renderera ANGLE Vulkan</String>
<String key="VSync">VSync</String> <String key="VSync">VSync</String>
<String key="SmokeDisplay">Wyświetlanie dymu</String> <String key="SmokeDisplay">Wyświetlanie dymu</String>
<String key="SmokeParticlesMultiplier">Mnożnik ilości cząsteczek dymu</String> <String key="SmokeParticlesMultiplier">Mnożnik ilości cząsteczek dymu</String>
@@ -290,6 +309,9 @@
<String key="KeepPrevLogs">Zachowuj poprzednie logi</String> <String key="KeepPrevLogs">Zachowuj poprzednie logi</String>
<String key="DisplaySimulation">Wyświetlaj symulację</String> <String key="DisplaySimulation">Wyświetlaj symulację</String>
<String key="CrashDamage">Uszkodzenia przy zderzeniu</String> <String key="CrashDamage">Uszkodzenia przy zderzeniu</String>
<String key="Friction">Mnożnik tarcia</String>
<String key="BrakeStep">Czułość zaworu hamulcowego</String>
<String key="BrakeSpeed">Czułość zaworu FV4a</String>
<String key="Sounds">Dżwięk</String> <String key="Sounds">Dżwięk</String>
<String key="EnableSounds">Włącz dźwięk</String> <String key="EnableSounds">Włącz dźwięk</String>
@@ -300,10 +322,30 @@
<String key="AmbientVolume">Głośność otoczenia</String> <String key="AmbientVolume">Głośność otoczenia</String>
<String key="VolumeDuringPause">Głośność podczas pauzy</String> <String key="VolumeDuringPause">Głośność podczas pauzy</String>
<String key="Advanced">Zaawansowane</String>
<String key="CompressTextures">Kompresuj tekstury TGA</String>
<String key="ScaleSpeculars">Skaluj odbicia specularne (kompatybilność ze starymi modelami)</String>
<String key="UseGLES">Używaj OpenGL ES</String>
<String key="ShaderGamma">Korekcja gamma w shaderach</String>
<String key="ScreenMipmaps">Generuj mipmapy dla tekstur ekranów kabinowych (Python)</String>
<String key="ExtendedModelConversion">Rozszerzona konwersja modeli E3D (eksperymentalne)</String>
<String key="GfxResourceMove">Ostrożne zarządzanie pamięcią tekstur</String>
<String key="GfxResourceSweep">Usuwaj nieużywane tekstury z pamięci GPU</String>
<String key="TrainPhysicsThreads">Wątki fizyki pojazdów (0 = wątek główny)</String>
<String key="IgnoreIrrelevantTrains">Pomijaj składy dekoracyjne przy starcie</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Profil ustawień:</String>
<String key="ProfileApply">Zastosuj</String>
<String key="ProfileSaveAs">Zapisz jako…</String>
<String key="ProfileNamePrompt">Nazwa profilu</String>
<String key="AutoCloseStarter">Zamknij starter automatycznie</String> <String key="AutoCloseStarter">Zamknij starter automatycznie</String>
<String key="LargeThumbnails">Duże miniaturki</String> <String key="LargeThumbnails">Duże miniaturki</String>
<String key="AutoExpandSceneryTree">Automatycznie rozwijanie drzewka scenerii</String> <String key="AutoExpandSceneryTree">Automatycznie rozwijanie drzewka scenerii</String>
<String key="BatteryDefault">Akumulator przy starcie:</String>
<String key="BatteryDefaultScenario">Domyślnie ze scenerii</String>
<String key="BatteryDefaultOff">Zawsze wyłączony</String>
<String key="BatteryDefaultOn">Zawsze włączony</String>
<!-- Sterowanie / przypisania klawiszy --> <!-- Sterowanie / przypisania klawiszy -->
<String key="Controls">Sterowanie</String> <String key="Controls">Sterowanie</String>

2
build.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
dotnet build StarterNG/StarterNG.csproj