Cut the comments from the refactored code

Three survive: why AppServices is hand-wired, the cp1250 registration trap, and
the IsDieselEngine note carried over from Physics.cs.
This commit is contained in:
maj00r
2026-09-04 22:24:54 +02:00
parent 7849ef9729
commit df21ec104f
60 changed files with 3 additions and 875 deletions

View File

@@ -2,11 +2,6 @@ namespace StarterNG.Application.Abstractions;
using System;
/// <summary>
/// Port over the system clock. Scenery weather defaults, timetable rendering and
/// log timestamps all depend on "now"; routing them through a clock keeps those
/// rules deterministic under test.
/// </summary>
public interface IClock
{
DateTime Now { get; }

View File

@@ -3,11 +3,6 @@ using System.Collections.Generic;
namespace StarterNG.Application.Abstractions;
/// <summary>
/// Port over the starter's error log. Domain and application code depends on
/// this interface only; the file-backed implementation and the message box that
/// surfaces faults to the user live in Infrastructure and Presentation.
/// </summary>
public interface IDiagnosticsLog
{
void Log(string message);
@@ -16,7 +11,6 @@ public interface IDiagnosticsLog
void Log(IEnumerable<string> messages);
/// <summary>Everything logged so far, newline separated.</summary>
string Text { get; }
void Clear();

View File

@@ -1,9 +1,5 @@
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; }
@@ -12,9 +8,7 @@ public interface IEnvironment
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

@@ -5,18 +5,6 @@ using System.Text;
namespace StarterNG.Application.Abstractions;
/// <summary>
/// Port over the file system. Everything that reads or writes the MaSzyna
/// installation goes through this interface instead of <see cref="File"/> and
/// <see cref="Directory"/>, so parsers and use cases can be exercised against an
/// in-memory installation in tests.
/// </summary>
/// <remarks>
/// Kept deliberately narrow: only the operations the application actually
/// performs are exposed. Paths are passed through verbatim to the adapter, which
/// resolves them relative to the process working directory just like the
/// framework methods it replaces.
/// </remarks>
public interface IFileSystem
{
bool FileExists(string path);
@@ -45,23 +33,13 @@ public interface IFileSystem
void CopyFile(string sourcePath, string destinationPath, bool overwrite);
/// <summary>Top-level files, empty when the directory does not exist.</summary>
IReadOnlyList<string> GetFiles(string path, string searchPattern = "*");
/// <summary>Top-level directories, empty when the directory does not exist.</summary>
IReadOnlyList<string> GetDirectories(string path);
/// <summary>
/// Files matching the pattern anywhere below the path. Empty when the
/// directory does not exist.
/// </summary>
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,34 +1,20 @@
namespace StarterNG.Application.Abstractions;
/// <summary>
/// The layout of a MaSzyna installation. Replaces the
/// <c>Directory.GetCurrentDirectory()</c> calls and hard-coded "scenery/",
/// "dynamic", "textures/mini/" literals that were previously spread across
/// parsers, views and the loader.
/// </summary>
public interface IGamePaths
{
/// <summary>Installation root: the folder holding eu07.exe.</summary>
string Root { get; }
/// <summary>Scenery definitions (*.scn).</summary>
string Scenery { get; }
/// <summary>Rolling stock: dynamic/&lt;maker&gt;/&lt;vehicle&gt;/textures.txt.</summary>
string Dynamic { get; }
/// <summary>Vehicle thumbnails used by the depot and the consist strip.</summary>
string MiniTextures { get; }
/// <summary>Simulator data files (load_weights.txt and friends).</summary>
string Data { get; }
/// <summary>Starter-owned configuration: presets, profiles, key bindings.</summary>
string StarterConfig { get; }
/// <summary>The starter's own error log (starter/bledy.txt).</summary>
string DiagnosticsLog { get; }
/// <summary>Resolves a path relative to <see cref="Root"/>.</summary>
string FromRoot(params string[] segments);
}

View File

@@ -1,19 +1,9 @@
namespace StarterNG.Application.Abstractions;
/// <summary>
/// Port over the translation table. Anything that needs a user-facing string
/// takes this, rather than reaching for the concrete localization service or a
/// static on <c>App</c>.
/// </summary>
public interface ILocalizedStrings
{
/// <summary>
/// Translation for a key. Missing keys return the key itself (and are logged
/// once), so an untranslated screen stays usable.
/// </summary>
string this[string key] { get; }
/// <summary>Lookup for a key that may legitimately be absent; "" when missing.</summary>
string Opt(string key);
string CurrentLanguage { get; }

View File

@@ -1,19 +1,12 @@
namespace StarterNG.Application.Abstractions;
/// <summary>
/// The thumbnails in textures/mini, looked up by name. Backed by one directory
/// scan; a name with no bitmap falls back to the generic "other" thumbnail.
/// </summary>
public interface IMiniTextureIndex
{
/// <summary>Builds the index up front, off the UI thread.</summary>
void Preload();
bool Has(string? miniName);
/// <summary>Bitmap for the name, the fallback thumbnail, or null when neither exists.</summary>
string? PathFor(string? miniName);
/// <summary>The generic thumbnail shown for unknown stock.</summary>
string? FallbackPath { get; }
}

View File

@@ -2,20 +2,11 @@ using StarterNG.Domain.Vehicles;
namespace StarterNG.Application.Abstractions;
/// <summary>
/// Looks up the .fiz physics of a vehicle type by model name.
/// </summary>
public interface IPhysicsRepository
{
/// <summary>Indexes the installation's .fiz files up front, off the UI thread.</summary>
void Preload();
/// <summary>How many vehicle types were indexed; zero means a broken installation.</summary>
int IndexedCount { get; }
/// <summary>
/// Physics for a model name (with or without extension), or null when the
/// installation has no .fiz for it.
/// </summary>
VehiclePhysics? For(string? dataFolder, string? model);
}

View File

@@ -4,12 +4,6 @@ using System.Threading.Tasks;
namespace StarterNG.Application.Abstractions;
/// <summary>
/// A process the starter has launched or adopted. Exposes only what the starter
/// needs — whether the simulator is still up, and a way to await its exit — so
/// the presentation layer never handles <see cref="System.Diagnostics.Process"/>
/// directly.
/// </summary>
public interface IProcessHandle
{
bool HasExited { get; }
@@ -17,26 +11,12 @@ public interface IProcessHandle
Task WaitForExitAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// Port over process creation: starting the simulator, opening a file, folder or
/// URL in the desktop shell, and adopting an already running simulator.
/// </summary>
public interface IProcessLauncher
{
/// <summary>
/// Starts an executable with the given arguments. Returns null when the
/// process could not be created; <paramref name="error"/> then carries the
/// reason for display.
/// </summary>
IProcessHandle? Start(string executablePath, IReadOnlyList<string> arguments, string? workingDirectory,
out string? error);
/// <summary>
/// Hands a path or URL to the desktop shell (explorer, xdg-open, ...).
/// Returns false and logs when the shell refused it.
/// </summary>
bool OpenInShell(string pathOrUrl);
/// <summary>The first running process with that name, or null.</summary>
IProcessHandle? FindRunning(string processName);
}

View File

@@ -1,15 +1,8 @@
namespace StarterNG.Application.Abstractions;
/// <summary>
/// Port over randomness. Scenery weather deliberately varies between runs (fog
/// distance, overcast presets, texture randomiser); routing it through a port
/// keeps those rules assertable instead of flaky.
/// </summary>
public interface IRandomSource
{
/// <summary>Inclusive lower bound, exclusive upper bound.</summary>
int Next(int minInclusive, int maxExclusive);
/// <summary>Uniform value in [0, 1).</summary>
double NextDouble();
}

View File

@@ -4,21 +4,11 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Application.Abstractions;
/// <summary>How far a scenery sweep has got, for the splash screen.</summary>
public readonly record struct SceneryLoadProgress(int Loaded, int Total, string FileName);
/// <summary>
/// Reads the scenarios of an installation. The only route from a .scn on disk to
/// a <see cref="Scenery"/>; nothing else constructs one from a path.
/// </summary>
public interface ISceneryRepository
{
/// <summary>
/// Every scenario in the installation, in no particular order. Files that
/// fail to parse are logged and skipped rather than failing the load.
/// </summary>
IReadOnlyList<Scenery> LoadAll(IProgress<SceneryLoadProgress>? progress = null);
/// <summary>Reloads a single scenario; null when it is gone or unreadable.</summary>
Scenery? Load(string path);
}

View File

@@ -2,22 +2,9 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Application.Abstractions;
/// <summary>
/// The translations a scenery ships alongside itself, in scenery/i18n/*.json.
/// </summary>
/// <remarks>
/// Scenery text may be an "@key" reference rather than literal prose, resolved
/// against the currently loaded scenery's table. The table is per scenery, so
/// <see cref="LoadFor"/> replaces it rather than adding to it.
/// </remarks>
public interface ISceneryTranslations
{
/// <summary>Loads the tables for one scenery, English first then the user's language.</summary>
void LoadFor(Scenery scenery, string langCode);
/// <summary>
/// Resolves an "@key" reference against the loaded table. Text that is not a
/// reference, or a key with no translation, comes back unchanged.
/// </summary>
string Translate(string? text);
}

View File

@@ -2,16 +2,7 @@ using StarterNG.Domain.Vehicles;
namespace StarterNG.Application.Abstractions;
/// <summary>
/// Reads the installation's rolling stock. The only route from the dynamic/
/// folder to a <see cref="VehicleCatalog"/>.
/// </summary>
public interface IVehicleRepository
{
/// <summary>
/// Fills the catalogue from every textures.txt under dynamic/. Returns the
/// number of liveries read, which the caller uses to spot an installation
/// with no rolling stock at all.
/// </summary>
int Load(VehicleCatalog catalog);
}

View File

@@ -9,22 +9,9 @@ using StarterNG.Services;
namespace StarterNG.Application;
/// <summary>
/// The composition root: the single place where concrete adapters are chosen and
/// wired to the ports the rest of the application depends on.
/// </summary>
/// <remarks>
/// Hand-written on purpose. The starter publishes with <c>PublishAot</c> and
/// <c>PublishTrimmed</c>, which rules out reflection-based containers; the object
/// graph is small enough that explicit construction is also the clearest
/// documentation of what depends on what.
///
/// <see cref="Current"/> is a service locator and is used in exactly one kind of
/// place — the entry point and the view constructors Avalonia creates for us,
/// which cannot take arguments. Everything below that boundary receives its
/// dependencies through its constructor; new code must not reach for
/// <see cref="Current"/>.
/// </remarks>
// Skladany recznie: PublishAot i PublishTrimmed wykluczaja kontener oparty na
// refleksji. Current jest service locatorem i wolno go czytac tylko z entry
// pointu i z konstruktorow widokow, ktorych Avalonia nie potrafi parametryzowac.
public sealed class AppServices
{
private static AppServices? _current;
@@ -61,16 +48,10 @@ public sealed class AppServices
StartSimulation = new StartSimulation(State, SettingsStore, files, Processes, Random, log);
}
/// <summary>
/// The graph built by the entry point. Throws when read before
/// <see cref="Initialize"/>, which would mean a static initializer somewhere
/// ran ahead of the composition root.
/// </summary>
public static AppServices Current =>
_current ?? throw new InvalidOperationException(
"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,
IEnvironment? environment = null)
{
@@ -114,16 +95,12 @@ public sealed class AppServices
public LoadWeightsRepository LoadWeights { get; }
/// <summary>Everything read out of the installation: rolling stock and scenarios.</summary>
public GameLibrary Library { get; }
/// <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; }
@@ -132,13 +109,7 @@ public sealed class AppServices
public MissingVehicleLog MissingVehicleLog { get; }
/// <summary>Starts the simulator on the current selection.</summary>
public StartSimulation StartSimulation { 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
/// the port.
/// </summary>
public LocalizationService Localization { get; }
}

View File

@@ -5,18 +5,8 @@ using StarterNG.Classes;
namespace StarterNG.Application;
/// <summary>
/// What the user currently has selected: which scenario, which consist in it and
/// which vehicle they will start in.
/// </summary>
/// <remarks>
/// Session state rather than domain state, which is why it lives in the
/// application layer. Built by the composition root instead of being an ambient
/// singleton; screens observe <see cref="Changed"/> to stay in step.
/// </remarks>
public sealed class AppState
{
/// <summary>Raised whenever any of the selections changes.</summary>
public event Action? Changed;
private Scenery? _currentScenery;

View File

@@ -7,7 +7,6 @@ using StarterNG.Domain.Vehicles;
namespace StarterNG.Application;
/// <summary>Which part of the installation the loader is working through.</summary>
public enum LoadPhase
{
Vehicles,
@@ -15,19 +14,8 @@ public enum LoadPhase
Done
}
/// <summary>How far the load has got, for the splash screen.</summary>
public readonly record struct LoadStatus(double Fraction, LoadPhase Phase, string? Detail);
/// <summary>
/// Everything the starter reads out of the installation once: the rolling stock
/// catalogue and the list of scenarios.
/// </summary>
/// <remarks>
/// Was the <c>GameData</c> singleton, which also owned directory enumeration and
/// parallel parsing. It is now an ordinary object built by the composition root,
/// delegating to the repositories; the loading policy (what is read, in what
/// order, what counts as progress) is all that is left here.
/// </remarks>
public sealed class GameLibrary
{
private readonly IVehicleRepository _vehicles;
@@ -53,11 +41,6 @@ public sealed class GameLibrary
public bool Loaded { get; private set; }
/// <summary>
/// Reads the whole installation. Vehicles first, because the scenery list
/// resolves consists against them; the thumbnail and physics indexes are
/// built alongside since they are independent directory scans.
/// </summary>
public void Load(IProgress<LoadStatus>? progress = null)
{
if (Loaded)
@@ -78,7 +61,6 @@ public sealed class GameLibrary
Loaded = true;
}
/// <summary>Re-reads the scenery folder, picking up files added since startup.</summary>
public void ReloadSceneries(IProgress<SceneryLoadProgress>? progress = null)
{
var reloaded = _sceneries.LoadAll(progress);

View File

@@ -5,15 +5,6 @@ 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;

View File

@@ -9,24 +9,11 @@ using StarterNG.Infrastructure.Adapters;
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;
@@ -54,25 +41,14 @@ public sealed class SettingsStore
_encoding = LegacyText.CodePage1250;
}
/// <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();
@@ -91,7 +67,6 @@ public sealed class SettingsStore
}
catch (Exception ex)
{
// An unreadable ini means defaults, not a failed start.
_log.Log($"reading {path}", ex);
_config = new ConfigFile();
}
@@ -100,7 +75,6 @@ public sealed class SettingsStore
TouchConfigAge();
}
/// <summary>Collects pending edits from the settings screen, then saves.</summary>
public void CaptureAndSave()
{
_capture?.CaptureInto(Settings);
@@ -134,7 +108,6 @@ public sealed class SettingsStore
}
}
/// <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;
@@ -148,10 +121,6 @@ public sealed class SettingsStore
}
}
/// <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;
@@ -160,7 +129,6 @@ public sealed class SettingsStore
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)
@@ -172,5 +140,4 @@ public sealed class SettingsStore
public string ResolveExecutable() => _executables.Resolve(Settings, out _);
public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem);
}

View File

@@ -10,28 +10,19 @@ using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Application;
/// <summary>Why a start attempt ended the way it did.</summary>
public enum SimulationStartOutcome
{
Started,
/// <summary>Nothing is selected to run.</summary>
NothingSelected,
/// <summary>The scenario could not be written for the simulator to read.</summary>
ExportFailed,
/// <summary>The executable is missing, not executable, or built for another platform.</summary>
ExecutableProblem,
/// <summary>The executable is fine but the process would not start.</summary>
LaunchFailed
}
/// <summary>
/// What happened, and what the caller needs in order to say so or to watch the
/// simulator it just started.
/// </summary>
public readonly record struct SimulationStartResult(
SimulationStartOutcome Outcome,
IProcessHandle? Process = null,
@@ -39,25 +30,10 @@ public readonly record struct SimulationStartResult(
ExeProblem Problem = ExeProblem.None,
string? Detail = null);
/// <summary>
/// Starts the simulator on the scenario and consist the user has selected.
/// </summary>
/// <remarks>
/// This was the middle of a 100-line click handler that also drove dialogs and
/// the loading screen. The sequence itself - settle the consist's starting
/// state, write the scenario the simulator will read, save the settings, launch -
/// is application logic and belongs away from the window; what is left in the
/// view is asking the user things and showing what came back.
/// </remarks>
public sealed class StartSimulation
{
/// <summary>
/// A scenario the starter has rewritten is saved beside the original under a
/// '$' name, which the simulator loads and the scenery list ignores.
/// </summary>
private const char ExportPrefix = '$';
/// <summary>Battery voltage stood in for by a token velocity, as the format demands.</summary>
private const float BatteryOnVelocity = 0.1f;
private readonly AppState _state;
@@ -78,10 +54,6 @@ public sealed class StartSimulation
_log = log;
}
/// <summary>
/// The vehicle the player would start in, or null when the consist has no one
/// aboard - which is what the caller warns about before committing.
/// </summary>
public static string? StartableVehicle(Trainset? trainset, string? preferred)
{
if (trainset is null || trainset.Vehicles.Count == 0)
@@ -101,8 +73,6 @@ public sealed class StartSimulation
return trainset.Vehicles.FirstOrDefault(CanStart)?.Name;
}
/// <param name="freeFly">Start as a free camera rather than in a vehicle.</param>
/// <param name="saveSettings">Write the settings before launching.</param>
public SimulationStartResult Execute(bool freeFly, bool saveSettings)
{
var scenery = _state.CurrentScenery;
@@ -116,8 +86,6 @@ public sealed class StartSimulation
?? StartableVehicle(trainset, _state.StartingVehicleName);
_state.StartingVehicleName = vehicle;
// The export always rewrites the weather block, so a run reflects what the
// weather tab shows even when the user never touched it.
scenery.Weather.Dirty = true;
string exportName = ExportPrefix + Path.GetFileName(scenery.Path);
@@ -155,11 +123,6 @@ public sealed class StartSimulation
return new SimulationStartResult(SimulationStartOutcome.Started, process, executable);
}
/// <summary>
/// Applies the user's battery preference to the consist. The scenery format
/// carries the battery state in the trainset's velocity field, so "always on"
/// means a token non-zero value when the consist is standing still.
/// </summary>
private void ApplyBatteryDefault(Trainset trainset)
{
if (trainset.Vehicles.Count == 0)
@@ -174,5 +137,4 @@ public sealed class StartSimulation
_ => trainset.OriginalVelocity
};
}
}

View File

@@ -2,28 +2,10 @@ using Avalonia.Controls;
namespace StarterNG.Controls;
/// <summary>
/// The read-only label/value lines the depot's detail tabs are built from.
/// </summary>
/// <remarks>
/// Shared so every tab reads the same way: label in the left column, value in
/// the right, aligned down the panel. Previously each tab built its own rows,
/// and the texture tab glued "label: value" into a single line, which lined up
/// with nothing.
/// </remarks>
public static class DetailRows
{
/// <summary>Shown in place of a reading a vehicle does not have.</summary>
public const string NoValue = "–";
/// <summary>
/// The one row shape every detail tab uses. The label column negotiates its
/// width through the shared-size group on the host panel, so all rows in a
/// panel line up without anyone naming a pixel count - see the Compact styles
/// in Settings.axaml. Omitting <paramref name="content"/> makes it a
/// read-only line; omitting <paramref name="label"/> indents a bare widget to
/// the control column.
/// </summary>
public static SettingRow Row(string? label = null, Control? content = null,
string? value = null, string? description = null,
string? tip = null)
@@ -43,11 +25,6 @@ public static class DetailRows
return row;
}
/// <summary>
/// A read-only label/value line. The dash keeps the row from collapsing when a
/// vehicle has no reading for the field; the full value is on the tooltip, so
/// a trimmed one is still legible.
/// </summary>
public static SettingRow Reading(string label, string? value)
{
bool empty = string.IsNullOrWhiteSpace(value);

View File

@@ -3,13 +3,6 @@ using Avalonia.Controls;
namespace StarterNG.Controls;
/// <summary>
/// One line of the settings panel: label on the left, the control itself in the
/// middle, a value read-out on the right, and underneath the eu07.ini key this
/// row writes plus an optional description.
/// Replaces the hand-rolled StackPanel/DockPanel pattern that used to be repeated
/// for every setting.
/// </summary>
public class SettingRow : ContentControl
{
public static readonly StyledProperty<string?> LabelProperty =
@@ -18,12 +11,6 @@ public class SettingRow : ContentControl
public static readonly StyledProperty<string?> DescriptionProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(Description));
/// <summary>
/// The eu07.ini key this row reads and writes, shown verbatim so a user can
/// match what they see here against the file, the wiki or a forum post. Rows
/// that are starter-only preferences carry their "starter.*" key just the
/// same; rows that write no key at all leave this empty.
/// </summary>
public static readonly StyledProperty<string?> ConfigKeyProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(ConfigKey));
@@ -71,19 +58,12 @@ public class SettingRow : ContentControl
UpdatePseudoClasses();
}
/// <summary>
/// Drives the template: an empty label lets the control span the label column,
/// an empty description or value collapses that part of the row entirely.
/// </summary>
private void UpdatePseudoClasses()
{
PseudoClasses.Set(":no-label", string.IsNullOrEmpty(Label));
PseudoClasses.Set(":no-description", string.IsNullOrEmpty(Description));
PseudoClasses.Set(":no-key", string.IsNullOrEmpty(ConfigKey));
PseudoClasses.Set(":no-value", string.IsNullOrEmpty(ValueText));
// A read-only row is a label and a reading with no widget between them. The
// reading then takes the middle column too, so a long path ellipsises instead
// of demanding its full width from an Auto column and getting cut off.
PseudoClasses.Set(":no-content", Content is null);
}
}

View File

@@ -3,11 +3,6 @@ using Avalonia.Controls.Primitives;
namespace StarterNG.Controls;
/// <summary>
/// A group of <see cref="SettingRow"/>s under a flat title with an accent rule.
/// <see cref="Toggle"/> holds an optional master switch shown on the right of the
/// title line; the section content gates itself off it.
/// </summary>
public class SettingsSection : HeaderedContentControl
{
public static readonly StyledProperty<object?> ToggleProperty =

View File

@@ -9,43 +9,22 @@ using StarterNG.Domain;
namespace StarterNG.Domain.Sceneries;
/// <summary>A file the scenery offers to open alongside it (map, manual, ...).</summary>
public sealed class SceneryAttachment
{
public string FilePath = "";
public string Label = "";
}
/// <summary>
/// An <c>include</c> the scenery marks as optional, so the user can decide
/// whether it ends up in the exported scenario.
/// </summary>
public sealed class SceneryInclude
{
public string FilePath = "";
public string Desc = "";
/// <summary>1 = on by default, 2 = only when no companion timetable exists.</summary>
public int Kind;
public bool Selected;
}
/// <summary>
/// A scenario the user can start: its consists, loose vehicles, optional
/// includes and weather, plus the ability to render all of that back into a .scn
/// the simulator can load.
/// </summary>
/// <remarks>
/// The aggregate is pure: it never reads or writes files. Parsing lives in
/// <c>Infrastructure.Sceneries.SceneryParser</c> and loading in
/// <c>SceneryRepository</c>, which is what lets the export rules be tested from
/// a string rather than an installation.
///
/// <see cref="Template"/> is the .scn text with every trainset replaced by a
/// <c>{{index}}</c> placeholder and the optional includes stripped, so
/// <see cref="BuildExportContent"/> only has to substitute rather than re-parse.
/// </remarks>
public sealed class Scenery
{
public Scenery(string path, string template)
@@ -54,13 +33,10 @@ public sealed class Scenery
Template = template;
}
/// <summary>Path of the .scn this was parsed from; the aggregate's identity.</summary>
public string Path { get; }
/// <summary>The .scn body with trainset placeholders, used for export.</summary>
public string Template { get; }
/// <summary>True when a companion .sbt timetable sits next to the .scn.</summary>
public bool HasCompanionTimetable { get; set; }
public string? Group { get; set; }
@@ -71,7 +47,6 @@ public sealed class Scenery
public string? ImageName { get; set; }
/// <summary>Scenery kept for historical interest; hidden unless asked for.</summary>
public bool Archival { get; set; }
public List<Trainset> Trainsets { get; } = new();
@@ -80,38 +55,21 @@ public sealed class Scenery
public List<SceneryInclude> Includes { get; } = new();
/// <summary>Vehicles placed directly as nodes rather than inside a trainset.</summary>
public List<Dynamic> LooseVehicles { get; } = new();
/// <summary>
/// Names of every loose vehicle, including those whose node failed to parse
/// and therefore has no <see cref="Dynamic"/>.
/// </summary>
public List<string> LooseVehicleNames { get; } = new();
/// <summary>Parse problems worth showing on the scenario's fault list.</summary>
public List<string> Faults { get; } = new();
public SceneryWeather Weather { get; } = new();
public string DisplayName => System.IO.Path.GetFileNameWithoutExtension(Path);
/// <summary>
/// The description with any "@key" references resolved against the scenery's
/// own translation files.
/// </summary>
public string LocalizedDescription(ISceneryTranslations translations) =>
string.IsNullOrEmpty(Description)
? ""
: string.Join("\n", Description.Split('\n').Select(translations.Translate));
/// <summary>
/// Renders the scenario the simulator should load: the original file with the
/// user's consists, weather and optional includes applied.
/// </summary>
/// <param name="skipDecorTrainsets">
/// Drops AI trainsets the user marked irrelevant, which shortens loading.
/// </param>
public string BuildExportContent(bool skipDecorTrainsets, IRandomSource random)
{
string result = Template;
@@ -154,8 +112,6 @@ public sealed class Scenery
var sb = new StringBuilder();
foreach (var include in Includes)
{
// Kind 2 is the scenery's own timetable stand-in: it is only wanted
// when the user has not supplied a companion .sbt.
if (include.Kind == 2)
{
if (!HasCompanionTimetable)
@@ -172,10 +128,6 @@ public sealed class Scenery
return InsertBeforeFirstInit(text, sb.ToString(), fallbackAtEnd: false);
}
/// <summary>
/// Places generated content ahead of <c>FirstInit</c>, which the simulator
/// treats as the end of the definition section.
/// </summary>
private static string InsertBeforeFirstInit(string text, string addition, bool fallbackAtEnd)
{
var firstInit = Regex.Match(text, @"\bFirstInit\b", RegexOptions.IgnoreCase);

View File

@@ -3,22 +3,11 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Domain.Sceneries;
/// <summary>
/// The weather and time-of-day a scenery starts with: what the .scn file
/// declared, what the user has changed it to, and whether that differs.
/// </summary>
/// <remarks>
/// Split out of <c>Scenery</c> so the "is this still the authored weather?"
/// rule lives next to the values it compares, instead of being spread over the
/// aggregate and the weather tab. The aggregate owns one of these; the parser
/// fills it and calls <see cref="AcceptAsAuthored"/> once.
/// </remarks>
public sealed class SceneryWeather
{
public const int FogMin = 10;
public const int FogMax = 10000;
/// <summary>Overcast values the "random" option picks between.</summary>
private static readonly double[] OvercastPresets = { -1.5, 0, 0.3, 0.7, 1, 1.1, 1.5 };
private string _authoredTime = "10:30";
@@ -33,7 +22,6 @@ public sealed class SceneryWeather
public string ScenarioTimeOverride { get; set; } = "10:30";
/// <summary>Day of the year driving the sun position (movelight).</summary>
public int Day { get; set; }
public double Temperature { get; set; } = 15;
@@ -44,16 +32,12 @@ public sealed class SceneryWeather
public double Overcast { get; set; }
/// <summary>When set, each export draws a fresh overcast from the presets.</summary>
public bool OvercastRandom { get; set; }
/// <summary>True when the .scn actually declared weather, rather than us defaulting it.</summary>
public bool IsAuthored { get; set; }
/// <summary>Set once the user has touched the weather, so export rewrites the config block.</summary>
public bool Dirty { get; set; }
/// <summary>Takes the current values as the baseline "what the file said".</summary>
public void AcceptAsAuthored()
{
_authoredTime = Time;
@@ -65,7 +49,6 @@ public sealed class SceneryWeather
_authoredOvercastRandom = OvercastRandom;
}
/// <summary>True when the user's weather differs from what the .scn declared.</summary>
public bool Changed =>
Day != _authoredDay ||
ScenarioTimeOverride != _authoredTimeOverride ||
@@ -74,11 +57,6 @@ public sealed class SceneryWeather
OvercastRandom != _authoredOvercastRandom ||
(!OvercastRandom && Math.Abs(Overcast - _authoredOvercast) > 0.001);
/// <summary>
/// Puts back the authored weather. Stays <see cref="Dirty"/>: the export must
/// still rewrite the config block, because the file on disk may already carry
/// an edit from an earlier run.
/// </summary>
public void Restore()
{
Time = _authoredTime;
@@ -91,7 +69,6 @@ public sealed class SceneryWeather
Dirty = true;
}
/// <summary>Clamps a parsed fog range and picks the distance to run with.</summary>
public void SetFogRange(int start, int end, IRandomSource random)
{
FogStart = Math.Clamp(start, FogMin, FogMax);
@@ -99,7 +76,6 @@ public sealed class SceneryWeather
FogEnd = end > FogStart ? random.Next(FogStart, end + 1) : end;
}
/// <summary>The overcast to export, drawing a preset when randomisation is on.</summary>
public double EffectiveOvercast(IRandomSource random) =>
OvercastRandom ? OvercastPresets[random.Next(0, OvercastPresets.Length)] : Overcast;
}

View File

@@ -6,17 +6,8 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Domain.Sceneries;
/// <summary>
/// Renders a <see cref="SceneryWeather"/> back into the .scn dialect: strips the
/// blocks the starter owns and prepends freshly written ones.
/// </summary>
/// <remarks>
/// A pure text transformation, kept out of the aggregate because it is entirely
/// about the file format rather than about the scenario.
/// </remarks>
public static class SceneryWeatherWriter
{
/// <summary>Config lines the starter writes itself and must not carry over.</summary>
private static readonly Regex ManagedConfigLine = new(
@"^\s*(movelight|scenario\.weather\.temperature|scenario\.time\.override)\b",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -51,14 +42,10 @@ public static class SceneryWeatherWriter
}
catch
{
// A scenery we cannot rewrite is still worth starting unmodified.
return text;
}
}
/// <summary>
/// The simulator's parser expects an unpadded hour ("7:30", not "07:30").
/// </summary>
private static string DelphiHourMinute(string hhmm)
{
int colon = hhmm.IndexOf(':');
@@ -70,10 +57,6 @@ public static class SceneryWeatherWriter
: "0" + hhmm[colon..];
}
/// <summary>
/// Config entries the scenery author wrote that are none of the starter's
/// business, preserved verbatim across a rewrite.
/// </summary>
private static string CarriedConfigLines(string text)
{
var block = Regex.Match(text, @"(?is)\bconfig\b(.*?)\bendconfig\b");

View File

@@ -2,43 +2,16 @@ 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; }
/// <summary>
/// Display name of the starter's language, as the picker shows it ("Polski",
/// "English", "中文"). Resolved from <see cref="LanguageCode"/> at startup.
/// </summary>
public string Language = "English";
/// <summary>
/// The language's code, as its startercfg/lang/*.xml declares it and as
/// eu07.ini stores it. This is what the simulator reads, so it has to carry
/// whatever language the user actually picked rather than being folded into
/// one of two known ones.
/// </summary>
public string LanguageCode = "";
public bool Fullscreen;
public bool PauseWhenInactive = true;
@@ -118,10 +91,6 @@ public sealed class SimulatorSettings
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;
@@ -146,11 +115,6 @@ public sealed class SimulatorSettings
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 =
@@ -158,16 +122,7 @@ public sealed class SimulatorSettings
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

@@ -4,18 +4,8 @@ using StarterNG.Classes;
namespace StarterNG.Domain.Vehicles;
/// <summary>
/// How heavy a tonne of each cargo type is, as declared by
/// data/load_weights.txt, used to turn a load into a mass.
/// </summary>
/// <remarks>
/// Was a static class that also read the file. The table is now a value the
/// repository builds; an unknown cargo weighs <see cref="DefaultWeight"/>, which
/// is what the simulator assumes.
/// </remarks>
public sealed class LoadWeightsTable
{
/// <summary>Weight assumed for a cargo the file does not mention.</summary>
public const int DefaultWeight = 1000;
private readonly Dictionary<string, int> _weights;
@@ -24,7 +14,6 @@ public sealed class LoadWeightsTable
{
_weights = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
// The pantograph state is carried as a pseudo load and weighs nothing.
[Dynamic.PantState] = 0
};

View File

@@ -6,17 +6,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Domain.Vehicles;
/// <summary>
/// Every livery the installation offers, indexed for the lookups the depot and
/// the consist editor make: by uuid, by skin file, and by the set a vehicle
/// belongs to.
/// </summary>
/// <remarks>
/// Was <c>VehicleCatalog</c>, which also owned file enumeration, textures.txt
/// parsing, a static thumbnail dictionary and a missing-asset scan. Those moved
/// to Infrastructure; what is left is the model and its resolution rules, filled
/// by a repository through <see cref="Ingest"/>.
/// </remarks>
public sealed class VehicleCatalog
{
private readonly IMiniTextureIndex _minis;
@@ -26,7 +15,6 @@ public sealed class VehicleCatalog
_minis = minis;
}
/// <summary>Every livery worth offering (wrecks excluded).</summary>
public List<VehicleTexture> Textures { get; } = new();
public Dictionary<string, VehicleGroup> GroupsById { get; } = new();
@@ -37,7 +25,6 @@ public sealed class VehicleCatalog
public Dictionary<string, VehicleTexture> TextureByUuid { get; } = new();
/// <summary>Liveries by skin file name without extension, lower cased.</summary>
public Dictionary<string, VehicleTexture> TextureBySkin { get; } = new();
public void BeginLoad()
@@ -50,7 +37,6 @@ public sealed class VehicleCatalog
TextureBySkin.Clear();
}
/// <summary>Adds one vehicle folder's liveries to the catalogue.</summary>
public void Ingest(VehicleEntry entry)
{
foreach (var group in entry.Groups)
@@ -74,14 +60,12 @@ public sealed class VehicleCatalog
Sets.AddRange(entry.Sets);
}
/// <summary>Builds the indexes that depend on the catalogue being complete.</summary>
public void EndLoad()
{
BuildSetIndex();
ResolveGroupInheritance();
}
/// <summary>The vehicles of the fixed set this livery leads or belongs to.</summary>
public List<VehicleTexture>? ResolveSet(VehicleTexture texture)
{
if (string.IsNullOrEmpty(texture.Uuid))
@@ -97,10 +81,6 @@ public sealed class VehicleCatalog
return cars.Count > 0 ? cars : null;
}
/// <summary>
/// True when the livery is a non-leading member of a set, and so is added
/// along with its leader rather than on its own.
/// </summary>
public bool IsSetFollower(VehicleTexture texture)
{
if (string.IsNullOrEmpty(texture.Uuid))
@@ -113,10 +93,6 @@ public sealed class VehicleCatalog
return lead is not null && !string.Equals(lead, texture.Uuid, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// The thumbnail to draw for a livery: its own if we have that bitmap,
/// otherwise the one its group declares.
/// </summary>
public string? ResolveMiniName(VehicleTexture texture)
{
if (_minis.Has(texture.TextureMini))
@@ -141,7 +117,6 @@ public sealed class VehicleCatalog
return TextureBySkin.TryGetValue(key, out var texture) ? texture : null;
}
/// <summary>Heading shown above a group in the depot browser.</summary>
public string GroupHeader(string? groupId)
{
if (string.IsNullOrEmpty(groupId) || !GroupsById.TryGetValue(groupId, out var group))
@@ -164,10 +139,6 @@ public sealed class VehicleCatalog
}
}
/// <summary>
/// Copies thumbnail, category and archived status down from each group onto
/// its liveries, so the browser can sort and filter without walking back up.
/// </summary>
private void ResolveGroupInheritance()
{
foreach (var texture in Textures)
@@ -182,7 +153,6 @@ public sealed class VehicleCatalog
string? category = group?.Category;
// "*" means "categorise by the first letter of the thumbnail name".
if (category == "*")
{
string source = !string.IsNullOrEmpty(mini) ? mini : texture.TextureMini ?? "";

View File

@@ -2,24 +2,12 @@ using System;
namespace StarterNG.Domain.Vehicles;
/// <summary>
/// The physical properties of a vehicle type, as declared by its .fiz file:
/// what it weighs, how fast it goes, what it can carry and what it couples to.
/// </summary>
/// <remarks>
/// Was <c>Classes.Physics</c>, which also owned the .fiz index, the parser and a
/// static cache. Those are now <c>Infrastructure.Vehicles.FizPhysicsRepository</c>;
/// this is the value the rest of the application reasons about.
/// </remarks>
public sealed class VehiclePhysics
{
/// <summary>Tare mass in kilograms.</summary>
public double Mass { get; set; }
/// <summary>Design speed in km/h.</summary>
public double VMax { get; set; }
/// <summary>Length over buffers in metres.</summary>
public double Length { get; set; }
public string LoadAccepted { get; set; } = "";
@@ -28,10 +16,8 @@ public sealed class VehiclePhysics
public int MaxLoad { get; set; }
/// <summary>Coupling capabilities of the front end.</summary>
public int AllowedFlagA { get; set; } = 3;
/// <summary>Coupling capabilities of the rear end.</summary>
public int AllowedFlagB { get; set; } = 3;
public string ControlTypeA { get; set; } = "";

View File

@@ -2,10 +2,6 @@ using System.Collections.Generic;
namespace StarterNG.Domain.Vehicles;
/// <summary>
/// One vehicle folder's contribution to the catalogue: the liveries it defines,
/// the groups they belong to and any fixed sets they form.
/// </summary>
public sealed class VehicleEntry
{
public string? Uuid { get; set; }
@@ -14,21 +10,15 @@ public sealed class VehicleEntry
public List<VehicleSet> Sets { get; } = new();
}
/// <summary>
/// A family of liveries sharing a thumbnail and a category letter, as declared
/// by one textures.txt block.
/// </summary>
public sealed class VehicleGroup
{
public string Id { get; set; } = "";
public string? Category { get; set; }
public string? Mini { get; set; }
/// <summary>Withdrawn stock, hidden from the depot unless asked for.</summary>
public bool Archived { get; set; }
}
/// <summary>A single livery: one skin file on one model.</summary>
public sealed class VehicleTexture
{
public string? Uuid { get; set; }
@@ -39,7 +29,6 @@ public sealed class VehicleTexture
public string? MiniRef { get; set; }
public string? TextureMini { get; set; }
/// <summary>Wreck liveries exist in the files but are not offered in the depot.</summary>
public bool Wreck { get; set; }
public List<VehicleAlias> Aliases { get; } = new();
@@ -47,17 +36,13 @@ public sealed class VehicleTexture
public string FullPath => Directory + Skinfile;
/// <summary>Thumbnail name after group inheritance; filled by the catalogue.</summary>
public string ResolvedClass { get; internal set; } = "";
/// <summary>Category letter after group inheritance; filled by the catalogue.</summary>
public string? ResolvedCategory { get; internal set; }
/// <summary>Whether the owning group is archived; filled by the catalogue.</summary>
public bool ResolvedArchived { get; internal set; }
}
/// <summary>An alternative model this same skin can be worn by.</summary>
public sealed class VehicleAlias
{
public string? Model { get; set; }
@@ -66,10 +51,6 @@ public sealed class VehicleAlias
public string? TextureMini { get; set; }
}
/// <summary>
/// The comma separated description a livery carries after "//": who made it, for
/// which operator and depot, and when it was last revised.
/// </summary>
public sealed class VehicleMeta
{
public string? Raw { get; set; }
@@ -84,10 +65,6 @@ public sealed class VehicleMeta
public List<string> Extra { get; } = new();
}
/// <summary>
/// Vehicles that belong together and are added to a consist as a unit, such as a
/// multiple unit or a fixed rake.
/// </summary>
public sealed class VehicleSet
{
public string? Uuid { get; set; }

View File

@@ -6,17 +6,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary>
/// <see cref="IDiagnosticsLog"/> that mirrors every line into starter/bledy.txt.
/// Writing is best-effort: a read-only or full installation directory degrades to
/// an in-memory log rather than taking the starter down, and a single retry is
/// made at <see cref="Flush"/> time.
/// </summary>
/// <remarks>
/// Instance state replaces the previous static <c>Diagnostics</c> accumulator, so
/// tests can assert on what a use case logged without touching disk (pair it with
/// an in-memory <see cref="IFileSystem"/>).
/// </remarks>
public sealed class FileDiagnosticsLog : IDiagnosticsLog
{
private readonly IFileSystem _files;
@@ -75,7 +64,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog
}
catch
{
// A log we cannot delete is not worth failing a user action over.
}
}
}
@@ -100,8 +88,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog
if (!_writerFailed && _writer is not null)
return;
// The streaming writer never came up (or died mid-session): fall back
// to one bulk write so the session is not lost entirely.
try
{
_files.CreateDirectory(Path.GetDirectoryName(_logPath)!);
@@ -109,7 +95,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog
}
catch
{
// Nothing left to try; the in-memory log still backs the UI.
}
}
}
@@ -143,7 +128,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog
}
catch
{
// Disposing a broken writer is allowed to fail.
}
_writer = null;
}

View File

@@ -3,10 +3,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary>
/// The stock MaSzyna layout, rooted at the process working directory (the
/// starter ships inside the installation folder, next to eu07.exe).
/// </summary>
public sealed class GamePaths : IGamePaths
{
public GamePaths(string root)
@@ -14,7 +10,6 @@ public sealed class GamePaths : IGamePaths
Root = root;
}
/// <summary>The installation the starter was launched from.</summary>
public static GamePaths ForCurrentDirectory() => new(Directory.GetCurrentDirectory());
public string Root { get; }

View File

@@ -17,14 +17,8 @@ namespace StarterNG.Infrastructure.Adapters;
/// </remarks>
public static class LegacyText
{
/// <summary>The .scn / .fiz / .ini code page, or Latin-1 if it is unavailable.</summary>
public static Encoding CodePage1250 { get; } = Resolve();
/// <summary>
/// True when the code page could not be loaded and text is being read as
/// Latin-1 instead. Polish characters will be wrong; the startup check
/// reports it rather than leaving it to be discovered on screen.
/// </summary>
public static bool IsFallback { get; private set; }
private static Encoding Resolve()

View File

@@ -6,10 +6,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary>
/// <see cref="IFileSystem"/> backed by the real disk. The only place in the
/// application allowed to touch <see cref="File"/> and <see cref="Directory"/>.
/// </summary>
public sealed class PhysicalFileSystem : IFileSystem
{
public bool FileExists(string path) => File.Exists(path);

View File

@@ -3,7 +3,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary><see cref="IClock"/> backed by the machine clock.</summary>
public sealed class SystemClock : IClock
{
public DateTime Now => DateTime.Now;

View File

@@ -3,7 +3,6 @@ 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();

View File

@@ -8,10 +8,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary>
/// <see cref="IProcessLauncher"/> backed by <see cref="Process"/>. The only place
/// in the application that starts an operating-system process.
/// </summary>
public sealed class SystemProcessLauncher : IProcessLauncher
{
private readonly IDiagnosticsLog _log;

View File

@@ -3,7 +3,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Adapters;
/// <summary><see cref="IRandomSource"/> backed by <see cref="Random.Shared"/>.</summary>
public sealed class SystemRandomSource : IRandomSource
{
public int Next(int minInclusive, int maxExclusive) => Random.Shared.Next(minInclusive, maxExclusive);

View File

@@ -9,16 +9,6 @@ using StarterNG.Views;
namespace StarterNG.Infrastructure;
/// <summary>
/// Compatibility facade over <see cref="Application.Abstractions.IDiagnosticsLog"/>
/// plus the fault dialog.
/// </summary>
/// <remarks>
/// The log itself now lives in <c>Infrastructure.Adapters.FileDiagnosticsLog</c>
/// and is reached through the composition root. This static shim keeps the
/// existing call sites compiling while they are migrated to constructor-injected
/// logging; new code should take <c>IDiagnosticsLog</c> instead.
/// </remarks>
public static class Diagnostics
{
private static Application.Abstractions.IDiagnosticsLog Log_ => AppServices.Current.Log;
@@ -63,9 +53,6 @@ public static class Diagnostics
Dispatcher.UIThread.Post(() => _ = ReportAsync(message, title));
}
/// <summary>
/// Sanity check of the installation the starter was launched from.
/// </summary>
public static List<string> CheckInstallation() =>
new InstallationCheck(
AppServices.Current.Paths,

View File

@@ -7,14 +7,6 @@ using StarterNG.Domain.Settings;
namespace StarterNG.Infrastructure;
/// <summary>
/// Verifies that the folder the starter runs from actually looks like a MaSzyna
/// installation, and reports what is missing in the user's language.
/// </summary>
/// <remarks>
/// Depends only on ports, so a broken installation can be described in a test by
/// handing it an empty in-memory file system.
/// </remarks>
public sealed class InstallationCheck
{
private static readonly string[] RequiredFolders = { "dynamic", "sounds", "models", "scenery", "textures" };

View File

@@ -5,15 +5,6 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Infrastructure.Sceneries;
/// <summary>
/// Finds the preview image a scenery names, which authors place in any of
/// several conventional spots relative to the .scn.
/// </summary>
/// <remarks>
/// Was a lazy property on the aggregate; moved out because it is a lookup on
/// disk. Results are cached per scenery path, which is what the lazy field
/// bought us before.
/// </remarks>
public sealed class SceneryImageLocator
{
private readonly IFileSystem _files;
@@ -24,7 +15,6 @@ public sealed class SceneryImageLocator
_files = files;
}
/// <summary>The image file for this scenery, or null when there is none.</summary>
public string? Resolve(Scenery scenery)
{
lock (_cache)

View File

@@ -10,15 +10,6 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Infrastructure.Sceneries;
/// <summary>
/// Turns the text of a .scn file into a <see cref="Scenery"/>.
/// </summary>
/// <remarks>
/// All knowledge of the .scn dialect lives here, so the aggregate stays free of
/// regular expressions and the parser can be exercised on a string. Anything
/// malformed is recorded as a fault rather than thrown: a scenery with one bad
/// consist is still worth listing.
/// </remarks>
public sealed class SceneryParser
{
private readonly IClock _clock;
@@ -32,8 +23,6 @@ public sealed class SceneryParser
public Scenery Parse(string path, string content)
{
// Directives, attachments and weather are read from the file as written;
// the placeholder rewriting below only shapes the export template.
string original = content;
var faults = new List<string>();
@@ -42,8 +31,6 @@ public sealed class SceneryParser
var includes = new List<SceneryInclude>();
content = StripOptionalIncludes(content, includes);
// Trainsets are lifted out first so the placeholders survive the loose
// vehicle sweep below, which would otherwise match nodes inside them.
var trainsetEntries = new List<string>();
content = Regex.Replace(content, @"trainset\b[\s\S]*?\bendtrainset\b",
match =>
@@ -98,7 +85,6 @@ public sealed class SceneryParser
string[] parts = rest.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
// A leading one- or two-character token is a legacy ordering hint.
if (parts.Length >= 3 && parts[0].Length <= 2)
attachments.Add(new SceneryAttachment
{
@@ -122,10 +108,6 @@ public sealed class SceneryParser
return attachments;
}
/// <summary>
/// Removes the includes the scenery marked <c>$optional</c> from the text and
/// records them, so the user can pick which ones the export puts back.
/// </summary>
private static string StripOptionalIncludes(string content, List<SceneryInclude> includes)
{
if (content.IndexOf("$optional", StringComparison.OrdinalIgnoreCase) < 0)
@@ -160,10 +142,6 @@ public sealed class SceneryParser
RegexOptions.IgnoreCase);
}
/// <summary>
/// Lifts <c>node ... dynamic ... enddynamic</c> entries that sit outside any
/// trainset, so the depot can show and reorder them like a consist.
/// </summary>
private static string ExtractLooseVehicles(string content, List<Dynamic> vehicles, List<string> names,
List<string> faults)
{
@@ -176,7 +154,6 @@ public sealed class SceneryParser
{
string name = m.Groups[3].Value;
// A placeholder means the node belongs to a trainset already lifted out.
if (name.StartsWith("{{", StringComparison.Ordinal))
return m.Value;
@@ -214,7 +191,6 @@ public sealed class SceneryParser
}
catch
{
// Leave the node in the template so the export stays faithful.
faults.Add($"# loose dynamic parse fault: {name}");
names.Add(name);
return m.Value;
@@ -239,7 +215,6 @@ public sealed class SceneryParser
}
else if (!time.Success)
{
// Nothing authored: start the scenario at the player's wall clock.
var now = _clock.Now;
weather.ScenarioTimeOverride = $"{now.Hour:D2}:{now.Minute:D2}";
}

View File

@@ -12,17 +12,8 @@ using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Sceneries;
/// <summary>
/// Loads scenarios from the installation's scenery folder.
/// </summary>
/// <remarks>
/// Sceneries are code page 1250 text, and a large installation holds hundreds of
/// them, so the sweep is parallel; each file that throws is logged and dropped
/// rather than taking the whole load down.
/// </remarks>
public sealed class SceneryRepository : ISceneryRepository
{
/// <summary>Files starting with '$' are authoring scratch, not scenarios.</summary>
private const char ExcludedPrefix = '$';
private readonly IFileSystem _files;
@@ -83,5 +74,4 @@ public sealed class SceneryRepository : ISceneryRepository
private IEnumerable<string> SceneryFiles() =>
_files.GetFiles(_paths.Scenery, "*.scn")
.Where(path => Path.GetFileName(path).FirstOrDefault() != ExcludedPrefix);
}

View File

@@ -7,18 +7,8 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Infrastructure.Sceneries;
/// <summary>
/// <see cref="ISceneryTranslations"/> reading the JSON files a scenery may ship
/// in its i18n folder.
/// </summary>
/// <remarks>
/// English is loaded first and the user's language layered over it, so a partial
/// translation falls back per key rather than per file. Nested objects are
/// flattened to dotted keys, which is how the scenery text references them.
/// </remarks>
public sealed class SceneryTranslations : ISceneryTranslations
{
/// <summary>Marks a scenery string as a key rather than literal text.</summary>
private const char ReferencePrefix = '@';
private const string BaseLanguage = "en";
@@ -99,10 +89,6 @@ public sealed class SceneryTranslations : ISceneryTranslations
}
}
/// <summary>
/// Sceneries name their files by two-letter code; the starter's language
/// setting may carry a display name instead.
/// </summary>
private static string NormalizeLanguage(string language)
{
string lower = language.Trim().ToLowerInvariant();

View File

@@ -6,15 +6,6 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Infrastructure.Sceneries;
/// <summary>
/// Finds the timetable file a consist names.
/// </summary>
/// <remarks>
/// Timetables live in the shared timetables/ folder, next to the scenery, or in
/// scenario/ depending on the installation's vintage, and the name may or may
/// not carry its .txt extension. "none" is the format's way of saying a consist
/// runs without one.
/// </remarks>
public sealed class TimetableLocator
{
private const string NoTimetable = "none";

View File

@@ -7,18 +7,8 @@ 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"
@@ -37,13 +27,8 @@ public sealed class ExecutableLocator
_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))
@@ -55,7 +40,6 @@ public sealed class ExecutableLocator
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,
@@ -87,7 +71,6 @@ public sealed class ExecutableLocator
return CanonicalName;
}
/// <summary>Executables the user can pick from, most recently built first.</summary>
public List<string> ListCandidates(string? directory = null)
{
try
@@ -122,11 +105,6 @@ public sealed class ExecutableLocator
}
}
/// <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];

View File

@@ -3,10 +3,6 @@ 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";
@@ -21,7 +17,6 @@ public sealed class SettingsPaths
_paths = paths;
}
/// <summary>The user's own settings file, which is what the starter writes.</summary>
public string UserConfigPath()
{
if (_environment.IsWindows)
@@ -46,7 +41,6 @@ public sealed class SettingsPaths
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() =>

View File

@@ -8,26 +8,11 @@ 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");
// Kept as the code the file holds. Startup hands it to the localisation
// service, which resolves it to a display name and writes that back; the
// starter must not decide here that an unrecognised code means English,
// because the same key is what tells the simulator which language to run
// in.
s.LanguageCode = c.GetString("lang", "");
s.Language = s.LanguageCode;
s.Fullscreen = c.GetBool("fullscreen", false);
@@ -167,11 +152,9 @@ public sealed class SettingsSerializer
public void Write(SimulatorSettings s, ConfigFile c)
{
foreach (string key in ObsoleteKeys)
c.Remove(key);
// Lower case, as the simulator expects and as the old starter wrote it.
c.Set("lang", string.IsNullOrWhiteSpace(s.LanguageCode)
? "en"
: s.LanguageCode.Trim().ToLowerInvariant());

View File

@@ -7,21 +7,13 @@ namespace StarterNG.Infrastructure;
public enum ShortcutScope
{
/// <summary>Works anywhere in the window.</summary>
Global,
/// <summary>Only while the depot page is on screen.</summary>
Depot,
/// <summary>Handled by the consist strip itself, once a vehicle card has focus.</summary>
Consist
}
/// <summary>
/// One row of the shortcut table. <see cref="Key"/> is <see cref="Avalonia.Input.Key.None"/>
/// for rows that only document a gesture handled elsewhere (the consist strip reads the
/// arrow keys itself, they are never dispatched from the window).
/// </summary>
public sealed record Shortcut(
string Id,
string Display,
@@ -36,12 +28,6 @@ public sealed record Shortcut(
IsDispatched && Key == key && Modifiers == modifiers;
}
/// <summary>
/// The single source of truth for the starter's own shortcuts: the window dispatches from
/// this table and the cheat sheet is printed from it, so the two cannot drift apart. These
/// are deliberately hardcoded - the configurable key list in Settings belongs to the
/// simulator's cab controls and mixing the two only confuses people.
/// </summary>
public static class Shortcuts
{
public const string Start = "start";

View File

@@ -4,10 +4,6 @@ using System.Text;
namespace StarterNG.Infrastructure;
/// <summary>
/// Case- and diacritic-insensitive substring matching, so that typing
/// "rozdzielczosc" in a search box still finds "Rozdzielczość".
/// </summary>
public static class TextMatch
{
public static bool Contains(string? haystack, string? needle)
@@ -20,10 +16,6 @@ public static class TextMatch
return Fold(haystack).Contains(Fold(needle), StringComparison.Ordinal);
}
/// <summary>
/// Lower-cases and strips combining marks, so "Ż" and "z" compare equal.
/// Polish "ł" has no decomposed form, so it is mapped by hand.
/// </summary>
public static string Fold(string text)
{
if (string.IsNullOrEmpty(text))

View File

@@ -11,24 +11,10 @@ using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Vehicles;
/// <summary>
/// Reads vehicle physics from the .fiz files under dynamic/.
/// </summary>
/// <remarks>
/// The format is a flat token stream of "section" headings and "key value"
/// pairs, with an <c>include file p1 p2 end</c> directive whose parameters are
/// substituted for <c>(1)</c>-style placeholders in the included file. Only the
/// handful of keys the starter shows are decoded; everything else is skipped.
///
/// Both the name index and the parsed results are cached, because the depot asks
/// for the same model repeatedly while the user scrolls.
/// </remarks>
public sealed class FizPhysicsRepository : IPhysicsRepository
{
/// <summary>Guard against an include cycle in hand written files.</summary>
private const int MaxIncludeDepth = 6;
/// <summary>Sentinel meaning "the rear coupler was never declared".</summary>
private const int UndeclaredCoupler = -1;
private static readonly HashSet<string> KnownKeys = new(StringComparer.OrdinalIgnoreCase)
@@ -80,7 +66,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
{
var index = Index();
// Unpowered variants are shipped as "<model>dumb.fiz".
if (!index.TryGetValue(name, out string? path) &&
!index.TryGetValue(name + "dumb", out path))
return null;
@@ -95,7 +80,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
_log.Log($"physics/{Path.GetFileName(path)}", ex);
}
// A vehicle that only declares one coupler has the same at both ends.
if (physics.AllowedFlagB == UndeclaredCoupler)
{
physics.AllowedFlagB = physics.AllowedFlagA;
@@ -189,7 +173,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
}
else
{
// Anything else at this level opens a section, e.g. "Param:" or "BuffCoupl1:".
section = new string(bare.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant();
}
}
@@ -228,10 +211,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
private static bool Is(string key, string name) => key.Equals(name, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// A negative flag means the coupling is present but disabled; the simulator
/// encodes that as its absolute value plus 128.
/// </summary>
private static int ParseFlag(string value, int fallback) =>
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int flag)
? flag < 0 ? -flag + 128 : flag
@@ -242,7 +221,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
? parsed
: fallback;
/// <summary>Substitutes an include's "(n)" placeholder with its n-th parameter.</summary>
private static string Resolve(string token, string[]? parameters)
{
if (parameters is null || token.Length < 3 || token[0] != '(' || token[^1] != ')')
@@ -270,5 +248,4 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
}

View File

@@ -9,14 +9,6 @@ using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Vehicles;
/// <summary>
/// Reads data/load_weights.txt into a <see cref="LoadWeightsTable"/>.
/// </summary>
/// <remarks>
/// The file is a brace-delimited list of "name : weight" pairs with '#', ';' and
/// '//' comments. It is read once and kept, since it does not change while the
/// starter runs.
/// </remarks>
public sealed class LoadWeightsRepository
{
private const string FileName = "load_weights.txt";
@@ -79,8 +71,6 @@ public sealed class LoadWeightsRepository
body.Append(line).Append(' ');
}
// Colons may be written tight against their neighbours, so space them out
// before tokenising.
string[] tokens = body.Replace(":", " : ").ToString()
.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries);
@@ -100,5 +90,4 @@ public sealed class LoadWeightsRepository
return weights;
}
}

View File

@@ -5,17 +5,8 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Infrastructure.Vehicles;
/// <summary>
/// <see cref="IMiniTextureIndex"/> over the installation's textures/mini folder.
/// </summary>
/// <remarks>
/// Replaces the static thumbnail dictionary that used to live on the vehicle
/// database: an installation-scoped lookup with its own lifetime, which is what
/// lets a test point the depot at thumbnails that only exist in memory.
/// </remarks>
public sealed class MiniTextureIndex : IMiniTextureIndex
{
/// <summary>Thumbnail shown when a vehicle names one we do not have.</summary>
private const string FallbackName = "other";
private readonly IFileSystem _files;

View File

@@ -5,14 +5,6 @@ using StarterNG.Domain.Vehicles;
namespace StarterNG.Infrastructure.Vehicles;
/// <summary>
/// Reports liveries whose skin or model file is not actually present, for the
/// optional "log missing vehicle files" diagnostic.
/// </summary>
/// <remarks>
/// Both assets are looked up under several extensions because the simulator
/// accepts a texture as .mat or .bmp and a model as .t3d or .e3d.
/// </remarks>
public sealed class MissingAssetScanner
{
private static readonly string[] TextureExtensions = { ".mat", ".bmp" };

View File

@@ -4,30 +4,10 @@ using StarterNG.Domain.Vehicles;
namespace StarterNG.Infrastructure.Vehicles;
/// <summary>
/// Reads one vehicle folder's textures.txt into a <see cref="VehicleEntry"/>.
/// </summary>
/// <remarks>
/// The format is line oriented and stateful: '$a' marks the rest of the file as
/// archived, '!=' sets the category letter, and '^n' opens a fixed set of n
/// vehicles. Everything else with an '=' is a livery. Unknown lines are skipped
/// rather than rejected, because the files are hand written.
///
/// Pure text in, model out: the repository does the reading, so the format can
/// be tested from a string array.
/// </remarks>
public sealed class TexturesTxtParser
{
/// <summary>Category letter meaning "derive it from the thumbnail name".</summary>
private const string DeriveCategory = "*";
/// <summary>
/// Parses the file's lines. Returns null when the folder declares no liveries.
/// </summary>
/// <param name="directory">
/// The folder path relative to dynamic/, with a trailing slash; it forms the
/// livery ids and is what the scenery format writes as the data folder.
/// </param>
public VehicleEntry? Parse(string directory, IReadOnlyList<string> lines)
{
var entry = new VehicleEntry { Uuid = "legacy:" + directory };
@@ -120,8 +100,6 @@ public sealed class TexturesTxtParser
if (skin.Length == 0)
return null;
// Every "=" segment is "model, thumbnail[, per-texture thumbnail]"; the
// first is the livery proper and the rest are alternative models.
var specs = new List<(string Model, string Mini, string TextureMini)>();
for (int i = 1; i < parts.Length; i++)
{
@@ -170,10 +148,6 @@ public sealed class TexturesTxtParser
return texture;
}
/// <summary>
/// The trailing comment is a fixed, comma separated credit line. Underscores
/// stand in for spaces, a convention from the era the format comes from.
/// </summary>
private static VehicleMeta? ParseDescription(string description)
{
if (string.IsNullOrWhiteSpace(description))

View File

@@ -8,10 +8,6 @@ using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Vehicles;
/// <summary>
/// Loads the catalogue from the dynamic/&lt;maker&gt;/&lt;vehicle&gt;/textures.txt
/// files an installation ships.
/// </summary>
public sealed class TexturesTxtVehicleRepository : IVehicleRepository
{
private const string TexturesFileName = "textures.txt";
@@ -55,8 +51,6 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository
{
try
{
// Read as text so the code page survives: the credit lines carry
// Polish characters that ReadAllLines' default encoding would mangle.
string[] lines = _files.ReadAllText(path, _encoding).Split('\n');
return _parser.Parse(RelativeDirectory(path), lines);
}
@@ -67,10 +61,6 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository
}
}
/// <summary>
/// Rolling stock sits exactly two levels below dynamic/: a maker folder and a
/// vehicle folder.
/// </summary>
private IEnumerable<string> TexturesFiles()
{
foreach (string maker in _files.GetDirectories(_paths.Dynamic))
@@ -82,10 +72,6 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository
}
}
/// <summary>
/// The folder as the scenery format spells it: relative to dynamic/, forward
/// slashes, trailing slash.
/// </summary>
private string RelativeDirectory(string texturesPath)
{
string directory = Path.GetDirectoryName(texturesPath) ?? "";
@@ -98,5 +84,4 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository
return relative.Replace('\\', '/').TrimEnd('/') + "/";
}
}

View File

@@ -4,19 +4,8 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Presentation.Scenarios;
/// <summary>
/// Converts between the weather tab's fog slider and a distance in metres.
/// </summary>
/// <remarks>
/// Visibility runs from 10 m to 10 km, a range no linear slider handles well:
/// the interesting part - thick fog, where a few metres matter - would be a
/// couple of pixels wide. The slider is therefore logarithmic over its 0-1000
/// travel, and the result snaps to a step that grows with the distance, so the
/// read-out lands on round numbers a driver would recognise.
/// </remarks>
public static class FogScale
{
/// <summary>Travel of the slider the scale is spread over.</summary>
public const double SliderRange = 1000.0;
public static int ToMetres(double sliderPosition)
@@ -37,7 +26,6 @@ public static class FogScale
Math.Log((double)SceneryWeather.FogMax / SceneryWeather.FogMin);
}
/// <summary>Metres below a kilometre, kilometres above it.</summary>
public static string Format(int metres, CultureInfo culture) =>
metres < 1000
? $"{metres} m"

View File

@@ -6,27 +6,11 @@ using StarterNG.Domain.Sceneries;
namespace StarterNG.Presentation.Scenarios;
/// <summary>
/// A node of the scenery list: either a scenario, or a group holding scenarios.
/// </summary>
/// <param name="Label">Text to show.</param>
/// <param name="SceneryIndex">Index into the scenery list, or -1 for a group.</param>
/// <param name="Children">Scenarios under a group; empty for a scenario.</param>
public sealed record SceneryTreeNode(string Label, int SceneryIndex, IReadOnlyList<SceneryTreeNode> Children)
{
public bool IsGroup => SceneryIndex < 0;
}
/// <summary>
/// Turns the flat scenery list into the tree the scenario picker shows:
/// scenarios that declare a group sit under it, the rest stay at the top, and
/// everything is sorted by name.
/// </summary>
/// <remarks>
/// Pulled out of the view so the grouping and the archival filter can be tested
/// without a TreeView. The view's job is reduced to turning these nodes into
/// controls.
/// </remarks>
public sealed class SceneryTreeBuilder
{
private readonly ISceneryTranslations _translations;
@@ -36,9 +20,6 @@ public sealed class SceneryTreeBuilder
_translations = translations;
}
/// <param name="sceneries">The list the node indices refer to.</param>
/// <param name="includeArchival">Whether scenarios marked archival are listed.</param>
/// <param name="langCode">Language for resolving each scenery's own translations.</param>
public IReadOnlyList<SceneryTreeNode> Build(IReadOnlyList<Scenery> sceneries, bool includeArchival,
string langCode)
{
@@ -52,8 +33,6 @@ public sealed class SceneryTreeBuilder
if (scenery.Archival && !includeArchival)
continue;
// The group name may itself be an "@key" reference into the scenery's
// own translation files, so the table has to be loaded per scenery.
_translations.LoadFor(scenery, langCode);
var node = new SceneryTreeNode(scenery.DisplayName, i, Array.Empty<SceneryTreeNode>());

View File

@@ -3,15 +3,6 @@ using StarterNG.Application.Abstractions;
namespace StarterNG.Presentation.Scenarios;
/// <summary>
/// Translates between the scenery's day-of-year (movelight) and the date the
/// weather tab shows.
/// </summary>
/// <remarks>
/// Day 0 is the scenery saying "whatever today is", which the tab presents as
/// the current date; the season presets are ordinary days of the year. Leap
/// years are honoured so 366 stays selectable in one.
/// </remarks>
public sealed class SeasonDates
{
private readonly IClock _clock;
@@ -21,13 +12,8 @@ public sealed class SeasonDates
_clock = clock;
}
/// <summary>The day-of-year a scenery means by 0: today.</summary>
public int Today => _clock.Now.DayOfYear;
/// <summary>
/// The date to show for a stored day-of-year, in the current year. Day 0 and
/// out-of-range values fall back to today, clamped to the year's length.
/// </summary>
public DateTimeOffset DateOf(int dayOfYear)
{
int year = _clock.Now.Year;

View File

@@ -9,10 +9,6 @@ using StarterNG.Infrastructure;
namespace StarterNG.Views;
/// <summary>
/// The cheat sheet, printed straight from <see cref="Shortcuts.All"/> so it can never
/// list a gesture the window does not actually dispatch.
/// </summary>
public static class ShortcutsWindow
{
public static Window Create()