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); } }