mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-16 23:49:19 +02:00
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:
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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/<maker>/<vehicle>/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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -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; } = "";
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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() =>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -8,10 +8,6 @@ using StarterNG.Infrastructure.Adapters;
|
||||
|
||||
namespace StarterNG.Infrastructure.Vehicles;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the catalogue from the dynamic/<maker>/<vehicle>/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('/') + "/";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user