Split Settings into model, serializer, paths and store

698 lines did all of it at once. The mutable CaptureFromUi callback becomes an
ISettingsCapture the settings view implements.
This commit is contained in:
maj00r
2026-09-04 21:13:06 +02:00
parent 8e5fe01104
commit 3c502f630d
30 changed files with 1371 additions and 779 deletions

View File

@@ -0,0 +1,82 @@
using StarterNG.Domain.Settings;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class ExecutableLocatorTests
{
private const string Elf = "\u007fELF binary";
private const string PortableExecutable = "MZ binary";
[Fact]
public void The_canonical_binary_is_preferred_over_other_candidates()
{
var files = new InMemoryFileSystem()
.WithExecutable(TestInstallation.At("eu07-dev"), Elf)
.WithExecutable(TestInstallation.At("eu07"), Elf);
var installation = new TestInstallation(files);
string resolved = installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.None, problem);
Assert.EndsWith("eu07", resolved);
}
[Fact]
public void A_binary_for_the_other_platform_is_reported_as_such()
{
var files = new InMemoryFileSystem().WithExecutable(TestInstallation.At("eu07"), PortableExecutable);
var installation = new TestInstallation(files);
installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.WrongPlatform, problem);
}
[Fact]
public void A_binary_without_the_execute_bit_is_reported_as_such()
{
var files = new InMemoryFileSystem().WithFile(TestInstallation.At("eu07"), Elf);
var installation = new TestInstallation(files);
installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.NotExecutable, problem);
}
[Fact]
public void An_empty_installation_reports_the_canonical_name_as_missing()
{
var installation = new TestInstallation();
string resolved = installation.Executables.Resolve(new SimulatorSettings(), out var problem);
Assert.Equal(ExeProblem.NotFound, problem);
Assert.Equal("eu07", resolved);
}
[Fact]
public void A_manually_chosen_path_is_used_as_given()
{
var files = new InMemoryFileSystem().WithExecutable(TestInstallation.At("eu07-custom"), Elf);
var installation = new TestInstallation(files);
var settings = new SimulatorSettings { SelectExeAutomatically = false, ExecutablePath = "eu07-custom" };
string resolved = installation.Executables.Resolve(settings, out var problem);
Assert.Equal("eu07-custom", resolved);
Assert.Equal(ExeProblem.None, problem);
}
[Fact]
public void Data_files_beside_the_binary_are_never_offered_as_candidates()
{
var files = new InMemoryFileSystem()
.WithFile(TestInstallation.At("eu07.ini"), "width 1280")
.WithFile(TestInstallation.At("eu07.log"), "log")
.WithExecutable(TestInstallation.At("eu07"), Elf);
var installation = new TestInstallation(files);
Assert.Equal(new[] { "eu07" }, installation.Executables.ListCandidates());
}
}

View File

@@ -0,0 +1,34 @@
using StarterNG.Application.Abstractions;
namespace StarterNG.Tests.Fakes;
/// <summary>A host machine described inline: platform, home folder, env vars.</summary>
public sealed class FakeEnvironment : IEnvironment
{
private readonly Dictionary<string, string> _variables = new(StringComparer.Ordinal);
public bool IsWindows { get; set; }
public bool IsLinux { get; set; } = true;
public bool IsMacOS { get; set; }
public string BaseDirectory { get; set; } = Path.Combine("game");
public string? GetVariable(string name) => _variables.TryGetValue(name, out var value) ? value : null;
public FakeEnvironment With(string name, string value)
{
_variables[name] = value;
return this;
}
/// <summary>Switches the fake to Windows, with an APPDATA folder.</summary>
public FakeEnvironment AsWindows(string appData = "C:/Users/kolejarz/AppData/Roaming")
{
IsWindows = true;
IsLinux = false;
IsMacOS = false;
return With("APPDATA", appData);
}
}

View File

@@ -15,6 +15,7 @@ public sealed class InMemoryFileSystem : IFileSystem
{
private readonly Dictionary<string, byte[]> _files = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _directories = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _executable = new(StringComparer.OrdinalIgnoreCase);
public DateTime LastWriteTimeUtc { get; set; } = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@@ -115,6 +116,16 @@ public sealed class InMemoryFileSystem : IFileSystem
public DateTime GetLastWriteTimeUtc(string path) => LastWriteTimeUtc;
/// <summary>Files added through <see cref="WithExecutable"/> carry the execute bit.</summary>
public bool IsExecutable(string path) => _executable.Contains(Normalize(path));
public InMemoryFileSystem WithExecutable(string path, string contents = "\u007fELF")
{
WithFile(path, contents);
_executable.Add(Normalize(path));
return this;
}
private byte[] Bytes(string path) =>
_files.TryGetValue(Normalize(path), out var bytes)
? bytes

View File

@@ -1,6 +1,7 @@
using StarterNG.Application;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Infrastructure.Settings;
using StarterNG.Infrastructure.Vehicles;
namespace StarterNG.Tests.Fakes;
@@ -28,6 +29,11 @@ public sealed class TestInstallation
Sceneries = new SceneryRepository(Files, Paths, Log, new SceneryParser(Clock, Random));
MissingAssets = new MissingAssetScanner(Files, Paths);
Library = new GameLibrary(Vehicles, Sceneries, MiniTextures, Physics, Log);
Environment = new FakeEnvironment();
SettingsPaths = new SettingsPaths(Environment, Paths);
Executables = new ExecutableLocator(Files, Paths, Environment, Log);
SettingsStore = new SettingsStore(Files, Clock, Log, SettingsPaths, new SettingsSerializer(), Executables);
}
public InMemoryFileSystem Files { get; }
@@ -42,6 +48,10 @@ public sealed class TestInstallation
public SceneryRepository Sceneries { get; }
public MissingAssetScanner MissingAssets { get; }
public GameLibrary Library { get; }
public FakeEnvironment Environment { get; }
public SettingsPaths SettingsPaths { get; }
public ExecutableLocator Executables { get; }
public SettingsStore SettingsStore { get; }
/// <summary>Path of a file inside the fake installation.</summary>
public static string At(params string[] segments) => Path.Combine(new[] { Root }.Concat(segments).ToArray());

View File

@@ -12,7 +12,7 @@ public class InstallationCheckTests
var installation = new TestInstallation();
var strings = installation.Strings.With("FaultNoDir", "missing {0}");
var check = new InstallationCheck(installation.Paths, installation.Files, strings,
installation.Physics, installation.Library);
installation.Physics, installation.Library, installation.SettingsStore);
var faults = check.Run();
@@ -35,7 +35,7 @@ public class InstallationCheckTests
var strings = installation.Strings.With("FaultNoDir", "missing {0}");
var faults = new InstallationCheck(installation.Paths, files, strings,
installation.Physics, installation.Library).Run();
installation.Physics, installation.Library, installation.SettingsStore).Run();
Assert.DoesNotContain(faults, fault => fault.StartsWith("missing "));
Assert.DoesNotContain("FaultNoWeights", faults);

View File

@@ -0,0 +1,131 @@
using StarterNG.Domain.Settings;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SettingsStoreTests
{
private const string UserIni = "/home/kolejarz/.config/MaSzyna/eu07.ini";
private static TestInstallation WithHome(InMemoryFileSystem? files = null)
{
var installation = new TestInstallation(files);
installation.Environment.With("HOME", "/home/kolejarz");
return installation;
}
[Fact]
public void Settings_are_read_from_the_users_ini()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "width 1920\nheight 1080\nfullscreen yes\nlang pl\n"));
installation.SettingsStore.Load();
var settings = installation.SettingsStore.Settings;
Assert.Equal(1920, settings.Width);
Assert.Equal(1080, settings.Height);
Assert.True(settings.Fullscreen);
Assert.Equal("Polski", settings.Language);
Assert.True(settings.LanguageWasSet);
Assert.Equal(UserIni, installation.SettingsStore.LoadedFrom);
}
[Fact]
public void A_first_run_falls_back_to_the_ini_shipped_with_the_installation()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(TestInstallation.At("eu07.ini"), "width 800\n"));
installation.SettingsStore.Load();
Assert.Equal(800, installation.SettingsStore.Settings.Width);
Assert.Equal(TestInstallation.At("eu07.ini"), installation.SettingsStore.LoadedFrom);
// Saving still goes to the user's own file, never back into the installation.
Assert.Equal(UserIni, installation.SettingsStore.SavePath);
}
[Fact]
public void With_no_ini_at_all_the_defaults_stand()
{
var installation = WithHome();
installation.SettingsStore.Load();
Assert.Equal(1280, installation.SettingsStore.Settings.Width);
Assert.False(installation.SettingsStore.Settings.LanguageWasSet);
}
[Fact]
public void A_save_round_trips_the_settings()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Settings.Width = 2560;
installation.SettingsStore.Settings.FieldOfView = 60;
installation.SettingsStore.Save();
var reopened = WithHome(installation.Files);
reopened.SettingsStore.Load();
Assert.Equal(2560, reopened.SettingsStore.Settings.Width);
Assert.Equal(60, reopened.SettingsStore.Settings.FieldOfView);
}
[Fact]
public void Keys_the_starter_does_not_manage_survive_a_round_trip()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "width 1280\nsomethingexotic 42\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("somethingexotic 42", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_registered_screen_is_asked_for_its_edits_before_a_save()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.RegisterCapture(new WidthCapture(3840));
installation.SettingsStore.CaptureAndSave();
Assert.Equal(3840, installation.SettingsStore.Settings.Width);
}
[Fact]
public void An_ini_written_by_someone_else_is_noticed()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "width 1280\n"));
installation.SettingsStore.Load();
Assert.False(installation.SettingsStore.IsConfigNewerOnDisk());
installation.Files.LastWriteTimeUtc = installation.Files.LastWriteTimeUtc.AddMinutes(5);
Assert.True(installation.SettingsStore.IsConfigNewerOnDisk());
}
[Fact]
public void On_windows_the_ini_lives_under_appdata()
{
var installation = new TestInstallation();
installation.Environment.AsWindows();
Assert.Equal(Path.Combine("C:/Users/kolejarz/AppData/Roaming", "MaSzyna", "eu07.ini"),
installation.SettingsPaths.UserConfigPath());
}
/// <summary>Stands in for the settings screen holding an unsaved edit.</summary>
private sealed class WidthCapture : StarterNG.Application.ISettingsCapture
{
private readonly int _width;
public WidthCapture(int width) => _width = width;
public void CaptureInto(SimulatorSettings settings) => settings.Width = _width;
}
}

View File

@@ -29,20 +29,20 @@ public partial class App : Avalonia.Application
{
AvaloniaXamlLoader.Load(this);
Settings.Instance.Load();
AppServices.Current.SettingsStore.Load();
KeyboardConfig.Instance.Load();
CultureInfo ci = CultureInfo.InstalledUICulture ;
var langName = Settings.Instance.LanguageWasSet
? Settings.Instance.Language
var langName = AppServices.Current.Settings.LanguageWasSet
? AppServices.Current.Settings.Language
: ci.Name switch
{
"pl-PL" => "Polski",
_ => "English"
};
Settings.Instance.Language = langName;
AppServices.Current.Settings.Language = langName;
ApplyLanguage(langName);
}
@@ -51,7 +51,7 @@ public partial class App : Avalonia.Application
{
var resolvedName = Loc.Load(langNameOrCode);
Settings.Instance.Language = resolvedName;
AppServices.Current.Settings.Language = resolvedName;
ApplyCulture(Loc.CurrentLangCode);
}
@@ -87,10 +87,10 @@ public partial class App : Avalonia.Application
desktop.ShutdownRequested += (_, _) =>
{
try { Settings.Instance.CaptureAndSave(); }
try { AppServices.Current.SettingsStore.CaptureAndSave(); }
catch (Exception ex) { Diagnostics.Log("Settings save", ex); }
Settings.Instance.DumpMissingVehicleLog();
AppServices.Current.MissingVehicleLog.Dump();
try
{

View File

@@ -0,0 +1,20 @@
namespace StarterNG.Application.Abstractions;
/// <summary>
/// The bits of the host machine the starter has to ask about: which operating
/// system it is on, and where that system keeps per-user configuration.
/// </summary>
public interface IEnvironment
{
bool IsWindows { get; }
bool IsLinux { get; }
bool IsMacOS { get; }
/// <summary>An environment variable, or null when it is not set.</summary>
string? GetVariable(string name);
/// <summary>The folder the starter's own executable lives in.</summary>
string BaseDirectory { get; }
}

View File

@@ -58,4 +58,10 @@ public interface IFileSystem
IReadOnlyList<string> GetFilesRecursive(string path, string searchPattern);
DateTime GetLastWriteTimeUtc(string path);
/// <summary>
/// Whether the file carries an execute bit. Always true on Windows, where
/// executability is a property of the file's contents rather than its mode.
/// </summary>
bool IsExecutable(string path);
}

View File

@@ -1,7 +1,9 @@
using System;
using StarterNG.Application.Abstractions;
using StarterNG.Domain.Settings;
using StarterNG.Infrastructure.Adapters;
using StarterNG.Infrastructure.Sceneries;
using StarterNG.Infrastructure.Settings;
using StarterNG.Infrastructure.Vehicles;
using StarterNG.Services;
@@ -27,11 +29,12 @@ public sealed class AppServices
{
private static AppServices? _current;
private AppServices(IGamePaths paths, IFileSystem files, IClock clock)
private AppServices(IGamePaths paths, IFileSystem files, IClock clock, IEnvironment environment)
{
Paths = paths;
Files = files;
Clock = clock;
Environment = environment;
var log = new FileDiagnosticsLog(files, clock, paths);
Log = log;
@@ -48,6 +51,12 @@ public sealed class AppServices
LoadWeights = new LoadWeightsRepository(files, paths, log);
Library = new GameLibrary(Vehicles, Sceneries, MiniTextures, Physics, log);
State = new AppState();
var settingsPaths = new SettingsPaths(environment, paths);
Executables = new ExecutableLocator(files, paths, environment, log);
SettingsStore = new SettingsStore(files, clock, log, settingsPaths, new SettingsSerializer(), Executables);
SettingsPaths = settingsPaths;
MissingVehicleLog = new MissingVehicleLog(SettingsStore.Settings, MissingAssets, Library, log);
}
/// <summary>
@@ -60,12 +69,14 @@ public sealed class AppServices
"AppServices.Current read before Initialize(); the composition root must be built first.");
/// <summary>Builds the production graph. Called once, from the entry point.</summary>
public static AppServices Initialize(IGamePaths? paths = null, IFileSystem? files = null, IClock? clock = null)
public static AppServices Initialize(IGamePaths? paths = null, IFileSystem? files = null, IClock? clock = null,
IEnvironment? environment = null)
{
_current = new AppServices(
paths ?? GamePaths.ForCurrentDirectory(),
files ?? new PhysicalFileSystem(),
clock ?? new SystemClock());
clock ?? new SystemClock(),
environment ?? new SystemEnvironment());
return _current;
}
@@ -75,6 +86,8 @@ public sealed class AppServices
public IClock Clock { get; }
public IEnvironment Environment { get; }
public IDiagnosticsLog Log { get; }
public IProcessLauncher Processes { get; }
@@ -103,6 +116,18 @@ public sealed class AppServices
/// <summary>What the user currently has selected.</summary>
public AppState State { get; }
/// <summary>The settings the starter runs with, and their load/save lifecycle.</summary>
public SettingsStore SettingsStore { get; }
/// <summary>Shorthand for the settings themselves.</summary>
public SimulatorSettings Settings => SettingsStore.Settings;
public ExecutableLocator Executables { get; }
public SettingsPaths SettingsPaths { get; }
public MissingVehicleLog MissingVehicleLog { get; }
/// <summary>
/// Exposed as the concrete service rather than <see cref="ILocalizedStrings"/>
/// because the XAML binds to it as a change-notifying source; code should take

View File

@@ -0,0 +1,49 @@
using System;
using StarterNG.Application.Abstractions;
using StarterNG.Domain.Settings;
using StarterNG.Infrastructure.Vehicles;
namespace StarterNG.Application;
/// <summary>
/// Writes the "vehicle files that are not there" report into the diagnostics
/// log, when the user has asked for it.
/// </summary>
/// <remarks>
/// Was a method on the settings object, which had no business scanning the
/// installation. The setting decides whether it runs; the scan itself belongs to
/// the vehicle side.
/// </remarks>
public sealed class MissingVehicleLog
{
private readonly SimulatorSettings _settings;
private readonly MissingAssetScanner _scanner;
private readonly GameLibrary _library;
private readonly IDiagnosticsLog _log;
public MissingVehicleLog(SimulatorSettings settings, MissingAssetScanner scanner, GameLibrary library,
IDiagnosticsLog log)
{
_settings = settings;
_scanner = scanner;
_library = library;
_log = log;
}
public void Dump()
{
if (!_settings.LogMissingVehicleFiles)
return;
try
{
var lines = _scanner.Scan(_library.Vehicles);
if (lines.Count > 0)
_log.Log(lines);
}
catch (Exception ex)
{
_log.Log("Missing vehicle log", ex);
}
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.IO;
using System.Text;
using StarterNG.Application.Abstractions;
using StarterNG.Classes;
using StarterNG.Domain.Settings;
using StarterNG.Infrastructure.Settings;
namespace StarterNG.Application;
/// <summary>
/// A screen that holds settings the user is editing and can be asked to write
/// them back into the model before a save.
/// </summary>
/// <remarks>
/// Replaces the mutable <c>Action? CaptureFromUi</c> the settings model used to
/// expose: the dependency now points the right way, from the store to an
/// interface the view implements.
/// </remarks>
public interface ISettingsCapture
{
void CaptureInto(SimulatorSettings settings);
}
/// <summary>
/// Owns the settings the starter runs with: where they were read from, when to
/// write them back, and which executable they point at.
/// </summary>
public sealed class SettingsStore
{
private readonly IFileSystem _files;
private readonly IClock _clock;
private readonly IDiagnosticsLog _log;
private readonly SettingsPaths _paths;
private readonly SettingsSerializer _serializer;
private readonly ExecutableLocator _executables;
private readonly Encoding _encoding;
private ConfigFile _config = new();
private string _savePath = string.Empty;
private DateTime _configAgeUtc = DateTime.MinValue;
private ISettingsCapture? _capture;
public SettingsStore(IFileSystem files, IClock clock, IDiagnosticsLog log, SettingsPaths paths,
SettingsSerializer serializer, ExecutableLocator executables)
{
_files = files;
_clock = clock;
_log = log;
_paths = paths;
_serializer = serializer;
_executables = executables;
_encoding = LegacyEncoding();
}
/// <summary>The settings the application is running with.</summary>
public SimulatorSettings Settings { get; } = new();
/// <summary>The file the current settings were read from.</summary>
public string LoadedFrom { get; private set; } = string.Empty;
/// <summary>The file a save will write to.</summary>
public string SavePath => _savePath;
/// <summary>
/// Registers the screen that holds pending edits, so a save can collect them
/// first. Called by the settings view when it is created.
/// </summary>
public void RegisterCapture(ISettingsCapture capture) => _capture = capture;
/// <summary>
/// Reads the user's settings, falling back to the copy shipped with the
/// installation on a first run.
/// </summary>
public void Load()
{
_savePath = _paths.UserConfigPath();
string source = _files.FileExists(_savePath) ? _savePath : _paths.InstallationConfigPath();
LoadedFrom = source;
LoadFrom(source);
}
public void LoadFrom(string path)
{
try
{
_config = _files.FileExists(path)
? ConfigFile.Parse(_files.ReadAllText(path, _encoding))
: new ConfigFile();
}
catch (Exception ex)
{
// An unreadable ini means defaults, not a failed start.
_log.Log($"reading {path}", ex);
_config = new ConfigFile();
}
_serializer.Read(Settings, _config);
TouchConfigAge();
}
/// <summary>Collects pending edits from the settings screen, then saves.</summary>
public void CaptureAndSave()
{
_capture?.CaptureInto(Settings);
Save();
}
public void Save()
{
if (string.IsNullOrEmpty(_savePath))
_savePath = _paths.UserConfigPath();
SaveTo(_savePath);
}
public void SaveTo(string path)
{
_serializer.Write(Settings, _config);
try
{
string? directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
_files.CreateDirectory(directory);
_files.WriteAllText(path, _config.ToText(), _encoding);
TouchConfigAge();
}
catch (Exception ex)
{
_log.Log($"saving {path}", ex);
}
}
/// <summary>Records the ini's timestamp, so an outside edit can be noticed.</summary>
public void TouchConfigAge()
{
string path = string.IsNullOrEmpty(_savePath) ? _paths.UserConfigPath() : _savePath;
try
{
_configAgeUtc = _files.FileExists(path) ? _files.GetLastWriteTimeUtc(path) : _clock.UtcNow;
}
catch (Exception)
{
_configAgeUtc = _clock.UtcNow;
}
}
/// <summary>
/// True when something else has written the ini since we last touched it -
/// the simulator itself, usually, which the starter must not clobber.
/// </summary>
public bool IsConfigNewerOnDisk()
{
string path = string.IsNullOrEmpty(_savePath) ? _paths.UserConfigPath() : _savePath;
try
{
if (!_files.FileExists(path) || _configAgeUtc == DateTime.MinValue)
return false;
// A second of slack: writing our own file must not count as an edit.
return _files.GetLastWriteTimeUtc(path) > _configAgeUtc + TimeSpan.FromSeconds(1);
}
catch (Exception)
{
return false;
}
}
public string ResolveExecutable() => _executables.Resolve(Settings, out _);
public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem);
/// <summary>
/// The .ini keeps its own code page. UTF-8 is a safe fallback if the code
/// page provider is unavailable.
/// </summary>
private static Encoding LegacyEncoding()
{
try
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(1250);
}
catch (Exception)
{
return Encoding.UTF8;
}
}
}

View File

@@ -1,699 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using StarterNG.Application;
namespace StarterNG.Classes;
public enum BatteryDefault { Default = 0, AlwaysOff = 1, AlwaysOn = 2 }
public enum ExeProblem { None, NotFound, NotExecutable, WrongPlatform }
public sealed class Settings
{
public static Settings Instance { get; } = new();
public Action? CaptureFromUi { get; set; }
public bool LanguageWasSet { get; private set; }
public string Language = "English";
public bool Fullscreen;
public bool PauseWhenInactive = true;
public bool PauseOnStart;
public int CursorSensitivity = 2;
public bool InvertMouseHorizontal;
public bool InvertMouseVertical;
public bool IgnoreGamepad;
public int FeedbackMode = 1;
public int FeedbackPort = 888;
public bool UartEnabled;
public string UartPort = "";
public string UartTune = "";
public bool UartDebug;
public bool UartMain, UartScnd, UartTrain, UartLocal, UartRadioVolume, UartRadioChannel;
public bool SelectExeAutomatically = true;
public string ExecutablePath = "eu07.exe";
public bool DebugMode;
public bool VirtualShunting = true;
public bool LogMissingVehicleFiles;
public int RenderEngine;
public int Width = 1280;
public int Height = 720;
public int BufferScalePercent = 100;
public int MaxTextureSize = 4096;
public int MaxCabTextureSize = 4096;
public int TextureFiltering = 8;
public int Multisampling = 2;
public int DynamicLights = 7;
public double DrawRangeFactor = 3.0;
public bool VSync;
public bool Smoke = true;
public int SmokeFidelity = 1;
public bool ChromaticAberration = true;
public bool MotionBlur = true;
public bool ExtraEffects = true;
public bool EnvMap = true;
public bool UseVbo = true;
public bool RenderShadows = true;
public int ReflectionsFramerate = 60;
public int ShadowMapResolution = 2048;
public int ShadowProjectionRange = 250;
public int CabShadowsRange;
public int ShadowRankCutoff = 3;
public int ReflectionsFidelity;
public int FieldOfView = 45;
public bool PythonScreens = true;
public bool PythonThreadedUpload = true;
public bool FpsLimitEnabled;
public int FpsLimit = 30;
public double ShadowAngleLimit = 0.2;
public bool FullscreenWindowed;
public bool RenderAngleVulkan;
public int SplineFidelity = 1;
public bool FullPhysics = true;
public bool EnableTraction = true;
public bool LiveTraction = true;
public bool PhysicsLog;
public bool DebugLog = true;
public bool MultipleLogs;
public bool DisplaySimulation = true;
public bool CrashDamage = true;
public double Friction = 1.0;
public double BrakeStep = 1.0;
public double BrakeSpeed = 3.0;
public bool SoundEnabled = true;
public int Volume = 50;
public int RadioVolume = 75;
public int VehiclesVolume = 100;
public int PositionalVolume = 100;
public int AmbientVolume = 100;
public bool SkipPipeline;
public int PythonScreenUpdateRate = 200;
public bool DebugLogVisible;
private int _debugLogExtraBits;
public bool CompressTextures = true;
public bool ScaleSpeculars = true;
public bool UseGLES;
public bool ShaderGamma;
public bool ScreenMipmaps = true;
public bool ExtendedModelConversion;
public bool GfxResourceMove;
public bool GfxResourceSweep = true;
public int TrainPhysicsThreads;
public bool IgnoreIrrelevantTrains;
public bool AutoCloseStarter;
public bool LargeThumbnails;
public bool AutoExpandSceneryTree;
public BatteryDefault BatteryDefault = BatteryDefault.Default;
public bool ShowAiVehicles = true;
public bool DrivableOnly = true;
public bool ShowArchivalSceneries = true;
public bool HideArchivalVehicles = true;
public string LastScenery = "";
/// <summary>
/// Starter's own window state, the way the old one kept WindowMaximized in
/// starter.ini. Only the maximised flag is remembered - a normal window
/// always opens at its design size, centred on the screen.
/// </summary>
public bool WindowMaximized;
public static readonly string[] RenderEngines =
{ "full", "legacy", "simpleshader", "simple", "off", "experimental" };
public static readonly int[] AnisotropySteps = { 1, 2, 4, 8, 16 };
private double _mouseScaleYMagnitude = 0.2;
private string[] _shadowTuneExtra = { "400", "300" };
private ConfigFile _config = new();
private string _savePath = string.Empty;
private DateTime _configAgeUtc = DateTime.MinValue;
public string LoadedFrom { get; private set; } = string.Empty;
public string SavePath => _savePath;
public static string UserConfigPath()
{
if (OperatingSystem.IsWindows())
{
string? appData = Environment.GetEnvironmentVariable("APPDATA");
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");
if (!string.IsNullOrEmpty(home))
return Path.Combine(home, ".config", "MaSzyna", "eu07.ini");
}
return Path.Combine(AppContext.BaseDirectory, "eu07.ini");
}
private static string DefaultConfigPath() =>
Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini");
public static string UserConfigDirectory() =>
Path.GetDirectoryName(UserConfigPath()) ?? AppContext.BaseDirectory;
public string ResolveExecutable() => ResolveExecutable(out _);
public string ResolveExecutable(out ExeProblem problem)
{
if (!SelectExeAutomatically && !string.IsNullOrWhiteSpace(ExecutablePath))
{
problem = ValidateExecutable(FullPath(ExecutablePath));
return ExecutablePath;
}
string canonical = OperatingSystem.IsWindows() ? "eu07.exe" : "eu07";
string? fallback = null;
try
{
var candidates = Directory.GetFiles(Directory.GetCurrentDirectory(), "eu07*")
.Where(IsExecutableCandidate)
.OrderBy(p => string.Equals(Path.GetFileName(p), canonical, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(p => Path.GetFileName(p)!.Length);
foreach (string path in candidates)
{
if (ValidateExecutable(path) == ExeProblem.None)
{
problem = ExeProblem.None;
return path;
}
fallback ??= path;
}
}
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.Log("looking for the simulator executable", ex);
}
if (fallback != null)
{
problem = ValidateExecutable(fallback);
return fallback;
}
problem = ExeProblem.NotFound;
return canonical;
}
private static string FullPath(string path) =>
Path.IsPathRooted(path) ? path : Path.Combine(Directory.GetCurrentDirectory(), path);
public static ExeProblem ValidateExecutable(string path)
{
try
{
if (!File.Exists(path))
return ExeProblem.NotFound;
if (!OperatingSystem.IsWindows())
{
const UnixFileMode anyExecute =
UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
if ((File.GetUnixFileMode(path) & anyExecute) == 0)
return ExeProblem.NotExecutable;
}
byte[] head = new byte[4];
using (var fs = File.OpenRead(path))
{
if (fs.Read(head, 0, 4) == 4)
{
bool pe = head[0] == 0x4D && head[1] == 0x5A;
bool elf = head[0] == 0x7F && head[1] == 0x45 && head[2] == 0x4C && head[3] == 0x46;
if (OperatingSystem.IsWindows() && elf)
return ExeProblem.WrongPlatform;
if (OperatingSystem.IsLinux() && pe)
return ExeProblem.WrongPlatform;
}
}
return ExeProblem.None;
}
catch
{
return ExeProblem.NotFound;
}
}
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>(); }
}
private static bool IsExecutableCandidate(string path)
{
string ext = Path.GetExtension(path).ToLowerInvariant();
if (ext is ".ini" or ".log" or ".txt" or ".cfg" or ".config" or ".json"
or ".dll" or ".so" or ".dat" or ".bak" or ".csv" or ".xml")
return false;
if (OperatingSystem.IsWindows())
return ext == ".exe";
if (OperatingSystem.IsLinux())
return ext.Length == 0 || ext is ".x86_64" or ".run" or ".appimage";
return ext.Length == 0 || ext is ".exe" or ".x86_64" or ".run" or ".appimage";
}
private static readonly Encoding FileEncoding = ResolveEncoding();
private static Encoding ResolveEncoding()
{
try
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(1250);
}
catch
{
return Encoding.UTF8;
}
}
public void Load()
{
_savePath = UserConfigPath();
string source = File.Exists(_savePath) ? _savePath : DefaultConfigPath();
LoadedFrom = source;
LoadFrom(source);
}
public void CaptureAndSave()
{
CaptureFromUi?.Invoke();
Save();
}
public void LoadFrom(string path)
{
try
{
_config = File.Exists(path)
? ConfigFile.Parse(File.ReadAllText(path, FileEncoding))
: new ConfigFile();
}
catch
{
_config = new ConfigFile();
}
ReadFromConfig();
TouchConfigAge();
}
public void Save()
{
if (string.IsNullOrEmpty(_savePath))
_savePath = UserConfigPath();
SaveTo(_savePath);
}
public void SaveTo(string path)
{
WriteToConfig();
try
{
string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
File.WriteAllText(path, _config.ToText(), FileEncoding);
TouchConfigAge();
}
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.Log($"saving {path}", ex);
}
}
public void TouchConfigAge()
{
string path = string.IsNullOrEmpty(_savePath) ? UserConfigPath() : _savePath;
try
{
_configAgeUtc = File.Exists(path)
? File.GetLastWriteTimeUtc(path)
: DateTime.UtcNow;
}
catch
{
_configAgeUtc = DateTime.UtcNow;
}
}
public bool IsConfigNewerOnDisk()
{
string path = string.IsNullOrEmpty(_savePath) ? UserConfigPath() : _savePath;
try
{
if (!File.Exists(path) || _configAgeUtc == DateTime.MinValue)
return false;
return File.GetLastWriteTimeUtc(path) > _configAgeUtc + TimeSpan.FromSeconds(1);
}
catch
{
return false;
}
}
public void DumpMissingVehicleLog()
{
if (!LogMissingVehicleFiles) return;
try
{
var lines = AppServices.Current.MissingAssets.Scan(AppServices.Current.Library.Vehicles);
if (lines.Count == 0) return;
Infrastructure.Diagnostics.Log(lines);
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log("Missing vehicle log", ex);
}
}
private void ReadFromConfig()
{
var c = _config;
LanguageWasSet = c.Has("lang");
Language = c.GetString("lang", "en").ToLowerInvariant() == "pl" ? "Polski" : "English";
Fullscreen = c.GetBool("fullscreen", false);
PauseWhenInactive = c.GetBool("inactivepause", true);
PauseOnStart = c.GetBool("pause", false);
var mouse = c.GetTokens("mousescale");
if (mouse.Length >= 1 && double.TryParse(mouse[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double mx))
{
InvertMouseHorizontal = mx < 0;
CursorSensitivity = Clamp((int)Math.Round(Math.Abs(mx)), 1, 5);
}
if (mouse.Length >= 2 && double.TryParse(mouse[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double my))
{
InvertMouseVertical = my < 0;
_mouseScaleYMagnitude = Math.Abs(my);
}
IgnoreGamepad = !c.GetBool("input.gamepad", true);
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<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");
}
else
{
UartMain = UartScnd = UartTrain = UartLocal = true;
UartRadioVolume = UartRadioChannel = false;
}
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", true);
LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false);
RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0);
Width = c.GetInt("width", 1280);
Height = c.GetInt("height", 720);
BufferScalePercent = Clamp(c.GetInt("starter.bufferscale", 100), 50, 200);
MaxTextureSize = c.GetInt("maxtexturesize", 4096);
MaxCabTextureSize = c.GetInt("maxcabtexturesize", 4096);
TextureFiltering = c.GetInt("anisotropicfiltering", 8);
Multisampling = Clamp(c.GetInt("multisampling", 2), 0, 3);
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);
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);
UseVbo = c.GetBool("usevbo", true);
RenderShadows = c.GetBool("shadows", true);
ReflectionsFramerate = c.GetInt("gfx.reflections.framerate", 60);
var shadowTune = c.GetTokens("shadowtune");
if (shadowTune.Length >= 1) ShadowMapResolution = ParseInt(shadowTune[0], 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", 0));
ShadowRankCutoff = c.GetInt("gfx.shadow.rank.cutoff", 3);
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);
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);
SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4);
FullPhysics = c.GetBool("fullphysics", true);
EnableTraction = c.GetBool("enabletraction", true);
LiveTraction = c.GetBool("livetraction", true);
PhysicsLog = c.GetBool("physicslog", false);
int debugLog = c.GetInt("debuglog", 3);
DebugLog = (debugLog & 1) != 0;
DebugLogVisible = (debugLog & 2) != 0;
_debugLogExtraBits = debugLog & ~3;
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);
SoundEnabled = c.GetBool("soundenabled", true);
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", 1.0) * 100), 1, 100);
SkipPipeline = c.GetBool("gfx.skippipeline", false);
PythonScreenUpdateRate = c.Has("python.updatetime")
? Clamp(c.GetInt("python.updatetime", 200), 0, 10000)
: MapOldPyScreenPriority(c.GetString("pyscreenrendererpriority", "normal"));
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);
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);
HideArchivalVehicles = c.GetBool("starter.hide.archivalvehicles", true);
LastScenery = c.GetString("starter.last.scenery", "");
WindowMaximized = c.GetBool("starter.window.maximized", false);
}
private static readonly string[] ObsoleteKeys =
{
"gfx.postfx.tonemapping",
};
private void WriteToConfig()
{
var c = _config;
foreach (string key in ObsoleteKeys)
c.Remove(key);
c.Set("lang", Language == "Polski" ? "pl" : "en");
c.SetBool("fullscreen", Fullscreen);
c.SetBool("inactivepause", PauseWhenInactive);
c.SetBool("pause", PauseOnStart);
double mx = CursorSensitivity * (InvertMouseHorizontal ? -1 : 1);
double my = _mouseScaleYMagnitude * (InvertMouseVertical ? -1 : 1);
c.Set("mousescale",
$"{mx.ToString("0.###", CultureInfo.InvariantCulture)} " +
$"{my.ToString("0.###", CultureInfo.InvariantCulture)}");
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());
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);
c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]);
c.SetInt("width", Width);
c.SetInt("height", Height);
c.SetInt("starter.bufferscale", BufferScalePercent);
int fbw = (int)Math.Round(Width * BufferScalePercent / 100.0);
int fbh = (int)Math.Round(Height * BufferScalePercent / 100.0);
c.SetInt("gfx.framebuffer.width", fbw);
c.SetInt("gfx.framebuffer.height", fbh);
c.SetInt("maxtexturesize", MaxTextureSize);
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.SetBool("gfx.postfx.chromaticaberration.enabled", ChromaticAberration);
c.SetBool("gfx.postfx.motionblur.enabled", MotionBlur);
c.SetBool("gfx.extraeffects", ExtraEffects);
c.SetBool("gfx.envmap.enabled", EnvMap);
c.SetBool("usevbo", UseVbo);
c.SetBool("shadows", RenderShadows);
c.SetBool("gfx.shadowmap.enabled", RenderShadows);
c.SetInt("gfx.reflections.framerate", ReflectionsFramerate);
c.Set("shadowtune",
$"{ShadowMapResolution} {ShadowProjectionRange} " +
$"{_shadowTuneExtra[0]} {_shadowTuneExtra[1]}");
c.SetInt("gfx.shadows.cab.range", CabShadowsRange);
c.SetInt("gfx.shadow.rank.cutoff", ShadowRankCutoff);
c.SetInt("gfx.reflections.fidelity", ReflectionsFidelity);
c.SetInt("fieldofview", FieldOfView);
c.SetBool("python.enabled", PythonScreens);
c.SetBool("python.threadedupload", PythonThreadedUpload);
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");
c.SetInt("splinefidelity", SplineFidelity);
c.SetBool("fullphysics", FullPhysics);
c.SetBool("enabletraction", EnableTraction);
c.SetBool("livetraction", LiveTraction);
c.SetBool("physicslog", PhysicsLog);
c.SetInt("debuglog",
(DebugLog ? 1 : 0) | (DebugLogVisible ? 2 : 0) | _debugLogExtraBits);
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);
c.SetBool("soundenabled", SoundEnabled);
c.SetDouble("sound.volume", Volume / 50.0);
c.SetDouble("sound.volume.radio", RadioVolume / 100.0);
c.SetDouble("sound.volume.vehicle", VehiclesVolume / 100.0);
c.SetDouble("sound.volume.positional", PositionalVolume / 100.0);
c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0);
c.SetBool("gfx.skippipeline", SkipPipeline);
c.SetInt("python.updatetime", PythonScreenUpdateRate);
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);
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);
c.SetBool("starter.hide.archivalvehicles", HideArchivalVehicles);
if (!string.IsNullOrWhiteSpace(LastScenery))
c.Set("starter.last.scenery", LastScenery);
else
c.Remove("starter.last.scenery");
c.SetBool("starter.window.maximized", WindowMaximized);
}
private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
private static int ParseInt(string s, int fallback) =>
int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) ? n : fallback;
private static int IndexOf(string[] values, string token, int fallback)
{
for (int i = 0; i < values.Length; i++)
if (string.Equals(values[i], token, StringComparison.OrdinalIgnoreCase))
return i;
return fallback;
}
private static int MapOldPyScreenPriority(string priority) =>
priority.ToLowerInvariant() switch
{
"lower" => 400,
"lowest" => 800,
"idle" => 1600,
"off" => 0,
_ => 200
};
}

View File

@@ -2,13 +2,14 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using StarterNG.Application;
namespace StarterNG.Classes;
public static class SettingsProfileStore
{
public static string DirectoryPath { get; } =
Path.Combine(Settings.UserConfigDirectory(), "starter", "profiles");
Path.Combine(AppServices.Current.SettingsPaths.UserConfigDirectory(), "starter", "profiles");
public static IReadOnlyList<string> ListProfiles()
{

View File

@@ -0,0 +1,161 @@
using System;
namespace StarterNG.Domain.Settings;
/// <summary>How the simulator should treat the battery switch when a run starts.</summary>
public enum BatteryDefault { Default = 0, AlwaysOff = 1, AlwaysOn = 2 }
/// <summary>What is wrong with the executable the starter would launch.</summary>
public enum ExeProblem { None, NotFound, NotExecutable, WrongPlatform }
/// <summary>
/// Everything the starter writes into eu07.ini, plus the handful of preferences
/// it keeps for itself.
/// </summary>
/// <remarks>
/// Plain state with sensible defaults, and nothing else: reading and writing the
/// ini is <c>Infrastructure.Settings.SettingsSerializer</c>, locating the file
/// and the executable is the rest of that folder, and the load/save lifecycle is
/// <c>Application.SettingsStore</c>. Splitting those out is what makes it
/// possible to test the ini mapping without a disk.
/// </remarks>
public sealed class SimulatorSettings
{
/// <summary>
/// False until the ini names a language, which is how the first run knows to
/// follow the operating system instead.
/// </summary>
public bool LanguageWasSet { get; internal set; }
public string Language = "English";
public bool Fullscreen;
public bool PauseWhenInactive = true;
public bool PauseOnStart;
public int CursorSensitivity = 2;
public bool InvertMouseHorizontal;
public bool InvertMouseVertical;
public bool IgnoreGamepad;
public int FeedbackMode = 1;
public int FeedbackPort = 888;
public bool UartEnabled;
public string UartPort = "";
public string UartTune = "";
public bool UartDebug;
public bool UartMain, UartScnd, UartTrain, UartLocal, UartRadioVolume, UartRadioChannel;
public bool SelectExeAutomatically = true;
public string ExecutablePath = "eu07.exe";
public bool DebugMode;
public bool VirtualShunting = true;
public bool LogMissingVehicleFiles;
public int RenderEngine;
public int Width = 1280;
public int Height = 720;
public int BufferScalePercent = 100;
public int MaxTextureSize = 4096;
public int MaxCabTextureSize = 4096;
public int TextureFiltering = 8;
public int Multisampling = 2;
public int DynamicLights = 7;
public double DrawRangeFactor = 3.0;
public bool VSync;
public bool Smoke = true;
public int SmokeFidelity = 1;
public bool ChromaticAberration = true;
public bool MotionBlur = true;
public bool ExtraEffects = true;
public bool EnvMap = true;
public bool UseVbo = true;
public bool RenderShadows = true;
public int ReflectionsFramerate = 60;
public int ShadowMapResolution = 2048;
public int ShadowProjectionRange = 250;
public int CabShadowsRange;
public int ShadowRankCutoff = 3;
public int ReflectionsFidelity;
public int FieldOfView = 45;
public bool PythonScreens = true;
public bool PythonThreadedUpload = true;
public bool FpsLimitEnabled;
public int FpsLimit = 30;
public double ShadowAngleLimit = 0.2;
public bool FullscreenWindowed;
public bool RenderAngleVulkan;
public int SplineFidelity = 1;
public bool FullPhysics = true;
public bool EnableTraction = true;
public bool LiveTraction = true;
public bool PhysicsLog;
public bool DebugLog = true;
public bool MultipleLogs;
public bool DisplaySimulation = true;
public bool CrashDamage = true;
public double Friction = 1.0;
public double BrakeStep = 1.0;
public double BrakeSpeed = 3.0;
public bool SoundEnabled = true;
public int Volume = 50;
public int RadioVolume = 75;
public int VehiclesVolume = 100;
public int PositionalVolume = 100;
public int AmbientVolume = 100;
public bool SkipPipeline;
public int PythonScreenUpdateRate = 200;
public bool DebugLogVisible;
/// <summary>
/// Debug log bits beyond the two the UI exposes, carried through a round trip
/// so a hand-configured log level is not silently narrowed.
/// </summary>
internal int DebugLogExtraBits { get; set; }
public bool CompressTextures = true;
public bool ScaleSpeculars = true;
public bool UseGLES;
public bool ShaderGamma;
public bool ScreenMipmaps = true;
public bool ExtendedModelConversion;
public bool GfxResourceMove;
public bool GfxResourceSweep = true;
public int TrainPhysicsThreads;
public bool IgnoreIrrelevantTrains;
public bool AutoCloseStarter;
public bool LargeThumbnails;
public bool AutoExpandSceneryTree;
public BatteryDefault BatteryDefault = BatteryDefault.Default;
public bool ShowAiVehicles = true;
public bool DrivableOnly = true;
public bool ShowArchivalSceneries = true;
public bool HideArchivalVehicles = true;
public string LastScenery = "";
/// <summary>
/// Starter's own window state, the way the old one kept WindowMaximized in
/// starter.ini. Only the maximised flag is remembered - a normal window
/// always opens at its design size, centred on the screen.
/// </summary>
public bool WindowMaximized;
public static readonly string[] RenderEngines =
{ "full", "legacy", "simpleshader", "simple", "off", "experimental" };
public static readonly int[] AnisotropySteps = { 1, 2, 4, 8, 16 };
/// <summary>
/// Vertical mouse scale as the ini stored it. The UI only exposes one
/// sensitivity, so the authored vertical magnitude is carried through a
/// round trip rather than being overwritten.
/// </summary>
internal double MouseScaleYMagnitude { get; set; } = 0.2;
/// <summary>
/// Shadow tuning values beyond the two the UI edits, carried through a round
/// trip unchanged.
/// </summary>
internal string[] ShadowTuneExtra { get; set; } = { "400", "300" };
}

View File

@@ -52,4 +52,14 @@ public sealed class PhysicalFileSystem : IFileSystem
Directory.Exists(path) ? Directory.GetDirectories(path) : Array.Empty<string>();
public DateTime GetLastWriteTimeUtc(string path) => File.GetLastWriteTimeUtc(path);
public bool IsExecutable(string path)
{
if (OperatingSystem.IsWindows())
return true;
const UnixFileMode anyExecute =
UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
return (File.GetUnixFileMode(path) & anyExecute) != 0;
}
}

View File

@@ -0,0 +1,18 @@
using System;
using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary><see cref="IEnvironment"/> backed by the running process.</summary>
public sealed class SystemEnvironment : IEnvironment
{
public bool IsWindows => OperatingSystem.IsWindows();
public bool IsLinux => OperatingSystem.IsLinux();
public bool IsMacOS => OperatingSystem.IsMacOS();
public string? GetVariable(string name) => Environment.GetEnvironmentVariable(name);
public string BaseDirectory => AppContext.BaseDirectory;
}

View File

@@ -72,6 +72,7 @@ public static class Diagnostics
AppServices.Current.Files,
AppServices.Current.Localization,
AppServices.Current.Physics,
AppServices.Current.Library)
AppServices.Current.Library,
AppServices.Current.SettingsStore)
.Run();
}

View File

@@ -3,6 +3,7 @@ using System.IO;
using StarterNG.Application.Abstractions;
using StarterNG.Classes;
using StarterNG.Application;
using StarterNG.Domain.Settings;
namespace StarterNG.Infrastructure;
@@ -23,15 +24,17 @@ public sealed class InstallationCheck
private readonly ILocalizedStrings _strings;
private readonly IPhysicsRepository _physics;
private readonly GameLibrary _library;
private readonly SettingsStore _settings;
public InstallationCheck(IGamePaths paths, IFileSystem files, ILocalizedStrings strings,
IPhysicsRepository physics, GameLibrary library)
IPhysicsRepository physics, GameLibrary library, SettingsStore settings)
{
_paths = paths;
_files = files;
_strings = strings;
_physics = physics;
_library = library;
_settings = settings;
}
public List<string> Run()
@@ -54,7 +57,8 @@ public sealed class InstallationCheck
if (_physics.IndexedCount == 0)
faults.Add(_strings["FaultNoPhysics"]);
if (Settings.Instance.ResolveExecutable(out var problem) is var _ && problem == ExeProblem.NotFound)
_settings.ResolveExecutable(out var problem);
if (problem == ExeProblem.NotFound)
faults.Add(_strings["FaultNoExe"]);
return faults;

View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using StarterNG.Application.Abstractions;
using StarterNG.Domain.Settings;
namespace StarterNG.Infrastructure.Settings;
/// <summary>
/// Finds and vets the simulator executable the starter will launch.
/// </summary>
/// <remarks>
/// Installations ship the binary under several names (eu07.exe, eu07,
/// eu07-vulkan, ...), and a folder shared between platforms may hold both a PE
/// and an ELF build. So a candidate is not just found by name: it is checked for
/// the execute bit and for a magic number matching the platform we are on.
/// </remarks>
public sealed class ExecutableLocator
{
/// <summary>Extensions that are never the simulator, whatever they are named.</summary>
private static readonly string[] NeverExecutable =
{
".ini", ".log", ".txt", ".cfg", ".config", ".json", ".dll", ".so", ".dat", ".bak", ".csv", ".xml"
};
private readonly IFileSystem _files;
private readonly IGamePaths _paths;
private readonly IEnvironment _environment;
private readonly IDiagnosticsLog _log;
public ExecutableLocator(IFileSystem files, IGamePaths paths, IEnvironment environment, IDiagnosticsLog log)
{
_files = files;
_paths = paths;
_environment = environment;
_log = log;
}
/// <summary>The canonical name for the platform, used when nothing is found.</summary>
public string CanonicalName => _environment.IsWindows ? "eu07.exe" : "eu07";
/// <summary>
/// The executable to launch for these settings, along with what is wrong with
/// it. A path is always returned so the UI can show what it was looking for.
/// </summary>
public string Resolve(SimulatorSettings settings, out ExeProblem problem)
{
if (!settings.SelectExeAutomatically && !string.IsNullOrWhiteSpace(settings.ExecutablePath))
{
problem = Validate(FullPath(settings.ExecutablePath));
return settings.ExecutablePath;
}
string? fallback = null;
try
{
// Prefer the canonical name, then the shortest: "eu07" over "eu07-dev".
var candidates = _files.GetFiles(_paths.Root, "eu07*")
.Where(IsCandidate)
.OrderBy(path => string.Equals(Path.GetFileName(path), CanonicalName,
StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(path => Path.GetFileName(path).Length);
foreach (string path in candidates)
{
if (Validate(path) == ExeProblem.None)
{
problem = ExeProblem.None;
return path;
}
fallback ??= path;
}
}
catch (Exception ex)
{
_log.Log("looking for the simulator executable", ex);
}
if (fallback is not null)
{
problem = Validate(fallback);
return fallback;
}
problem = ExeProblem.NotFound;
return CanonicalName;
}
/// <summary>Executables the user can pick from, most recently built first.</summary>
public List<string> ListCandidates(string? directory = null)
{
try
{
return _files.GetFiles(directory ?? _paths.Root, "eu07*")
.Where(IsCandidate)
.OrderByDescending(_files.GetLastWriteTimeUtc)
.Select(Path.GetFileName)
.ToList()!;
}
catch (Exception)
{
return new List<string>();
}
}
public ExeProblem Validate(string path)
{
try
{
if (!_files.FileExists(path))
return ExeProblem.NotFound;
if (!_environment.IsWindows && !_files.IsExecutable(path))
return ExeProblem.NotExecutable;
return MatchesPlatform(path) ? ExeProblem.None : ExeProblem.WrongPlatform;
}
catch (Exception)
{
return ExeProblem.NotFound;
}
}
/// <summary>
/// Reads the file's magic number: "MZ" for a Windows binary, 0x7F "ELF" for a
/// Linux one. A file we cannot read four bytes from is given the benefit of
/// the doubt.
/// </summary>
private bool MatchesPlatform(string path)
{
byte[] head = new byte[4];
using var stream = _files.OpenRead(path);
if (stream.Read(head, 0, 4) != 4)
return true;
bool portableExecutable = head[0] == 0x4D && head[1] == 0x5A;
bool elf = head[0] == 0x7F && head[1] == 0x45 && head[2] == 0x4C && head[3] == 0x46;
if (_environment.IsWindows && elf)
return false;
if (_environment.IsLinux && portableExecutable)
return false;
return true;
}
private bool IsCandidate(string path)
{
string extension = Path.GetExtension(path).ToLowerInvariant();
if (NeverExecutable.Contains(extension))
return false;
if (_environment.IsWindows)
return extension == ".exe";
if (_environment.IsLinux)
return extension.Length == 0 || extension is ".x86_64" or ".run" or ".appimage";
return extension.Length == 0 || extension is ".exe" or ".x86_64" or ".run" or ".appimage";
}
private string FullPath(string path) =>
Path.IsPathRooted(path) ? path : Path.Combine(_paths.Root, path);
}

View File

@@ -0,0 +1,54 @@
using System.IO;
using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Settings;
/// <summary>
/// Where the simulator's eu07.ini lives: per user on each platform, with the
/// installation's own copy as the template for a first run.
/// </summary>
public sealed class SettingsPaths
{
private const string ConfigFileName = "eu07.ini";
private const string VendorFolder = "MaSzyna";
private readonly IEnvironment _environment;
private readonly IGamePaths _paths;
public SettingsPaths(IEnvironment environment, IGamePaths paths)
{
_environment = environment;
_paths = paths;
}
/// <summary>The user's own settings file, which is what the starter writes.</summary>
public string UserConfigPath()
{
if (_environment.IsWindows)
{
string? appData = _environment.GetVariable("APPDATA");
if (!string.IsNullOrEmpty(appData))
return Path.Combine(appData, VendorFolder, ConfigFileName);
}
else if (_environment.IsMacOS)
{
string? home = _environment.GetVariable("HOME");
if (!string.IsNullOrEmpty(home))
return Path.Combine(home, "Library", "Application Support", VendorFolder, ConfigFileName);
}
else
{
string? home = _environment.GetVariable("HOME");
if (!string.IsNullOrEmpty(home))
return Path.Combine(home, ".config", VendorFolder, ConfigFileName);
}
return Path.Combine(_environment.BaseDirectory, ConfigFileName);
}
/// <summary>The ini shipped with the installation, read when the user has none.</summary>
public string InstallationConfigPath() => _paths.FromRoot(ConfigFileName);
public string UserConfigDirectory() =>
Path.GetDirectoryName(UserConfigPath()) ?? _environment.BaseDirectory;
}

View File

@@ -0,0 +1,310 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using StarterNG.Classes;
using StarterNG.Domain.Settings;
namespace StarterNG.Infrastructure.Settings;
/// <summary>
/// Maps <see cref="SimulatorSettings"/> onto the simulator's eu07.ini and back.
/// </summary>
/// <remarks>
/// Every key the starter understands is named exactly once, here. Reads clamp
/// and default so a hand-edited ini cannot put the UI into an impossible state,
/// and writes preserve the keys the starter does not manage - which is why a
/// round trip through this class is worth testing on its own.
/// </remarks>
public sealed class SettingsSerializer
{
public void Read(SimulatorSettings s, ConfigFile c)
{
s.LanguageWasSet = c.Has("lang");
s.Language = c.GetString("lang", "en").ToLowerInvariant() == "pl" ? "Polski" : "English";
s.Fullscreen = c.GetBool("fullscreen", false);
s.PauseWhenInactive = c.GetBool("inactivepause", true);
s.PauseOnStart = c.GetBool("pause", false);
var mouse = c.GetTokens("mousescale");
if (mouse.Length >= 1 && double.TryParse(mouse[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double mx))
{
s.InvertMouseHorizontal = mx < 0;
s.CursorSensitivity = Clamp((int)Math.Round(Math.Abs(mx)), 1, 5);
}
if (mouse.Length >= 2 && double.TryParse(mouse[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double my))
{
s.InvertMouseVertical = my < 0;
s.MouseScaleYMagnitude = Math.Abs(my);
}
s.IgnoreGamepad = !c.GetBool("input.gamepad", true);
s.FeedbackMode = Clamp(c.GetInt("feedbackmode", 1), 0, 5);
s.FeedbackPort = c.GetInt("feedbackport", 888);
s.UartEnabled = c.Has("uart");
s.UartPort = c.GetString("uart", "");
s.UartTune = c.GetString("uarttune", "");
s.UartDebug = c.GetBool("uartdebug", false);
if (c.Has("uartfeature"))
{
var uartTokens = (c.GetRaw("uartfeature") ?? "").Split('|', StringSplitOptions.RemoveEmptyEntries);
var uf = new HashSet<string>(uartTokens, StringComparer.OrdinalIgnoreCase);
s.UartMain = uf.Contains("main"); s.UartScnd = uf.Contains("scnd"); s.UartTrain = uf.Contains("train");
s.UartLocal = uf.Contains("local"); s.UartRadioVolume = uf.Contains("radiovolume"); s.UartRadioChannel = uf.Contains("radiochannel");
}
else
{
s.UartMain = s.UartScnd = s.UartTrain = s.UartLocal = true;
s.UartRadioVolume = s.UartRadioChannel = false;
}
s.SelectExeAutomatically = c.GetBool("starter.exe.auto", true);
s.ExecutablePath = c.GetString("starter.exe.path", "eu07.exe");
s.DebugMode = c.GetBool("debugmode", false);
s.VirtualShunting = c.GetBool("ai.trainman", true);
s.LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false);
s.RenderEngine = IndexOf(SimulatorSettings.RenderEngines, c.GetString("gfxrenderer", "full"), 0);
s.Width = c.GetInt("width", 1280);
s.Height = c.GetInt("height", 720);
s.BufferScalePercent = Clamp(c.GetInt("starter.bufferscale", 100), 50, 200);
s.MaxTextureSize = c.GetInt("maxtexturesize", 4096);
s.MaxCabTextureSize = c.GetInt("maxcabtexturesize", 4096);
s.TextureFiltering = c.GetInt("anisotropicfiltering", 8);
s.Multisampling = Clamp(c.GetInt("multisampling", 2), 0, 3);
s.DynamicLights = Clamp(c.GetInt("dynamiclights", 7), 0, 7);
s.DrawRangeFactor = c.GetDouble("gfx.drawrange.factor.max", 3.0);
s.VSync = c.GetBool("vsync", false);
s.Smoke = c.GetBool("gfx.smoke", true);
s.SmokeFidelity = Clamp(c.GetInt("gfx.smoke.fidelity", 1), 1, 4);
s.ChromaticAberration = c.GetBool("gfx.postfx.chromaticaberration.enabled", true);
s.MotionBlur = c.GetBool("gfx.postfx.motionblur.enabled", true);
s.ExtraEffects = c.GetBool("gfx.extraeffects", true);
s.EnvMap = c.GetBool("gfx.envmap.enabled", true);
s.UseVbo = c.GetBool("usevbo", true);
s.RenderShadows = c.GetBool("shadows", true);
s.ReflectionsFramerate = c.GetInt("gfx.reflections.framerate", 60);
var shadowTune = c.GetTokens("shadowtune");
if (shadowTune.Length >= 1) s.ShadowMapResolution = ParseInt(shadowTune[0], 2048);
if (shadowTune.Length >= 2) s.ShadowProjectionRange = ParseInt(shadowTune[1], 250);
if (shadowTune.Length >= 4) s.ShadowTuneExtra = new[] { shadowTune[2], shadowTune[3] };
s.CabShadowsRange = (int)Math.Round(c.GetDouble("gfx.shadows.cab.range", 0));
s.ShadowRankCutoff = c.GetInt("gfx.shadow.rank.cutoff", 3);
s.ReflectionsFidelity = Clamp(c.GetInt("gfx.reflections.fidelity", 0), 0, 2);
s.FieldOfView = Clamp((int)Math.Round(c.GetDouble("fieldofview", 45)), 15, 75);
s.PythonScreens = c.GetBool("python.enabled", true);
s.PythonThreadedUpload = c.GetBool("python.threadedupload", true);
s.FpsLimitEnabled = c.Has("fpslimit");
s.FpsLimit = Clamp(c.GetInt("fpslimit", 30), 1, 100);
s.ShadowAngleLimit = c.GetDouble("gfx.shadow.angle.min", 0.2);
s.FullscreenWindowed = c.GetBool("fullscreenwindowed", false);
s.RenderAngleVulkan = string.Equals(c.GetString("gfx.angleplatform", ""), "vulkan", StringComparison.OrdinalIgnoreCase);
s.SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4);
s.FullPhysics = c.GetBool("fullphysics", true);
s.EnableTraction = c.GetBool("enabletraction", true);
s.LiveTraction = c.GetBool("livetraction", true);
s.PhysicsLog = c.GetBool("physicslog", false);
int debugLog = c.GetInt("debuglog", 3);
s.DebugLog = (debugLog & 1) != 0;
s.DebugLogVisible = (debugLog & 2) != 0;
s.DebugLogExtraBits = debugLog & ~3;
s.MultipleLogs = c.GetBool("multiplelogs", false);
s.DisplaySimulation = !c.GetBool("gfx.skiprendering", false);
s.CrashDamage = c.GetBool("crashdamage", true);
s.Friction = c.GetDouble("friction", 1.0);
s.BrakeStep = c.GetDouble("brakestep", 1.0);
s.BrakeSpeed = c.GetDouble("brakespeed", 3.0);
s.SoundEnabled = c.GetBool("soundenabled", true);
s.Volume = Clamp((int)Math.Round(c.GetDouble("sound.volume", 1.0) * 50), 1, 100);
s.RadioVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.radio", 0.75) * 100), 1, 100);
s.VehiclesVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.vehicle", 1.0) * 100), 1, 100);
s.PositionalVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.positional", 1.0) * 100), 1, 100);
s.AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 1.0) * 100), 1, 100);
s.SkipPipeline = c.GetBool("gfx.skippipeline", false);
s.PythonScreenUpdateRate = c.Has("python.updatetime")
? Clamp(c.GetInt("python.updatetime", 200), 0, 10000)
: MapOldPyScreenPriority(c.GetString("pyscreenrendererpriority", "normal"));
s.CompressTextures = c.GetBool("compresstex", true);
s.ScaleSpeculars = c.GetBool("scalespeculars", true);
s.UseGLES = c.GetBool("gfx.usegles", false);
s.ShaderGamma = c.GetBool("gfx.shadergamma", false);
s.ScreenMipmaps = c.GetBool("python.mipmaps", true);
s.ExtendedModelConversion = c.GetInt("convertmodels", 0) != 0;
s.GfxResourceMove = c.GetBool("gfx.resource.move", false);
s.GfxResourceSweep = c.GetBool("gfx.resource.sweep", true);
s.TrainPhysicsThreads = Clamp(c.GetInt("async.trainThreads", 0), 0, Environment.ProcessorCount);
s.IgnoreIrrelevantTrains = c.GetBool("starter.ignoreirrelevant", false);
s.AutoCloseStarter = c.GetBool("starter.autoclose", false);
s.LargeThumbnails = c.GetBool("starter.largethumbnails", false);
s.AutoExpandSceneryTree = c.GetBool("starter.expandtree", false);
s.BatteryDefault = (BatteryDefault)Clamp(c.GetInt("starter.batterydefault", 0), 0, 2);
s.ShowAiVehicles = c.GetBool("starter.show.ai", true);
s.DrivableOnly = c.GetBool("starter.drivableonly", true);
s.ShowArchivalSceneries = c.GetBool("starter.show.archival", true);
s.HideArchivalVehicles = c.GetBool("starter.hide.archivalvehicles", true);
s.LastScenery = c.GetString("starter.last.scenery", "");
s.WindowMaximized = c.GetBool("starter.window.maximized", false);
}
private static readonly string[] ObsoleteKeys =
{
"gfx.postfx.tonemapping",
};
public void Write(SimulatorSettings s, ConfigFile c)
{
foreach (string key in ObsoleteKeys)
c.Remove(key);
c.Set("lang", s.Language == "Polski" ? "pl" : "en");
c.SetBool("fullscreen", s.Fullscreen);
c.SetBool("inactivepause", s.PauseWhenInactive);
c.SetBool("pause", s.PauseOnStart);
double mx = s.CursorSensitivity * (s.InvertMouseHorizontal ? -1 : 1);
double my = s.MouseScaleYMagnitude * (s.InvertMouseVertical ? -1 : 1);
c.Set("mousescale",
$"{mx.ToString("0.###", CultureInfo.InvariantCulture)} " +
$"{my.ToString("0.###", CultureInfo.InvariantCulture)}");
c.SetBool("input.gamepad", !s.IgnoreGamepad);
c.SetInt("feedbackmode", s.FeedbackMode);
c.SetInt("feedbackport", s.FeedbackPort);
if (s.UartEnabled && !string.IsNullOrWhiteSpace(s.UartPort)) c.Set("uart", s.UartPort); else c.Remove("uart");
c.Set("uarttune", s.UartTune);
c.SetBool("uartdebug", s.UartDebug);
var sb = new StringBuilder();
if (s.UartMain) sb.Append("main|");
if (s.UartScnd) sb.Append("scnd|");
if (s.UartTrain) sb.Append("train|");
if (s.UartLocal) sb.Append("local|");
if (s.UartRadioVolume) sb.Append("radiovolume|");
if (s.UartRadioChannel) sb.Append("radiochannel|");
c.Set("uartfeature", sb.ToString());
c.SetBool("starter.exe.auto", s.SelectExeAutomatically);
c.Set("starter.exe.path", s.ExecutablePath);
c.SetBool("debugmode", s.DebugMode);
c.SetBool("ai.trainman", s.VirtualShunting);
c.SetBool("starter.logmissingvehicles", s.LogMissingVehicleFiles);
c.Set("gfxrenderer", SimulatorSettings.RenderEngines[Clamp(s.RenderEngine, 0, SimulatorSettings.RenderEngines.Length - 1)]);
c.SetInt("width", s.Width);
c.SetInt("height", s.Height);
c.SetInt("starter.bufferscale", s.BufferScalePercent);
int fbw = (int)Math.Round(s.Width * s.BufferScalePercent / 100.0);
int fbh = (int)Math.Round(s.Height * s.BufferScalePercent / 100.0);
c.SetInt("gfx.framebuffer.width", fbw);
c.SetInt("gfx.framebuffer.height", fbh);
c.SetInt("maxtexturesize", s.MaxTextureSize);
c.SetInt("maxcabtexturesize", s.MaxCabTextureSize);
c.SetInt("anisotropicfiltering", s.TextureFiltering);
c.SetInt("multisampling", s.Multisampling);
c.SetInt("dynamiclights", s.DynamicLights);
c.SetDouble("gfx.drawrange.factor.max", s.DrawRangeFactor);
c.SetBool("vsync", s.VSync);
c.SetBool("gfx.smoke", s.Smoke);
c.SetInt("gfx.smoke.fidelity", s.SmokeFidelity);
c.SetBool("gfx.postfx.chromaticaberration.enabled", s.ChromaticAberration);
c.SetBool("gfx.postfx.motionblur.enabled", s.MotionBlur);
c.SetBool("gfx.extraeffects", s.ExtraEffects);
c.SetBool("gfx.envmap.enabled", s.EnvMap);
c.SetBool("usevbo", s.UseVbo);
c.SetBool("shadows", s.RenderShadows);
c.SetBool("gfx.shadowmap.enabled", s.RenderShadows);
c.SetInt("gfx.reflections.framerate", s.ReflectionsFramerate);
c.Set("shadowtune",
$"{s.ShadowMapResolution} {s.ShadowProjectionRange} " +
$"{s.ShadowTuneExtra[0]} {s.ShadowTuneExtra[1]}");
c.SetInt("gfx.shadows.cab.range", s.CabShadowsRange);
c.SetInt("gfx.shadow.rank.cutoff", s.ShadowRankCutoff);
c.SetInt("gfx.reflections.fidelity", s.ReflectionsFidelity);
c.SetInt("fieldofview", s.FieldOfView);
c.SetBool("python.enabled", s.PythonScreens);
c.SetBool("python.threadedupload", s.PythonThreadedUpload);
if (s.FpsLimitEnabled) c.SetInt("fpslimit", s.FpsLimit); else c.Remove("fpslimit");
c.SetDouble("gfx.shadow.angle.min", s.ShadowAngleLimit);
c.SetBool("fullscreenwindowed", s.FullscreenWindowed);
if (s.RenderAngleVulkan) c.Set("gfx.angleplatform", "vulkan"); else c.Remove("gfx.angleplatform");
c.SetInt("splinefidelity", s.SplineFidelity);
c.SetBool("fullphysics", s.FullPhysics);
c.SetBool("enabletraction", s.EnableTraction);
c.SetBool("livetraction", s.LiveTraction);
c.SetBool("physicslog", s.PhysicsLog);
c.SetInt("debuglog",
(s.DebugLog ? 1 : 0) | (s.DebugLogVisible ? 2 : 0) | s.DebugLogExtraBits);
c.SetBool("multiplelogs", s.MultipleLogs);
c.SetBool("gfx.skiprendering", !s.DisplaySimulation);
c.SetBool("crashdamage", s.CrashDamage);
c.SetDouble("friction", s.Friction);
c.SetDouble("brakestep", s.BrakeStep);
c.SetDouble("brakespeed", s.BrakeSpeed);
c.SetBool("soundenabled", s.SoundEnabled);
c.SetDouble("sound.volume", s.Volume / 50.0);
c.SetDouble("sound.volume.radio", s.RadioVolume / 100.0);
c.SetDouble("sound.volume.vehicle", s.VehiclesVolume / 100.0);
c.SetDouble("sound.volume.positional", s.PositionalVolume / 100.0);
c.SetDouble("sound.volume.ambient", s.AmbientVolume / 100.0);
c.SetBool("gfx.skippipeline", s.SkipPipeline);
c.SetInt("python.updatetime", s.PythonScreenUpdateRate);
c.SetBool("compresstex", s.CompressTextures);
c.SetBool("scalespeculars", s.ScaleSpeculars);
c.SetBool("gfx.usegles", s.UseGLES);
c.SetBool("gfx.shadergamma", s.ShaderGamma);
c.SetBool("python.mipmaps", s.ScreenMipmaps);
c.Set("convertmodels", s.ExtendedModelConversion ? "143" : "0");
c.SetBool("gfx.resource.move", s.GfxResourceMove);
c.SetBool("gfx.resource.sweep", s.GfxResourceSweep);
c.SetInt("async.trainThreads", s.TrainPhysicsThreads);
c.SetBool("starter.ignoreirrelevant", s.IgnoreIrrelevantTrains);
c.SetBool("starter.autoclose", s.AutoCloseStarter);
c.SetBool("starter.largethumbnails", s.LargeThumbnails);
c.SetBool("starter.expandtree", s.AutoExpandSceneryTree);
c.SetInt("starter.batterydefault", (int)s.BatteryDefault);
c.SetBool("starter.show.ai", s.ShowAiVehicles);
c.SetBool("starter.drivableonly", s.DrivableOnly);
c.SetBool("starter.show.archival", s.ShowArchivalSceneries);
c.SetBool("starter.hide.archivalvehicles", s.HideArchivalVehicles);
if (!string.IsNullOrWhiteSpace(s.LastScenery))
c.Set("starter.last.scenery", s.LastScenery);
else
c.Remove("starter.last.scenery");
c.SetBool("starter.window.maximized", s.WindowMaximized);
}
private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
private static int ParseInt(string s, int fallback) =>
int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) ? n : fallback;
private static int IndexOf(string[] values, string token, int fallback)
{
for (int i = 0; i < values.Length; i++)
if (string.Equals(values[i], token, StringComparison.OrdinalIgnoreCase))
return i;
return fallback;
}
private static int MapOldPyScreenPriority(string priority) =>
priority.ToLowerInvariant() switch
{
"lower" => 400,
"lowest" => 800,
"idle" => 1600,
"off" => 0,
_ => 200
};
}

View File

@@ -16,9 +16,9 @@ using StarterNG.Domain;
using StarterNG.Infrastructure;
using StarterNG.Services;
using StarterNG.Views;
using SettingsModel = StarterNG.Classes.Settings;
using StarterNG.Application;
using StarterNG.Domain.Sceneries;
using StarterNG.Domain.Settings;
namespace StarterNG;
@@ -37,13 +37,13 @@ public partial class MainWindow : Window
ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaTitleBarHeightHint = TitleBarHeight;
if (SettingsModel.Instance.WindowMaximized)
if (AppServices.Current.Settings.WindowMaximized)
WindowState = WindowState.Maximized;
PropertyChanged += (_, e) =>
{
if (e.Property == WindowStateProperty && WindowState != WindowState.Minimized)
SettingsModel.Instance.WindowMaximized = WindowState == WindowState.Maximized;
AppServices.Current.Settings.WindowMaximized = WindowState == WindowState.Maximized;
};
if (OperatingSystem.IsWindows())
@@ -429,7 +429,7 @@ public partial class MainWindow : Window
if (trainset.Vehicles.Count > 0)
{
trainset.Velocity = SettingsModel.Instance.BatteryDefault switch
trainset.Velocity = AppServices.Current.Settings.BatteryDefault switch
{
BatteryDefault.AlwaysOff => 0f,
BatteryDefault.AlwaysOn => MathF.Abs(trainset.OriginalVelocity) < 0.01f ? 0.1f : trainset.OriginalVelocity,
@@ -452,7 +452,7 @@ public partial class MainWindow : Window
string exportPath = Path.Combine(dir, exportName);
try
{
File.WriteAllText(exportPath, scenery.BuildExportContent(SettingsModel.Instance.IgnoreIrrelevantTrains, AppServices.Current.Random), Encoding.GetEncoding(1250));
File.WriteAllText(exportPath, scenery.BuildExportContent(AppServices.Current.Settings.IgnoreIrrelevantTrains, AppServices.Current.Random), Encoding.GetEncoding(1250));
}
catch (Exception ex)
{
@@ -461,9 +461,9 @@ public partial class MainWindow : Window
}
if (saveSettings)
SettingsModel.Instance.CaptureAndSave();
AppServices.Current.SettingsStore.CaptureAndSave();
string exe = Path.GetFullPath(SettingsModel.Instance.ResolveExecutable(out var exeProblem));
string exe = Path.GetFullPath(AppServices.Current.SettingsStore.ResolveExecutable(out var exeProblem));
if (exeProblem != ExeProblem.None)
{
await ShowExeProblem(exeProblem, exe);
@@ -488,7 +488,7 @@ public partial class MainWindow : Window
return;
}
if (SettingsModel.Instance.AutoCloseStarter)
if (AppServices.Current.Settings.AutoCloseStarter)
{
Close();
return;
@@ -525,7 +525,7 @@ public partial class MainWindow : Window
{
if (sim is null)
{
string name = Path.GetFileNameWithoutExtension(SettingsModel.Instance.ResolveExecutable());
string name = Path.GetFileNameWithoutExtension(AppServices.Current.SettingsStore.ResolveExecutable());
sim = Process.GetProcessesByName(name).FirstOrDefault();
}
@@ -555,7 +555,7 @@ public partial class MainWindow : Window
{
if ((DateTime.UtcNow - _openedUtc).TotalSeconds < 3)
return;
if (!SettingsModel.Instance.IsConfigNewerOnDisk())
if (!AppServices.Current.SettingsStore.IsConfigNewerOnDisk())
return;
var result = await MessageBox.Show(
@@ -565,11 +565,11 @@ public partial class MainWindow : Window
MessageBoxButtons.YesNo);
if (result)
{
SettingsModel.Instance.Load();
AppServices.Current.SettingsStore.Load();
SettingsView?.ReloadFromSettings();
}
else
SettingsModel.Instance.TouchConfigAge();
AppServices.Current.SettingsStore.TouchConfigAge();
}
private void RestoreFromSimulator()

View File

@@ -4,7 +4,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Infrastructure;
using SettingsModel = StarterNG.Classes.Settings;
using StarterNG.Application;
namespace StarterNG.Views;
@@ -156,7 +156,7 @@ public sealed class AdvancedSettingsWindow : Window
private void Load()
{
var s = SettingsModel.Instance;
var s = AppServices.Current.Settings;
_compressTex.IsChecked = s.CompressTextures;
_mipmaps.IsChecked = s.ScreenMipmaps;
_scaleSpeculars.IsChecked = s.ScaleSpeculars;
@@ -172,7 +172,7 @@ public sealed class AdvancedSettingsWindow : Window
private void Store()
{
var s = SettingsModel.Instance;
var s = AppServices.Current.Settings;
s.CompressTextures = _compressTex.IsChecked == true;
s.ScreenMipmaps = _mipmaps.IsChecked == true;
s.ScaleSpeculars = _scaleSpeculars.IsChecked == true;

View File

@@ -91,7 +91,7 @@ public partial class Depot : UserControl
Dispatcher.UIThread.Post(() =>
{
hideArchivalCheck.IsChecked = StarterNG.Classes.Settings.Instance.HideArchivalVehicles;
hideArchivalCheck.IsChecked = AppServices.Current.Settings.HideArchivalVehicles;
_browser.InitClassComboTemplates();
_browser.PopulateCategoryCombo();
PopulateSceneryConsists();
@@ -232,8 +232,8 @@ public partial class Depot : UserControl
private void ConsistFilter_OnChanged(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (_suppress) return;
StarterNG.Classes.Settings.Instance.ShowAiVehicles = depotShowAiCheck.IsChecked == true;
StarterNG.Classes.Settings.Instance.DrivableOnly = depotDrivableOnlyCheck.IsChecked == true;
AppServices.Current.Settings.ShowAiVehicles = depotShowAiCheck.IsChecked == true;
AppServices.Current.Settings.DrivableOnly = depotDrivableOnlyCheck.IsChecked == true;
PopulateSceneryConsists();
}
@@ -242,8 +242,8 @@ public partial class Depot : UserControl
_suppress = true;
sceneryConsistList.Items.Clear();
depotShowAiCheck.IsChecked = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
depotDrivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
depotShowAiCheck.IsChecked = AppServices.Current.Settings.ShowAiVehicles;
depotDrivableOnlyCheck.IsChecked = AppServices.Current.Settings.DrivableOnly;
_listScenery = AppServices.Current.State.CurrentScenery ?? _sceneries.FirstOrDefault();
sceneryConsistsTab.Header = App.Loc["SceneryConsists"];
@@ -252,8 +252,8 @@ public partial class Depot : UserControl
if (_listScenery != null)
{
bool showAi = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
bool drivableOnly = StarterNG.Classes.Settings.Instance.DrivableOnly;
bool showAi = AppServices.Current.Settings.ShowAiVehicles;
bool drivableOnly = AppServices.Current.Settings.DrivableOnly;
foreach (var trainset in _listScenery.Trainsets)
{

View File

@@ -16,6 +16,7 @@ using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Domain.Sceneries;
using StarterNG.Domain.Vehicles;
using StarterNG.Application;
namespace StarterNG.Views;
@@ -35,7 +36,7 @@ public sealed class VehicleBrowser
public void SyncHideArchival()
{
bool hideArchival = Classes.Settings.Instance.HideArchivalVehicles;
bool hideArchival = AppServices.Current.Settings.HideArchivalVehicles;
if (hideArchivalCheck.IsChecked == hideArchival)
return;
@@ -246,7 +247,7 @@ public sealed class VehicleBrowser
_db.Textures
.Where(t => category == null || category(VehicleInfo.CategoryOf(t)))
.Where(t => !_db.IsSetFollower(t))
.Where(t => !StarterNG.Classes.Settings.Instance.HideArchivalVehicles || !t.ResolvedArchived)
.Where(t => !AppServices.Current.Settings.HideArchivalVehicles || !t.ResolvedArchived)
.Select(VehicleInfo.ClassOf)
.Where(s => !string.IsNullOrEmpty(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
@@ -469,7 +470,7 @@ public sealed class VehicleBrowser
if (_db.IsSetFollower(t))
return false;
if (StarterNG.Classes.Settings.Instance.HideArchivalVehicles && t.ResolvedArchived)
if (AppServices.Current.Settings.HideArchivalVehicles && t.ResolvedArchived)
return false;
if (hasSearch && !Matches(t, search))
@@ -515,8 +516,8 @@ public sealed class VehicleBrowser
public void HideArchivalCheck_OnChanged(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (_suppress) return;
StarterNG.Classes.Settings.Instance.HideArchivalVehicles = hideArchivalCheck.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.Save();
AppServices.Current.Settings.HideArchivalVehicles = hideArchivalCheck.IsChecked ?? true;
AppServices.Current.SettingsStore.Save();
Rebuild();
}

View File

@@ -1,4 +1,5 @@
using Avalonia.Media;
using StarterNG.Application;
namespace StarterNG.Views;
@@ -14,5 +15,5 @@ public static class VehicleCardStyle
public static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080"));
public static int ThumbHeight =>
Classes.Settings.Instance.LargeThumbnails ? 68 : 34;
AppServices.Current.Settings.LargeThumbnails ? 68 : 34;
}

View File

@@ -41,7 +41,7 @@ public partial class Scenarios : UserControl
Sceneries = AppServices.Current.Library.Sceneries;
_loadingFilters = true;
archivalSwitch.IsChecked = !StarterNG.Classes.Settings.Instance.ShowArchivalSceneries;
archivalSwitch.IsChecked = !AppServices.Current.Settings.ShowArchivalSceneries;
_loadingFilters = false;
BuildSceneryTree();
@@ -75,7 +75,7 @@ public partial class Scenarios : UserControl
if (scenery.Archival && !showArchival)
continue;
bool expand = StarterNG.Classes.Settings.Instance.AutoExpandSceneryTree;
bool expand = AppServices.Current.Settings.AutoExpandSceneryTree;
AppServices.Current.SceneryTexts.LoadFor(scenery, App.Loc.CurrentLangCode);
string label = scenery.DisplayName;
@@ -123,7 +123,7 @@ public partial class Scenarios : UserControl
private void RestoreLastScenery()
{
string last = StarterNG.Classes.Settings.Instance.LastScenery;
string last = AppServices.Current.Settings.LastScenery;
string want = Path.GetFileNameWithoutExtension(last);
TreeViewItem? MatchLeaf(TreeViewItem node)
@@ -193,10 +193,10 @@ public partial class Scenarios : UserControl
{
string name = scenery.DisplayName;
if (string.Equals(StarterNG.Classes.Settings.Instance.LastScenery, name, StringComparison.OrdinalIgnoreCase))
if (string.Equals(AppServices.Current.Settings.LastScenery, name, StringComparison.OrdinalIgnoreCase))
return;
StarterNG.Classes.Settings.Instance.LastScenery = name;
StarterNG.Classes.Settings.Instance.Save();
AppServices.Current.Settings.LastScenery = name;
AppServices.Current.SettingsStore.Save();
}
private static string TrainsetListLabel(Trainset trainset) =>
@@ -215,8 +215,8 @@ public partial class Scenarios : UserControl
{
vehicleList.Items.Clear();
bool showAi = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
bool drivableOnly = StarterNG.Classes.Settings.Instance.DrivableOnly;
bool showAi = AppServices.Current.Settings.ShowAiVehicles;
bool drivableOnly = AppServices.Current.Settings.DrivableOnly;
for (int i = 0; i < scn.Trainsets.Count; i++)
{
@@ -250,8 +250,8 @@ public partial class Scenarios : UserControl
{
if (_loadingFilters) return;
StarterNG.Classes.Settings.Instance.ShowArchivalSceneries = archivalSwitch.IsChecked != true;
StarterNG.Classes.Settings.Instance.Save();
AppServices.Current.Settings.ShowArchivalSceneries = archivalSwitch.IsChecked != true;
AppServices.Current.SettingsStore.Save();
BuildSceneryTree();
RestoreLastScenery();
@@ -405,7 +405,7 @@ public partial class Scenarios : UserControl
BuildSceneryTree();
if (!string.IsNullOrEmpty(selected))
{
StarterNG.Classes.Settings.Instance.LastScenery =
AppServices.Current.Settings.LastScenery =
Path.GetFileNameWithoutExtension(selected);
RestoreLastScenery();
}
@@ -428,7 +428,7 @@ public partial class Scenarios : UserControl
BuildSceneryTree();
if (!string.IsNullOrEmpty(selected))
{
StarterNG.Classes.Settings.Instance.LastScenery =
AppServices.Current.Settings.LastScenery =
Path.GetFileNameWithoutExtension(selected);
}
RestoreLastScenery();
@@ -895,7 +895,7 @@ public partial class Scenarios : UserControl
string miniName = db.MiniForSkin(train.SkinFile) ?? train.SkinFile;
if (!AppServices.Current.MiniTextures.Has(miniName) && AppServices.Current.MiniTextures.Has(train.SkinFile))
miniName = train.SkinFile;
int thumbH = StarterNG.Classes.Settings.Instance.LargeThumbnails ? 64 : 32;
int thumbH = AppServices.Current.Settings.LargeThumbnails ? 64 : 32;
var bmp = LoadMini(miniName, thumbH);
Control visual = bmp is not null

View File

@@ -19,10 +19,12 @@ using StarterNG.Infrastructure;
using StarterNG.Services;
using KeyBinding = StarterNG.Classes.KeyBinding;
using StarterNG.Domain.Settings;
using StarterNG.Application;
namespace StarterNG.Views;
public partial class Settings : UserControl
public partial class Settings : UserControl, ISettingsCapture
{
private bool _loading;
@@ -33,14 +35,14 @@ public partial class Settings : UserControl
public event Action? ThumbnailSizeChanged;
/// <summary>Thumbnail size the other tabs were last drawn at.</summary>
private bool _drawnThumbs = StarterNG.Classes.Settings.Instance.LargeThumbnails;
private bool _drawnThumbs = AppServices.Current.Settings.LargeThumbnails;
public Settings()
{
InitializeComponent();
SettingsPathText.Text = StarterNG.Classes.Settings.Instance.LoadedFrom;
ToolTip.SetTip(SettingsPathText, StarterNG.Classes.Settings.Instance.LoadedFrom);
SettingsPathText.Text = AppServices.Current.SettingsStore.LoadedFrom;
ToolTip.SetTip(SettingsPathText, AppServices.Current.SettingsStore.LoadedFrom);
this.AttachedToVisualTree += (_, _) =>
{
@@ -60,7 +62,7 @@ public partial class Settings : UserControl
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb);
StarterNG.Classes.Settings.Instance.CaptureFromUi = ReadFromUi;
AppServices.Current.SettingsStore.RegisterCapture(this);
PopulateProfileCombo();
ApplyToUi();
@@ -111,7 +113,7 @@ public partial class Settings : UserControl
return;
string path = SettingsProfileStore.PathFor(name);
if (!File.Exists(path)) return;
StarterNG.Classes.Settings.Instance.LoadFrom(path);
AppServices.Current.SettingsStore.LoadFrom(path);
ApplyToUi();
RedrawThumbnailsIfNeeded();
}
@@ -129,8 +131,8 @@ public partial class Settings : UserControl
void Commit()
{
if (string.IsNullOrWhiteSpace(nameBox.Text)) return;
ReadFromUi();
StarterNG.Classes.Settings.Instance.SaveTo(SettingsProfileStore.PathFor(nameBox.Text));
CaptureInto(AppServices.Current.Settings);
AppServices.Current.SettingsStore.SaveTo(SettingsProfileStore.PathFor(nameBox.Text));
PopulateProfileCombo();
int idx = FindComboIndexByContent(ProfileCb, nameBox.Text.Trim());
if (idx >= 0) ProfileCb.SelectedIndex = idx;
@@ -147,7 +149,7 @@ public partial class Settings : UserControl
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())
foreach (string exe in AppServices.Current.Executables.ListCandidates())
SelectExeCb.Items.Add(new ComboBoxItem { Content = exe });
if (current is not null)
{
@@ -160,7 +162,7 @@ public partial class Settings : UserControl
private void ApplyToUi()
{
var s = StarterNG.Classes.Settings.Instance;
var s = AppServices.Current.Settings;
_loading = true;
try
{
@@ -268,9 +270,12 @@ public partial class Settings : UserControl
UpdateQualityCaptions();
}
private void ReadFromUi()
/// <summary>
/// Writes the pending edits on this screen into the settings, so a save from
/// anywhere in the application picks them up.
/// </summary>
public void CaptureInto(SimulatorSettings s)
{
var s = StarterNG.Classes.Settings.Instance;
s.Language = (ChangeLanguageCb.SelectedItem as ComboBoxItem)?.Content?.ToString()
?? App.Loc.CurrentLanguage;
@@ -297,8 +302,8 @@ public partial class Settings : UserControl
s.BufferScalePercent = (int)bufferScale.Value;
s.MaxTextureSize = 1 << (int)textureResolutionSlider.Value;
s.MaxCabTextureSize = 1 << (int)cabTextureResolutionSlider.Value;
s.TextureFiltering = StarterNG.Classes.Settings.AnisotropySteps[
Clamp((int)TexFilteringSlider.Value - 1, 0, StarterNG.Classes.Settings.AnisotropySteps.Length - 1)];
s.TextureFiltering = SimulatorSettings.AnisotropySteps[
Clamp((int)TexFilteringSlider.Value - 1, 0, SimulatorSettings.AnisotropySteps.Length - 1)];
s.Multisampling = Clamp((int)MultisamplingSlider.Value - 1, 0, 3);
s.DynamicLights = (int)DynamicLightsSlider.Value;
s.DrawRangeFactor = RenderRangeSlider.Value;
@@ -363,8 +368,8 @@ public partial class Settings : UserControl
private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
{
ReadFromUi();
StarterNG.Classes.Settings.Instance.Save();
CaptureInto(AppServices.Current.Settings);
AppServices.Current.SettingsStore.Save();
KeyboardConfig.Instance.Save();
RedrawThumbnailsIfNeeded();
_dirty = false;
@@ -375,7 +380,7 @@ public partial class Settings : UserControl
private void RedrawThumbnailsIfNeeded()
{
bool large = StarterNG.Classes.Settings.Instance.LargeThumbnails;
bool large = AppServices.Current.Settings.LargeThumbnails;
if (large == _drawnThumbs)
return;
@@ -385,7 +390,7 @@ public partial class Settings : UserControl
private void ResetButton_OnClick(object? sender, RoutedEventArgs e)
{
StarterNG.Classes.Settings.Instance.Load();
AppServices.Current.SettingsStore.Load();
ApplyToUi();
RedrawThumbnailsIfNeeded();
CancelCapture();
@@ -413,7 +418,7 @@ public partial class Settings : UserControl
ResolutionCb.SelectedItem = added;
}
private void ReadResolution(Classes.Settings s)
private void ReadResolution(SimulatorSettings s)
{
var text = (ResolutionCb.SelectedItem as ComboBoxItem)?.Content?.ToString();
if (string.IsNullOrEmpty(text))
@@ -688,7 +693,7 @@ public partial class Settings : UserControl
private static int AnisotropyToSlider(int anisotropy)
{
var steps = StarterNG.Classes.Settings.AnisotropySteps;
var steps = SimulatorSettings.AnisotropySteps;
for (int i = 0; i < steps.Length; i++)
if (steps[i] == anisotropy)
return i + 1;

View File

@@ -2,7 +2,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using SettingsModel = StarterNG.Classes.Settings;
using StarterNG.Application;
namespace StarterNG.Views;
@@ -99,7 +99,7 @@ public sealed class UartWindow : Window
private void Load()
{
var s = SettingsModel.Instance;
var s = AppServices.Current.Settings;
_enabled.IsChecked = s.UartEnabled;
_port.Text = s.UartPort;
_tune.Text = s.UartTune;
@@ -114,7 +114,7 @@ public sealed class UartWindow : Window
private void Store()
{
var s = SettingsModel.Instance;
var s = AppServices.Current.Settings;
s.UartEnabled = _enabled.IsChecked == true;
s.UartPort = _port.Text ?? "";
s.UartTune = _tune.Text ?? "";