using StarterNG.Domain.Settings;
using StarterNG.Tests.Fakes;
namespace StarterNG.Tests;
public class SettingsStoreTests
{
// Joined the way SettingsPaths joins it, so it matches on a Windows host too.
private static readonly string UserIni = Path.Combine("/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;
}
///
/// The saved file with each of its lines ended by \n, whichever ending the host
/// writes, so a check for "key value\n" holds the line to exactly that.
///
private static string SavedLines(TestInstallation installation) =>
installation.Files.ReadAllText(UserIni).Replace("\r\n", "\n") + "\n";
/// What the file says for a key, its value included; null when no line sets it.
private static string? LineFor(TestInstallation installation, string key) =>
SavedLines(installation).Split('\n')
.SingleOrDefault(line => line == key || line.StartsWith(key + " "));
///
/// Simulator keys left with no value. The simulator would take the next word of
/// the file for it; a starter.* key is exempt, as it skips those.
///
private static string[] BareSimulatorKeys(TestInstallation installation) =>
SavedLines(installation).Split('\n')
.Select(line => line.Trim())
.Where(line => line.Length > 0 && !line.StartsWith("//") && !line.Any(char.IsWhiteSpace))
.Where(key => !key.StartsWith("starter.", StringComparison.OrdinalIgnoreCase) &&
!key.Equals("endconfig", StringComparison.OrdinalIgnoreCase))
.ToArray();
[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("pl", settings.LanguageCode);
Assert.True(settings.LanguageWasSet);
Assert.Equal(UserIni, installation.SettingsStore.LoadedFrom);
}
[Fact]
public void A_language_the_starter_does_not_ship_survives_a_round_trip()
{
// A user-supplied translation must reach the simulator as written; folding
// every unknown code into "en" is what forced eu07.exe into English.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "lang zh\n"));
installation.SettingsStore.Load();
Assert.Equal("zh", installation.SettingsStore.Settings.LanguageCode);
installation.SettingsStore.Save();
Assert.Contains("lang zh", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_language_chosen_in_the_picker_is_written_as_its_own_code()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Settings.Language = "中文";
installation.SettingsStore.Settings.LanguageCode = "zh";
installation.SettingsStore.Save();
Assert.Contains("lang zh", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void Uart_features_with_nothing_selected_are_written_out_in_full()
{
var installation = WithHome();
installation.SettingsStore.Load();
var settings = installation.SettingsStore.Settings;
settings.UartMain = settings.UartScnd = settings.UartTrain = settings.UartLocal = false;
installation.SettingsStore.Save();
var lines = installation.Files.ReadAllText(UserIni).Split('\n').Select(l => l.Trim());
Assert.Contains("uartfeature no no no no", lines);
Assert.DoesNotContain("uartfeature", lines);
}
[Theory]
[InlineData("uartfeature\nuartdebug no\n", false, false)]
[InlineData("uartfeature none\n", false, false)]
[InlineData("uartfeature no no no no\n", false, false)]
[InlineData("uartfeature yes yes yes yes\n", true, true)]
[InlineData("uartfeature YES no no no\n", true, false)]
[InlineData("uartfeature main|scnd|\n", true, true)]
public void Uart_features_are_read_in_every_form_the_simulator_takes(string file, bool main, bool scnd)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, file));
installation.SettingsStore.Load();
Assert.Equal(main, installation.SettingsStore.Settings.UartMain);
Assert.Equal(scnd, installation.SettingsStore.Settings.UartScnd);
}
[Theory]
[InlineData("")]
[InlineData("uartdebug no")]
[InlineData("1 2 3")]
public void A_uart_tune_that_is_not_eighteen_numbers_is_not_written(string tune)
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Settings.UartTune = tune;
installation.SettingsStore.Save();
Assert.DoesNotContain(installation.Files.ReadAllText(UserIni).Split('\n'),
line => line.TrimStart().StartsWith("uarttune", StringComparison.Ordinal));
}
[Fact]
public void With_no_language_recorded_the_file_still_names_one()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("lang en", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void The_language_code_is_written_in_lower_case()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Settings.LanguageCode = "ZH";
installation.SettingsStore.Save();
Assert.Contains("lang zh", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void The_consist_state_override_keeps_the_key_the_old_starter_used()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "starter.batterydefault 2\n"));
installation.SettingsStore.Load();
Assert.Equal(ConsistReadyOverride.AlwaysReady, installation.SettingsStore.Settings.ReadyOverride);
installation.SettingsStore.Settings.ReadyOverride = ConsistReadyOverride.AlwaysCold;
installation.SettingsStore.Save();
Assert.Contains("starter.batterydefault 1", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void An_out_of_range_override_falls_back_to_leaving_the_scenery_alone()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "starter.batterydefault 9\n"));
installation.SettingsStore.Load();
Assert.Equal(ConsistReadyOverride.Scenery, installation.SettingsStore.Settings.ReadyOverride);
}
[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);
}
[Theory]
[InlineData("uarttune uartdebug no\n", "")]
[InlineData("uarttune 1 2 3\n", "1 2 3")]
public void A_calibration_is_shown_only_if_the_file_holds_numbers_for_it(string line, string shown)
{
// The next key of the file on the same line is no calibration, but a wrong count of numbers is one to fix.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, line));
installation.SettingsStore.Load();
Assert.Equal(shown, installation.SettingsStore.Settings.UartTune);
}
[Theory]
[InlineData("uarttune COM3\n", null)]
[InlineData("", null)]
[InlineData("uarttune 0 65535 0 65535 10 65535 10 65535 10 65535 10 65535 100000 65535 10000 65535 150 65535\n",
"uarttune 0 65535 0 65535 10 65535 10 65535 10 65535 10 65535 100000 65535 10000 65535 150 65535")]
public void The_uart_calibration_is_written_only_as_its_eighteen_numbers(string line, string? written)
{
// Anything else and the simulator reads the next keys of the file for them:
// maxcabtexturesize 2048 went so into one.
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, line + "maxcabtexturesize 2048\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
var lines = installation.Files.ReadAllText(UserIni).Split('\n').Select(l => l.TrimEnd('\r'));
Assert.Equal(written, lines.SingleOrDefault(l => l.StartsWith("uarttune")));
}
[Fact]
public void The_uart_line_names_port_speed_and_interval()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "uart /dev/ttyACM0 115200 0.05\n"));
installation.SettingsStore.Load();
var s = installation.SettingsStore.Settings;
Assert.True(s.UartEnabled);
Assert.Equal("/dev/ttyACM0", s.UartPort);
Assert.Equal(115200, s.UartBaud);
Assert.Equal(0.05, s.UartUpdateTime);
s.UartPort = "COM3";
installation.SettingsStore.Save();
Assert.Contains("uart COM3 115200 0.05", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_bare_uart_port_is_completed_rather_than_left_to_swallow_the_next_keys()
{
// The simulator reads three words; "uart COM3" alone took the next line's key
// and value for the speed and interval.
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "uart COM3\nmaxcabtexturesize 2048\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
var text = installation.Files.ReadAllText(UserIni);
Assert.Contains($"uart COM3 {Uart.DefaultBaud} 0.1", text);
Assert.Contains("maxcabtexturesize 2048", text);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("COM 3")]
public void Without_a_one_word_port_no_uart_line_is_written(string port)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "uart COM3 57600 0.1\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.UartPort = port;
installation.SettingsStore.Save();
var lines = installation.Files.ReadAllText(UserIni).Split('\n');
Assert.DoesNotContain(lines, l => l.StartsWith("uart "));
}
[Fact]
public void A_disabled_uart_leaves_no_uart_line()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "uart COM3 57600 0.1\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.UartEnabled = false;
installation.SettingsStore.Save();
var lines = installation.Files.ReadAllText(UserIni).Split('\n');
Assert.DoesNotContain(lines, l => l.StartsWith("uart "));
}
[Fact]
public void The_graphics_card_choice_survives_a_round_trip()
{
var installation = WithHome();
installation.SettingsStore.Load();
Assert.Equal(GpuPreference.Discrete, installation.SettingsStore.Settings.Gpu);
installation.SettingsStore.Settings.Gpu = GpuPreference.System;
installation.SettingsStore.Save();
Assert.Contains("starter.gpu system", installation.Files.ReadAllText(UserIni));
var reopened = WithHome(installation.Files);
reopened.SettingsStore.Load();
Assert.Equal(GpuPreference.System, reopened.SettingsStore.Settings.Gpu);
}
[Fact]
public void A_silenced_processor_notice_stays_silent()
{
var installation = WithHome();
installation.SettingsStore.Load();
Assert.True(installation.SettingsStore.Settings.CpuNotice);
installation.SettingsStore.Settings.CpuNotice = false;
installation.SettingsStore.Save();
var reopened = WithHome(installation.Files);
reopened.SettingsStore.Load();
Assert.False(reopened.SettingsStore.Settings.CpuNotice);
}
[Fact]
public void Uart_features_all_off_are_written_as_four_noes()
{
// "none" only newer simulators read; every build reads the four words.
var installation = WithHome();
installation.SettingsStore.Load();
var s = installation.SettingsStore.Settings;
s.UartMain = s.UartScnd = s.UartTrain = s.UartLocal = s.UartRadioVolume = s.UartRadioChannel = false;
installation.SettingsStore.Save();
Assert.Equal("uartfeature no no no no", LineFor(installation, "uartfeature"));
}
[Fact]
public void Dynamic_brake_alone_is_written_in_the_pipe_form()
{
// The four words cannot say it, and would switch it off.
var installation = WithHome();
installation.SettingsStore.Load();
var s = installation.SettingsStore.Settings;
s.UartMain = s.UartScnd = s.UartTrain = s.UartLocal = false;
s.UartDynamicBrake = true;
installation.SettingsStore.Save();
Assert.Equal("uartfeature dynamicbrake|", LineFor(installation, "uartfeature"));
}
[Theory]
// Globals.cpp: a first word with a pipe, "none" or a feature's name is the list.
[InlineData("main|scnd|train|local|radiovolume|radiochannel|dynamicbrake", "main scnd train local radiovolume radiochannel dynamicbrake")]
[InlineData("main|dynamicbrake|", "main dynamicbrake")]
[InlineData("MAIN|Train", "main train")]
[InlineData("main", "main")]
[InlineData("dynamicbrake", "dynamicbrake")]
[InlineData("nonsense|local", "local")]
[InlineData("none", "")]
[InlineData("NONE", "")]
// Anything else four words, for the first four features: parser.cpp takes true, yes and 1.
[InlineData("yes yes yes yes", "main scnd train local")]
[InlineData("no no no no", "")]
[InlineData("YES no Yes NO", "main train")]
[InlineData("1 0 1 0", "main train")]
[InlineData("true false false true", "main local")]
// Bare: nothing selected, whatever the next line says.
[InlineData("", "")]
public void A_uartfeature_line_is_read_as_the_simulator_reads_it(string value, string features)
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, $"uartfeature {value}\nwidth 800\n"));
installation.SettingsStore.Load();
var s = installation.SettingsStore.Settings;
var on = new List();
if (s.UartMain) on.Add("main");
if (s.UartScnd) on.Add("scnd");
if (s.UartTrain) on.Add("train");
if (s.UartLocal) on.Add("local");
if (s.UartRadioVolume) on.Add("radiovolume");
if (s.UartRadioChannel) on.Add("radiochannel");
if (s.UartDynamicBrake) on.Add("dynamicbrake");
Assert.Equal(features, string.Join(' ', on));
Assert.Equal(800, s.Width);
}
[Theory]
[InlineData("main|scnd|train|local|radiovolume|radiochannel|dynamicbrake")]
[InlineData("main|dynamicbrake")]
[InlineData("dynamicbrake")]
[InlineData("yes yes yes yes")]
[InlineData("no yes no YES")]
[InlineData("no no no no")]
public void A_uartfeature_line_nobody_changed_stays_as_the_file_has_it(string value)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, $"uartfeature {value}\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Equal($"uartfeature {value}", LineFor(installation, "uartfeature"));
}
[Theory]
[InlineData("uartfeature none", "uartfeature no no no no")]
[InlineData("uartfeature", "uartfeature no no no no")]
// Short of its four words, the simulator would read the next key for the rest.
[InlineData("uartfeature yes yes", "uartfeature main|scnd|")]
[InlineData("uartfeature nonsense", "uartfeature no no no no")]
public void A_uartfeature_line_not_every_simulator_reads_whole_is_put_right(string line, string written)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, line + "\nmaxcabtexturesize 2048\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Equal(written, LineFor(installation, "uartfeature"));
Assert.Equal("maxcabtexturesize 2048", LineFor(installation, "maxcabtexturesize"));
}
[Theory]
[InlineData("yes yes yes yes", "uartfeature scnd|train|local|")]
[InlineData("main|dynamicbrake", "uartfeature dynamicbrake|")]
public void A_uart_feature_taken_off_rewrites_the_line_in_the_pipe_form(string value, string written)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, $"uartfeature {value}\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.UartMain = false;
installation.SettingsStore.Save();
Assert.Equal(written, LineFor(installation, "uartfeature"));
}
[Fact]
public void The_uart_interval_is_shown_within_what_its_box_holds()
{
Assert.Equal(0.05m, Uart.ShownUpdateTime(0.05));
// 1e30 is more than a decimal can hold: the window died on opening.
Assert.Equal(10m, Uart.ShownUpdateTime(1e30));
Assert.Equal(10m, Uart.ShownUpdateTime(double.PositiveInfinity));
Assert.Equal((decimal)Uart.DefaultUpdateTime, Uart.ShownUpdateTime(double.NaN));
Assert.Equal(0m, Uart.ShownUpdateTime(-1));
}
[Theory]
[InlineData("mousescale 1.5 0.5", false, true, "mousescale 1.5 -0.5")]
[InlineData("mousescale 1.5 0.5", true, false, "mousescale -1.5 0.5")]
[InlineData("mousescale -1.5 -0.5", false, false, "mousescale 1.5 0.5")]
[InlineData("mousescale 2.5 0.123456", false, true, "mousescale 2.5 -0.123456")]
// Further than the slider goes.
[InlineData("mousescale 12 0.5", true, false, "mousescale -12 0.5")]
public void A_mouse_scale_keeps_its_numbers_when_an_axis_is_inverted(
string line, bool invertX, bool invertY, string written)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, line + "\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.InvertMouseHorizontal = invertX;
installation.SettingsStore.Settings.InvertMouseVertical = invertY;
installation.SettingsStore.Save();
Assert.Equal(written, LineFor(installation, "mousescale"));
}
[Fact]
public void A_mouse_scale_nobody_changed_stays_as_the_file_has_it()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "mousescale 1.5 0.5\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Equal("mousescale 1.5 0.5", LineFor(installation, "mousescale"));
}
[Fact]
public void Moving_the_sensitivity_replaces_the_scale_it_stood_for()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "mousescale 1.5 0.5\n"));
installation.SettingsStore.Load();
Assert.Equal(2, installation.SettingsStore.Settings.CursorSensitivity);
installation.SettingsStore.Settings.CursorSensitivity = 3;
installation.SettingsStore.Save();
Assert.Equal("mousescale 3 0.5", LineFor(installation, "mousescale"));
}
[Fact]
public void The_default_mouse_scale_is_written_as_before()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Equal("mousescale 2 0.2", LineFor(installation, "mousescale"));
}
// ── A file that could not be read ────────────────────────────────────────
[Fact]
public void A_file_that_could_not_be_read_is_not_saved_over()
{
const string held = "width 1920\nsomethingexotic 42\n";
var files = new InMemoryFileSystem().WithFile(UserIni, held).FailReading(UserIni);
var installation = WithHome(files);
installation.SettingsStore.Load();
Assert.True(installation.SettingsStore.LoadFailed);
Assert.Equal(1280, installation.SettingsStore.Settings.Width);
installation.SettingsStore.Save();
installation.SettingsStore.CaptureAndSave();
files.AllowReading(UserIni);
Assert.Equal(held, files.ReadAllText(UserIni));
Assert.Contains("not saved", installation.Log.Text);
// The screen must not say "saved" over this.
Assert.True(installation.SettingsStore.LastSaveFailed);
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.False(installation.SettingsStore.LastSaveFailed);
}
[Fact]
public void A_profile_of_the_settings_is_not_written_from_defaults_either()
{
const string profile = "/home/kolejarz/profile.ini";
var files = new InMemoryFileSystem().WithFile(UserIni, "width 1920\n").FailReading(UserIni);
var installation = WithHome(files);
installation.SettingsStore.Load();
installation.SettingsStore.SaveTo(profile);
Assert.False(files.FileExists(profile));
}
[Fact]
public void A_read_that_works_again_lets_the_settings_be_saved()
{
var files = new InMemoryFileSystem().WithFile(UserIni, "width 1920\n").FailReading(UserIni);
var installation = WithHome(files);
installation.SettingsStore.Load();
files.AllowReading(UserIni);
installation.SettingsStore.Load();
Assert.False(installation.SettingsStore.LoadFailed);
installation.SettingsStore.Settings.Height = 1080;
installation.SettingsStore.Save();
Assert.Equal("width 1920", LineFor(installation, "width"));
Assert.Equal("height 1080", LineFor(installation, "height"));
}
[Fact]
public void A_shipped_ini_that_could_not_be_read_leaves_no_file_of_defaults_in_its_way()
{
// The user's own file, once there, is the one read from then on.
var files = new InMemoryFileSystem()
.WithFile(TestInstallation.At("eu07.ini"), "width 800\n")
.FailReading(TestInstallation.At("eu07.ini"));
var installation = WithHome(files);
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.False(files.FileExists(UserIni));
}
// ── endconfig ────────────────────────────────────────────────────────────
[Fact]
public void A_key_the_file_lacks_goes_before_endconfig()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "width 1920\nendconfig\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
var lines = SavedLines(installation).Split('\n').ToList();
int end = lines.IndexOf("endconfig");
Assert.True(end > lines.IndexOf("fullscreen no") && lines.IndexOf("fullscreen no") >= 0);
Assert.DoesNotContain(lines.Skip(end + 1), line => line.Trim().Length > 0);
}
[Fact]
public void A_key_an_older_save_left_past_endconfig_moves_before_it()
{
// Set there by this starter's own earlier save, and never read by the simulator.
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "width 1920\nendconfig\nfieldofview 60\nnotes about this file\n"));
var store = installation.SettingsStore;
store.Load();
Assert.Equal(60, store.Settings.FieldOfView);
Assert.Contains(new StarterNG.Application.IniChange("fieldofview", null, "60"), store.Changes(store.Settings));
store.Save();
var lines = SavedLines(installation).Split('\n').ToList();
Assert.True(lines.IndexOf("fieldofview 60") < lines.IndexOf("endconfig"));
Assert.Single(lines, "fieldofview 60");
// What is no key of the starter's stays where the simulator does not read it.
Assert.True(lines.IndexOf("notes about this file") > lines.IndexOf("endconfig"));
}
[Fact]
public void A_disabled_uart_past_endconfig_is_taken_out_rather_than_moved()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "endconfig\nuart COM3 57600 0.1\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.UartEnabled = false;
installation.SettingsStore.Save();
var lines = installation.Files.ReadAllText(UserIni).Replace("\r\n", "\n").Split('\n');
Assert.DoesNotContain(lines, l => l.StartsWith("uart "));
}
// ── No bare keys ─────────────────────────────────────────────────────────
[Fact]
public void Saving_the_default_settings_leaves_no_simulator_key_bare()
{
// A bare key takes the next word of the file for its value: uartfeature the
// next four, which is how keys after it went unread.
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Empty(BareSimulatorKeys(installation));
}
[Fact]
public void Everything_emptied_leaves_no_simulator_key_bare()
{
var installation = WithHome();
installation.SettingsStore.Load();
var s = installation.SettingsStore.Settings;
s.LanguageCode = " ";
s.UartEnabled = true;
s.UartPort = " ";
s.UartTune = " ";
s.UartMain = s.UartScnd = s.UartTrain = s.UartLocal = s.UartRadioVolume = s.UartRadioChannel = false;
s.ExecutablePath = "";
s.LastScenery = "";
foreach (var key in SimulatorKeys.All)
s.Extra[key.Key] = " ";
installation.SettingsStore.Save();
Assert.Empty(BareSimulatorKeys(installation));
}
[Theory]
[InlineData("uart")]
[InlineData("uarttune")]
[InlineData("uartfeature")]
[InlineData("mousescale")]
[InlineData("fullscreen")]
[InlineData("width")]
[InlineData("screenshotsdir")]
[InlineData("fpslimit")]
[InlineData("gfx.angleplatform")]
public void A_key_the_file_itself_leaves_bare_does_not_stay_so(string key)
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, $"{key}\nmaxcabtexturesize 2048\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Empty(BareSimulatorKeys(installation));
Assert.Equal("maxcabtexturesize 2048", LineFor(installation, "maxcabtexturesize"));
}
[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());
}
[Theory]
[InlineData("144")]
[InlineData("360")]
[InlineData("59.94")]
public void A_frame_rate_limit_the_slider_once_cut_to_100_survives_a_save(string limit)
{
// The simulator takes any rate above zero.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, $"fpslimit {limit}\n"));
installation.SettingsStore.Load();
Assert.True(installation.SettingsStore.Settings.FpsLimitEnabled);
installation.SettingsStore.Save();
Assert.Contains($"fpslimit {limit}", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_frame_rate_limit_of_zero_is_no_limit()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fpslimit 0\n"));
installation.SettingsStore.Load();
Assert.False(installation.SettingsStore.Settings.FpsLimitEnabled);
}
[Theory]
[InlineData("1")]
[InlineData("0.5")]
[InlineData("60")]
public void A_reflection_rate_below_the_old_slider_survives_a_save(string rate)
{
// The game's own eu07.ini has 1; the slider's floor of 5 raised it on any save.
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, $"gfx.reflections.framerate {rate}\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains($"gfx.reflections.framerate {rate}\n", SavedLines(installation));
}
[Fact]
public void Without_a_reflection_rate_the_simulators_own_is_written()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("gfx.reflections.framerate 0.003333", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void The_shadow_range_is_the_third_number_of_shadowtune()
{
// map size, unread, projection range, unread - as the game's eu07.ini says.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "shadowtune 1024 100 150 300\n"));
installation.SettingsStore.Load();
Assert.Equal(1024, installation.SettingsStore.Settings.ShadowMapResolution);
Assert.Equal(150, installation.SettingsStore.Settings.ShadowProjectionRange);
installation.SettingsStore.Settings.ShadowProjectionRange = 200;
installation.SettingsStore.Save();
Assert.Contains("shadowtune 1024 100 200 300", installation.Files.ReadAllText(UserIni));
}
[Theory]
[InlineData(1024, 75)]
[InlineData(2048, 75)]
[InlineData(4096, 150)]
[InlineData(8192, 300)]
public void The_shadow_range_floor_follows_the_map_size(int mapSize, int floor) =>
Assert.Equal(floor, SimulatorSettings.ShadowRangeFloor(mapSize));
[Theory]
[InlineData(150, 1024, false, 150)]
[InlineData(150, 4096, false, 300)]
[InlineData(150, 4096, true, 150)]
[InlineData(100, 8192, false, 1200)]
[InlineData(400, 8192, false, 1600)]
public void Shadows_reach_as_far_as_the_renderer_stretches_them(int range, int mapSize, bool legacy, int reach) =>
Assert.Equal(reach, SimulatorSettings.ShadowReach(range, mapSize, legacy));
[Theory]
[InlineData("anisotropicfiltering 12")]
[InlineData("multisampling 4")]
public void A_value_the_slider_does_not_offer_survives_a_save(string line)
{
// x16 multisampling and an odd anisotropy are the simulator's to take.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, line + "\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains(line, installation.Files.ReadAllText(UserIni));
}
[Theory]
[InlineData("default")]
[InlineData("simpleshader")]
[InlineData("off")]
public void A_renderer_word_the_simulator_reads_as_another_stays_until_changed(string word)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, $"gfxrenderer {word}\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains($"gfxrenderer {word}\n", SavedLines(installation));
}
[Theory]
[InlineData(0, "full")]
[InlineData(1, "legacy")]
[InlineData(2, "simple")]
[InlineData(3, "experimental")]
public void A_chosen_renderer_is_saved_as_the_simulator_reads_it(int engine, string saved)
{
// application.cpp knows full (or default), experimental and simple; every
// other word - "simpleshader" and "off" from older starters - is legacy.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "gfxrenderer simpleshader\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.RenderEngine = engine == 1 ? 0 : engine;
installation.SettingsStore.Save();
installation.SettingsStore.Settings.RenderEngine = engine;
installation.SettingsStore.Save();
Assert.Contains($"gfxrenderer {saved}\n", SavedLines(installation));
}
[Fact]
public void A_field_of_view_down_to_ten_degrees_survives_a_save()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fieldofview 10\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("fieldofview 10", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_silenced_radio_stays_silent()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "sound.volume.radio 0\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("sound.volume.radio 0\n", SavedLines(installation));
}
/// Stands in for the settings screen holding an unsaved edit.
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;
}
[Fact]
public void A_simulator_key_nobody_touched_stays_out_of_the_file()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.DoesNotContain("basedrawrange", installation.Files.ReadAllText(UserIni));
Assert.DoesNotContain("ui.scale", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_simulator_key_keeps_its_value_and_comment_through_a_save()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "basedrawrange 15000\t// further than the slider goes\n"));
installation.SettingsStore.Load();
Assert.Equal("15000", installation.SettingsStore.Settings.Extra["basedrawrange"]);
installation.SettingsStore.Save();
Assert.Contains("basedrawrange 15000\t// further than the slider goes",
installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_changed_simulator_key_is_written()
{
var installation = WithHome();
installation.SettingsStore.Load();
installation.SettingsStore.Settings.Extra["shakefactor"] =
SimulatorKeys.All.Single(k => k.Key == "shakefactor").FormatNumber(0.5);
installation.SettingsStore.Save();
Assert.Contains("shakefactor 0.5 0.5 0.5", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void An_emptied_word_is_taken_out_rather_than_left_bare()
{
// A bare "screenshotsdir" would swallow the next line's key as its value.
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "screenshotsdir shots\nfullscreen yes\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.Extra["screenshotsdir"] = "";
installation.SettingsStore.Save();
string text = installation.Files.ReadAllText(UserIni);
Assert.Contains("// screenshotsdir shots", text);
Assert.Contains("fullscreen yes", text);
}
// ── A save changes only what was changed ─────────────────────────────────
[Theory]
[InlineData("fieldofview 80")]
[InlineData("dynamiclights 9")]
[InlineData("anisotropicfiltering 16.0\t// as far as it goes")]
[InlineData("gfx.smoke.fidelity 1.5")]
public void A_line_nobody_changed_stays_as_the_file_has_it(string line)
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, line + "\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains(line + "\n", SavedLines(installation));
}
[Fact]
public void A_changed_value_replaces_the_files()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fieldofview 80\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Settings.FieldOfView = 50;
installation.SettingsStore.Save();
Assert.Contains("fieldofview 50", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_key_the_file_lacks_is_written_out()
{
// So the simulator and the screen go by the same value.
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fieldofview 45\n"));
installation.SettingsStore.Load();
installation.SettingsStore.Save();
Assert.Contains("fullscreen no", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_save_reports_what_it_would_change_before_making_it()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "fullscreen no\nfieldofview 80\ngfx.postfx.tonemapping yes\n"));
var store = installation.SettingsStore;
store.Load();
store.Settings.Fullscreen = true;
var changes = store.Changes(store.Settings);
Assert.Contains(new StarterNG.Application.IniChange("fullscreen", "no", "yes"), changes);
Assert.Contains(new StarterNG.Application.IniChange("gfx.postfx.tonemapping", "yes", null), changes);
Assert.DoesNotContain(changes, change => change.Key == "fieldofview");
Assert.Contains("fullscreen no", installation.Files.ReadAllText(UserIni));
}
/// Stands in for a screen whose combo box cannot show the file's shadow rank.
private sealed class ShadowRankScreen : StarterNG.Application.ISettingsCapture
{
public int Shown = 3;
public void CaptureInto(SimulatorSettings settings) => settings.ShadowRankCutoff = Shown;
public IReadOnlyDictionary ShownUnlikeSettings { get; } =
new Dictionary
{
["gfx.shadow.rank.cutoff"] = new("3", "5")
};
}
[Fact]
public void A_value_the_screen_could_not_show_survives_until_moved()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "gfx.shadow.rank.cutoff 5\n"));
var store = installation.SettingsStore;
var screen = new ShadowRankScreen();
store.RegisterCapture(screen);
store.Load();
store.CaptureAndSave();
Assert.Contains("gfx.shadow.rank.cutoff 5", installation.Files.ReadAllText(UserIni));
screen.Shown = 2;
store.CaptureAndSave();
Assert.Contains("gfx.shadow.rank.cutoff 2", installation.Files.ReadAllText(UserIni));
// Moved back to what it first showed, it is a choice like any other.
screen.Shown = 3;
store.CaptureAndSave();
Assert.Contains("gfx.shadow.rank.cutoff 3", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_section_put_back_takes_the_simulators_own_values_for_its_keys_alone()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni,
"fieldofview 60\nmaxtexturesize 1024\nshadowtune 4096 250 500 300\nbasedrawrange 4000\n"));
var store = installation.SettingsStore;
store.Load();
var reset = store.WithDefaults(store.Settings, new[] { "fieldofview", "shadowtune", "basedrawrange" });
Assert.Equal(45, reset.FieldOfView);
Assert.Equal(2048, reset.ShadowMapResolution);
Assert.Equal(250, reset.ShadowProjectionRange);
Assert.False(reset.Extra.ContainsKey("basedrawrange"));
Assert.Equal(1024, reset.MaxTextureSize);
// The settings themselves wait for the screen's save.
Assert.Equal(60, store.Settings.FieldOfView);
}
// ── What the screen shows otherwise ──────────────────────────────────────
[Fact]
public void Settings_shown_as_they_are_held_have_nothing_unlike()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fieldofview 60\nbasedrawrange 4000\n"));
var store = installation.SettingsStore;
store.Load();
Assert.Empty(store.ShownUnlike(store.Settings.Copy()));
}
[Fact]
public void A_value_shown_otherwise_comes_with_both_sides_under_any_spelling_of_its_key()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fieldofview 60\n"));
var store = installation.SettingsStore;
store.Load();
var shown = store.Settings.Copy();
shown.FieldOfView = 45;
var unlike = store.ShownUnlike(shown);
Assert.Equal("fieldofview", Assert.Single(unlike).Key);
Assert.Equal(new StarterNG.Application.ShownValue("45", "60"), unlike["FIELDOFVIEW"]);
}
[Fact]
public void A_key_on_one_side_alone_shows_as_null_on_the_other()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "basedrawrange 4000\n"));
var store = installation.SettingsStore;
store.Load();
var added = store.Settings.Copy();
added.Extra["heightbase"] = "2";
Assert.Equal(new StarterNG.Application.ShownValue("2", null), store.ShownUnlike(added)["heightbase"]);
var taken = store.Settings.Copy();
taken.Extra.Remove("basedrawrange");
Assert.Equal(new StarterNG.Application.ShownValue(null, "4000"), store.ShownUnlike(taken)["basedrawrange"]);
}
// ── Applying a profile ───────────────────────────────────────────────────
private static readonly string Profile = Path.Combine("/home/kolejarz", "profiles", "wieczor.ini");
[Fact]
public void A_readable_profile_is_loaded()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "fieldofview 60\n").WithFile(Profile, "fieldofview 50\n"));
var store = installation.SettingsStore;
store.Load();
Assert.True(store.TryLoadProfile(Profile));
Assert.Equal(50, store.Settings.FieldOfView);
Assert.False(store.LoadFailed);
}
[Fact]
public void A_missing_profile_leaves_the_settings_as_they_are()
{
var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "fieldofview 60\n"));
var store = installation.SettingsStore;
store.Load();
store.Settings.FieldOfView = 70;
Assert.False(store.TryLoadProfile(Profile));
Assert.Equal(70, store.Settings.FieldOfView);
}
[Fact]
public void An_unreadable_profile_leaves_the_real_settings_not_defaults_to_save_over_them()
{
var installation = WithHome(new InMemoryFileSystem()
.WithFile(UserIni, "fieldofview 60\n").WithFile(Profile, "fieldofview 50\n").FailReading(Profile));
var store = installation.SettingsStore;
store.Load();
Assert.False(store.TryLoadProfile(Profile));
Assert.False(store.LoadFailed);
Assert.Equal(60, store.Settings.FieldOfView);
store.Save();
Assert.False(store.LastSaveFailed);
Assert.Contains("fieldofview 60", installation.Files.ReadAllText(UserIni));
}
[Fact]
public void A_missing_profile_gives_settings_that_could_not_be_read_another_try()
{
var files = new InMemoryFileSystem().WithFile(UserIni, "fieldofview 60\n").FailReading(UserIni);
var installation = WithHome(files);
var store = installation.SettingsStore;
store.Load();
Assert.True(store.LoadFailed);
files.AllowReading(UserIni);
Assert.False(store.TryLoadProfile(Profile));
Assert.False(store.LoadFailed);
Assert.Equal(60, store.Settings.FieldOfView);
}
}