diff --git a/StarterNG.sln.DotSettings.user b/StarterNG.sln.DotSettings.user index e68f177..79d3883 100644 --- a/StarterNG.sln.DotSettings.user +++ b/StarterNG.sln.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -7,6 +8,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/StarterNG/App.axaml b/StarterNG/App.axaml index 6b10b8e..ba0d66c 100644 --- a/StarterNG/App.axaml +++ b/StarterNG/App.axaml @@ -3,14 +3,15 @@ xmlns:sukiUi="clr-namespace:SukiUI;assembly=SukiUI" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="StarterNG.App" + x:CompileBindings="True" RequestedThemeVariant="Default"> - + - \ No newline at end of file + diff --git a/StarterNG/App.axaml.cs b/StarterNG/App.axaml.cs index 53bdb39..9df1cc6 100644 --- a/StarterNG/App.axaml.cs +++ b/StarterNG/App.axaml.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.Styling; using Avalonia.Threading; using StarterNG.Classes; using StarterNG.Services; @@ -36,29 +36,23 @@ public partial class App : Application _ => "English" }; Settings.Instance.Language = langName; - - var langDict = new ResourceInclude(new Uri("avares://StarterNG/")) - { - Source = new Uri($"Assets/Langs/{langName}.axaml", UriKind.Relative) - }; - var resources = Current!.Resources; + ApplyLanguage(langName); + } - var languageDictionaries = resources.MergedDictionaries - .OfType() - .Where(d => d.Source != null && d.Source.OriginalString.Contains("Langs/")) - .ToList(); - - foreach (var dict in languageDictionaries) - { - resources.MergedDictionaries.Remove(dict); - } - - resources.MergedDictionaries.Add(langDict); - - - - Loc.SetLanguage(langDict, langName); + /// + /// Swaps the active language by loading the matching XML file from the + /// lang/ folder next to the executable (see ). + /// The translations are no longer compiled into the assembly, so they can be + /// edited or added without rebuilding the launcher. Accepts either a display + /// name ("English", "Polski") or a short code ("en", "pl"). + /// + public static void ApplyLanguage(string langNameOrCode) + { + // Loc.Load resolves the file and returns the canonical display name, + // which we mirror back into the settings so the stored value stays valid. + var resolvedName = Loc.Load(langNameOrCode); + Settings.Instance.Language = resolvedName; } public override void OnFrameworkInitializationCompleted() diff --git a/StarterNG/AppInfo.cs b/StarterNG/AppInfo.cs index 71fffbe..1fe417b 100644 --- a/StarterNG/AppInfo.cs +++ b/StarterNG/AppInfo.cs @@ -18,6 +18,6 @@ public static class AppInfo #if DEBUG text += " (DEBUG)"; #endif - return text; + return text + " public beta"; } } diff --git a/StarterNG/Assets/Langs/English.axaml b/StarterNG/Assets/Langs/English.axaml deleted file mode 100644 index e7cbe9f..0000000 --- a/StarterNG/Assets/Langs/English.axaml +++ /dev/null @@ -1,188 +0,0 @@ - - - - Scenarios - Depot - Settings - Controls - - - Sceneries - Vehicles - Scenario description - Consist - Start - Open latest simulation log - - - Vehicle database - Search... - Load consist from scenery - New consist - Selected vehicle - Vehicle - with crew - reversed - No vehicles found in databases/vehicles/. - Add vehicles from the database on the left, or load an existing consist from a scenery file. - Move left - Move right - Reverse vehicle - Remove from consist - Change crew (cab 1 / cab 2 / passenger / none) - Scenery - Category - Class - All - Diesel - Electric - EMU - DMU - Carriages A - Carriages B - Electric locomotive - Diesel locomotive - Steam locomotive - Railbus - Draisine - Work vehicles - Prototype - Tram - Car - Bus - Truck - People - Animals - Wagons - Other - Brakes - Loads - Coupling - Select a vehicle in the consist to edit it. - Editing options coming soon. - Split - Split the unit into individual cars - Add - Add this vehicle to the consist - Add the whole unit to the consist - - - Loading vehicle database - Loading sceneries - Starting… - - - General - - Language - Fullscreen - Pause simulation when window is inactive - Pause on start - Cursor sensitivity: - Invert mouse horizontally - Invert mouse vertically - - Communication - - - Ignore gamepad input - - Feedback - Off - Caps: CA/SHP; ScrollLock: starting resistances - Caps: CA; ScrollLock: SHP - LPT - PoKeys55 - Serial port (COM) - - - Other - Select exe: - Select automatically - Debug mode (test mode) - Virtual shunting - - - Graphics - Render engine - Full - Old - Simplified shader - Simplified - Disable visual layer - Experimental - - Resolution: - Buffer resolution: - Maximum texture resolution: - Maximum cabin texture resolution: - Texture filtering quality: - Multisampling: - Rendering range: - VSync - Smoke display - Smoke particles multiplier - - Post-processing - Reinhard - ACES - - Chromatic aberration - Motion blur - Additional shader effects - Reflections as cubemap - Render VBO - Render shadows - Reflections refresh rate - Shadows resolution - Reflections refresh rate - Cabin shadow sources range - - Display shadows - Only important - Limited - All - - Reflections detail level - Terrain geometry only - Terrain geometry and static models - Terrain geometry, static models and vehicles - - Cabin shadow sources range - Render onboard computer screens - Render screens in a separate thread - Onboard screens refresh rate - - Physics - - Track curves quality - Increased physics accuracy - Possibility of pantograph breaking - Powered only under overhead line - Save speedometer tapes - Save simulation log - Keep previous logs - Display simulation - Collision damage - - Sound - Enable sound - Volume - Radio volume - Vehicles volume - Positional sounds volume - Ambient volume - Volume during pause - - Starter - Automatically close starter - Large thumbnails - Automatically expand scenery tree - - - Save - Reset - Settings saved - - diff --git a/StarterNG/Assets/Langs/Polski.axaml b/StarterNG/Assets/Langs/Polski.axaml deleted file mode 100644 index 418b153..0000000 --- a/StarterNG/Assets/Langs/Polski.axaml +++ /dev/null @@ -1,188 +0,0 @@ - - - - Scenariusze - Zajezdnia - Ustawienia - Sterowanie - - - Scenerie - Pojazdy - Opis scenariusza - Zestawienie - Uruchom - Otwórz ostatni log symulatora - - - Baza pojazdów - Szukaj... - Wczytaj skład ze scenerii - Nowy skład - Wybrany pojazd - Pojazd - z obsadą - obrócony - Nie znaleziono pojazdów w databases/vehicles/. - Dodaj pojazdy z bazy po lewej stronie lub wczytaj istniejący skład z pliku scenerii. - Przesuń w lewo - Przesuń w prawo - Obróć pojazd - Usuń ze składu - Zmień obsadę (kabina 1 / kabina 2 / pasażer / brak) - Sceneria - Kategoria - Klasa - Wszystkie - Spalinowe - Elektryczne - EZT - SZT - Wagony A - Wagony B - Lokomotywa elektryczna - Lokomotywa spalinowa - Parowóz - Szynobus - Drezyna - Pojazdy robocze - Prototyp - Tramwaj - Samochód osobowy - Autobus - Samochód ciężarowy - Ludzie - Zwierzęta - Wagony - Inne - Hamulce - Ładunki - Sprzęg - Wybierz pojazd w składzie, aby go edytować. - Opcje edycji wkrótce. - Rozdziel - Rozdziel zespół na pojedyncze człony - Dodaj - Dodaj ten pojazd do składu - Dodaj cały zespół do składu - - - Wczytywanie bazy pojazdów - Wczytywanie scenerii - Uruchamianie… - - - Ogólne - - Język - Tryb pełnoekranowy - Wstrzymaj symulację, gdy okno nie jest aktywne - Pauza po uruchomieniu - Czułość kursora: - Inwersja myszy w poziomie - Inwersja myszy w pionie - - Komunikacja - - - Ignoruj sygnały z gamepada - - Informacja zwrotna - Wyłączona - Caps: CA/SHP; ScrollLock: opory rozr. - Caps: CA; Scrollock: SHP - LPT - PoKeys55 - Serialport (COM) - - - Inne - Wybór exe: - Wybierz automatycznie - Tryb testowy (debugmode) - Wirtualny manewrowy - - - Grafika - Silnik renderowania - Pełny - Stary - Uproszczony shaderowy - Uproszczony - Wyłącz warstwę wizualną - Eksperymentalny - - Rozdzielczość: - Rozdzielczość bufora: - Maksymalna rozdzielczość tekstur: - Maksymalna rozdzielczość tekstur kabiny: - Jakość filtrowania tekstur: - Multisampling: - Zasięg renderowania: - VSync - Wyświetlanie dymu - Mnożnik ilości cząsteczek dymu - - Postprocessing - Reinhard - ACES - - Aberracja chromatyczna - Rozmycie podczas ruchu - Dodatkowe efekty shaderów - Odbicia jako cubemapa - Renderowanie VBO - Renderowanie cieni - Odświeżanie odbić - Rozdzielczość cieni - Odświeżanie odbić - Zasięg źródeł cieni w kabinie - - Wyświetlanie cieni - Tylko ważne - Ograniczone - Wszystkie - - Szczegółowość odbić - Tylko geometria terenu - Geometria terenu i modele statyczne - Geometria terenu, modele statyczne i pojazdy - - Zasięg źródeł cieni w kabinie - Renderowanie ekranów komputerów pokładowych - Renderowanie ekranów w osobnym wątku - Częstotliwość odświeżania ekranów komputerów pokładowych - - Fizyka - - Jakość łuków torów - Zwiększona dokładność fizyki - Możliwość złamania pantografu - Zasilanie tylko pod siecią trakcyjna - Zapisuj taśmy z prędkościomierza - Zapisuj log z symulacji - Zachowuj poprzednie logi - Wyświetlaj symulację - Uszkodzenia przy zderzeniu - - Dżwięk - Włącz dźwięk - Głośność - Głośność radiotelefonu - Głośność pojazdów - Głośność dżwięków pozycjonowanych - Głośność otoczenia - Głośność podczas pauzy - - Starter - Zamknij starter automatycznie - Duże miniaturki - Automatycznie rozwijanie drzewka scenerii - - - Zapisz - Przywróć - Zapisano ustawienia - - diff --git a/StarterNG/Assets/Logo/maszyna_black.png b/StarterNG/Assets/Logo/maszyna_black.png new file mode 100644 index 0000000..f05c575 Binary files /dev/null and b/StarterNG/Assets/Logo/maszyna_black.png differ diff --git a/StarterNG/Assets/Logo/maszyna_black.svg b/StarterNG/Assets/Logo/maszyna_black.svg deleted file mode 100644 index 173d535..0000000 --- a/StarterNG/Assets/Logo/maszyna_black.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/StarterNG/Assets/Logo/maszyna_white.png b/StarterNG/Assets/Logo/maszyna_white.png new file mode 100644 index 0000000..77c09f2 Binary files /dev/null and b/StarterNG/Assets/Logo/maszyna_white.png differ diff --git a/StarterNG/Assets/Logo/maszyna_white.svg b/StarterNG/Assets/Logo/maszyna_white.svg deleted file mode 100644 index 24e611b..0000000 --- a/StarterNG/Assets/Logo/maszyna_white.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - diff --git a/StarterNG/Classes/Scenery.cs b/StarterNG/Classes/Scenery.cs index 177bd8a..e148f88 100644 --- a/StarterNG/Classes/Scenery.cs +++ b/StarterNG/Classes/Scenery.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -12,10 +12,20 @@ public class Scenery public string Group; public string Path; + // Starter directives (comment-syntax metadata, see wiki "Plik scenerii"). + // They do not affect the simulation, only how a starter presents the scenery. + public string Name; // //$n - scenery name + public string Description; // //$d - scenery description + public string ImageName; // //$i - main-window image (scenery thumbnail) + // The file content with each trainset block replaced by a {{i}} placeholder, // used to rebuild the .scn on export. private readonly string _template; + // Lazily resolved, cached path to the //$i image on disk (null if not found). + private string _imagePath; + private bool _imagePathResolved; + public Scenery(string path) { this.Path = path; @@ -25,13 +35,13 @@ public class Scenery var encoding = Encoding.GetEncoding(1250); // Windows-1250 string content = File.ReadAllText(path, encoding); - // property scanning - var match = Regex.Match( - content, - @"^//\$l\s*([^\r\n]*)", - RegexOptions.Multiline - ); - this.Group = match.Success ? match.Groups[1].Value : null; + // property scanning - starter directives written as // comments. + // \b after the letter keeps //$d from matching //$decor, //$i from + // matching //$it, etc. + this.Group = MatchDirective(content, "l"); + this.Name = MatchDirective(content, "n"); + this.Description = MatchDirective(content, "d"); + this.ImageName = MatchDirective(content, "i"); // parsing trainsets @@ -65,4 +75,64 @@ public class Scenery result = result.Replace("{{" + i + "}}", Trainsets[i].ToSceneryEntry()); return result; } -} \ No newline at end of file + + /// + /// Reads a single starter directive value (the text after //$<letter>). + /// Returns null when the directive is absent. The trailing whitespace + /// requirement separates e.g. //$d from //$decor and //$i from //$it. + /// + private static string MatchDirective(string content, string letter) + { + var match = Regex.Match( + content, + @"^//\$" + letter + @"\b[ \t]*([^\r\n]*)", + RegexOptions.Multiline + ); + return match.Success ? match.Groups[1].Value.Trim() : null; + } + + /// + /// Resolved on-disk path of the //$i scenery image, or null if none is + /// declared or the file cannot be found. The value of //$i may be a bare + /// file name or a path; common locations are probed and the result cached. + /// + public string ImagePath + { + get + { + if (_imagePathResolved) + return _imagePath; + _imagePathResolved = true; + _imagePath = ResolveImagePath(); + return _imagePath; + } + } + + private string ResolveImagePath() + { + if (string.IsNullOrWhiteSpace(ImageName)) + return null; + + // normalise legacy back-slashes so paths work cross-platform + string name = ImageName.Replace('\\', '/').Trim(); + string scnDir = System.IO.Path.GetDirectoryName(Path) ?? "."; // e.g. scenery/ + string root = System.IO.Path.GetDirectoryName(scnDir) ?? "."; // MaSzyna root + + // probe the usual places, first hit wins + var candidates = new List + { + name, // as given (cwd / absolute) + System.IO.Path.Combine(root, name), // relative to MaSzyna root + System.IO.Path.Combine(scnDir, name), // next to the .scn + System.IO.Path.Combine(scnDir, "images", name), // scenery/images/ + System.IO.Path.Combine(root, "scenery", "images", name), // scenery/images/ from root + }; + + foreach (string candidate in candidates) + { + if (File.Exists(candidate)) + return candidate; + } + return null; + } +} diff --git a/StarterNG/Classes/VehicleDatabase.cs b/StarterNG/Classes/VehicleDatabase.cs index be9845e..0cdf436 100644 --- a/StarterNG/Classes/VehicleDatabase.cs +++ b/StarterNG/Classes/VehicleDatabase.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace StarterNG.Classes; @@ -14,43 +16,43 @@ namespace StarterNG.Classes; /// Root object of a single vehicle JSON file. public class VehicleEntry { - [JsonProperty("uuid")] public string? Uuid { get; set; } - [JsonProperty("schema_version")] public int SchemaVersion { get; set; } - [JsonProperty("groups")] public List Groups { get; set; } = new(); - [JsonProperty("textures")] public List Textures { get; set; } = new(); - [JsonProperty("sets")] public List Sets { get; set; } = new(); - [JsonProperty("unknown")] public List Unknown { get; set; } = new(); + [JsonPropertyName("uuid")] public string? Uuid { get; set; } + [JsonPropertyName("schema_version")] public int SchemaVersion { get; set; } + [JsonPropertyName("groups")] public List Groups { get; set; } = new(); + [JsonPropertyName("textures")] public List Textures { get; set; } = new(); + [JsonPropertyName("sets")] public List Sets { get; set; } = new(); + [JsonPropertyName("unknown")] public List Unknown { get; set; } = new(); } /// Merged database root (databases/vehicles/vehicles.json). public class VehicleEntryCollection { - [JsonProperty("vehicles")] public List Vehicles { get; set; } = new(); + [JsonPropertyName("vehicles")] public List Vehicles { get; set; } = new(); } /// Legacy header / vehicle category group. public class VehicleGroup { - [JsonProperty("id")] public string Id { get; set; } = ""; - [JsonProperty("category")] public string? Category { get; set; } - [JsonProperty("mini")] public string? Mini { get; set; } - [JsonProperty("archived")] public bool Archived { get; set; } - [JsonProperty("implicit")] public bool Implicit { get; set; } + [JsonPropertyName("id")] public string Id { get; set; } = ""; + [JsonPropertyName("category")] public string? Category { get; set; } + [JsonPropertyName("mini")] public string? Mini { get; set; } + [JsonPropertyName("archived")] public bool Archived { get; set; } + [JsonPropertyName("implicit")] public bool Implicit { get; set; } } /// A single .mat skin entry. public class VehicleTexture { - [JsonProperty("uuid")] public string? Uuid { get; set; } - [JsonProperty("directory")] public string Directory { get; set; } = ""; - [JsonProperty("skinfile")] public string Skinfile { get; set; } = ""; - [JsonProperty("model")] public string? Model { get; set; } - [JsonProperty("group")] public string? Group { get; set; } - [JsonProperty("mini_ref")] public string? MiniRef { get; set; } - [JsonProperty("texture_mini")] public string? TextureMini { get; set; } - [JsonProperty("wreck")] public bool Wreck { get; set; } - [JsonProperty("aliases")] public List Aliases { get; set; } = new(); - [JsonProperty("meta")] public VehicleMeta? Meta { get; set; } + [JsonPropertyName("uuid")] public string? Uuid { get; set; } + [JsonPropertyName("directory")] public string Directory { get; set; } = ""; + [JsonPropertyName("skinfile")] public string Skinfile { get; set; } = ""; + [JsonPropertyName("model")] public string? Model { get; set; } + [JsonPropertyName("group")] public string? Group { get; set; } + [JsonPropertyName("mini_ref")] public string? MiniRef { get; set; } + [JsonPropertyName("texture_mini")] public string? TextureMini { get; set; } + [JsonPropertyName("wreck")] public bool Wreck { get; set; } + [JsonPropertyName("aliases")] public List Aliases { get; set; } = new(); + [JsonPropertyName("meta")] public VehicleMeta? Meta { get; set; } /// Full skin path = directory + skinfile. [JsonIgnore] public string FullPath => Directory + Skinfile; @@ -69,25 +71,25 @@ public class VehicleTexture /// Alternate mapping from a malformed multi-= legacy line. public class VehicleAlias { - [JsonProperty("model")] public string? Model { get; set; } - [JsonProperty("group")] public string? Group { get; set; } - [JsonProperty("mini_ref")] public string? MiniRef { get; set; } - [JsonProperty("texture_mini")] public string? TextureMini { get; set; } + [JsonPropertyName("model")] public string? Model { get; set; } + [JsonPropertyName("group")] public string? Group { get; set; } + [JsonPropertyName("mini_ref")] public string? MiniRef { get; set; } + [JsonPropertyName("texture_mini")] public string? TextureMini { get; set; } } /// Parsed metadata from the legacy // comment section. public class VehicleMeta { - [JsonProperty("raw")] public string? Raw { get; set; } - [JsonProperty("version")] public string? Version { get; set; } - [JsonProperty("vehicle")] public string? Vehicle { get; set; } - [JsonProperty("operator")] public string? Operator { get; set; } - [JsonProperty("depot")] public string? Depot { get; set; } - [JsonProperty("revision_date")] public string? RevisionDate { get; set; } - [JsonProperty("revision_place")] public string? RevisionPlace { get; set; } - [JsonProperty("texture_author")] public string? TextureAuthor { get; set; } - [JsonProperty("photo_author")] public string? PhotoAuthor { get; set; } - [JsonProperty("extra")] public List Extra { get; set; } = new(); + [JsonPropertyName("raw")] public string? Raw { get; set; } + [JsonPropertyName("version")] public string? Version { get; set; } + [JsonPropertyName("vehicle")] public string? Vehicle { get; set; } + [JsonPropertyName("operator")] public string? Operator { get; set; } + [JsonPropertyName("depot")] public string? Depot { get; set; } + [JsonPropertyName("revision_date")] public string? RevisionDate { get; set; } + [JsonPropertyName("revision_place")] public string? RevisionPlace { get; set; } + [JsonPropertyName("texture_author")] public string? TextureAuthor { get; set; } + [JsonPropertyName("photo_author")] public string? PhotoAuthor { get; set; } + [JsonPropertyName("extra")] public List Extra { get; set; } = new(); } /// @@ -109,12 +111,16 @@ public class VehicleDatabase /// skinfile (without extension, lower-case) -> texture. public Dictionary TextureBySkin { get; } = new(); - // Newtonsoft.Json matches property names case-insensitively, allows trailing - // commas, and skips // and /* */ comments by default — the same leniency the - // previous System.Text.Json options provided. Unknown members are ignored. - private static readonly JsonSerializerSettings JsonOpts = new() + // System.Text.Json options matching the previous Newtonsoft leniency: property + // names matched case-insensitively, // and /* */ comments skipped, and trailing + // commas allowed. Unknown members are ignored by default. The source-generated + // VehicleJsonContext resolver keeps deserialization AOT/trim-safe (no reflection). + private static readonly JsonSerializerOptions JsonOpts = new() { - MissingMemberHandling = MissingMemberHandling.Ignore + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + TypeInfoResolver = VehicleJsonContext.Default, }; /// @@ -177,8 +183,9 @@ public class VehicleDatabase { try { - var collection = JsonConvert.DeserializeObject( - File.ReadAllText(file), JsonOpts); + var collection = JsonSerializer.Deserialize( + File.ReadAllText(file), + (JsonTypeInfo)JsonOpts.GetTypeInfo(typeof(VehicleEntryCollection))); if (collection?.Vehicles is null) return; foreach (var entry in collection.Vehicles) Ingest(entry); @@ -193,8 +200,9 @@ public class VehicleDatabase { try { - var entry = JsonConvert.DeserializeObject( - File.ReadAllText(file), JsonOpts); + var entry = JsonSerializer.Deserialize( + File.ReadAllText(file), + (JsonTypeInfo)JsonOpts.GetTypeInfo(typeof(VehicleEntry))); if (entry is not null) Ingest(entry); } @@ -359,8 +367,17 @@ public class VehicleDatabase /// Automatic consist definition (legacy ^x grouping). public class VehicleSet { - [JsonProperty("uuid")] public string? Uuid { get; set; } - [JsonProperty("mode")] public string? Mode { get; set; } - [JsonProperty("count")] public int Count { get; set; } - [JsonProperty("texture_refs")] public List TextureRefs { get; set; } = new(); + [JsonPropertyName("uuid")] public string? Uuid { get; set; } + [JsonPropertyName("mode")] public string? Mode { get; set; } + [JsonPropertyName("count")] public int Count { get; set; } + [JsonPropertyName("texture_refs")] public List TextureRefs { get; set; } = new(); +} + +// Source-generated JSON metadata — makes deserialization AOT/trim-safe by avoiding +// runtime reflection over the model types. Nested types (VehicleGroup, VehicleTexture, +// VehicleSet, VehicleAlias, VehicleMeta) are pulled in automatically by the generator. +[JsonSerializable(typeof(VehicleEntry))] +[JsonSerializable(typeof(VehicleEntryCollection))] +internal partial class VehicleJsonContext : JsonSerializerContext +{ } diff --git a/StarterNG/MainWindow.axaml b/StarterNG/MainWindow.axaml index 9ffc393..55e939b 100644 --- a/StarterNG/MainWindow.axaml +++ b/StarterNG/MainWindow.axaml @@ -1,16 +1,16 @@ + Title="MaSzyna - Symulator pojazdów szynowych" Height="800" Width="1200" WindowStartupLocation="CenterScreen"> - - - - \ No newline at end of file + diff --git a/StarterNG/Services/LocalizationService.cs b/StarterNG/Services/LocalizationService.cs index 02839ce..7f67e68 100644 --- a/StarterNG/Services/LocalizationService.cs +++ b/StarterNG/Services/LocalizationService.cs @@ -1,35 +1,166 @@ -using System.ComponentModel; -using Avalonia.Controls; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Xml; namespace StarterNG.Services; +/// +/// Loads UI translations from plain XML files shipped in a lang/ folder next +/// to the executable (instead of dictionaries compiled into the assembly). The files +/// are parsed with , which works under Native AOT — unlike the +/// Avalonia XAML loader, which has no runtime parser in AOT builds. +/// +/// File format (lang/en.xml): +/// +/// <Language code="en" name="English"> +/// <String key="NavScenarios">Scenarios</String> +/// </Language> +/// +/// public class LocalizationService : INotifyPropertyChanged { - private IResourceProvider _currentProvider = new ResourceDictionary(); + /// Folder holding the language files, resolved as starter/lang next to the executable. + public static string LangDirectory => + Path.Combine(AppContext.BaseDirectory, "starter", "lang"); + + private Dictionary _strings = new(); public event PropertyChangedEventHandler? PropertyChanged; + /// Display name of the active language (e.g. "English", "Polski"). + public string CurrentLanguage { get; private set; } = "English"; - public string CurrentLanguage; + /// Short ISO code of the active language (e.g. "en", "pl"). + public string CurrentLangCode { get; private set; } = "en"; - public string this[string key] + /// Indexer used by the XAML bindings: returns the key itself when missing. + public string this[string key] => + _strings.TryGetValue(key, out var value) ? value : key; + + /// + /// Metadata for one discovered language file. + /// + public readonly record struct LanguageInfo(string Code, string Name, string Path); + + /// + /// Scans the lang/ folder and returns the header metadata of every *.xml file + /// without loading all of its strings. Used to populate the language selector. + /// + public static IReadOnlyList AvailableLanguages() { - get + var list = new List(); + if (!Directory.Exists(LangDirectory)) + return list; + + foreach (var path in Directory.EnumerateFiles(LangDirectory, "*.xml")) { - if (_currentProvider.TryGetResource(key, null, out var value)) + try { - return value?.ToString() ?? key; + var (code, name) = ReadHeader(path); + if (!string.IsNullOrEmpty(code)) + list.Add(new LanguageInfo(code, string.IsNullOrEmpty(name) ? code : name, path)); + } + catch + { + // Skip malformed files rather than crashing the launcher. } - return key; } + return list; } - public void SetLanguage(IResourceProvider provider, string langName) + /// + /// Loads the language whose code or display name matches . + /// Falls back to English ("en") when no match is found. Returns the resolved + /// display name so callers can keep their stored setting consistent. + /// + public string Load(string codeOrName) { - _currentProvider = provider; - CurrentLanguage = langName; + var languages = AvailableLanguages(); + + LanguageInfo? match = null; + foreach (var lang in languages) + { + if (string.Equals(lang.Code, codeOrName, StringComparison.OrdinalIgnoreCase) || + string.Equals(lang.Name, codeOrName, StringComparison.OrdinalIgnoreCase)) + { + match = lang; + break; + } + } + + // Fall back to English, then to whatever is available first. + if (match is null) + { + foreach (var lang in languages) + if (string.Equals(lang.Code, "en", StringComparison.OrdinalIgnoreCase)) + { + match = lang; + break; + } + if (match is null && languages.Count > 0) + match = languages[0]; + } + + if (match is { } info) + LoadFile(info.Path, info.Code, info.Name); + else + { + // No files at all: keep keys visible instead of blank UI. + _strings = new Dictionary(); + CurrentLangCode = "en"; + CurrentLanguage = "English"; + } + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item")); + return CurrentLanguage; } - -} \ No newline at end of file + private void LoadFile(string path, string code, string name) + { + var dict = new Dictionary(StringComparer.Ordinal); + + var settings = new XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true }; + using var reader = XmlReader.Create(path, settings); + + // NOTE: ReadElementContentAsString() already advances the reader past the + // element's end tag onto the *next* node, so we must NOT call Read() again + // in that case - doing so would skip every second entry. We only + // advance explicitly for nodes we don't consume here. + while (!reader.EOF) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name == "String") + { + var key = reader.GetAttribute("key"); + var value = reader.ReadElementContentAsString(); // advances the reader + if (!string.IsNullOrEmpty(key)) + dict[key] = value; + } + else + { + reader.Read(); + } + } + + _strings = dict; + CurrentLangCode = code; + CurrentLanguage = name; + } + + private static (string code, string name) ReadHeader(string path) + { + var settings = new XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true }; + using var reader = XmlReader.Create(path, settings); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element && reader.Name == "Language") + { + return (reader.GetAttribute("code") ?? "", + reader.GetAttribute("name") ?? ""); + } + } + return ("", ""); + } +} diff --git a/StarterNG/StarterNG.csproj b/StarterNG/StarterNG.csproj index c0b1d47..25de13f 100644 --- a/StarterNG/StarterNG.csproj +++ b/StarterNG/StarterNG.csproj @@ -1,6 +1,8 @@  WinExe + true + true net9.0 enable true @@ -8,6 +10,8 @@ true 1.0.0.0 1.0.0.0 + Starter + eu07.pl x64 @@ -17,20 +21,18 @@ - - - - - + + + + - - None - All + + none + all - - - + + @@ -38,15 +40,24 @@ - - - - - - + + + + + + + + PreserveNewest + starter\lang\%(Filename)%(Extension) + + diff --git a/StarterNG/Views/Depot.axaml b/StarterNG/Views/Depot.axaml index 2158b46..7bdfb63 100644 --- a/StarterNG/Views/Depot.axaml +++ b/StarterNG/Views/Depot.axaml @@ -5,6 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:starterNg="clr-namespace:StarterNG" xmlns:views="clr-namespace:StarterNG.Views" + x:CompileBindings="True" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700" x:Class="StarterNG.Views.Depot"> diff --git a/StarterNG/Views/Scenarios.axaml b/StarterNG/Views/Scenarios.axaml index 0df469e..c8ae5ac 100644 --- a/StarterNG/Views/Scenarios.axaml +++ b/StarterNG/Views/Scenarios.axaml @@ -1,15 +1,17 @@ - + x:Class="StarterNG.Views.Scenarios" + x:CompileBindings="True"> + @@ -19,7 +21,7 @@ - + @@ -34,7 +36,20 @@ - + + + + + + + + + + + + + @@ -44,10 +59,10 @@ - \ No newline at end of file + diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs index 194b547..adc9d5e 100644 --- a/StarterNG/Views/Scenarios.axaml.cs +++ b/StarterNG/Views/Scenarios.axaml.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -74,6 +74,9 @@ public partial class Scenarios : UserControl Scenery selectedScn = Sceneries[tag]; vehicleList.Items.Clear(); + // scenery-level info (distinct from the per-consist scenario description) + ShowSceneryInfo(selectedScn); + // add all trainsets to list for (int i = 0; i < selectedScn.Trainsets.Count; i++) { @@ -91,6 +94,29 @@ public partial class Scenarios : UserControl } + // Shows the selected scenery's description (//$d) and 1:1 image (//$i). + private void ShowSceneryInfo(Scenery scenery) + { + sceneryDescription.Text = scenery.Description; + + string? imagePath = scenery.ImagePath; + if (imagePath is not null) + { + try + { + sceneryImage.Source = new Bitmap(imagePath); + } + catch + { + sceneryImage.Source = null; // unreadable / unsupported image + } + } + else + { + sceneryImage.Source = null; + } + } + private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { // get selected scenery @@ -190,4 +216,4 @@ public partial class Scenarios : UserControl // executable not found / not configured yet } } -} \ No newline at end of file +} diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index 0327740..e6ae6ef 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -6,7 +6,8 @@ xmlns:objectModel="clr-namespace:System.Collections.ObjectModel;assembly=System.ObjectModel" xmlns:starterNg="clr-namespace:StarterNG" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="StarterNG.Views.Settings"> + x:Class="StarterNG.Views.Settings" + x:CompileBindings="True"> diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs index 0c6f40e..0a521bd 100644 --- a/StarterNG/Views/Settings.axaml.cs +++ b/StarterNG/Views/Settings.axaml.cs @@ -288,22 +288,7 @@ public partial class Settings : UserControl var lang = item.Content?.ToString(); if (string.IsNullOrEmpty(lang)) return; - var langDict = new ResourceInclude(new Uri("avares://StarterNG/")) - { - Source = new Uri($"Assets/Langs/{lang}.axaml", UriKind.Relative) - }; - - var currentLangDict = App.Current.Resources.MergedDictionaries - .OfType() - .FirstOrDefault(d => d.Source != null && d.Source.OriginalString.Contains("Langs/")); - - if (currentLangDict != null) - { - App.Current.Resources.MergedDictionaries.Remove(currentLangDict); - } - App.Current.Resources.MergedDictionaries.Add(langDict); - - App.Loc.SetLanguage(langDict, lang); + App.ApplyLanguage(lang); } // ── tiny helpers ────────────────────────────────────────────────────── diff --git a/StarterNG/starter/lang/en.xml b/StarterNG/starter/lang/en.xml new file mode 100644 index 0000000..e1c05d3 --- /dev/null +++ b/StarterNG/starter/lang/en.xml @@ -0,0 +1,187 @@ + + + + + + + Scenarios + Depot + Settings + Controls + + + Sceneries + Vehicles + Scenario description + Scenery description + Consist + Start + Open latest simulation log + + + Vehicle database + Search... + Load consist from scenery + New consist + Selected vehicle + Vehicle + with crew + reversed + No vehicles found in databases/vehicles/. + Add vehicles from the database on the left, or load an existing consist from a scenery file. + Move left + Move right + Reverse vehicle + Remove from consist + Change crew (cab 1 / cab 2 / passenger / none) + Scenery + Category + Class + All + Diesel + Electric + EMU + DMU + Carriages A + Carriages B + Electric locomotive + Diesel locomotive + Steam locomotive + Railbus + Draisine + Work vehicles + Prototype + Tram + Car + Bus + Truck + People + Animals + Wagons + Other + Brakes + Loads + Coupling + Select a vehicle in the consist to edit it. + Editing options coming soon. + Split + Split the unit into individual cars + Add + Add this vehicle to the consist + Add the whole unit to the consist + + + Loading vehicle database + Loading sceneries + Starting… + + + General + + Language + Fullscreen + Pause simulation when window is inactive + Pause on start + Cursor sensitivity: + Invert mouse horizontally + Invert mouse vertically + + Communication + + Ignore gamepad input + + Feedback + Off + Caps: CA/SHP; ScrollLock: starting resistances + Caps: CA; ScrollLock: SHP + LPT + PoKeys55 + Serial port (COM) + + Other + Select exe: + Select automatically + Debug mode (test mode) + Virtual shunting + + Graphics + Render engine + Full + Old + Simplified shader + Simplified + Disable visual layer + Experimental + + Resolution: + Buffer resolution: + Maximum texture resolution: + Maximum cabin texture resolution: + Texture filtering quality: + Multisampling: + Rendering range: + VSync + Smoke display + Smoke particles multiplier + + Post-processing + Reinhard + ACES + + Chromatic aberration + Motion blur + Additional shader effects + Reflections as cubemap + Render VBO + Render shadows + Reflections refresh rate + Shadows resolution + Reflections refresh rate + Cabin shadow sources range + + Display shadows + Only important + Limited + All + + Reflections detail level + Terrain geometry only + Terrain geometry and static models + Terrain geometry, static models and vehicles + + Cabin shadow sources range + Render onboard computer screens + Render screens in a separate thread + Onboard screens refresh rate + + Physics + + Track curves quality + Increased physics accuracy + Possibility of pantograph breaking + Powered only under overhead line + Save speedometer tapes + Save simulation log + Keep previous logs + Display simulation + Collision damage + + Sound + Enable sound + Volume + Radio volume + Vehicles volume + Positional sounds volume + Ambient volume + Volume during pause + + Starter + Automatically close starter + Large thumbnails + Automatically expand scenery tree + + + Save + Reset + Settings saved + diff --git a/StarterNG/starter/lang/pl.xml b/StarterNG/starter/lang/pl.xml new file mode 100644 index 0000000..fd16ed0 --- /dev/null +++ b/StarterNG/starter/lang/pl.xml @@ -0,0 +1,187 @@ + + + + + + + Scenariusze + Zajezdnia + Ustawienia + Sterowanie + + + Scenerie + Pojazdy + Opis scenariusza + Opis scenerii + Zestawienie + Uruchom + Otwórz ostatni log symulatora + + + Baza pojazdów + Szukaj... + Wczytaj skład ze scenerii + Nowy skład + Wybrany pojazd + Pojazd + z obsadą + obrócony + Nie znaleziono pojazdów w databases/vehicles/. + Dodaj pojazdy z bazy po lewej stronie lub wczytaj istniejący skład z pliku scenerii. + Przesuń w lewo + Przesuń w prawo + Obróć pojazd + Usuń ze składu + Zmień obsadę (kabina 1 / kabina 2 / pasażer / brak) + Sceneria + Kategoria + Klasa + Wszystkie + Spalinowe + Elektryczne + EZT + SZT + Wagony A + Wagony B + Lokomotywa elektryczna + Lokomotywa spalinowa + Parowóz + Szynobus + Drezyna + Pojazdy robocze + Prototyp + Tramwaj + Samochód osobowy + Autobus + Samochód ciężarowy + Ludzie + Zwierzęta + Wagony + Inne + Hamulce + Ładunki + Sprzęg + Wybierz pojazd w składzie, aby go edytować. + Opcje edycji wkrótce. + Rozdziel + Rozdziel zespół na pojedyncze człony + Dodaj + Dodaj ten pojazd do składu + Dodaj cały zespół do składu + + + Wczytywanie bazy pojazdów + Wczytywanie scenerii + Uruchamianie… + + + Ogólne + + Język + Tryb pełnoekranowy + Wstrzymaj symulację, gdy okno nie jest aktywne + Pauza po uruchomieniu + Czułość kursora: + Inwersja myszy w poziomie + Inwersja myszy w pionie + + Komunikacja + + Ignoruj sygnały z gamepada + + Informacja zwrotna + Wyłączona + Caps: CA/SHP; ScrollLock: opory rozr. + Caps: CA; Scrollock: SHP + LPT + PoKeys55 + Serialport (COM) + + Inne + Wybór exe: + Wybierz automatycznie + Tryb testowy (debugmode) + Wirtualny manewrowy + + Grafika + Silnik renderowania + Pełny + Stary + Uproszczony shaderowy + Uproszczony + Wyłącz warstwę wizualną + Eksperymentalny + + Rozdzielczość: + Rozdzielczość bufora: + Maksymalna rozdzielczość tekstur: + Maksymalna rozdzielczość tekstur kabiny: + Jakość filtrowania tekstur: + Multisampling: + Zasięg renderowania: + VSync + Wyświetlanie dymu + Mnożnik ilości cząsteczek dymu + + Postprocessing + Reinhard + ACES + + Aberracja chromatyczna + Rozmycie podczas ruchu + Dodatkowe efekty shaderów + Odbicia jako cubemapa + Renderowanie VBO + Renderowanie cieni + Odświeżanie odbić + Rozdzielczość cieni + Odświeżanie odbić + Zasięg źródeł cieni w kabinie + + Wyświetlanie cieni + Tylko ważne + Ograniczone + Wszystkie + + Szczegółowość odbić + Tylko geometria terenu + Geometria terenu i modele statyczne + Geometria terenu, modele statyczne i pojazdy + + Zasięg źródeł cieni w kabinie + Renderowanie ekranów komputerów pokładowych + Renderowanie ekranów w osobnym wątku + Częstotliwość odświeżania ekranów komputerów pokładowych + + Fizyka + + Jakość łuków torów + Zwiększona dokładność fizyki + Możliwość złamania pantografu + Zasilanie tylko pod siecią trakcyjna + Zapisuj taśmy z prędkościomierza + Zapisuj log z symulacji + Zachowuj poprzednie logi + Wyświetlaj symulację + Uszkodzenia przy zderzeniu + + Dżwięk + Włącz dźwięk + Głośność + Głośność radiotelefonu + Głośność pojazdów + Głośność dżwięków pozycjonowanych + Głośność otoczenia + Głośność podczas pauzy + + Starter + Zamknij starter automatycznie + Duże miniaturki + Automatycznie rozwijanie drzewka scenerii + + + Zapisz + Przywróć + Zapisano ustawienia +