Files
StarterNG/StarterNG/Application/SettingsStore.cs
maj00r d95182f17c List a key taken out of eu07.ini once
A uart or uarttune the settings no longer write was found missing twice, as a
key of set length and as one the file had, and showed twice among the changes.
2026-11-03 21:51:54 +01:00

415 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using StarterNG.Application.Abstractions;
using StarterNG.Classes;
using StarterNG.Domain.Settings;
using StarterNG.Infrastructure.Settings;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Application;
public interface ISettingsCapture
{
void CaptureInto(SimulatorSettings settings);
/// <summary>
/// Keys the screen shows otherwise than the settings hold them - a value its
/// control cannot show - by key, in the file's terms.
/// </summary>
IReadOnlyDictionary<string, ShownValue> ShownUnlikeSettings => new Dictionary<string, ShownValue>();
}
/// <summary>A key as the screen shows it, and as the settings held it when it was put there; null for absent.</summary>
public sealed record ShownValue(string? Shown, string? Held);
/// <summary>A key a save changes: its value in the file before and after; null where it has none.</summary>
public sealed record IniChange(string Key, string? Before, string? After);
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;
// The settings in the file's terms when it was last read or written. A key
// they still put the same way was not changed here, so the file keeps it as
// it has it: a value outside what the starter offers, its spacing, its comment.
private IReadOnlyDictionary<string, string> _synced = new Dictionary<string, string>();
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 = LegacyText.CodePage1250;
}
public SimulatorSettings Settings { get; } = new();
public string LoadedFrom { get; private set; } = string.Empty;
/// <summary>
/// The last read failed, so <see cref="Settings"/> holds defaults, not the file's.
/// Nothing is saved until a read works: that would put them in its place.
/// </summary>
public bool LoadFailed { get; private set; }
/// <summary>The last save did not reach the file; the log says why.</summary>
public bool LastSaveFailed { get; private set; }
public string SavePath => _savePath;
public void RegisterCapture(ISettingsCapture capture) => _capture = capture;
public void Load()
{
_savePath = _paths.UserConfigPath();
string source = _files.FileExists(_savePath) ? _savePath : _paths.InstallationConfigPath();
LoadedFrom = source;
LoadFrom(source);
}
/// <summary>A player with no settings of their own yet starts at High, in the file at once so the simulator goes by it too.</summary>
public void StartAtHighWithoutOwnSettings()
{
if (LoadFailed || _files.FileExists(_savePath))
return;
GraphicsPreset.High.ApplyTo(Settings);
Save();
}
public void LoadFrom(string path)
{
try
{
_config = _files.FileExists(path)
? ConfigFile.Parse(_files.ReadAllText(path, _encoding))
: new ConfigFile();
LoadFailed = false;
}
catch (Exception ex)
{
_log.Log($"reading {path}", ex);
_config = new ConfigFile();
LoadFailed = true;
}
_serializer.Read(Settings, _config);
_synced = Render(Settings);
TouchConfigAge();
}
/// <summary>
/// Reads a profile into the settings; true when it read cleanly. A read that failed
/// would leave defaults to be saved over the real settings, so those are read again.
/// </summary>
public bool TryLoadProfile(string path)
{
if (_files.FileExists(path))
{
LoadFrom(path);
if (!LoadFailed)
return true;
}
if (LoadFailed)
Load();
return false;
}
public void CaptureAndSave()
{
_capture?.CaptureInto(Settings);
Save();
}
public void Save()
{
if (string.IsNullOrEmpty(_savePath))
_savePath = _paths.UserConfigPath();
SaveTo(_savePath);
}
private void SaveTo(string path)
{
LastSaveFailed = true;
if (LoadFailed)
{
_log.Log($"{path} not saved: the settings could not be read, what is held is defaults");
return;
}
ApplyChanges(_config, Changes(Settings));
_synced = Render(Settings);
try
{
string? directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
_files.CreateDirectory(directory);
_files.WriteAllBytesAtomic(path, _encoding.GetBytes(_config.ToText()));
TouchConfigAge();
LastSaveFailed = false;
}
catch (Exception ex)
{
_log.Log($"saving {path}", ex);
}
}
private static void ApplyChanges(ConfigFile config, IReadOnlyList<IniChange> changes)
{
foreach (var change in changes)
{
if (change.After is null)
config.Remove(change.Key);
else
config.Set(change.Key, change.After);
}
// Each sets the mode as the simulator reads it, so the new one has to come later to count.
// Untouched, the two keep their order: moving them would change which one decides.
if (changes.Any(change => change.Key is "gfx.antialiasing" or "multisampling"))
config.MoveAfter("gfx.antialiasing", "multisampling");
}
/// <summary>Whether the simulator can have DLSS on this platform at all.</summary>
public bool DlssPlatform => _serializer.DlssPlatform;
/// <summary>
/// Takes the settings of another eu07.ini (a backup, a file from another machine)
/// into the current ones, keeping what belongs to this installation: the chosen
/// simulator, its card, the last scenery and the window state. The lines of this
/// user's file the starter does not manage stay as they are. Nothing is written until Save.
/// </summary>
public bool ImportFrom(string path)
{
ConfigFile imported;
try
{
imported = ConfigFile.Parse(_files.ReadAllText(path, _encoding));
}
catch (Exception ex)
{
_log.Log($"importing {path}", ex);
return false;
}
var s = Settings;
bool selectAutomatically = s.SelectExeAutomatically;
string executable = s.ExecutablePath;
var gpu = s.Gpu;
string lastScenery = s.LastScenery;
bool maximized = s.WindowMaximized;
// Only read for the dependencies and never written, so they stay as this user's file has them.
bool cabLampLight = s.CabLampLight;
bool depthPrepassScene = s.DepthPrepassScene;
_serializer.Read(s, imported);
s.SelectExeAutomatically = selectAutomatically;
s.ExecutablePath = executable;
s.Gpu = gpu;
s.LastScenery = lastScenery;
s.WindowMaximized = maximized;
s.CabLampLight = cabLampLight;
s.DepthPrepassScene = depthPrepassScene;
return true;
}
/// <summary>
/// Writes the settings as a complete eu07.ini somewhere else - this user's file
/// with the settings applied - leaving the file itself and its state alone.
/// </summary>
public bool ExportTo(string path)
{
if (LoadFailed)
{
_log.Log($"{path} not written: the settings could not be read, what is held is defaults");
return false;
}
var copy = ConfigFile.Parse(_config.ToText());
ApplyChanges(copy, Changes(Settings));
try
{
string? directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
_files.CreateDirectory(directory);
_files.WriteAllBytesAtomic(path, _encoding.GetBytes(copy.ToText()));
return true;
}
catch (Exception ex)
{
_log.Log($"exporting {path}", ex);
return false;
}
}
/// <summary>Every key as <paramref name="settings"/> would put it, in the order they are written.</summary>
public IReadOnlyDictionary<string, string> Render(SimulatorSettings settings)
{
var config = new ConfigFile();
_serializer.Write(settings, config);
var keys = new OrderedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var (key, value) in config.Entries())
keys[key] = value;
return keys;
}
/// <summary>
/// What saving <paramref name="settings"/> would change in the file: the keys
/// changed since it was read, those it lacks, and the ones no simulator reads.
/// A key the screen shows otherwise than the file has it is left alone until
/// moved. One the file leaves bare or past endconfig counts as lacking.
/// </summary>
public IReadOnlyList<IniChange> Changes(SimulatorSettings settings)
{
var now = Render(settings);
var shown = _capture?.ShownUnlikeSettings ?? new Dictionary<string, ShownValue>();
var changes = new List<IniChange>();
var listed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string key in SettingsSerializer.ObsoleteKeys)
Add(key, null);
foreach (string key in SettingsSerializer.FixedLengthKeys.Where(key => !now.ContainsKey(key)))
Add(key, null);
foreach (string key in SettingsSerializer.OptionalKeys.Concat(SimulatorKeys.All.Select(key => key.Key)))
if (!now.ContainsKey(key) && _config.GetRaw(key) is { Length: 0 })
Add(key, null);
foreach (var (key, value) in now)
if (!Kept(key, value))
Add(key, value);
foreach (string key in _synced.Keys.Where(key => !now.ContainsKey(key)))
if (!Kept(key, null))
Add(key, null);
return changes;
bool Kept(string key, string? value)
{
// Past endconfig the simulator never gets to it; bare, it takes the next
// word of the file for its value.
if (_config.IsPastEnd(key) || _config.GetRaw(key) is { Length: 0 })
return false;
if (string.Equals(key, "uartfeature", StringComparison.OrdinalIgnoreCase))
return Uart.FeaturesStand(_config.GetRaw(key), value);
if (SettingsSerializer.FixedLengthKeys.Contains(key, StringComparer.OrdinalIgnoreCase))
return _config.GetRaw(key) == value;
string? synced = _synced.GetValueOrDefault(key);
if (shown.TryGetValue(key, out var onScreen) && onScreen.Shown == value && onScreen.Held == synced)
return true;
return _config.Has(key) && synced == value;
}
void Add(string key, string? value)
{
// To the simulator a key past endconfig is not set yet, whatever it says.
string? before = value is not null && _config.IsPastEnd(key) ? null : _config.GetRaw(key);
// A key gone from the file can be found gone more ways than one.
if (before != value && listed.Add(key))
changes.Add(new IniChange(key, before, value));
}
}
/// <summary>
/// The keys <paramref name="shown"/> puts otherwise than the settings hold them,
/// with both values, in the file's terms.
/// </summary>
public IReadOnlyDictionary<string, ShownValue> ShownUnlike(SimulatorSettings shown)
{
var held = Render(Settings);
var now = Render(shown);
var unlike = new Dictionary<string, ShownValue>(StringComparer.OrdinalIgnoreCase);
foreach (string key in held.Keys.Union(now.Keys, StringComparer.OrdinalIgnoreCase))
{
string? before = held.GetValueOrDefault(key), after = now.GetValueOrDefault(key);
if (before != after)
unlike[key] = new ShownValue(after, before);
}
return unlike;
}
/// <summary>
/// A copy of <paramref name="settings"/> with <paramref name="keys"/> back at
/// what the simulator goes by when eu07.ini does not set them.
/// </summary>
public SimulatorSettings WithDefaults(SimulatorSettings settings, IReadOnlyCollection<string> keys)
{
var defaults = new SimulatorSettings();
_serializer.Read(defaults, new ConfigFile());
return WithKeysOf(settings, defaults, keys);
}
/// <summary>A copy of <paramref name="settings"/> with <paramref name="keys"/> as <paramref name="source"/> has them.</summary>
public SimulatorSettings WithKeysOf(SimulatorSettings settings, SimulatorSettings source, IReadOnlyCollection<string> keys)
{
var taken = Render(source);
var config = new ConfigFile();
foreach (var (key, value) in Render(settings))
if (!keys.Contains(key, StringComparer.OrdinalIgnoreCase))
config.Set(key, value);
foreach (string key in keys)
if (taken.TryGetValue(key, out string? value))
config.Set(key, value);
var copy = settings.Copy();
_serializer.Read(copy, config);
return copy;
}
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;
}
}
public bool IsConfigNewerOnDisk()
{
string path = string.IsNullOrEmpty(_savePath) ? _paths.UserConfigPath() : _savePath;
try
{
if (!_files.FileExists(path) || _configAgeUtc == DateTime.MinValue)
return false;
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);
}