diff --git a/StarterNG.Tests/LoadWeightsTests.cs b/StarterNG.Tests/LoadWeightsTests.cs
new file mode 100644
index 0000000..ae6e520
--- /dev/null
+++ b/StarterNG.Tests/LoadWeightsTests.cs
@@ -0,0 +1,41 @@
+using StarterNG.Classes;
+using StarterNG.Domain.Vehicles;
+using StarterNG.Infrastructure.Vehicles;
+using StarterNG.Tests.Fakes;
+
+namespace StarterNG.Tests;
+
+public class LoadWeightsTests
+{
+ private static LoadWeightsRepository Repository(InMemoryFileSystem files)
+ {
+ var installation = new TestInstallation(files);
+ return new LoadWeightsRepository(files, installation.Paths, installation.Log);
+ }
+
+ [Fact]
+ public void Weights_are_read_from_the_pairs_in_the_file()
+ {
+ var files = new InMemoryFileSystem()
+ .WithFile(TestInstallation.At("data", "load_weights.txt"),
+ "{\n coal : 850 # węgiel\n sand : 1600\n}");
+
+ var table = Repository(files).Table;
+
+ Assert.Equal(850, table.WeightOf("coal"));
+ Assert.Equal(1600, table.WeightOf("SAND"));
+ }
+
+ [Fact]
+ public void An_unknown_cargo_falls_back_to_the_default_weight()
+ {
+ Assert.Equal(LoadWeightsTable.DefaultWeight,
+ Repository(new InMemoryFileSystem()).Table.WeightOf("nieznany"));
+ }
+
+ [Fact]
+ public void The_pantograph_pseudo_load_weighs_nothing()
+ {
+ Assert.Equal(0, new LoadWeightsTable().WeightOf(Dynamic.PantState));
+ }
+}
diff --git a/StarterNG.Tests/SceneryTranslationsTests.cs b/StarterNG.Tests/SceneryTranslationsTests.cs
new file mode 100644
index 0000000..a2d422b
--- /dev/null
+++ b/StarterNG.Tests/SceneryTranslationsTests.cs
@@ -0,0 +1,52 @@
+using StarterNG.Infrastructure.Sceneries;
+using StarterNG.Tests.Fakes;
+
+namespace StarterNG.Tests;
+
+public class SceneryTranslationsTests
+{
+ private static string I18n(string file) => Path.Combine(Path.GetFullPath(TestInstallation.At("scenery")),
+ "i18n", file);
+
+ [Fact]
+ public void An_at_reference_is_resolved_against_the_sceneries_own_table()
+ {
+ var files = new InMemoryFileSystem()
+ .WithFile(TestInstallation.At("scenery", "kato.scn"), "//$d @mission.brief")
+ .WithFile(I18n("kato_en.json"), """{ "mission": { "brief": "Take the stopper to Katowice" } }""");
+ var installation = new TestInstallation(files);
+ var scenery = Assert.Single(installation.Sceneries.LoadAll());
+ var translations = new SceneryTranslations(files, installation.Log);
+
+ translations.LoadFor(scenery, "en");
+
+ Assert.Equal("Take the stopper to Katowice", scenery.LocalizedDescription(translations));
+ }
+
+ [Fact]
+ public void The_users_language_is_layered_over_english()
+ {
+ var files = new InMemoryFileSystem()
+ .WithFile(TestInstallation.At("scenery", "kato.scn"), "//$d @brief")
+ .WithFile(I18n("kato_en.json"), """{ "brief": "English" }""")
+ .WithFile(I18n("kato_pl.json"), """{ "brief": "Polski" }""");
+ var installation = new TestInstallation(files);
+ var scenery = Assert.Single(installation.Sceneries.LoadAll());
+ var translations = new SceneryTranslations(files, installation.Log);
+
+ translations.LoadFor(scenery, "Polski");
+
+ Assert.Equal("Polski", scenery.LocalizedDescription(translations));
+ }
+
+ [Fact]
+ public void Literal_text_and_unknown_keys_are_left_alone()
+ {
+ var installation = new TestInstallation();
+ var translations = new SceneryTranslations(installation.Files, installation.Log);
+
+ Assert.Equal("zwykły opis", translations.Translate("zwykły opis"));
+ Assert.Equal("@nieznany.klucz", translations.Translate("@nieznany.klucz"));
+ Assert.Equal("", translations.Translate(null));
+ }
+}
diff --git a/StarterNG/Application/Abstractions/ISceneryTranslations.cs b/StarterNG/Application/Abstractions/ISceneryTranslations.cs
new file mode 100644
index 0000000..4ffdc17
--- /dev/null
+++ b/StarterNG/Application/Abstractions/ISceneryTranslations.cs
@@ -0,0 +1,23 @@
+using StarterNG.Domain.Sceneries;
+
+namespace StarterNG.Application.Abstractions;
+
+///
+/// The translations a scenery ships alongside itself, in scenery/i18n/*.json.
+///
+///
+/// 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
+/// replaces it rather than adding to it.
+///
+public interface ISceneryTranslations
+{
+ /// Loads the tables for one scenery, English first then the user's language.
+ void LoadFor(Scenery scenery, string langCode);
+
+ ///
+ /// Resolves an "@key" reference against the loaded table. Text that is not a
+ /// reference, or a key with no translation, comes back unchanged.
+ ///
+ string Translate(string? text);
+}
diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs
index 38108a7..6b166fe 100644
--- a/StarterNG/Application/AppServices.cs
+++ b/StarterNG/Application/AppServices.cs
@@ -44,7 +44,10 @@ public sealed class AppServices
Vehicles = new TexturesTxtVehicleRepository(files, paths, log, new TexturesTxtParser());
Physics = new FizPhysicsRepository(files, paths, log);
MissingAssets = new MissingAssetScanner(files, paths);
+ SceneryTexts = new SceneryTranslations(files, log);
+ LoadWeights = new LoadWeightsRepository(files, paths, log);
Library = new GameLibrary(Vehicles, Sceneries, MiniTextures, Physics, log);
+ State = new AppState();
}
///
@@ -90,9 +93,16 @@ public sealed class AppServices
public MissingAssetScanner MissingAssets { get; }
+ public ISceneryTranslations SceneryTexts { get; }
+
+ public LoadWeightsRepository LoadWeights { get; }
+
/// Everything read out of the installation: rolling stock and scenarios.
public GameLibrary Library { get; }
+ /// What the user currently has selected.
+ public AppState State { get; }
+
///
/// Exposed as the concrete service rather than
/// because the XAML binds to it as a change-notifying source; code should take
diff --git a/StarterNG/Classes/AppState.cs b/StarterNG/Application/AppState.cs
similarity index 66%
rename from StarterNG/Classes/AppState.cs
rename to StarterNG/Application/AppState.cs
index d0f0845..105e6c7 100644
--- a/StarterNG/Classes/AppState.cs
+++ b/StarterNG/Application/AppState.cs
@@ -1,12 +1,22 @@
using System;
using StarterNG.Domain.Sceneries;
-namespace StarterNG.Classes;
+using StarterNG.Classes;
+namespace StarterNG.Application;
+
+///
+/// What the user currently has selected: which scenario, which consist in it and
+/// which vehicle they will start in.
+///
+///
+/// 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 to stay in step.
+///
public sealed class AppState
{
- public static AppState Instance { get; } = new();
-
+ /// Raised whenever any of the selections changes.
public event Action? Changed;
private Scenery? _currentScenery;
diff --git a/StarterNG/Domain/Consist.cs b/StarterNG/Domain/Consist.cs
index a60eca0..a41d0d0 100644
--- a/StarterNG/Domain/Consist.cs
+++ b/StarterNG/Domain/Consist.cs
@@ -6,6 +6,7 @@ using System.Linq;
using StarterNG.Classes;
using StarterNG.Domain.Sceneries;
using StarterNG.Domain.Vehicles;
+using StarterNG.Application;
namespace StarterNG.Domain;
@@ -170,7 +171,7 @@ public sealed class Consist : IReadOnlyList
var driven = item.Cars.FirstOrDefault(c =>
c.DriverType is eDriverType.Headdriver or eDriverType.Reardriver);
- AppState.Instance.StartingVehicleName = (driven ?? item.Cars[0]).Name;
+ AppServices.Current.State.StartingVehicleName = (driven ?? item.Cars[0]).Name;
}
public Dynamic MakeDynamic(VehicleTexture texture, Scenery? scenery)
@@ -324,7 +325,7 @@ public sealed class Consist : IReadOnlyList
if (ReferenceEquals(Selected, item))
SyncStartingVehicle();
Raise();
- AppState.Instance.NotifyChanged();
+ AppServices.Current.State.NotifyChanged();
}
public void Split(ConsistItem item)
diff --git a/StarterNG/Domain/LoadWeights.cs b/StarterNG/Domain/LoadWeights.cs
deleted file mode 100644
index f888a33..0000000
--- a/StarterNG/Domain/LoadWeights.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-
-using StarterNG.Classes;
-
-namespace StarterNG.Domain;
-
-public static class LoadWeights
-{
- private static Dictionary? _weights;
-
- public static int WeightOf(string? name)
- {
- Load();
- return !string.IsNullOrEmpty(name) && _weights!.TryGetValue(name!, out int w) ? w : 1000;
- }
-
- public static string Describe(string name) =>
- string.Equals(name, Dynamic.PantState, StringComparison.OrdinalIgnoreCase)
- ? App.Loc["LoadPantState"]
- : name;
-
- private static void Load()
- {
- if (_weights != null)
- return;
-
- var weights = new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- [Dynamic.PantState] = 0
- };
- try
- {
- string path = Path.Combine("data", "load_weights.txt");
- if (File.Exists(path))
- {
- var body = new StringBuilder();
- foreach (var raw in File.ReadAllLines(path, Encoding.GetEncoding(1250)))
- {
- string line = raw;
- int comment = line.IndexOfAny(new[] { '#', ';' });
- if (comment >= 0) line = line[..comment];
- int slashes = line.IndexOf("//", StringComparison.Ordinal);
- if (slashes >= 0) line = line[..slashes];
- body.Append(line).Append(' ');
- }
-
- var tokens = body.Replace(":", " : ").ToString()
- .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
-
- for (int i = 1; i + 1 < tokens.Length; i++)
- {
- if (tokens[i] != ":") continue;
- string name = tokens[i - 1];
- if (name is "{" or "}") continue;
- if (!int.TryParse(tokens[i + 1], NumberStyles.Integer,
- CultureInfo.InvariantCulture, out int w))
- continue;
- weights[name] = w;
- }
- }
- }
- catch (Exception ex)
- {
- Infrastructure.Diagnostics.Log("data/load_weights.txt", ex);
- }
-
- _weights = weights;
- }
-}
diff --git a/StarterNG/Domain/Sceneries/Scenery.cs b/StarterNG/Domain/Sceneries/Scenery.cs
index d340b6c..df9a079 100644
--- a/StarterNG/Domain/Sceneries/Scenery.cs
+++ b/StarterNG/Domain/Sceneries/Scenery.cs
@@ -96,10 +96,14 @@ public sealed class Scenery
public string DisplayName => System.IO.Path.GetFileNameWithoutExtension(Path);
- public string LocalizedDescription =>
+ ///
+ /// The description with any "@key" references resolved against the scenery's
+ /// own translation files.
+ ///
+ public string LocalizedDescription(ISceneryTranslations translations) =>
string.IsNullOrEmpty(Description)
? ""
- : string.Join("\n", Description.Split('\n').Select(SceneryI18n.T));
+ : string.Join("\n", Description.Split('\n').Select(translations.Translate));
///
/// Renders the scenario the simulator should load: the original file with the
diff --git a/StarterNG/Domain/SceneryI18n.cs b/StarterNG/Domain/SceneryI18n.cs
deleted file mode 100644
index 2b570fd..0000000
--- a/StarterNG/Domain/SceneryI18n.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.Json;
-using StarterNG.Classes;
-using StarterNG.Domain.Sceneries;
-
-namespace StarterNG.Domain;
-
-public static class SceneryI18n
-{
- private static readonly Dictionary Map =
- new(StringComparer.OrdinalIgnoreCase);
-
- public static void Clear() => Map.Clear();
-
- public static void LoadFor(Scenery scenery, string langCode = "en")
- {
- Map.Clear();
- string scnDir = Path.GetDirectoryName(Path.GetFullPath(scenery.Path)) ?? "scenery";
- string id = Path.GetFileNameWithoutExtension(scenery.Path);
- string lang = NormalizeLang(langCode);
- string i18nDir = Path.Combine(scnDir, "i18n");
-
- TryLoad(Path.Combine(i18nDir, $"{id}_en.json"));
- if (!string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase))
- TryLoad(Path.Combine(i18nDir, $"{id}_{lang}.json"));
- }
-
- public static string T(string? text)
- {
- if (string.IsNullOrWhiteSpace(text)) return text ?? "";
- string t = text.TrimStart();
- if (t.Length == 0 || t[0] != '@') return text;
- string key = t[1..].Trim();
- return Map.TryGetValue(key, out var v) ? v : text;
- }
-
- private static void TryLoad(string path)
- {
- if (!File.Exists(path)) return;
- try
- {
- using var doc = JsonDocument.Parse(File.ReadAllText(path));
- if (doc.RootElement.ValueKind == JsonValueKind.Object)
- Flatten("", doc.RootElement);
- }
- catch (Exception ex)
- {
- StarterNG.Infrastructure.Diagnostics.Log($"scenery i18n/{Path.GetFileName(path)}", ex);
- }
- }
-
- private static void Flatten(string prefix, JsonElement obj)
- {
- foreach (var prop in obj.EnumerateObject())
- {
- string key = string.IsNullOrEmpty(prefix) ? prop.Name : prefix + "." + prop.Name;
- switch (prop.Value.ValueKind)
- {
- case JsonValueKind.String:
- Map[key] = prop.Value.GetString() ?? "";
- break;
- case JsonValueKind.Number:
- case JsonValueKind.True:
- case JsonValueKind.False:
- Map[key] = prop.Value.ToString();
- break;
- case JsonValueKind.Object:
- Flatten(key, prop.Value);
- break;
- }
- }
- }
-
- private static string NormalizeLang(string language)
- {
- string l = language.Trim().ToLowerInvariant();
- return l switch
- {
- "polski" or "pl" => "pl",
- "english" or "en" => "en",
- "čeština" or "cesky" or "czech" or "cs" or "cz" => "cz",
- "magyar" or "hungarian" or "hu" => "hu",
- "русский" or "russian" or "ru" => "ru",
- _ => l.Length >= 2 ? l[..2] : "en"
- };
- }
-}
diff --git a/StarterNG/Domain/Vehicles/LoadWeightsTable.cs b/StarterNG/Domain/Vehicles/LoadWeightsTable.cs
new file mode 100644
index 0000000..52d8501
--- /dev/null
+++ b/StarterNG/Domain/Vehicles/LoadWeightsTable.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using StarterNG.Classes;
+
+namespace StarterNG.Domain.Vehicles;
+
+///
+/// How heavy a tonne of each cargo type is, as declared by
+/// data/load_weights.txt, used to turn a load into a mass.
+///
+///
+/// Was a static class that also read the file. The table is now a value the
+/// repository builds; an unknown cargo weighs , which
+/// is what the simulator assumes.
+///
+public sealed class LoadWeightsTable
+{
+ /// Weight assumed for a cargo the file does not mention.
+ public const int DefaultWeight = 1000;
+
+ private readonly Dictionary _weights;
+
+ public LoadWeightsTable(IReadOnlyDictionary? weights = null)
+ {
+ _weights = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // The pantograph state is carried as a pseudo load and weighs nothing.
+ [Dynamic.PantState] = 0
+ };
+
+ if (weights is null)
+ return;
+
+ foreach (var (name, weight) in weights)
+ _weights[name] = weight;
+ }
+
+ public int WeightOf(string? name) =>
+ !string.IsNullOrEmpty(name) && _weights.TryGetValue(name, out int weight) ? weight : DefaultWeight;
+}
diff --git a/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs b/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs
new file mode 100644
index 0000000..593e425
--- /dev/null
+++ b/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using StarterNG.Application.Abstractions;
+using StarterNG.Domain.Sceneries;
+
+namespace StarterNG.Infrastructure.Sceneries;
+
+///
+/// reading the JSON files a scenery may ship
+/// in its i18n folder.
+///
+///
+/// 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.
+///
+public sealed class SceneryTranslations : ISceneryTranslations
+{
+ /// Marks a scenery string as a key rather than literal text.
+ private const char ReferencePrefix = '@';
+
+ private const string BaseLanguage = "en";
+
+ private readonly IFileSystem _files;
+ private readonly IDiagnosticsLog _log;
+ private readonly Dictionary _map = new(StringComparer.OrdinalIgnoreCase);
+
+ public SceneryTranslations(IFileSystem files, IDiagnosticsLog log)
+ {
+ _files = files;
+ _log = log;
+ }
+
+ public void LoadFor(Scenery scenery, string langCode)
+ {
+ _map.Clear();
+
+ string sceneryDir = Path.GetDirectoryName(Path.GetFullPath(scenery.Path)) ?? "scenery";
+ string id = Path.GetFileNameWithoutExtension(scenery.Path);
+ string i18nDir = Path.Combine(sceneryDir, "i18n");
+ string language = NormalizeLanguage(langCode);
+
+ TryLoad(Path.Combine(i18nDir, $"{id}_{BaseLanguage}.json"));
+ if (!string.Equals(language, BaseLanguage, StringComparison.OrdinalIgnoreCase))
+ TryLoad(Path.Combine(i18nDir, $"{id}_{language}.json"));
+ }
+
+ public string Translate(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return text ?? "";
+
+ string trimmed = text.TrimStart();
+ if (trimmed.Length == 0 || trimmed[0] != ReferencePrefix)
+ return text;
+
+ string key = trimmed[1..].Trim();
+ return _map.TryGetValue(key, out string? translated) ? translated : text;
+ }
+
+ private void TryLoad(string path)
+ {
+ if (!_files.FileExists(path))
+ return;
+
+ try
+ {
+ using var document = JsonDocument.Parse(_files.ReadAllText(path));
+ if (document.RootElement.ValueKind == JsonValueKind.Object)
+ Flatten("", document.RootElement);
+ }
+ catch (Exception ex)
+ {
+ _log.Log($"scenery i18n/{Path.GetFileName(path)}", ex);
+ }
+ }
+
+ private void Flatten(string prefix, JsonElement element)
+ {
+ foreach (var property in element.EnumerateObject())
+ {
+ string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name;
+ switch (property.Value.ValueKind)
+ {
+ case JsonValueKind.String:
+ _map[key] = property.Value.GetString() ?? "";
+ break;
+ case JsonValueKind.Number:
+ case JsonValueKind.True:
+ case JsonValueKind.False:
+ _map[key] = property.Value.ToString();
+ break;
+ case JsonValueKind.Object:
+ Flatten(key, property.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Sceneries name their files by two-letter code; the starter's language
+ /// setting may carry a display name instead.
+ ///
+ private static string NormalizeLanguage(string language)
+ {
+ string lower = language.Trim().ToLowerInvariant();
+ return lower switch
+ {
+ "polski" or "pl" => "pl",
+ "english" or "en" => "en",
+ "čeština" or "cesky" or "czech" or "cs" or "cz" => "cz",
+ "magyar" or "hungarian" or "hu" => "hu",
+ "русский" or "russian" or "ru" => "ru",
+ _ => lower.Length >= 2 ? lower[..2] : "en"
+ };
+ }
+}
diff --git a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs
new file mode 100644
index 0000000..486c991
--- /dev/null
+++ b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using StarterNG.Application.Abstractions;
+using StarterNG.Domain.Vehicles;
+
+namespace StarterNG.Infrastructure.Vehicles;
+
+///
+/// Reads data/load_weights.txt into a .
+///
+///
+/// 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.
+///
+public sealed class LoadWeightsRepository
+{
+ private const string FileName = "load_weights.txt";
+
+ private readonly IFileSystem _files;
+ private readonly IGamePaths _paths;
+ private readonly IDiagnosticsLog _log;
+ private readonly object _gate = new();
+
+ private LoadWeightsTable? _table;
+
+ public LoadWeightsRepository(IFileSystem files, IGamePaths paths, IDiagnosticsLog log)
+ {
+ _files = files;
+ _paths = paths;
+ _log = log;
+ }
+
+ public LoadWeightsTable Table
+ {
+ get
+ {
+ lock (_gate)
+ return _table ??= Read();
+ }
+ }
+
+ private LoadWeightsTable Read()
+ {
+ string path = Path.Combine(_paths.Data, FileName);
+ if (!_files.FileExists(path))
+ return new LoadWeightsTable();
+
+ try
+ {
+ return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyEncoding())));
+ }
+ catch (Exception ex)
+ {
+ _log.Log($"data/{FileName}", ex);
+ return new LoadWeightsTable();
+ }
+ }
+
+ private static Dictionary ParsePairs(string text)
+ {
+ var body = new StringBuilder();
+ foreach (string raw in text.Split('\n'))
+ {
+ string line = raw;
+
+ int comment = line.IndexOfAny(new[] { '#', ';' });
+ if (comment >= 0)
+ line = line[..comment];
+
+ int slashes = line.IndexOf("//", StringComparison.Ordinal);
+ if (slashes >= 0)
+ line = line[..slashes];
+
+ 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);
+
+ var weights = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ for (int i = 1; i + 1 < tokens.Length; i++)
+ {
+ if (tokens[i] != ":")
+ continue;
+
+ string name = tokens[i - 1];
+ if (name is "{" or "}")
+ continue;
+
+ if (int.TryParse(tokens[i + 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int weight))
+ weights[name] = weight;
+ }
+
+ return weights;
+ }
+
+ private static Encoding LegacyEncoding()
+ {
+ try
+ {
+ return Encoding.GetEncoding(1250);
+ }
+ catch (Exception)
+ {
+ return Encoding.Latin1;
+ }
+ }
+}
diff --git a/StarterNG/MainWindow.axaml.cs b/StarterNG/MainWindow.axaml.cs
index 6e80542..02232c6 100644
--- a/StarterNG/MainWindow.axaml.cs
+++ b/StarterNG/MainWindow.axaml.cs
@@ -76,7 +76,7 @@ public partial class MainWindow : Window
// why OnShortcutKey has to be careful about text entry and key capture.
AddHandler(KeyDownEvent, OnShortcutKey, RoutingStrategies.Tunnel);
- AppState.Instance.Changed += UpdateStartButton;
+ AppServices.Current.State.Changed += UpdateStartButton;
Opened += (_, _) =>
{
@@ -342,8 +342,8 @@ public partial class MainWindow : Window
{
if (startButton is null) return;
- var scenery = AppState.Instance.CurrentScenery;
- var trainset = AppState.Instance.CurrentTrainset;
+ var scenery = AppServices.Current.State.CurrentScenery;
+ var trainset = AppServices.Current.State.CurrentTrainset;
if (scenery is null)
{
@@ -369,7 +369,7 @@ public partial class MainWindow : Window
static bool CanStart(Dynamic v) =>
v.DriverType is eDriverType.Headdriver or eDriverType.Reardriver or eDriverType.Passenger;
- string? selected = AppState.Instance.StartingVehicleName;
+ string? selected = AppServices.Current.State.StartingVehicleName;
if (!string.IsNullOrEmpty(selected))
{
var pick = trainset.Vehicles.FirstOrDefault(v =>
@@ -415,10 +415,10 @@ public partial class MainWindow : Window
private async Task LaunchAsync(bool saveSettings, bool freeFly)
{
- var scenery = AppState.Instance.CurrentScenery;
+ var scenery = AppServices.Current.State.CurrentScenery;
if (scenery is null)
return;
- var trainset = AppState.Instance.CurrentTrainset;
+ var trainset = AppServices.Current.State.CurrentTrainset;
if (trainset is null)
return;
@@ -438,10 +438,10 @@ public partial class MainWindow : Window
}
string? vehicle = TrainsetDisplay.UniquifyForLaunch(
- trainset, scenery, AppState.Instance.StartingVehicleName);
+ trainset, scenery, AppServices.Current.State.StartingVehicleName);
if (string.IsNullOrEmpty(vehicle))
vehicle = ResolveStartVehicle(trainset);
- AppState.Instance.StartingVehicleName = vehicle;
+ AppServices.Current.State.StartingVehicleName = vehicle;
LoadingScreen.Prepare(trainset.Logo, Path.GetFileNameWithoutExtension(scenery.Path));
@@ -508,7 +508,7 @@ public partial class MainWindow : Window
static bool CanStart(Dynamic v) =>
v.DriverType is eDriverType.Headdriver or eDriverType.Reardriver or eDriverType.Passenger;
- string? selected = AppState.Instance.StartingVehicleName;
+ string? selected = AppServices.Current.State.StartingVehicleName;
if (!string.IsNullOrEmpty(selected))
{
var pick = trainset.Vehicles.FirstOrDefault(v =>
diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs
index aa5738c..85b75c0 100644
--- a/StarterNG/Views/Depot.axaml.cs
+++ b/StarterNG/Views/Depot.axaml.cs
@@ -146,7 +146,7 @@ public partial class Depot : UserControl
_drag.VehicleListPressed(sender, e);
private Dynamic NewVehicle(VehicleTexture texture) =>
- _consist.MakeDynamic(texture, _listScenery ?? AppState.Instance.CurrentScenery);
+ _consist.MakeDynamic(texture, _listScenery ?? AppServices.Current.State.CurrentScenery);
private void SyncFromSelection()
{
@@ -155,7 +155,7 @@ public partial class Depot : UserControl
PopulateSceneryConsists();
- var trainset = AppState.Instance.CurrentTrainset;
+ var trainset = AppServices.Current.State.CurrentTrainset;
if (trainset != null && !ReferenceEquals(trainset, _consist.EditingTrainset))
_consist.LoadFrom(trainset);
else if (_consist.EditingTrainset != null)
@@ -220,7 +220,7 @@ public partial class Depot : UserControl
_consist.EditingTrainset,
_info.PhysicsFor,
car => _db.TextureForSkin(car.SkinFile) is { } t ? VehicleInfo.CategoryOf(t) : null,
- LoadWeights.WeightOf));
+ AppServices.Current.LoadWeights.Table.WeightOf));
}
private void RemoveVehicleButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -245,7 +245,7 @@ public partial class Depot : UserControl
depotShowAiCheck.IsChecked = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
depotDrivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
- _listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
+ _listScenery = AppServices.Current.State.CurrentScenery ?? _sceneries.FirstOrDefault();
sceneryConsistsTab.Header = App.Loc["SceneryConsists"];
ToolTip.SetTip(sceneryConsistsTab,
_listScenery != null ? Path.GetFileNameWithoutExtension(_listScenery.Path) : App.Loc["NoSceneryConsists"]);
@@ -276,7 +276,7 @@ public partial class Depot : UserControl
randomTurn: ToolRandomOrientation);
sceneryConsistList.Items.Add(entry);
- if (ReferenceEquals(trainset, AppState.Instance.CurrentTrainset))
+ if (ReferenceEquals(trainset, AppServices.Current.State.CurrentTrainset))
sceneryConsistList.SelectedItem = entry;
}
}
@@ -289,9 +289,9 @@ public partial class Depot : UserControl
if (sceneryConsistList.SelectedItem is not ListBoxItem { Tag: Trainset trainset })
return;
- AppState.Instance.CurrentScenery = _listScenery;
- AppState.Instance.CurrentTrainset = trainset;
- AppState.Instance.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(trainset);
+ AppServices.Current.State.CurrentScenery = _listScenery;
+ AppServices.Current.State.CurrentTrainset = trainset;
+ AppServices.Current.State.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(trainset);
_consist.LoadFrom(trainset);
}
@@ -313,8 +313,8 @@ public partial class Depot : UserControl
private void ApplyConsist(Trainset target, List vehicles)
{
target.Vehicles = vehicles;
- AppState.Instance.CurrentScenery = _listScenery;
- AppState.Instance.CurrentTrainset = target;
+ AppServices.Current.State.CurrentScenery = _listScenery;
+ AppServices.Current.State.CurrentTrainset = target;
_consist.LoadFrom(target);
PopulateSceneryConsists();
}
@@ -322,8 +322,8 @@ public partial class Depot : UserControl
private void RemoveAllVehicles(Trainset target)
{
target.Vehicles = new List();
- AppState.Instance.CurrentScenery = _listScenery;
- AppState.Instance.CurrentTrainset = target;
+ AppServices.Current.State.CurrentScenery = _listScenery;
+ AppServices.Current.State.CurrentTrainset = target;
_consist.LoadFrom(target);
PopulateSceneryConsists();
}
@@ -355,7 +355,7 @@ public partial class Depot : UserControl
public async Task RandomizeCurrentTrainsetAsync()
{
- if (AppState.Instance.CurrentTrainset is not { } trainset)
+ if (AppServices.Current.State.CurrentTrainset is not { } trainset)
return;
if (!ReferenceEquals(trainset, _consist.EditingTrainset))
diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs
index 01ec9c6..ba89097 100644
--- a/StarterNG/Views/Depot/VehicleDetails.cs
+++ b/StarterNG/Views/Depot/VehicleDetails.cs
@@ -13,6 +13,7 @@ using StarterNG.Classes;
using StarterNG.Controls;
using StarterNG.Domain;
using StarterNG.Domain.Vehicles;
+using StarterNG.Application;
namespace StarterNG.Views;
@@ -277,7 +278,7 @@ public sealed class VehicleDetails
if (ReferenceEquals(_consist.Selected, item))
_consist.SyncStartingVehicle();
_redraw();
- AppState.Instance.NotifyChanged();
+ AppServices.Current.State.NotifyChanged();
};
driver.MinWidth = 0;
driver.HorizontalAlignment = HorizontalAlignment.Stretch;
@@ -617,7 +618,7 @@ public sealed class VehicleDetails
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 120 };
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
foreach (var name in available)
- typeCombo.Items.Add(new ComboBoxItem { Content = LoadWeights.Describe(name), Tag = name });
+ typeCombo.Items.Add(new ComboBoxItem { Content = DescribeLoad(name), Tag = name });
typeCombo.SelectedIndex = 0;
if (!string.IsNullOrEmpty(lead.LoadType))
@@ -754,4 +755,14 @@ public sealed class VehicleDetails
countRow,
BuildVehicleLoadTools(item));
}
+
+ ///
+ /// Label for a cargo type. The pantograph state is carried as a pseudo load
+ /// and gets a translated name rather than its raw token.
+ ///
+ private static string DescribeLoad(string name) =>
+ string.Equals(name, Dynamic.PantState, StringComparison.OrdinalIgnoreCase)
+ ? App.Loc["LoadPantState"]
+ : name;
+
}
diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs
index 74ee29f..8b4df2c 100644
--- a/StarterNG/Views/Scenarios.axaml.cs
+++ b/StarterNG/Views/Scenarios.axaml.cs
@@ -76,7 +76,7 @@ public partial class Scenarios : UserControl
continue;
bool expand = StarterNG.Classes.Settings.Instance.AutoExpandSceneryTree;
- SceneryI18n.LoadFor(scenery, App.Loc.CurrentLangCode);
+ AppServices.Current.SceneryTexts.LoadFor(scenery, App.Loc.CurrentLangCode);
string label = scenery.DisplayName;
if (string.IsNullOrEmpty(scenery.Group))
@@ -93,7 +93,7 @@ public partial class Scenarios : UserControl
{
groupNode = new TreeViewItem
{
- Header = SceneryI18n.T(scenery.Group),
+ Header = AppServices.Current.SceneryTexts.Translate(scenery.Group),
IsExpanded = expand
};
groupNodes[scenery.Group] = groupNode;
@@ -261,7 +261,7 @@ public partial class Scenarios : UserControl
{
trainset.Vehicles = vehicles;
item.Content = TrainsetListContent(trainset);
- if (ReferenceEquals(AppState.Instance.CurrentTrainset, trainset))
+ if (ReferenceEquals(AppServices.Current.State.CurrentTrainset, trainset))
ShowConsist(trainset);
}
@@ -309,8 +309,8 @@ public partial class Scenarios : UserControl
private void ShowSceneryInfo(Scenery scenery)
{
- SceneryI18n.LoadFor(scenery, App.Loc.CurrentLangCode);
- sceneryDescription.Text = scenery.LocalizedDescription;
+ AppServices.Current.SceneryTexts.LoadFor(scenery, App.Loc.CurrentLangCode);
+ sceneryDescription.Text = scenery.LocalizedDescription(AppServices.Current.SceneryTexts);
string? imagePath = AppServices.Current.SceneryImages.Resolve(scenery);
if (imagePath is not null)
@@ -401,7 +401,7 @@ public partial class Scenarios : UserControl
public void RebuildAfterLanguageChange()
{
- string? selected = AppState.Instance.CurrentScenery?.Path;
+ string? selected = AppServices.Current.State.CurrentScenery?.Path;
BuildSceneryTree();
if (!string.IsNullOrEmpty(selected))
{
@@ -409,8 +409,8 @@ public partial class Scenarios : UserControl
Path.GetFileNameWithoutExtension(selected);
RestoreLastScenery();
}
- if (AppState.Instance.CurrentScenery != null)
- ShowSceneryInfo(AppState.Instance.CurrentScenery);
+ if (AppServices.Current.State.CurrentScenery != null)
+ ShowSceneryInfo(AppServices.Current.State.CurrentScenery);
RefreshSelectedConsist();
}
@@ -422,7 +422,7 @@ public partial class Scenarios : UserControl
_reloading = true;
try
{
- string? selected = AppState.Instance.CurrentScenery?.Path;
+ string? selected = AppServices.Current.State.CurrentScenery?.Path;
await Task.Run(() => AppServices.Current.Library.ReloadSceneries());
Sceneries = AppServices.Current.Library.Sceneries;
BuildSceneryTree();
@@ -443,14 +443,14 @@ public partial class Scenarios : UserControl
private void RandomTexturesButton_OnClick(object? sender, RoutedEventArgs e)
{
- if (AppState.Instance.CurrentScenery is { } scenery)
+ if (AppServices.Current.State.CurrentScenery is { } scenery)
RandomizeTexturesRequested?.Invoke(scenery);
}
public void RefreshConsistView()
{
RefreshVehicleLabels();
- if (AppState.Instance.CurrentTrainset is { } trainset)
+ if (AppServices.Current.State.CurrentTrainset is { } trainset)
ShowConsist(trainset);
}
@@ -505,7 +505,7 @@ public partial class Scenarios : UserControl
private void OpenSceneryDir_OnClick(object? sender, RoutedEventArgs e)
{
- var scn = AppState.Instance.CurrentScenery;
+ var scn = AppServices.Current.State.CurrentScenery;
string dir = scn != null
? (Path.GetDirectoryName(Path.GetFullPath(scn.Path)) ?? "scenery")
: Path.GetFullPath("scenery");
@@ -538,9 +538,9 @@ public partial class Scenarios : UserControl
return;
Trainset selectedTrainset = selectedScn.Trainsets[vTag];
- AppState.Instance.CurrentScenery = selectedScn;
- AppState.Instance.CurrentTrainset = selectedTrainset;
- AppState.Instance.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(selectedTrainset);
+ AppServices.Current.State.CurrentScenery = selectedScn;
+ AppServices.Current.State.CurrentTrainset = selectedTrainset;
+ AppServices.Current.State.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(selectedTrainset);
ShowConsist(selectedTrainset);
ShowTimetable(selectedScn, selectedTrainset);
@@ -874,13 +874,13 @@ public partial class Scenarios : UserControl
trainset.Vehicles.Remove(v);
bool wasPicked = !string.IsNullOrEmpty(v.Name) &&
- string.Equals(v.Name, AppState.Instance.StartingVehicleName,
+ string.Equals(v.Name, AppServices.Current.State.StartingVehicleName,
StringComparison.OrdinalIgnoreCase);
if (wasPicked && trainset.Vehicles.Count > 0)
{
int next = Math.Clamp(gap, 0, trainset.Vehicles.Count - 1);
- AppState.Instance.StartingVehicleName = trainset.Vehicles[next].Name;
- AppState.Instance.NotifyChanged();
+ AppServices.Current.State.StartingVehicleName = trainset.Vehicles[next].Name;
+ AppServices.Current.State.NotifyChanged();
}
RefreshVehicleLabels();
@@ -909,7 +909,7 @@ public partial class Scenarios : UserControl
: MissingMiniBox(train.SkinFile, thumbH);
bool selected = !string.IsNullOrEmpty(train.Name) &&
- string.Equals(train.Name, AppState.Instance.StartingVehicleName,
+ string.Equals(train.Name, AppServices.Current.State.StartingVehicleName,
StringComparison.OrdinalIgnoreCase);
var slot = new Border
@@ -946,7 +946,7 @@ public partial class Scenarios : UserControl
string desc = trainset.Description ?? "";
if (desc.TrimStart().StartsWith('-'))
desc = desc.TrimStart()[1..].TrimStart();
- missionDescription.Text = SceneryI18n.T(desc);
+ missionDescription.Text = AppServices.Current.SceneryTexts.Translate(desc);
UpdateTrainStats(trainset);
}
@@ -995,8 +995,8 @@ public partial class Scenarios : UserControl
index >= clicked.Vehicles.Count)
return;
- AppState.Instance.StartingVehicleName = clicked.Vehicles[index].Name;
- AppState.Instance.NotifyChanged();
+ AppServices.Current.State.StartingVehicleName = clicked.Vehicles[index].Name;
+ AppServices.Current.State.NotifyChanged();
ShowConsist(clicked);
return;
}
@@ -1204,7 +1204,7 @@ public partial class Scenarios : UserControl
private void RefreshSelectedConsist()
{
- if (AppState.Instance.CurrentTrainset is { } trainset)
+ if (AppServices.Current.State.CurrentTrainset is { } trainset)
ShowConsist(trainset);
}
}