From 3b7a8135eaf567ef978a7386a20736d0584a08ad Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 29 Aug 2026 16:41:22 +0200 Subject: [PATCH] Strip all comments from the C# sources Removes every // and /// comment from the 28 .cs files, -1331 lines. Done with a lexer rather than a regex: the sources carry plenty of // inside string literals (@"//\$decor\b", the //$o trainset directives, sb.Append("// "), "https://wiki.eu07.pl/") and those must survive. It tracks regular, verbatim and interpolated strings, interpolation holes and char literals. Every file was checked by comparing the sequence of string-literal characters before and after the strip; all 28 match. BOMs preserved, blank-line runs collapsed to one. Claude-Session: https://claude.ai/code/session_01HamVNWE9mwmz1vs2iEpNQq --- StarterNG/App.axaml.cs | 25 +- StarterNG/AppInfo.cs | 7 +- StarterNG/Classes/AppState.cs | 12 - StarterNG/Classes/ConfigFile.cs | 47 +--- StarterNG/Classes/GameData.cs | 23 +- StarterNG/Classes/KeyMap.cs | 37 +-- StarterNG/Classes/KeyboardConfig.cs | 50 +--- StarterNG/Classes/Physics.cs | 41 +-- StarterNG/Classes/PresetStore.cs | 39 +-- StarterNG/Classes/Scenery.cs | 128 ++-------- StarterNG/Classes/Settings.cs | 284 ++++++++------------- StarterNG/Classes/Trainset.cs | 143 ++--------- StarterNG/Classes/VehicleDatabase.cs | 85 +----- StarterNG/Domain/SceneryI18n.cs | 9 +- StarterNG/Domain/TexRandomizer.cs | 18 +- StarterNG/Domain/TrainsetDisplay.cs | 20 +- StarterNG/Infrastructure/LoadingScreen.cs | 6 +- StarterNG/Infrastructure/MonoFont.cs | 18 +- StarterNG/MainWindow.axaml.cs | 62 +---- StarterNG/Program.cs | 7 +- StarterNG/Services/LocalizationService.cs | 47 +--- StarterNG/SplashWindow.axaml.cs | 1 - StarterNG/Views/ConsistContextMenu.cs | 24 +- StarterNG/Views/Depot.axaml.cs | 257 +++---------------- StarterNG/Views/MessageBox.cs | 2 +- StarterNG/Views/Scenarios.axaml.cs | 93 ++----- StarterNG/Views/Settings.axaml.cs | 76 +----- StarterNG/Views/TextureBaseWindow.axaml.cs | 1 - 28 files changed, 274 insertions(+), 1288 deletions(-) diff --git a/StarterNG/App.axaml.cs b/StarterNG/App.axaml.cs index 9c7f902..064e786 100644 --- a/StarterNG/App.axaml.cs +++ b/StarterNG/App.axaml.cs @@ -15,18 +15,15 @@ namespace StarterNG; public partial class App : Application { - + public static LocalizationService Loc { get; } = new(); - + public override void Initialize() { AvaloniaXamlLoader.Load(this); - // Load the persisted configuration up front so the saved language wins; - // fall back to the system culture only when the file has no lang entry. Settings.Instance.Load(); - // Key bindings live in eu07_input-keyboard.ini next to eu07.ini. KeyboardConfig.Instance.Load(); CultureInfo ci = CultureInfo.InstalledUICulture ; @@ -43,17 +40,9 @@ public partial class App : Application ApplyLanguage(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; } @@ -62,11 +51,9 @@ public partial class App : Application { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - // Code page 1250 is needed to parse scenery files (done during load). + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - // Persist settings when the launcher is closing. Key bindings are only - // rewritten when actually edited, so an untouched file keeps its layout. desktop.ShutdownRequested += (_, _) => { Settings.Instance.CaptureAndSave(); @@ -84,7 +71,7 @@ public partial class App : Application { Dispatcher.UIThread.Post(() => { - // Show splash only if loading takes more than 300ms + if (sw.ElapsedMilliseconds > 300 && splash == null) { splash = new SplashWindow(); @@ -110,4 +97,4 @@ public partial class App : Application base.OnFrameworkInitializationCompleted(); } -} \ No newline at end of file +} diff --git a/StarterNG/AppInfo.cs b/StarterNG/AppInfo.cs index 1fe417b..ddeb6f1 100644 --- a/StarterNG/AppInfo.cs +++ b/StarterNG/AppInfo.cs @@ -2,14 +2,9 @@ using System.Reflection; namespace StarterNG; -/// Static, display-ready information about the running launcher build. public static class AppInfo { - /// - /// The launcher's assembly version (e.g. "1.2.3.4"), with a " (DEBUG)" suffix - /// appended for Debug builds so debug and release binaries are easy to tell - /// apart. Computed once; safe to bind with {x:Static}. - /// + public static string Version { get; } = BuildVersion(); private static string BuildVersion() diff --git a/StarterNG/Classes/AppState.cs b/StarterNG/Classes/AppState.cs index 624fc14..3b9bbec 100644 --- a/StarterNG/Classes/AppState.cs +++ b/StarterNG/Classes/AppState.cs @@ -2,12 +2,6 @@ namespace StarterNG.Classes; -/// -/// Shared selection state across views: the scenery and trainset currently in -/// focus. The depot edits this trainset in place, so changes made in the depot -/// are reflected on the scenery (both views share the same Scenery/Trainset -/// objects from ). -/// public sealed class AppState { public static AppState Instance { get; } = new(); @@ -40,11 +34,6 @@ public sealed class AppState } } - /// - /// Node name of the vehicle the player starts in (-v), like Pascal - /// SelVehicle. Updated when a consist car is clicked in the depot or a - /// trainset is picked in Scenarios. - /// public string? StartingVehicleName { get => _startingVehicleName; @@ -56,6 +45,5 @@ public sealed class AppState } } - /// Notify listeners after in-place edits that affect Start enablement. public void NotifyChanged() => Changed?.Invoke(); } diff --git a/StarterNG/Classes/ConfigFile.cs b/StarterNG/Classes/ConfigFile.cs index f38895d..e551b2c 100644 --- a/StarterNG/Classes/ConfigFile.cs +++ b/StarterNG/Classes/ConfigFile.cs @@ -5,35 +5,17 @@ using System.Text; namespace StarterNG.Classes; -/// -/// A line-oriented reader/writer for the EU07 eu07.ini configuration -/// file. -/// -/// The file is not a classic INI: it is a flat list of key value [value…] -/// entries, one per line, with // comments (either trailing a line or -/// commenting the whole line out). The simulator parses it as a stream of -/// whitespace-separated tokens. -/// -/// This class keeps every line verbatim and only rewrites the value of a key -/// when is called. Comments, blank lines, commented-out -/// entries and — crucially — any keys the launcher does not understand are -/// preserved on save, so editing a handful of settings never drops the rest of -/// the user's configuration. -/// public sealed class ConfigFile { private readonly List _lines = new(); - // Active (uncommented) setting key -> index into _lines. Last occurrence wins, - // matching the simulator which lets later entries override earlier ones. private readonly Dictionary _index = new(StringComparer.OrdinalIgnoreCase); - /// Parses raw file text, preserving line breaks and content. public static ConfigFile Parse(string text) { var cfg = new ConfigFile(); - // Normalise CRLF/CR to LF for splitting; output uses Environment.NewLine. + text = text.Replace("\r\n", "\n").Replace('\r', '\n'); foreach (var line in text.Split('\n')) cfg.AddLine(line); @@ -45,13 +27,11 @@ public sealed class ConfigFile int idx = _lines.Count; _lines.Add(raw); if (TryParseKey(raw, out string key)) - _index[key] = idx; // later duplicates override earlier ones + _index[key] = idx; } - /// True if the key is present as an active (uncommented) entry. public bool Has(string key) => _index.ContainsKey(key); - /// Raw value (all tokens after the key, comment stripped), or null. public string? GetRaw(string key) { if (!_index.TryGetValue(key, out int i)) @@ -89,7 +69,6 @@ public sealed class ConfigFile CultureInfo.InvariantCulture, out double d) ? d : fallback; } - /// All whitespace-separated value tokens for a key (never null). public string[] GetTokens(string key) { var v = GetRaw(key); @@ -98,11 +77,6 @@ public sealed class ConfigFile : v.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); } - /// - /// Sets the value of . If the key already exists its - /// line is rewritten in place, preserving original indentation and any - /// trailing comment. Otherwise a new line is appended. - /// public void Set(string key, string value) { value ??= string.Empty; @@ -125,11 +99,6 @@ public sealed class ConfigFile } } - /// - /// Deactivates by turning its line into a comment, - /// preserving indentation, the key/value that was there and any trailing - /// comment. No-op if the key is not currently active. - /// public void Remove(string key) { if (!_index.TryGetValue(key, out int i)) @@ -153,28 +122,19 @@ public sealed class ConfigFile public void SetDouble(string key, double value) => Set(key, value.ToString("0.###", CultureInfo.InvariantCulture)); - /// Serialises back to text using the platform newline. public string ToText() => string.Join(Environment.NewLine, _lines); - // --- line parsing helpers ------------------------------------------------- - private static bool TryParseKey(string raw, out string key) { SplitLine(raw, out _, out key, out _, out _); return key.Length > 0; } - /// - /// Decomposes a raw line into leading whitespace, key, value (tokens after - /// the key) and trailing comment (including the //). For blank or - /// fully commented-out lines, key is empty. - /// private static void SplitLine(string raw, out string indent, out string key, out string value, out string comment) { indent = key = value = comment = string.Empty; - // leading whitespace int start = 0; while (start < raw.Length && char.IsWhiteSpace(raw[start])) start++; @@ -182,7 +142,6 @@ public sealed class ConfigFile string body = raw.Substring(start); - // A line whose first non-space content is "//" is a pure comment. if (body.StartsWith("//")) { comment = body; @@ -191,8 +150,6 @@ public sealed class ConfigFile if (body.Length == 0) return; - // Split off a trailing comment: the first "//" preceded by whitespace - // (so URLs such as tcp://… inside a value are not mistaken for one). int commentAt = FindCommentStart(body); string content = commentAt >= 0 ? body.Substring(0, commentAt) : body; if (commentAt >= 0) diff --git a/StarterNG/Classes/GameData.cs b/StarterNG/Classes/GameData.cs index 76522ec..72587c6 100644 --- a/StarterNG/Classes/GameData.cs +++ b/StarterNG/Classes/GameData.cs @@ -13,14 +13,8 @@ public enum LoadPhase Done } -/// A single progress update emitted while the game data loads. public readonly record struct LoadStatus(double Fraction, LoadPhase Phase, string? Detail); -/// -/// Loads and holds the data shared across the launcher (vehicle database and -/// parsed sceneries) so it is parsed once, at startup, behind the splash — -/// instead of inside each view's constructor. -/// public sealed class GameData { public static GameData Instance { get; } = new(); @@ -30,10 +24,6 @@ public sealed class GameData public bool Loaded { get; private set; } - /// - /// Parses the vehicle database and all sceneries, reporting progress. - /// Runs synchronously; call it from a background thread. Never throws. - /// public void Load(IProgress? progress = null, string vehiclesDir = "databases/vehicles/", string sceneryDir = "scenery/", @@ -47,27 +37,21 @@ public sealed class GameData int total = Math.Max(1, vehicleFiles.Count + scnFiles.Count); int done = 0; - // Phase 1 - vehicle database Vehicles.BeginLoad(); foreach (string file in vehicleFiles) { progress?.Report(new LoadStatus((double)done / total, LoadPhase.Vehicles, Path.GetFileName(file))); try { Vehicles.LoadFile(file); } - catch { /* skip malformed file */ } + catch { } done++; } Vehicles.EndLoad(); - // Preload the miniature .bmp index now (behind the splash) so the depot's - // first thumbnail render doesn't scan textures/mini/ on the UI thread. VehicleDatabase.PreloadMiniIndex(miniDir); - // Likewise index every .fiz under dynamic/ once, so the consist's physics - // (length / mass / couplings) resolve without a UI-thread directory scan. Physics.PreloadIndex(); - // Phase 2 - sceneries (parallelized) var sceneryLock = new object(); var doneCount = done; Parallel.ForEach(scnFiles, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, @@ -84,7 +68,7 @@ public sealed class GameData Path.GetFileName(file))); } } - catch { /* skip unreadable scenery */ } + catch { } }); done = doneCount; @@ -92,7 +76,6 @@ public sealed class GameData Loaded = true; } - /// Re-reads scenery/*.scn (Pascal actReloadScenarios). public void ReloadSceneries(string sceneryDir = "scenery/") { Sceneries.Clear(); @@ -107,7 +90,7 @@ public sealed class GameData lock (sceneryLock) Sceneries.Add(scenery); } - catch { /* skip */ } + catch { } }); } diff --git a/StarterNG/Classes/KeyMap.cs b/StarterNG/Classes/KeyMap.cs index 7fdba7b..741c606 100644 --- a/StarterNG/Classes/KeyMap.cs +++ b/StarterNG/Classes/KeyMap.cs @@ -4,16 +4,11 @@ using Avalonia.Input; namespace StarterNG.Classes; -/// -/// One cap on the on-screen keyboard. is the simulator key -/// token a binding would reference; when null the cap is a non-bindable filler -/// (Shift, Ctrl, Tab …) drawn only to keep the layout recognisable. -/// public sealed class KeyCap { public string Label = string.Empty; - public string? Token; // null => filler / not bindable - public double Width = 1.0; // relative width (1 == a standard key) + public string? Token; + public double Width = 1.0; public KeyCap(string label, string? token = null, double width = 1.0) { @@ -23,20 +18,12 @@ public sealed class KeyCap } } -/// -/// Translates between physical keyboard input, simulator key tokens and the -/// on-screen keyboard layout used by the controls visualisation. -/// public static class KeyMap { - /// - /// Resolves a pressed key to its simulator token, using the physical key to - /// disambiguate the numeric keypad. Returns null for keys with no token - /// (e.g. bare modifiers). - /// + public static string? FromInput(Key key, PhysicalKey physical) { - // Numeric keypad first - distinguished only by the physical key. + switch (physical) { case PhysicalKey.NumPad0: return "num_0"; @@ -57,15 +44,12 @@ public static class KeyMap case PhysicalKey.NumPadEnter: return "num_enter"; } - // Numeric keypad via the virtual key (fallback when PhysicalKey is Unknown). if (key >= Key.NumPad0 && key <= Key.NumPad9) return "num_" + (char)('0' + (key - Key.NumPad0)); - // Letters. if (key >= Key.A && key <= Key.Z) return ((char)('a' + (key - Key.A))).ToString(); - // Top-row digits. if (key >= Key.D0 && key <= Key.D9) return ((char)('0' + (key - Key.D0))).ToString(); @@ -98,14 +82,12 @@ public static class KeyMap }; } - /// True for keys that only act as modifiers and never form a binding alone. public static bool IsModifierKey(Key key) => key is Key.LeftShift or Key.RightShift or Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt or Key.LWin or Key.RWin; - /// Friendly, explicit name for a token, used in the binding list text. public static string DisplayName(string? token) { if (string.IsNullOrEmpty(token) || @@ -144,14 +126,9 @@ public static class KeyMap }; } - // ── On-screen keyboard layout ────────────────────────────────────────── - // Filler caps (Token == null) keep the shape familiar; only caps with a - // token are coloured by assignment state. - - /// Rows of the main (alphanumeric) keyboard block. public static readonly KeyCap[][] MainBlock = { - // F-row keys are reserved by the simulator, so they carry no token. + new[] { new KeyCap("Esc", null, 1.4), @@ -205,7 +182,6 @@ public static class KeyMap }, }; - /// Rows of the numeric-keypad block, drawn to the right of the main block. public static readonly KeyCap[][] NumpadBlock = { new[] @@ -234,10 +210,9 @@ public static class KeyMap }, }; - /// Navigation cluster (Insert/Home/PageUp · Delete/End/PageDown), drawn between the blocks. public static readonly KeyCap[][] NavBlock = { - // PgUp / PgDn are reserved by the simulator (no token), like [ ] and Enter. + new[] { new KeyCap("Ins", "insert"), new KeyCap("Home", "home"), new KeyCap("PgUp"), diff --git a/StarterNG/Classes/KeyboardConfig.cs b/StarterNG/Classes/KeyboardConfig.cs index 840f7b5..5a3e689 100644 --- a/StarterNG/Classes/KeyboardConfig.cs +++ b/StarterNG/Classes/KeyboardConfig.cs @@ -5,60 +5,33 @@ using System.Text; namespace StarterNG.Classes; -/// -/// A single keyboard command binding parsed from eu07_input-keyboard.ini. -/// A binding is a simulator command name, an optional pair of modifier flags -/// (shift / ctrl, either or both) and the key it is mapped to. -/// none (or an empty key) means the command is intentionally unbound. -/// public sealed class KeyBinding { - /// Simulator command token, e.g. mastercontrollerincrease. public string Command = string.Empty; - /// True when the binding requires the Shift modifier. public bool Shift; - /// True when the binding requires the Ctrl modifier. public bool Ctrl; - /// The key token (e.g. q, num_+) or none when unbound. public string Key = "none"; - /// Human description taken from the line's trailing comment (no //). public string Description = string.Empty; - /// Index into the backing line list, so the original line can be rewritten in place. internal int LineIndex = -1; - /// True when the command is mapped to an actual key. public bool IsAssigned => !string.IsNullOrEmpty(Key) && !Key.Equals("none", StringComparison.OrdinalIgnoreCase); } -/// -/// Reader/writer for the simulator's keyboard layout file, -/// eu07_input-keyboard.ini, which lives next to eu07.ini -/// (%APPDATA%\MaSzyna\ on Windows, ~/.config/MaSzyna/ elsewhere). -/// -/// The file is a flat list of command [shift] [ctrl] key // comment -/// lines. Modifiers are the literal tokens shift and ctrl appearing -/// in any order between the command and the key; the last value token is the key. -/// Blank lines, pure comments and any lines the launcher does not recognise are -/// preserved verbatim on save - only the binding lines the user actually edits -/// are rewritten. -/// public sealed class KeyboardConfig { public static KeyboardConfig Instance { get; } = new(); private readonly List _lines = new(); - /// All recognised bindings, in file order. public List Bindings { get; private set; } = new(); - /// True when a binding has been changed since the last load/save. public bool Dirty { get; set; } private string _savePath = string.Empty; @@ -67,7 +40,7 @@ public sealed class KeyboardConfig private static Encoding ResolveEncoding() { - // Comments use Polish diacritics in Windows-1250, like eu07.ini; preserve them. + try { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -79,7 +52,6 @@ public sealed class KeyboardConfig } } - /// Per-user keyboard config path for the current OS (next to eu07.ini). public static string UserConfigPath() { if (OperatingSystem.IsWindows()) @@ -103,15 +75,12 @@ public sealed class KeyboardConfig return Path.Combine(AppContext.BaseDirectory, "eu07_input-keyboard.ini"); } - /// Default file shipped in the simulator's working directory. private static string WorkingDirDefaultPath() => Path.Combine(Directory.GetCurrentDirectory(), "eu07_input-keyboard.ini"); - /// Fallback template bundled with the launcher (startercfg/). private static string BundledDefaultPath() => Path.Combine(AppContext.BaseDirectory, "startercfg", "eu07_input-keyboard.ini"); - /// Loads the per-user file, falling back to the working-dir or bundled default. public void Load() { _savePath = UserConfigPath(); @@ -120,7 +89,6 @@ public sealed class KeyboardConfig Dirty = false; } - /// Reloads the shipped defaults, discarding the user's current edits. public void LoadDefaults() { if (string.IsNullOrEmpty(_savePath)) @@ -151,7 +119,6 @@ public sealed class KeyboardConfig return paths.Length > 0 ? paths[^1] : string.Empty; } - /// Parses raw file text into , keeping every line verbatim. public void Parse(string text) { _lines.Clear(); @@ -170,10 +137,6 @@ public sealed class KeyboardConfig Bindings = bindings; } - /// - /// Parses a single line into a binding. Returns false for blank lines, pure - /// comments and anything without a command token. - /// private static bool TryParseBinding(string raw, int lineIndex, out KeyBinding binding) { binding = new KeyBinding { LineIndex = lineIndex }; @@ -181,7 +144,6 @@ public sealed class KeyboardConfig string body = raw; string description = string.Empty; - // Split off a trailing "// comment". int commentAt = body.IndexOf("//", StringComparison.Ordinal); if (commentAt >= 0) { @@ -191,7 +153,7 @@ public sealed class KeyboardConfig body = body.Trim(); if (body.Length == 0) - return false; // blank or pure-comment line + return false; var tokens = body.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (tokens.Length == 0) @@ -200,7 +162,6 @@ public sealed class KeyboardConfig binding.Command = tokens[0]; binding.Description = description; - // Everything after the command is modifiers + (at most) one key. bool shift = false, ctrl = false; string key = "none"; for (int i = 1; i < tokens.Length; i++) @@ -208,7 +169,7 @@ public sealed class KeyboardConfig string t = tokens[i]; if (t.Equals("shift", StringComparison.OrdinalIgnoreCase)) shift = true; else if (t.Equals("ctrl", StringComparison.OrdinalIgnoreCase)) ctrl = true; - else key = t; // last non-modifier token wins as the key + else key = t; } binding.Shift = shift; @@ -217,7 +178,6 @@ public sealed class KeyboardConfig return true; } - /// Rewrites each binding's line, then serialises the file to text. public string ToText() { foreach (var b in Bindings) @@ -229,7 +189,6 @@ public sealed class KeyboardConfig return string.Join(Environment.NewLine, _lines); } - /// Formats one binding as command [shift] [ctrl] key // comment. private static string FormatLine(KeyBinding b) { var sb = new StringBuilder(); @@ -243,7 +202,6 @@ public sealed class KeyboardConfig return sb.ToString(); } - /// Writes the bindings back to the per-user keyboard file. public void Save() { if (string.IsNullOrEmpty(_savePath)) @@ -259,7 +217,7 @@ public sealed class KeyboardConfig } catch { - // Could not write (permissions / path). Leave values in memory. + } } } diff --git a/StarterNG/Classes/Physics.cs b/StarterNG/Classes/Physics.cs index 3d65f63..9dd14db 100644 --- a/StarterNG/Classes/Physics.cs +++ b/StarterNG/Classes/Physics.cs @@ -6,17 +6,11 @@ using System.Text; namespace StarterNG.Classes; -/// -/// Vehicle physics read from a .fiz file (dynamic/<dir>/<model>.fiz), mirroring -/// the original Starter's TLexParser.ParsePhysics. Provides mass, top speed, -/// length, accepted loads and the coupler AllowedFlag / ControlType used for the -/// automatic coupling calculation. -/// public sealed class Physics { - public double Mass; // kg, as written in the .fiz - public double VMax; // km/h - public double Length; // metres + public double Mass; + public double VMax; + public double Length; public string LoadAccepted = ""; public int MaxLoad; public int AllowedFlagA = 3; @@ -26,12 +20,8 @@ public sealed class Physics private static readonly Dictionary Cache = new(StringComparer.OrdinalIgnoreCase); - // Index of every .fiz under dynamic/, keyed by its file name without extension - // (e.g. "en57", "en57dumb"). Built once - the directory layout of the data - // folder vs the scenery's mmd token is unreliable, so we resolve by model name. private static Dictionary? _fizIndex; - /// Builds the .fiz index up front (call from the startup load). public static void PreloadIndex(string dynamicRoot = "dynamic") => FizIndex(dynamicRoot); private static Dictionary FizIndex(string dynamicRoot) @@ -46,23 +36,18 @@ public sealed class Physics foreach (string f in Directory.EnumerateFiles(dynamicRoot, "*.fiz", SearchOption.AllDirectories)) index[Path.GetFileNameWithoutExtension(f)] = f; } - catch { /* unreadable dynamic/ - no physics */ } + catch { } _fizIndex = index; return index; } - /// - /// Loads (and caches) the physics for a model name (the .fiz is located by name - /// anywhere under dynamic/). The is unused now but - /// kept for call-site clarity. Returns null when nothing matches. Never throws. - /// public static Physics? For(string? dataFolder, string? model, string dynamicRoot = "dynamic") { if (string.IsNullOrEmpty(model)) return null; - string m = Path.GetFileNameWithoutExtension(model!); // strip any .mmd/.t3d/etc. + string m = Path.GetFileNameWithoutExtension(model!); if (m.Length == 0) return null; @@ -77,12 +62,10 @@ public sealed class Physics Physics? phys = null; if (path != null) { - phys = new Physics { AllowedFlagB = -1 }; // -1 = "B end not declared yet" + phys = new Physics { AllowedFlagB = -1 }; try { Parse(phys, path, Path.GetDirectoryName(path) ?? "", null, 0); } - catch { /* partial data still useful */ } + catch { } - // The original copies the A-end coupler onto the B-end when only one - // BuffCoupl section is declared. if (phys.AllowedFlagB == -1) { phys.AllowedFlagB = phys.AllowedFlagA; @@ -116,7 +99,6 @@ public sealed class Physics if (tok == "=") continue; - // include end -> parse the included physics if (tok.Equals("include", StringComparison.OrdinalIgnoreCase)) { if (++i >= tokens.Count) break; @@ -149,13 +131,11 @@ public sealed class Physics } else if (KnownKeys.Contains(t)) { - pendingKey = t; // value comes in a following token (spaced "Key = value") + pendingKey = t; } else { - // any other bare identifier is a section header (Param, Load, - // Dimensions, BuffCoupl/1/2, or one we don't read). Keep only - // letters/digits so "Param:" or "Param," still match. + section = new string(t.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant(); } } @@ -188,7 +168,6 @@ public sealed class Physics } } - // A negative AllowedFlag means a permanent coupling: abs + 128 (workshop-lock bit). private static int Flag(string v, int fallback) => int.TryParse(v, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int f) @@ -201,8 +180,6 @@ public sealed class Physics ? d : fallback; - // Substitutes an include placeholder like "(c1)" with the matching include - // parameter (1-based), otherwise returns the token unchanged. private static string Resolve(string token, string[]? prms) { if (prms == null || token.Length < 3 || token[0] != '(' || token[^1] != ')') diff --git a/StarterNG/Classes/PresetStore.cs b/StarterNG/Classes/PresetStore.cs index 6807f97..cc48bb3 100644 --- a/StarterNG/Classes/PresetStore.cs +++ b/StarterNG/Classes/PresetStore.cs @@ -17,30 +17,18 @@ public enum PresetSort Track } -/// One saved consist in the user's vehicle warehouse. public sealed class TrainsetPreset { - /// Display name shown in the "load from warehouse" menu. public string Name { get; set; } = ""; - /// Complete "trainset … endtrainset" scenery entry (self-contained). public string Entry { get; set; } = ""; } -/// Root object persisted to userpresets.json. public sealed class PresetCollection { public List Presets { get; set; } = new(); } -/// -/// The user's vehicle/consist "warehouse": named trainset presets persisted as JSON. -/// Stored under the per-user config directory so it survives reinstalls: -/// Windows: %APPDATA%\MaSzyna\starter\userpresets.json -/// macOS: ~/Library/Application Support/MaSzyna/starter/userpresets.json -/// Linux: ~/.config/MaSzyna/starter/userpresets.json -/// All operations are best-effort and never throw. -/// public static class PresetStore { private static string ResolvePath() @@ -63,11 +51,8 @@ public static class PresetStore public static string FilePath { get; } = ResolvePath(); - /// Pascal miSortByVehicleName / miSortByTrackName for the warehouse menu. public static PresetSort SortMode { get; set; } = PresetSort.Name; - // Source-generated metadata keeps (de)serialisation AOT/trim-safe, matching the - // approach used for the vehicle database. private static readonly JsonSerializerOptions Options = new() { WriteIndented = true, @@ -77,7 +62,6 @@ public static class PresetStore private static JsonTypeInfo TypeInfo => (JsonTypeInfo)Options.GetTypeInfo(typeof(PresetCollection)); - /// All saved presets, sorted per . public static IReadOnlyList All() { var list = Load().Presets.ToList(); @@ -88,10 +72,6 @@ public static class PresetStore return list; } - /// - /// Saves (or overwrites, by name) a consist preset. No-op on a blank name or - /// empty entry. - /// public static void Save(string? name, string? entry) { name = name?.Trim() ?? ""; @@ -105,7 +85,6 @@ public static class PresetStore Persist(col); } - /// Removes a preset by name (case-insensitive). public static void Delete(string name) { var col = Load(); @@ -113,10 +92,6 @@ public static class PresetStore Persist(col); } - /// - /// Import Pascal starter/magazyn.ini (or legacy starter.ini / RAINSTED.INI) - /// into the JSON warehouse. Returns how many consists were added. - /// public static int ImportMagazyn(string? path = null) { path ??= FindMagazynPath(); @@ -197,7 +172,7 @@ public static class PresetStore } catch { - // unreadable / malformed - start from an empty warehouse + } return new PresetCollection(); } @@ -211,24 +186,15 @@ public static class PresetStore } catch { - // best-effort: a failed save just means the warehouse isn't updated + } } } -/// -/// Helpers to convert a consist to/from its canonical scenery text - the full -/// "trainset … endtrainset" block, exactly what is placed on the clipboard. -/// public static class ConsistText { - /// The complete scenery entry for a trainset (incl. endtrainset). public static string Serialize(Trainset trainset) => trainset.ToSceneryEntry(); - /// - /// Parses the vehicles out of a complete trainset entry. Returns null when the - /// text holds no usable node::dynamic vehicles. - /// public static List? VehiclesFrom(string? text) { if (string.IsNullOrWhiteSpace(text)) @@ -245,7 +211,6 @@ public static class ConsistText } } -// Source-generated JSON metadata (AOT/trim-safe; no runtime reflection). [JsonSerializable(typeof(PresetCollection))] [JsonSerializable(typeof(TrainsetPreset))] internal partial class PresetJsonContext : JsonSerializerContext diff --git a/StarterNG/Classes/Scenery.cs b/StarterNG/Classes/Scenery.cs index f42f300..cc8a2ca 100644 --- a/StarterNG/Classes/Scenery.cs +++ b/StarterNG/Classes/Scenery.cs @@ -18,7 +18,6 @@ public sealed class SceneryInclude { public string FilePath = ""; public string Desc = ""; - /// 0 = default off, 1 = default on, 2 = terrain (auto if no .sbt). public int Kind; public bool Selected; } @@ -29,49 +28,33 @@ public class Scenery public List Trainsets; public List Attachments = new(); public List Includes = new(); - /// - /// Names of node … dynamic entries outside trainsets (kept in the - /// template on export; collected so the depot can avoid name clashes). - /// + public List LooseVehicleNames = new(); - /// Parsed loose dynamics (Pascal SCN.Vehicles). + public List LooseVehicles = new(); - /// Parse / vehicle faults for the Info tab (Pascal FaultList). + public List Faults = new(); 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) + public string Name; + public string Description; + public string ImageName; - /// True when the scenery declares the //$a "archival" flag. public bool Archival; - // Weather / environment. Like the original Starter, these are editable and are - // written into the scenery's "config" block on launch (see RewriteWeather). - // - // Two clocks, matching Pascal TConfig.StartTime / TConfig.Time: - // Time -> classic "time … endtime" (from the SCN; UI does not edit it) - // ScenarioTimeOverride -> "scenario.time.override" (what the Weather picker edits) - // Defaults: classic start 10:30; override falls back to "now" when the SCN has neither. - public string Time = "10:30"; // h:mm -> "time … endtime" - public string ScenarioTimeOverride = "10:30"; // h:mm -> "scenario.time.override" - public int Day = 0; // "movelight " (day of year / season) - public double Temperature = 15; // "scenario.weather.temperature" - public int FogStart = 10; // atmo fog start (metres) - public int FogEnd = 2000; // visibility in metres (atmo fog range) - public double Overcast = 0; // atmo overcast factor (-1.5 .. 1.5) + public string Time = "10:30"; + public string ScenarioTimeOverride = "10:30"; + public int Day = 0; + public double Temperature = 15; + public int FogStart = 10; + public int FogEnd = 2000; + public double Overcast = 0; - /// True when the scenery actually declared any weather command. public bool HasWeather; - /// Set once the user edits the weather, so export rewrites the config. public bool WeatherDirty; - // Snapshot of weather as loaded from the SCN (Pascal actRestoreWeather). private string _origTime = "10:30"; private string _origOverride = "10:30"; private int _origDay; @@ -79,11 +62,8 @@ public class Scenery private int _origFogEnd = 2000; private double _origOvercast; - // The file content with each trainset block replaced by a {{i}} placeholder, - // used to rebuild the .scn on export. private readonly string _template; - /// Display label: //$n when set (with @token i18n), otherwise the file name. public string DisplayName { get @@ -94,12 +74,10 @@ public class Scenery } } - /// //$d lines with @token translation applied. public string LocalizedDescription => string.IsNullOrEmpty(Description) ? "" : string.Join("\n", Description.Split('\n').Select(SceneryI18n.T)); - // Lazily resolved, cached path to the //$i image on disk (null if not found). private string _imagePath; private bool _imagePathResolved; @@ -109,29 +87,23 @@ public class Scenery Trainsets = new List(); if (!File.Exists(path)) throw new FileNotFoundException(path); - var encoding = Encoding.GetEncoding(1250); // Windows-1250 + var encoding = Encoding.GetEncoding(1250); string content = File.ReadAllText(path, encoding); - // 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"); - // //$d may appear on several lines; each is one line of the description. + this.Description = MatchAllDirectives(content, "d"); this.ImageName = MatchDirective(content, "i"); - // //$a marks an archival scenery (present = archival, regardless of value). + this.Archival = MatchDirective(content, "a") != null; - // weather/environment is read from the raw text before trainset blocks - // are stripped out below (the atmosphere commands live outside trainsets) ParseWeather(content); SnapshotWeather(); ParseAttachments(content); content = ParseAndStripOptionalIncludes(content); - // parsing trainsets List trainsetEntries = new List(); Regex regex = new Regex( @"trainset\b[\s\S]*?\bendtrainset\b", @@ -146,8 +118,6 @@ public class Scenery content = ExtractLooseVehicles(content); _template = content; - // 1:1 with placeholders - the Trainset ctor never throws (unparsable - // blocks are kept verbatim), so indices stay aligned for export. foreach (string trainsetEntry in trainsetEntries) { var ts = new Trainset(trainsetEntry); @@ -157,8 +127,6 @@ public class Scenery } } - // Pull standalone node…dynamic blocks out of the template (Pascal SCN.Vehicles) - // so export can re-emit them via PrepareNode(false) and names stay editable. private string ExtractLooseVehicles(string content) { LooseVehicles.Clear(); @@ -212,10 +180,6 @@ public class Scenery }); } - /// - /// Rebuilds the full .scn content with the (possibly modified) trainsets - /// substituted back into their original positions. - /// public string BuildExportContent(bool skipDecorTrainsets = false) { string result = _template; @@ -260,7 +224,7 @@ public class Scenery string rest = m.Groups[1].Value.Trim(); if (rest.Length == 0) continue; string[] parts = rest.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - // "$f XX path label…" (Pascal) or "$f path label" + if (parts.Length >= 3 && parts[0].Length <= 2) Attachments.Add(new SceneryAttachment { @@ -282,8 +246,6 @@ public class Scenery } } - // Pulls include … end //$optional… … endoptional out of the template so Start - // can re-inject only the checked ones (Pascal ParseInc / actStartExecute). private string ParseAndStripOptionalIncludes(string content) { Includes.Clear(); @@ -346,16 +308,9 @@ public class Scenery return sb + text; } - /// - /// Reads the scenery's environment commands into the editable weather fields. - /// Mirrors the original Starter (config: movelight / scenario.weather.temperature - /// / scenario.time.override, plus top-level time and the atmo fog/overcast block). - /// private void ParseWeather(string content) { - // Classic "time h:mm" seeds both clocks (Pascal: Config.Time + Config.StartTime). - // "scenario.time.override" seeds only the UI clock (Pascal ignored it on load; - // we honour it so a previously exported $scn round-trips correctly). + var time = Regex.Match(content, @"(?im)^\s*time\s+(\d{1,2})[:.](\d{2})\b"); if (time.Success) { @@ -371,12 +326,11 @@ public class Scenery } else if (!time.Success) { - // No clock in the SCN → UI shows "now", classic export stays 10:30 (Pascal). + var now = System.DateTime.Now; ScenarioTimeOverride = $"{now.Hour:D2}:{now.Minute:D2}"; } - // "movelight " - day of the year (sun elevation / season) var move = Regex.Match(content, @"(?im)\bmovelight\s+(-?\d+)"); if (move.Success && int.TryParse(move.Groups[1].Value, out int day)) { @@ -384,7 +338,6 @@ public class Scenery HasWeather = true; } - // "scenario.weather.temperature <°C>" var temp = Regex.Match(content, @"(?i)scenario\.weather\.temperature\s+(-?\d+(?:\.\d+)?)"); if (temp.Success && double.TryParse(temp.Groups[1].Value, System.Globalization.NumberStyles.Float, @@ -394,8 +347,6 @@ public class Scenery HasWeather = true; } - // "atmo R G B fogStart fogEnd R G B overcast endatmo" - // Pascal ParseAtmo: FogEnd := RandomRange(FogStart, FogEnd). var atmo = Regex.Match(content, @"(?is)\batmo\b(.*?)\bendatmo\b"); if (atmo.Success) { @@ -429,7 +380,6 @@ public class Scenery _origOvercast = Overcast; } - /// Restores weather fields to the values parsed from the SCN file. public void RestoreWeather() { Time = _origTime; @@ -441,17 +391,11 @@ public class Scenery WeatherDirty = true; } - /// - /// Replaces the scenery's environment commands with a fresh config block built - /// from the current weather fields (the C# equivalent of the original Starter's - /// TLexParser.ChangeConfig). On any failure the original text is returned - /// unchanged so a launch is never blocked by a bad rewrite. - /// private string RewriteWeather(string text) { try { - // strip the existing weather commands + string s = text; s = Regex.Replace(s, @"(?is)\bconfig\b.*?\bendconfig\b", " "); s = Regex.Replace(s, @"(?is)\btime\b\s+\d[^\r\n]*?\bendtime\b", " "); @@ -461,7 +405,7 @@ public class Scenery s = Regex.Replace(s, @"(?i)scenario\.time\.override\s+\S+", " "); var inv = System.Globalization.CultureInfo.InvariantCulture; - // Override = picker value; classic time = original SCN start (unchanged by UI). + string config = "config\r\n" + $"movelight {Day}\r\n" + @@ -479,10 +423,6 @@ public class Scenery } } - /// - /// Collects every //$<letter> line (e.g. all //$d lines) and joins them - /// into one multi-line string. Returns null when none are present. - /// private static string MatchAllDirectives(string content, string letter) { var matches = Regex.Matches( @@ -495,11 +435,6 @@ public class Scenery return string.Join("\n", matches.Select(m => m.Groups[1].Value.TrimEnd())); } - /// - /// 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( @@ -510,11 +445,6 @@ public class Scenery 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 @@ -532,19 +462,17 @@ public class Scenery 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 + string scnDir = System.IO.Path.GetDirectoryName(Path) ?? "."; + string root = System.IO.Path.GetDirectoryName(scnDir) ?? "."; - // 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 + name, + System.IO.Path.Combine(root, name), + System.IO.Path.Combine(scnDir, name), + System.IO.Path.Combine(scnDir, "images", name), + System.IO.Path.Combine(root, "scenery", "images", name), }; foreach (string candidate in candidates) diff --git a/StarterNG/Classes/Settings.cs b/StarterNG/Classes/Settings.cs index 6a36eaa..477399b 100644 --- a/StarterNG/Classes/Settings.cs +++ b/StarterNG/Classes/Settings.cs @@ -9,163 +9,126 @@ namespace StarterNG.Classes; public enum BatteryDefault { Default = 0, AlwaysOff = 1, AlwaysOn = 2 } -/// -/// Application settings backing the Settings view, persisted to the simulator's -/// eu07.ini. -/// -/// Where it loads/saves. The active file lives next to the -/// simulator's own config: %APPDATA%\MaSzyna\eu07.ini on Windows or -/// ~/.config/MaSzyna/eu07.ini elsewhere. If that file does not exist yet, -/// defaults are read from an eu07.ini in the launcher's working directory -/// (the same fallback the simulator uses). Saving always writes to the per-user -/// path. -/// -/// Unknown keys are kept. The whole file is round-tripped through -/// ; only the keys the UI actually edits are rewritten, -/// so any extra settings the launcher does not model survive a save. -/// -/// Some mappings are best-effort where the wiki does not document an exact -/// key; those are marked with NOTE and can be adjusted against the running exe. -/// public sealed class Settings { public static Settings Instance { get; } = new(); - /// - /// Pulls the current values out of the Settings view into this instance. - /// The view assigns this when it is constructed; null if the view was never - /// opened (in which case Save just re-writes the loaded values). - /// public Action? CaptureFromUi { get; set; } - /// True if the loaded config explicitly contained a lang entry. public bool LanguageWasSet { get; private set; } - // ── General ─────────────────────────────────────────────────────────── - public string Language = "English"; // lang (pl/en) - public bool Fullscreen; // fullscreen - public bool PauseWhenInactive = true; // inactivepause - public bool PauseOnStart; // pause - public int CursorSensitivity = 2; // |mousescale x| - public bool InvertMouseHorizontal; // mousescale x < 0 - public bool InvertMouseVertical; // mousescale y < 0 + public string Language = "English"; + public bool Fullscreen; + public bool PauseWhenInactive = true; + public bool PauseOnStart; + public int CursorSensitivity = 2; + public bool InvertMouseHorizontal; + public bool InvertMouseVertical; - // ── Communication ───────────────────────────────────────────────────── - public bool IgnoreGamepad; // input.gamepad (inverted) - public int FeedbackMode = 1; // feedbackmode (0-5) - public int FeedbackPort = 888; // feedbackport - public bool UartEnabled; // uart presence - public string UartPort = ""; // uart - public string UartTune = ""; // uarttune - public bool UartDebug; // uartdebug - public bool UartMain, UartScnd, UartTrain, UartLocal, UartRadioVolume, UartRadioChannel; // uartfeature tokens + public bool IgnoreGamepad; + public int FeedbackMode = 1; + public int FeedbackPort = 888; + public bool UartEnabled; + public string UartPort = ""; + public string UartTune = ""; + public bool UartDebug; + public bool UartMain, UartScnd, UartTrain, UartLocal, UartRadioVolume, UartRadioChannel; - // ── Other ───────────────────────────────────────────────────────────── - public bool SelectExeAutomatically = true; // starter.exe.auto (launcher-only) - public string ExecutablePath = "eu07.exe"; // starter.exe.path (launcher-only) - public bool DebugMode; // debugmode - public bool VirtualShunting = true; // ai.trainman - public bool LogMissingVehicleFiles; // starter.logmissingvehicles → bledy.txt dump + public bool SelectExeAutomatically = true; + public string ExecutablePath = "eu07.exe"; + public bool DebugMode; + public bool VirtualShunting = true; + public bool LogMissingVehicleFiles; - // ── Graphics ────────────────────────────────────────────────────────── - public int RenderEngine; // gfxrenderer (index, see RenderEngines) - public int Width = 1280; // width - public int Height = 720; // height - public int BufferScalePercent = 100; // starter.bufferscale → gfx.framebuffer.fidelity - public int MaxTextureSize = 4096; // maxtexturesize - public int MaxCabTextureSize = 4096; // maxcabtexturesize - public int TextureFiltering = 8; // anisotropicfiltering (1/2/4/8/16) - public int Multisampling = 2; // multisampling (0-3) - public int DynamicLights = 7; // dynamiclights (0-7) - public double DrawRangeFactor = 3.0; // gfx.drawrange.factor.max - public bool VSync; // vsync - public bool Smoke = true; // gfx.smoke - public int SmokeFidelity = 1; // gfx.smoke.fidelity (1-4) - public bool ChromaticAberration = true; // gfx.postfx.chromaticaberration.enabled - public bool MotionBlur = true; // gfx.postfx.motionblur.enabled - public bool ExtraEffects = true; // gfx.extraeffects - public bool EnvMap = true; // gfx.envmap.enabled - public bool UseVbo = true; // usevbo - public bool RenderShadows = true; // shadows + gfx.shadowmap.enabled - public int ReflectionsFramerate = 60; // gfx.reflections.framerate - public int ShadowMapResolution = 2048; // shadowtune[0] - public int ShadowProjectionRange = 250; // shadowtune[1] - public int CabShadowsRange; // gfx.shadows.cab.range - public int ShadowRankCutoff = 3; // gfx.shadow.rank.cutoff (NOTE) - public int ReflectionsFidelity; // gfx.reflections.fidelity (0-2) - public int FieldOfView = 45; // fieldofview (15-75) NOTE: 'fovSlider' in XAML - public bool PythonScreens = true; // python.enabled - public bool PythonThreadedUpload = true; // python.threadedupload - public bool FpsLimitEnabled; // fpslimit (presence) - public int FpsLimit = 30; // fpslimit (1-100) - public double ShadowAngleLimit = 0.2; // gfx.shadow.angle.min (0.2-1.0) - public bool FullscreenWindowed; // fullscreenwindowed - public bool RenderAngleVulkan; // gfx.angleplatform ("vulkan") + public int RenderEngine; + public int Width = 1280; + public int Height = 720; + public int BufferScalePercent = 100; + public int MaxTextureSize = 4096; + public int MaxCabTextureSize = 4096; + public int TextureFiltering = 8; + public int Multisampling = 2; + public int DynamicLights = 7; + public double DrawRangeFactor = 3.0; + public bool VSync; + public bool Smoke = true; + public int SmokeFidelity = 1; + public bool ChromaticAberration = true; + public bool MotionBlur = true; + public bool ExtraEffects = true; + public bool EnvMap = true; + public bool UseVbo = true; + public bool RenderShadows = true; + public int ReflectionsFramerate = 60; + public int ShadowMapResolution = 2048; + public int ShadowProjectionRange = 250; + public int CabShadowsRange; + public int ShadowRankCutoff = 3; + public int ReflectionsFidelity; + public int FieldOfView = 45; + public bool PythonScreens = true; + public bool PythonThreadedUpload = true; + public bool FpsLimitEnabled; + public int FpsLimit = 30; + public double ShadowAngleLimit = 0.2; + public bool FullscreenWindowed; + public bool RenderAngleVulkan; - // ── Physics ─────────────────────────────────────────────────────────── - public int SplineFidelity = 1; // splinefidelity (1-4) - public bool FullPhysics = true; // fullphysics - public bool EnableTraction = true; // enabletraction (pantograph can break) - public bool LiveTraction = true; // livetraction - public bool PhysicsLog; // physicslog (speedometer tapes) - public bool DebugLog = true; // debuglog (3 / 0) - public bool MultipleLogs; // multiplelogs - public bool DisplaySimulation = true; // gfx.skiprendering (inverted) - public bool CrashDamage = true; // crashdamage - public double Friction = 1.0; // friction (0.1-3.0) - public double BrakeStep = 1.0; // brakestep (0.4-3.0) - public double BrakeSpeed = 3.0; // brakespeed (0.4-3.0) + public int SplineFidelity = 1; + public bool FullPhysics = true; + public bool EnableTraction = true; + public bool LiveTraction = true; + public bool PhysicsLog; + public bool DebugLog = true; + public bool MultipleLogs; + public bool DisplaySimulation = true; + public bool CrashDamage = true; + public double Friction = 1.0; + public double BrakeStep = 1.0; + public double BrakeSpeed = 3.0; - // ── Sound ───────────────────────────────────────────────────────────── - public bool SoundEnabled = true; // soundenabled - public int Volume = 50; // sound.volume (0-2 → 0-100) - public int RadioVolume = 75; // sound.volume.radio (0-1 → 0-100) - public int VehiclesVolume = 100; // sound.volume.vehicle - public int PositionalVolume = 100; // sound.volume.positional - public int AmbientVolume = 100; // sound.volume.ambient + public bool SoundEnabled = true; + public int Volume = 50; + public int RadioVolume = 75; + public int VehiclesVolume = 100; + public int PositionalVolume = 100; + public int AmbientVolume = 100; - // ── Advanced ────────────────────────────────────────────────────────── - public bool CompressTextures = true; // compresstex - public bool ScaleSpeculars = true; // scalespeculars - public bool UseGLES; // gfx.usegles - public bool ShaderGamma; // gfx.shadergamma - public bool ScreenMipmaps = true; // python.mipmaps - public bool ExtendedModelConversion; // convertmodels ("143"/"0") - public bool GfxResourceMove; // gfx.resource.move - public bool GfxResourceSweep = true; // gfx.resource.sweep - public int TrainPhysicsThreads; // async.trainThreads - public bool IgnoreIrrelevantTrains; // starter.ignoreirrelevant (launcher-only) + public bool CompressTextures = true; + public bool ScaleSpeculars = true; + public bool UseGLES; + public bool ShaderGamma; + public bool ScreenMipmaps = true; + public bool ExtendedModelConversion; + public bool GfxResourceMove; + public bool GfxResourceSweep = true; + public int TrainPhysicsThreads; + public bool IgnoreIrrelevantTrains; - // ── Starter (launcher-only, stored under starter.* keys) ─────────────── - public bool AutoCloseStarter; // starter.autoclose - public bool LargeThumbnails; // starter.largethumbnails - public bool AutoExpandSceneryTree; // starter.expandtree - public BatteryDefault BatteryDefault = BatteryDefault.Default; // starter.batterydefault (launcher-only) + public bool AutoCloseStarter; + public bool LargeThumbnails; + public bool AutoExpandSceneryTree; + public BatteryDefault BatteryDefault = BatteryDefault.Default; - // List filters (launcher-only). Default on. - public bool ShowAiVehicles = true; // starter.show.ai (show //$o "-" consists) - public bool DrivableOnly = true; // starter.drivableonly (hide //$decor consists) - public bool ShowArchivalSceneries = true; // starter.show.archival - public bool HideArchivalVehicles = true; // starter.hide.archivalvehicles - /// Last selected scenery file name (without path), restored on next launch. - public string LastScenery = ""; // starter.last.scenery + public bool ShowAiVehicles = true; + public bool DrivableOnly = true; + public bool ShowArchivalSceneries = true; + public bool HideArchivalVehicles = true; + + public string LastScenery = ""; - /// gfxrenderer tokens in the order shown by the render-engine combo. public static readonly string[] RenderEngines = { "full", "legacy", "simpleshader", "simple", "off", "experimental" }; - /// Anisotropic-filtering steps for the texture-quality slider (1-5). public static readonly int[] AnisotropySteps = { 1, 2, 4, 8, 16 }; - // Tokens of mousescale / shadowtune that the UI does not edit but must keep. private double _mouseScaleYMagnitude = 0.2; - private string[] _shadowTuneExtra = { "400", "300" }; // tokens [2], [3] + private string[] _shadowTuneExtra = { "400", "300" }; private ConfigFile _config = new(); private string _savePath = string.Empty; private DateTime _configAgeUtc = DateTime.MinValue; - /// Resolves the per-user eu07.ini path for the current OS. public static string UserConfigPath() { if (OperatingSystem.IsWindows()) @@ -186,24 +149,16 @@ public sealed class Settings if (!string.IsNullOrEmpty(home)) return Path.Combine(home, ".config", "MaSzyna", "eu07.ini"); } - // Last resort: alongside the launcher. + return Path.Combine(AppContext.BaseDirectory, "eu07.ini"); } - /// Default config shipped in the launcher's working directory. private static string DefaultConfigPath() => Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini"); - /// Directory holding the per-user eu07.ini, for locating launcher-only data alongside it. public static string UserConfigDirectory() => Path.GetDirectoryName(UserConfigPath()) ?? AppContext.BaseDirectory; - /// - /// The simulator executable to launch. With auto-selection on, the working - /// directory is scanned for an eu07* binary - eu07 on Linux/macOS, - /// eu07.exe on Windows - so the same procedure works on every OS. With it - /// off, the explicitly chosen path is used. - /// public string ResolveExecutable() { if (!SelectExeAutomatically && !string.IsNullOrWhiteSpace(ExecutablePath)) @@ -219,23 +174,18 @@ public sealed class Settings continue; string file = Path.GetFileName(path); if (string.Equals(file, canonical, StringComparison.OrdinalIgnoreCase)) - return path; // exact canonical name wins + return path; if (best == null || file.Length < Path.GetFileName(best).Length) - best = path; // otherwise prefer the shortest eu07* name + best = path; } if (best != null) return best; } - catch { /* fall through to the canonical name */ } + catch { } return canonical; } - /// - /// Scans (defaults to the working directory) for - /// plausible eu07* launcher binaries, newest first, for populating the - /// manual-selection dropdown. Does not affect . - /// public static List ListCandidateExecutables(string? directory = null) { directory ??= Directory.GetCurrentDirectory(); @@ -250,8 +200,6 @@ public sealed class Settings catch { return new List(); } } - // Filters the eu07* matches down to plausible launcher binaries, skipping data - // and config files (eu07.ini, eu07.log, libraries, …). private static bool IsExecutableCandidate(string path) { string ext = Path.GetExtension(path).ToLowerInvariant(); @@ -260,9 +208,7 @@ public sealed class Settings return false; if (OperatingSystem.IsWindows()) return ext == ".exe"; - // Linux / macOS: no extension (eu07), a known binary suffix, or .exe - // (some builds - notably this project's own build-macos.sh - ship the - // macOS binary under the historical "eu07.exe" name). + return ext.Length == 0 || ext is ".exe" or ".x86_64" or ".run" or ".appimage"; } @@ -270,7 +216,7 @@ public sealed class Settings private static Encoding ResolveEncoding() { - // eu07.ini comments use Polish diacritics in Windows-1250; preserve them. + try { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -282,7 +228,6 @@ public sealed class Settings } } - /// Loads settings from the per-user file, or the working-dir default. public void Load() { _savePath = UserConfigPath(); @@ -290,14 +235,12 @@ public sealed class Settings LoadFrom(source); } - /// Captures the latest UI values (if the view is open) and saves. public void CaptureAndSave() { CaptureFromUi?.Invoke(); Save(); } - /// Loads settings from an arbitrary path without changing where writes. public void LoadFrom(string path) { try @@ -315,7 +258,6 @@ public sealed class Settings TouchConfigAge(); } - /// Persists settings to the per-user eu07.ini, keeping unknown keys. public void Save() { if (string.IsNullOrEmpty(_savePath)) @@ -324,7 +266,6 @@ public sealed class Settings SaveTo(_savePath); } - /// Writes settings to an arbitrary path without changing where writes. public void SaveTo(string path) { WriteToConfig(); @@ -341,7 +282,7 @@ public sealed class Settings } catch { - // Could not write (permissions / path). Leave values in memory. + } } @@ -360,7 +301,6 @@ public sealed class Settings } } - /// Pascal CheckSettingsFile: true when eu07.ini is newer than last load/save. public bool IsConfigNewerOnDisk() { string path = string.IsNullOrEmpty(_savePath) ? UserConfigPath() : _savePath; @@ -376,7 +316,6 @@ public sealed class Settings } } - // Map BufferScalePercent → gfx.framebuffer.fidelity like Pascal's 720/1080/1440/Custom. private void ApplyFramebufferFidelity() { int fidelity = BufferScalePercent switch @@ -392,10 +331,9 @@ public sealed class Settings _config.Remove("gfx.framebuffer.fidelity"); } - /// Copy the Reinhard GLSL into shaders/tonemapping.glsl when present. public void ApplyHdrShader() { - // Only meaningful for full / legacy / experimental renderers (Pascal). + if (RenderEngine is not (0 or 1 or 5)) return; @@ -408,7 +346,7 @@ public sealed class Settings Directory.CreateDirectory(destDir); File.Copy(src, Path.Combine(destDir, "tonemapping.glsl"), overwrite: true); } - catch { /* missing shaders dir / permissions */ } + catch { } } private static string? FindBundled(string fileName) @@ -426,7 +364,6 @@ public sealed class Settings return null; } - /// Pascal chLogExt on exit: dump missing vehicle assets to starter/bledy.txt. public void DumpMissingVehicleLog() { if (!LogMissingVehicleFiles) return; @@ -441,12 +378,10 @@ public sealed class Settings catch { } } - // ── config → fields ─────────────────────────────────────────────────── private void ReadFromConfig() { var c = _config; - // General LanguageWasSet = c.Has("lang"); Language = c.GetString("lang", "en").ToLowerInvariant() == "pl" ? "Polski" : "English"; Fullscreen = c.GetBool("fullscreen", false); @@ -465,7 +400,6 @@ public sealed class Settings _mouseScaleYMagnitude = Math.Abs(my); } - // Communication IgnoreGamepad = !c.GetBool("input.gamepad", true); FeedbackMode = Clamp(c.GetInt("feedbackmode", 1), 0, 5); FeedbackPort = c.GetInt("feedbackport", 888); @@ -486,14 +420,12 @@ public sealed class Settings UartRadioVolume = UartRadioChannel = false; } - // Other SelectExeAutomatically = c.GetBool("starter.exe.auto", true); ExecutablePath = c.GetString("starter.exe.path", "eu07.exe"); DebugMode = c.GetBool("debugmode", false); VirtualShunting = c.GetBool("ai.trainman", true); LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false); - // Graphics RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0); Width = c.GetInt("width", 1280); Height = c.GetInt("height", 720); @@ -532,7 +464,6 @@ public sealed class Settings FullscreenWindowed = c.GetBool("fullscreenwindowed", false); RenderAngleVulkan = string.Equals(c.GetString("gfx.angleplatform", ""), "vulkan", StringComparison.OrdinalIgnoreCase); - // Physics SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4); FullPhysics = c.GetBool("fullphysics", true); EnableTraction = c.GetBool("enabletraction", true); @@ -546,7 +477,6 @@ public sealed class Settings BrakeStep = c.GetDouble("brakestep", 1.0); BrakeSpeed = c.GetDouble("brakespeed", 3.0); - // Sound SoundEnabled = c.GetBool("soundenabled", true); Volume = Clamp((int)Math.Round(c.GetDouble("sound.volume", 1.0) * 50), 1, 100); RadioVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.radio", 0.75) * 100), 1, 100); @@ -554,7 +484,6 @@ public sealed class Settings PositionalVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.positional", 1.0) * 100), 1, 100); AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 1.0) * 100), 1, 100); - // Advanced CompressTextures = c.GetBool("compresstex", true); ScaleSpeculars = c.GetBool("scalespeculars", true); UseGLES = c.GetBool("gfx.usegles", false); @@ -566,7 +495,6 @@ public sealed class Settings TrainPhysicsThreads = Clamp(c.GetInt("async.trainThreads", 0), 0, Environment.ProcessorCount); IgnoreIrrelevantTrains = c.GetBool("starter.ignoreirrelevant", false); - // Starter AutoCloseStarter = c.GetBool("starter.autoclose", false); LargeThumbnails = c.GetBool("starter.largethumbnails", false); AutoExpandSceneryTree = c.GetBool("starter.expandtree", false); @@ -578,12 +506,10 @@ public sealed class Settings LastScenery = c.GetString("starter.last.scenery", ""); } - // ── fields → config ─────────────────────────────────────────────────── private void WriteToConfig() { var c = _config; - // General c.Set("lang", Language == "Polski" ? "pl" : "en"); c.SetBool("fullscreen", Fullscreen); c.SetBool("inactivepause", PauseWhenInactive); @@ -595,7 +521,6 @@ public sealed class Settings $"{mx.ToString("0.###", CultureInfo.InvariantCulture)} " + $"{my.ToString("0.###", CultureInfo.InvariantCulture)}"); - // Communication c.SetBool("input.gamepad", !IgnoreGamepad); c.SetInt("feedbackmode", FeedbackMode); c.SetInt("feedbackport", FeedbackPort); @@ -611,14 +536,12 @@ public sealed class Settings if (UartRadioChannel) sb.Append("radiochannel|"); c.Set("uartfeature", sb.ToString()); - // Other c.SetBool("starter.exe.auto", SelectExeAutomatically); c.Set("starter.exe.path", ExecutablePath); c.SetBool("debugmode", DebugMode); c.SetBool("ai.trainman", VirtualShunting); c.SetBool("starter.logmissingvehicles", LogMissingVehicleFiles); - // Graphics c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]); c.SetInt("width", Width); c.SetInt("height", Height); @@ -654,7 +577,6 @@ public sealed class Settings c.SetBool("fullscreenwindowed", FullscreenWindowed); if (RenderAngleVulkan) c.Set("gfx.angleplatform", "vulkan"); else c.Remove("gfx.angleplatform"); - // Physics c.SetInt("splinefidelity", SplineFidelity); c.SetBool("fullphysics", FullPhysics); c.SetBool("enabletraction", EnableTraction); @@ -668,7 +590,6 @@ public sealed class Settings c.SetDouble("brakestep", BrakeStep); c.SetDouble("brakespeed", BrakeSpeed); - // Sound c.SetBool("soundenabled", SoundEnabled); c.SetDouble("sound.volume", Volume / 50.0); c.SetDouble("sound.volume.radio", RadioVolume / 100.0); @@ -676,7 +597,6 @@ public sealed class Settings c.SetDouble("sound.volume.positional", PositionalVolume / 100.0); c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0); - // Advanced c.SetBool("compresstex", CompressTextures); c.SetBool("scalespeculars", ScaleSpeculars); c.SetBool("gfx.usegles", UseGLES); @@ -688,7 +608,6 @@ public sealed class Settings c.SetInt("async.trainThreads", TrainPhysicsThreads); c.SetBool("starter.ignoreirrelevant", IgnoreIrrelevantTrains); - // Starter c.SetBool("starter.autoclose", AutoCloseStarter); c.SetBool("starter.largethumbnails", LargeThumbnails); c.SetBool("starter.expandtree", AutoExpandSceneryTree); @@ -703,7 +622,6 @@ public sealed class Settings c.Remove("starter.last.scenery"); } - // ── small helpers ────────────────────────────────────────────────────── private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v); private static int ParseInt(string s, int fallback) => diff --git a/StarterNG/Classes/Trainset.cs b/StarterNG/Classes/Trainset.cs index 2733bad..17b003d 100644 --- a/StarterNG/Classes/Trainset.cs +++ b/StarterNG/Classes/Trainset.cs @@ -10,7 +10,7 @@ namespace StarterNG.Classes; public enum eDriverType { Headdriver, - Reardriver, + Reardriver, Passenger, Nobody } @@ -25,19 +25,14 @@ public class Trainset public string Description; public List Vehicles; - /// Original .scn text of this trainset block (incl. endtrainset). public string RawEntry; - /// True if the block parsed cleanly; false blocks are exported verbatim. public bool Parsed; - /// True when the block carries the //$decor flag (decoration, not drivable). public bool Decor; - /// Loading-screen logo key from //$il. public string? Logo; - /// Optional trainset mini from //$it. public string? Mini; public Trainset(string trainsetEntry) @@ -65,8 +60,6 @@ public class Trainset .Where(t => !t.StartsWith("//") && !t.StartsWith("/*")) .ToList(); - // handle specific descriptions - // //$o var match = Regex.Match( trainsetEntry, @"^\s*//\$o\s*(.*)$", @@ -82,15 +75,12 @@ public class Trainset var mini = Regex.Match(trainsetEntry, @"^\s*//\$it\b[ \t]*([^\r\n]*)", RegexOptions.Multiline); if (mini.Success) Mini = mini.Groups[1].Value.Trim(); - - // parse trainset - + for (int i = 0; i < tokens.Count; i++) { if (tokens[i] == "endtrainset") break; - - // trainset properties + if (tokens[i] == "trainset") { this.Name = tokens[++i]; @@ -100,24 +90,22 @@ public class Trainset this.OriginalVelocity = this.Velocity; continue; } - - // skip entire assignments block + if (tokens[i] == "assignments") { while (i < tokens.Count && tokens[i] != "endassignment") i++; - i++; // jump over endassignment + i++; continue; } - - // load vehicles + if (tokens[i] == "node") { Dynamic nodeDynamic = new Dynamic(); nodeDynamic.RangeMax = float.Parse(tokens[++i], CultureInfo.InvariantCulture); nodeDynamic.RangeMin = float.Parse(tokens[++i], CultureInfo.InvariantCulture); nodeDynamic.Name = tokens[++i]; - i++; // dynamic keyword + i++; nodeDynamic.DataFolder = tokens[++i]; nodeDynamic.SkinFile = tokens[++i]; nodeDynamic.MmdFile = tokens[++i]; @@ -131,37 +119,26 @@ public class Trainset _ => eDriverType.Nobody }; - // couplingdata: numeric bit-mask plus optional ".B/.W/.T" parameter - // codes (brake / wheel / thermo settings) kept verbatim. nodeDynamic.Coupling = Coupling.Parse(tokens[++i]); - // Everything before enddynamic is the optional, positional tail - // documented for node::dynamic: - // [Lx] velocity [loadcount [loadtype]] [destination [destination]] var trailing = new List(); while (i + 1 < tokens.Count && tokens[i + 1] != "enddynamic") trailing.Add(tokens[++i]); nodeDynamic.ReadTrailing(trailing); - i++; // jump over enddynamic + i++; Vehicles.Add(nodeDynamic); } } } - - /// - /// The .scn text for this trainset for export. Unparsed blocks are written - /// back verbatim; parsed blocks are regenerated from the current vehicles. - /// + public string ToSceneryEntry() => Parsed ? GetTrainsetEntry() + "endtrainset\n" : RawEntry; public string GetTrainsetEntry() { - // Starter metadata comments (parsed for filters/UI). Pascal's launch rewrite - // omits them from $scn; we keep them so a regenerated block stays filterable - // if the template is re-opened or compared. + var sb = new StringBuilder(); if (Decor) sb.Append("//$decor\n"); @@ -205,8 +182,7 @@ public class Trainset $"{vehicle.Offset.ToString(CultureInfo.InvariantCulture)} " + $"{driverType} {vehicle.Coupling}{vehicle.WriteTrailing()} enddynamic\n"; } - - + return entry; } } @@ -223,65 +199,38 @@ public class Dynamic public float Offset; public eDriverType DriverType; - /// - /// Coupling with the next vehicle (the couplingdata field): the bit-mask - /// plus any ".B/.W/.T" parameter codes (brake / wheel / thermo settings). - /// public Coupling Coupling = new(); - /// - /// Low byte of the coupling mask, exposed for the depot's per-bit editor. - /// Backed by so the two never diverge. - /// public byte couplingData { get => (byte)(Coupling.AbsFlags & 0xFF); set => Coupling.Flags = value; } - // --- optional node::dynamic trailing parameters (all positional, all optional) --- - - /// "Lx" MaxLoad override token (e.g. "L0"), or null when absent. public string? MaxLoad; - /// Whether a per-vehicle velocity token is present / should be written. public bool HasVelocity; - /// Per-vehicle starting velocity. public float Velocity; - /// Cargo amount (loadcount). public int LoadCount; - /// Cargo type (loadtype) — only meaningful when > 0. public string? LoadType; - /// Optional cargo destination tokens (rarely used; preserved verbatim). public List Destinations = new(); - // --- depot / consist-builder extras (not part of the .scn syntax) --- - - /// Explicit miniature name (from the vehicle database). Falls back to SkinFile. public string? MiniName; - /// Whether the vehicle is visually reversed in the consist. public bool Flipped; - /// - /// Parses the optional positional parameters that follow couplingdata: - /// [Lx] velocity [loadcount [loadtype]] [destination [destination]]. - /// See https://wiki.eu07.pl/index.php?title=Obiekt_node::dynamic - /// internal void ReadTrailing(List t) { int p = 0; - // Lx — MaxLoad override (e.g. "L0"); only when it genuinely looks like one. if (p < t.Count && (t[p][0] == 'L' || t[p][0] == 'l') && t[p].Length > 1 && char.IsDigit(t[p][1])) MaxLoad = t[p++]; - // velocity — the first plain number after the coupling if (p < t.Count && float.TryParse(t[p], NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) { @@ -290,7 +239,6 @@ public class Dynamic p++; } - // loadcount [loadtype] — loadtype is present only when loadcount > 0 if (p < t.Count && int.TryParse(t[p], NumberStyles.Integer, CultureInfo.InvariantCulture, out int lc)) { @@ -300,16 +248,10 @@ public class Dynamic LoadType = t[p++]; } - // remaining tokens: optional destinations, kept verbatim while (p < t.Count) Destinations.Add(t[p++]); } - /// - /// Regenerates the trailing parameter string (each present token prefixed with - /// a space), the inverse of . A loadcount of 0 is - /// omitted, matching the canonical trainset entries the simulator expects. - /// internal string WriteTrailing() { var sb = new StringBuilder(); @@ -328,7 +270,6 @@ public class Dynamic return sb.ToString(); } - /// Deep copy, used when importing a trainset into the editable consist. public Dynamic Clone() => new Dynamic { RangeMax = RangeMax, @@ -351,7 +292,6 @@ public class Dynamic Flipped = Flipped }; - /// Pascal PrepareNode(..., TrainSet=False). public string ToLooseNode() { string driver = DriverType switch @@ -372,7 +312,6 @@ public class Dynamic } } -/// One entry in the depot consist: a single vehicle or a locked multi-car unit. public sealed class ConsistItem { public List Cars { get; set; } = new(); @@ -381,39 +320,24 @@ public sealed class ConsistItem public eDriverType Driver { get; set; } = eDriverType.Nobody; } -/// -/// The couplingdata value joining a vehicle to the next one: a bit-mask of -/// the active coupling types, optionally followed by ".B/.W/.T" parameter codes. -/// A negative mask means the coupling is locked (cannot be uncoupled in-sim); the -/// simulator takes its absolute value before testing the individual bits. -/// See https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow -/// public sealed class Coupling { - // Mask bit values, per the wiki. - public const int Mechanical = 1; // mechanical link - public const int BrakePipe = 2; // pneumatic 5 atm (brakes) - public const int ControlMU = 4; // multiple-unit control - public const int HighVoltage = 8; // high voltage - public const int Gangway = 16; // passage between vehicles - public const int AuxPneumatic = 32; // auxiliary pneumatic 8 atm - public const int Heating = 64; // heating - public const int WorkshopLock = 128; // workshop coupling (uncouple lock) - /// Signed mask exactly as written; negative = locked. + public const int Mechanical = 1; + public const int BrakePipe = 2; + public const int ControlMU = 4; + public const int HighVoltage = 8; + public const int Gangway = 16; + public const int AuxPneumatic = 32; + public const int Heating = 64; + public const int WorkshopLock = 128; + public int Flags; - /// - /// Textual parameter codes that follow the mask after a '.', in order - /// (e.g. "BR", "WH25F5"). Preserved verbatim so brake / wheel / thermo - /// settings survive a round-trip even when the launcher does not edit them. - /// public List Parameters = new(); - /// Mask with the lock sign removed (what the simulator tests bits on). public int AbsFlags => Flags < 0 ? -Flags : Flags; - /// True when the coupling is marked permanent (negative mask). public bool Locked { get => Flags < 0; @@ -422,7 +346,6 @@ public sealed class Coupling public bool Has(int bit) => (AbsFlags & bit) != 0; - /// Sets or clears a mask bit, preserving the lock sign. public void Set(int bit, bool on) { int abs = on ? (AbsFlags | bit) : (AbsFlags & ~bit); @@ -444,12 +367,10 @@ public sealed class Coupling return c; } - /// The brake-rack setting carried in the "B" parameter, or null. public BrakeSetting? GetBrake() => BrakeSetting.FromParameter( Parameters.FirstOrDefault(p => p.StartsWith("B", StringComparison.Ordinal))); - /// Replaces (or clears, when null) the "B" brake parameter. public void SetBrake(BrakeSetting? brake) { Parameters.RemoveAll(p => p.StartsWith("B", StringComparison.Ordinal)); @@ -457,12 +378,10 @@ public sealed class Coupling Parameters.Insert(0, brake.ToParameter()); } - /// The wheel/damage setting carried in the "W" parameter, or null. public WheelSettings? GetWheels() => WheelSettings.FromParameter( Parameters.FirstOrDefault(p => p.StartsWith("W", StringComparison.Ordinal))); - /// Replaces (or clears, when empty/null) the "W" wheel parameter. public void SetWheels(WheelSettings? wheels) { Parameters.RemoveAll(p => p.StartsWith("W", StringComparison.Ordinal)); @@ -470,7 +389,6 @@ public sealed class Coupling Parameters.Add(wheels.ToParameter()); } - /// Coolant at ambient temperature (.TA), like Pascal ThermoDynamic. public bool ThermoDynamic { get => Parameters.Exists(p => p.Equals("TA", StringComparison.OrdinalIgnoreCase)); @@ -487,7 +405,6 @@ public sealed class Coupling Parameters = new List(Parameters) }; - /// Serialises back to "<flags>[.param[.param…]]". public override string ToString() { var sb = new StringBuilder(); @@ -498,23 +415,14 @@ public sealed class Coupling } } -/// -/// A vehicle's brake-rack setting, carried in the "B" parameter of its coupling -/// field: B<mode>[<load>][<switch>] (e.g. "BP", "BRA", "BG1"). -/// See https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow -/// public sealed class BrakeSetting { - /// Brake modes, longest first so "R+Mg" wins over "R" while parsing. public static readonly string[] Modes = { "R+Mg", "G", "P", "R", "Q", "O", "A" }; - /// Brake mode: G (freight), P (passenger), R (express), R+Mg, Q, O (off), A (auto). public string Mode = "P"; - /// Load adaptation: T (empty), H (medium), F (loaded), A (auto), or null. public string? Load; - /// Brake on/off switch: 0 (off), 1 (off ~10%), A (on), or null. public string? Switch; public static BrakeSetting? FromParameter(string? param) @@ -539,23 +447,14 @@ public sealed class BrakeSetting public string ToParameter() => "B" + Mode + (Load ?? "") + (Switch ?? ""); } -/// -/// Wheel-damage settings carried in the "W" parameter of a vehicle's coupling -/// field: W[H<sway%>][F<flat mm>][R<random flat mm>][P<flat prob%>] -/// (e.g. "WH25F5R10P8"). See https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow -/// public sealed class WheelSettings { - /// H — sway ("wężykowanie") probability, %. public int Sway; - /// F — flat spot ("podkucie") of this size, mm. public int Flatness; - /// R — additional random flat spot, 0..x mm. public int FlatnessRand; - /// P — flat-spot probability, % (guaranteed when omitted). public int FlatnessProb; public bool IsEmpty => Sway <= 0 && Flatness <= 0 && FlatnessRand <= 0 && FlatnessProb <= 0; @@ -594,4 +493,4 @@ public sealed class WheelSettings if (FlatnessProb > 0) sb.Append('P').Append(FlatnessProb); return sb.ToString(); } -} \ No newline at end of file +} diff --git a/StarterNG/Classes/VehicleDatabase.cs b/StarterNG/Classes/VehicleDatabase.cs index 34e8b3d..3ec29c2 100644 --- a/StarterNG/Classes/VehicleDatabase.cs +++ b/StarterNG/Classes/VehicleDatabase.cs @@ -8,12 +8,6 @@ using System.Text.Json.Serialization.Metadata; namespace StarterNG.Classes; -// ===================================================================== -// Vehicle JSON database (databases/vehicles/*.json) -// Format documented in vehicleEntryDoc.md (schema_version 1). -// ===================================================================== - -/// Root object of a single vehicle JSON file. public class VehicleEntry { [JsonPropertyName("uuid")] public string? Uuid { get; set; } @@ -24,13 +18,11 @@ public class VehicleEntry [JsonPropertyName("unknown")] public List Unknown { get; set; } = new(); } -/// Merged database root (databases/vehicles/vehicles.json). public class VehicleEntryCollection { [JsonPropertyName("vehicles")] public List Vehicles { get; set; } = new(); } -/// Legacy header / vehicle category group. public class VehicleGroup { [JsonPropertyName("id")] public string Id { get; set; } = ""; @@ -40,7 +32,6 @@ public class VehicleGroup [JsonPropertyName("implicit")] public bool Implicit { get; set; } } -/// A single .mat skin entry. public class VehicleTexture { [JsonPropertyName("uuid")] public string? Uuid { get; set; } @@ -54,24 +45,15 @@ public class VehicleTexture [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; - // ── cached lookups, populated once after load (VehicleDatabase.BuildTextureIndex) ── - // These mirror the depot's ClassOf / CategoryOf so the search/filter hot path - // never recomputes them (no per-keystroke dictionary lookups over every texture). - - /// Group's "mini" (or this texture's mini_ref) — the vehicle class. Never null. [JsonIgnore] public string ResolvedClass { get; internal set; } = ""; - /// Group's "category" letter, or null when the group is unknown. [JsonIgnore] public string? ResolvedCategory { get; internal set; } - /// True when the texture's group is marked archival (archived in JSON). [JsonIgnore] public bool ResolvedArchived { get; internal set; } } -/// Alternate mapping from a malformed multi-= legacy line. public class VehicleAlias { [JsonPropertyName("model")] public string? Model { get; set; } @@ -80,7 +62,6 @@ public class VehicleAlias [JsonPropertyName("texture_mini")] public string? TextureMini { get; set; } } -/// Parsed metadata from the legacy // comment section. public class VehicleMeta { [JsonPropertyName("raw")] public string? Raw { get; set; } @@ -95,29 +76,18 @@ public class VehicleMeta [JsonPropertyName("extra")] public List Extra { get; set; } = new(); } -/// -/// Aggregated, in-memory view over every vehicle JSON file found in -/// databases/vehicles/. Provides lookup helpers for the depot UI. -/// public class VehicleDatabase { public List Textures { get; } = new(); public Dictionary GroupsById { get; } = new(); public List Sets { get; } = new(); - /// Texture-uuid -> automatic-consist set that contains it. public Dictionary SetByTextureUuid { get; } = new(); - /// Texture-uuid -> texture (includes wrecks, so set refs resolve). public Dictionary TextureByUuid { get; } = new(); - /// skinfile (without extension, lower-case) -> texture. public Dictionary TextureBySkin { get; } = 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() { PropertyNameCaseInsensitive = true, @@ -126,11 +96,6 @@ public class VehicleDatabase TypeInfoResolver = VehicleJsonContext.Default, }; - /// - /// Loads the database in one call. When a merged vehicles.json is present - /// it is used, otherwise every per-vehicle *.json file is read. - /// Never throws: unreadable files are skipped. - /// public void Load(string directory = "databases/vehicles/") { BeginLoad(); @@ -139,7 +104,6 @@ public class VehicleDatabase EndLoad(); } - /// Resets the aggregate before an incremental load. public void BeginLoad() { Textures.Clear(); @@ -150,17 +114,12 @@ public class VehicleDatabase TextureBySkin.Clear(); } - /// Finalises an incremental load (builds lookup indexes). public void EndLoad() { BuildSetIndex(); BuildTextureIndex(); } - /// - /// Files that make up the database: the merged vehicles.json if present, - /// otherwise every per-vehicle *.json. Used to drive load progress. - /// public static List EnumerateFiles(string directory = "databases/vehicles/") { if (!Directory.Exists(directory)) @@ -173,7 +132,6 @@ public class VehicleDatabase return Directory.GetFiles(directory, "*.json").ToList(); } - /// Ingests a single database file (merged or per-vehicle). public void LoadFile(string file) { if (string.Equals(Path.GetFileName(file), "vehicles.json", StringComparison.OrdinalIgnoreCase)) @@ -195,7 +153,7 @@ public class VehicleDatabase } catch { - // ignore malformed merged database + } } @@ -211,7 +169,7 @@ public class VehicleDatabase } catch { - // ignore malformed single-vehicle file + } } @@ -225,13 +183,12 @@ public class VehicleDatabase foreach (var texture in entry.Textures) { - // index every texture (wrecks too) so set / skin references resolve + if (!string.IsNullOrEmpty(texture.Uuid)) TextureByUuid[texture.Uuid!] = texture; if (!string.IsNullOrEmpty(texture.Skinfile)) TextureBySkin.TryAdd(Path.GetFileNameWithoutExtension(texture.Skinfile).ToLowerInvariant(), texture); - // skip wrecks from the standard browser list if (texture.Wreck) continue; Textures.Add(texture); } @@ -239,10 +196,6 @@ public class VehicleDatabase Sets.AddRange(entry.Sets); } - /// - /// If the texture belongs to an automatic-consist set, returns all of that - /// set's textures in their defined order; otherwise null. - /// public List? ResolveSet(VehicleTexture texture) { if (string.IsNullOrEmpty(texture.Uuid)) return null; @@ -271,9 +224,6 @@ public class VehicleDatabase } } - // Resolves each texture's class (group mini / mini_ref) and category letter - // once, after all groups are loaded, so the depot reads cached fields instead - // of doing a GroupsById lookup per texture on every search keystroke. private void BuildTextureIndex() { foreach (var t in Textures) @@ -288,9 +238,6 @@ public class VehicleDatabase string? category = g?.Category; - // A "*" category is a wildcard: the actual category is the first letter - // of the mini (upper-cased, like the original Starter's UpCase(mini[1]) - // wagon-typing), e.g. a "Bdhpumn" mini -> category "B". if (category == "*") { string source = !string.IsNullOrEmpty(mini) ? mini : (t.TextureMini ?? ""); @@ -304,10 +251,6 @@ public class VehicleDatabase } } - /// - /// True when the texture is a non-leading member of an auto-consist set - /// (subsequent multi-unit cars that the depot browser should hide). - /// public bool IsSetFollower(VehicleTexture texture) { if (string.IsNullOrEmpty(texture.Uuid)) return false; @@ -320,10 +263,6 @@ public class VehicleDatabase !string.Equals(lead, texture.Uuid, StringComparison.OrdinalIgnoreCase); } - /// - /// Miniature name for a texture: the texture_mini property, but if no .bmp - /// for it exists, falls back to the mini of the group it belongs to. - /// public string? ResolveMiniName(VehicleTexture texture) { if (!string.IsNullOrEmpty(texture.TextureMini) && MiniPath(texture.TextureMini) != null) @@ -336,7 +275,6 @@ public class VehicleDatabase return texture.TextureMini; } - /// Resolved mini for a skin file (matched case-insensitively), or null. public string? MiniForSkin(string? skinFile) { if (string.IsNullOrEmpty(skinFile)) return null; @@ -344,7 +282,6 @@ public class VehicleDatabase return TextureBySkin.TryGetValue(key, out var tex) ? ResolveMiniName(tex) : null; } - /// The texture for a skin file (matched case-insensitively), or null. public VehicleTexture? TextureForSkin(string? skinFile) { if (string.IsNullOrEmpty(skinFile)) return null; @@ -352,7 +289,6 @@ public class VehicleDatabase return TextureBySkin.TryGetValue(key, out var tex) ? tex : null; } - /// Lines for starter/bledy.txt (Pascal chLogExt / missing assets). public List CollectMissingAssetLines() { var lines = new List(); @@ -377,7 +313,6 @@ public class VehicleDatabase return lines; } - /// Header label for the group a texture belongs to. public string GroupHeader(string? groupId) { if (!string.IsNullOrEmpty(groupId) && GroupsById.TryGetValue(groupId, out var grp)) @@ -388,21 +323,11 @@ public class VehicleDatabase return groupId ?? ""; } - // Case-insensitive index of mini .bmp files (built once). private static Dictionary? _miniIndex; - /// - /// Builds the miniature .bmp index up front (called from the startup load, - /// behind the splash) so the first thumbnail render never stalls the UI - /// thread scanning textures/mini/. No-op if it was already built. - /// public static void PreloadMiniIndex(string miniDir = "textures/mini/") => _miniIndex ??= BuildMiniIndex(miniDir); - /// - /// Resolves a miniature .bmp path under textures/mini/ case-insensitively, - /// or null if missing. The directory is indexed once and reused. - /// public static string? MiniPath(string? miniName, string miniDir = "textures/mini/") { if (string.IsNullOrEmpty(miniName)) return null; @@ -423,7 +348,6 @@ public class VehicleDatabase } } -/// Automatic consist definition (legacy ^x grouping). public class VehicleSet { [JsonPropertyName("uuid")] public string? Uuid { get; set; } @@ -432,9 +356,6 @@ public class VehicleSet [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/Domain/SceneryI18n.cs b/StarterNG/Domain/SceneryI18n.cs index 898dc80..f1e9aa4 100644 --- a/StarterNG/Domain/SceneryI18n.cs +++ b/StarterNG/Domain/SceneryI18n.cs @@ -6,10 +6,6 @@ using StarterNG.Classes; namespace StarterNG.Domain; -/// -/// Pascal TI18n / TrToken: scenery strings starting with @ -/// resolve against scenery/i18n/{id}_{lang}.json. -/// public static class SceneryI18n { private static readonly Dictionary Map = @@ -17,7 +13,6 @@ public static class SceneryI18n public static void Clear() => Map.Clear(); - /// Short UI language code (pl/en/cz/hu/ru). public static void LoadFor(Scenery scenery, string langCode = "en") { Map.Clear(); @@ -26,13 +21,11 @@ public static class SceneryI18n string lang = NormalizeLang(langCode); string i18nDir = Path.Combine(scnDir, "i18n"); - // Fallback first (en), then active language overlays — same as Pascal. TryLoad(Path.Combine(i18nDir, $"{id}_en.json")); if (!string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase)) TryLoad(Path.Combine(i18nDir, $"{id}_{lang}.json")); } - /// Translate @token / @nested.key; plain text passes through. public static string T(string? text) { if (string.IsNullOrWhiteSpace(text)) return text ?? ""; @@ -51,7 +44,7 @@ public static class SceneryI18n if (doc.RootElement.ValueKind == JsonValueKind.Object) Flatten("", doc.RootElement); } - catch { /* ignore bad i18n files */ } + catch { } } private static void Flatten(string prefix, JsonElement obj) diff --git a/StarterNG/Domain/TexRandomizer.cs b/StarterNG/Domain/TexRandomizer.cs index 7925fd6..926e20d 100644 --- a/StarterNG/Domain/TexRandomizer.cs +++ b/StarterNG/Domain/TexRandomizer.cs @@ -6,11 +6,6 @@ using StarterNG.Classes; namespace StarterNG.Domain; -/// -/// Skin shuffle for the depot consist — same idea as Pascal TTexRandomizer: -/// pick another texture of a similar mini/class (expanded by starter/reguly.txt), -/// optionally clamped by operator and revision year. -/// public sealed class TexRandomizer { private readonly VehicleDatabase _db; @@ -60,13 +55,10 @@ public sealed class TexRandomizer } catch { - // bad rules file — keep going with an empty stock + } } - /// - /// Reassigns skins on in place. Returns how many units changed. - /// public int Apply(IList consist, Func makeCar) { int changed = 0; @@ -82,7 +74,6 @@ public sealed class TexRandomizer string? mini = ResolveFilterMini(item, i, consist, current); if (string.IsNullOrEmpty(mini)) continue; - // Pascal skips units that have a revision filter on but no parseable year. int? revYear = null; if (RevisionTolerance > 0) { @@ -92,7 +83,7 @@ public sealed class TexRandomizer string? owner = null; string? cat = current?.ResolvedCategory; - // Keep operator match for non-EMU / non-railbus (Pascal skips EZT/SZYNOBUS). + if (current != null && cat is not ("z" or "a")) owner = current.Meta?.Operator; @@ -134,7 +125,6 @@ public sealed class TexRandomizer return string.IsNullOrEmpty(mini) ? null : mini; } - // No texture in DB — borrow from a neighbour like Pascal does for loose dynamics. if (index > 0) { var prev = _db.TextureForSkin(consist[index - 1].Cars[0].SkinFile); @@ -208,20 +198,16 @@ public sealed class TexRandomizer result.Add(t); } - // Always keep the current skin as a possible "no change" so Count < 2 means - // there is nothing else to pick from. if (current != null && !result.Contains(current)) result.Add(current); return result; } - /// Pulls a 4-digit year from dates like 12.05.2018 or 2018-05-12. public static int? ParseRevisionYear(string? revision) { if (string.IsNullOrWhiteSpace(revision)) return null; - // Pascal: Copy(Revision, 7, 4) on dd.mm.yyyy if (revision.Length >= 10 && revision[2] == '.' && revision[5] == '.' && int.TryParse(revision.AsSpan(6, 4), out int yDot) && yDot is >= 1900 and <= 2100) return yDot; diff --git a/StarterNG/Domain/TrainsetDisplay.cs b/StarterNG/Domain/TrainsetDisplay.cs index 1e0d345..50fb367 100644 --- a/StarterNG/Domain/TrainsetDisplay.cs +++ b/StarterNG/Domain/TrainsetDisplay.cs @@ -8,17 +8,12 @@ using StarterNG.Classes; namespace StarterNG.Domain; -/// Shared trainset list labels and status-bar formatting for Scenarios / Depot. public static class TrainsetDisplay { public static bool IsEmpty(Trainset trainset) => trainset.Vehicles.Count == 0 || trainset.Vehicles.All(v => string.IsNullOrWhiteSpace(v.Name) && string.IsNullOrWhiteSpace(v.SkinFile)); - /// - /// Composition caption like Pascal PrepareTrainsetDesc: first loco/EZT by - /// name, then collapse consecutive same-class wagons as Mini(N). - /// public static string? CompositionText(Trainset trainset) { if (IsEmpty(trainset)) @@ -78,10 +73,6 @@ public static class TrainsetDisplay string.IsNullOrWhiteSpace(v.SkinFile) ? "?" : System.IO.Path.GetFileNameWithoutExtension(v.SkinFile); - /// - /// Like Pascal SelectTrain: prefer the last car if it has cab crew - /// (push-pull), else the first staffed/passenger car, else the first car. - /// public static string? DefaultStartingVehicle(Trainset trainset) { if (trainset.Vehicles.Count == 0) @@ -96,10 +87,6 @@ public static class TrainsetDisplay return staffed?.Name ?? trainset.Vehicles[0].Name; } - /// - /// Length / mass / vehicle count / track (+ speed), with Pascal mass rules - /// (IncludeVehicleToMass + 200 t AllVehicles retry). - /// public static string FormatStats(Trainset? trainset, Func physicsFor, string lengthLabel, string massLabel, string vehiclesLabel, string trackLabel, Func? categoryOf = null, @@ -171,11 +158,6 @@ public static class TrainsetDisplay return p.LoadAccepted.Contains(loadType, StringComparison.OrdinalIgnoreCase); } - /// - /// Pascal UniqueVehicleName / UniqueVehiclesName at launch: - /// EZT/szynobus → rename every car; otherwise only the start vehicle. - /// Returns the (possibly new) name of the start vehicle. - /// public static string? UniquifyForLaunch(Trainset trainset, Scenery scenery, string? startName) { var used = CollectNames(scenery); @@ -220,7 +202,7 @@ public static class TrainsetDisplay var tex = GameData.Instance.Vehicles.TextureForSkin(v.SkinFile); if (tex != null) { - // Prefer texture_mini when it is more specific than the class mini (≈ MiniD). + if (!string.IsNullOrEmpty(tex.TextureMini) && !string.Equals(tex.TextureMini, tex.ResolvedClass, StringComparison.OrdinalIgnoreCase)) return SanitizeName(tex.TextureMini!); diff --git a/StarterNG/Infrastructure/LoadingScreen.cs b/StarterNG/Infrastructure/LoadingScreen.cs index 47e662c..107438d 100644 --- a/StarterNG/Infrastructure/LoadingScreen.cs +++ b/StarterNG/Infrastructure/LoadingScreen.cs @@ -8,10 +8,6 @@ using Avalonia.Platform; namespace StarterNG.Infrastructure; -/// -/// Pascal PrepareLoadingScreen: pick a logo and write textures/logo.bmp -/// for the simulator splash. -/// public static class LoadingScreen { public static void Prepare(string? logoKey, string? sceneryName = null) @@ -35,7 +31,7 @@ public static class LoadingScreen } catch { - // never block launch on a bad logo + } } diff --git a/StarterNG/Infrastructure/MonoFont.cs b/StarterNG/Infrastructure/MonoFont.cs index 9672f51..2cd43f4 100644 --- a/StarterNG/Infrastructure/MonoFont.cs +++ b/StarterNG/Infrastructure/MonoFont.cs @@ -5,23 +5,14 @@ using Avalonia.Media; namespace StarterNG.Infrastructure; -/// -/// A fixed-width font family that actually exists on this machine. -/// Avalonia treats the extra names in "Consolas, monospace" as glyph fallbacks, -/// not as family alternatives, and "monospace" is a fontconfig alias it never -/// resolves — so that stack silently renders in the proportional UI font -/// everywhere except Windows. The timetable and the fault/preview boxes align -/// their columns with spaces, so they need a real monospace family: probe the -/// installed ones once and keep the first match. -/// public static class MonoFont { private static readonly string[] Preferred = { - "Consolas", // Windows - "Menlo", // macOS + "Consolas", + "Menlo", "SF Mono", - "DejaVu Sans Mono", // most Linux distros + "DejaVu Sans Mono", "Liberation Mono", "Noto Sans Mono", "Ubuntu Mono", @@ -42,13 +33,12 @@ public static class MonoFont if (installed.Contains(name)) return new FontFamily(name); - // None of the usual suspects: take any family that calls itself Mono. string? anyMono = installed.FirstOrDefault( n => n.Contains("Mono", StringComparison.OrdinalIgnoreCase)); if (anyMono is not null) return new FontFamily(anyMono); } - catch { /* no font manager yet - fall back to the default family */ } + catch { } return FontFamily.Default; } diff --git a/StarterNG/MainWindow.axaml.cs b/StarterNG/MainWindow.axaml.cs index fa6d87f..0ea06b4 100644 --- a/StarterNG/MainWindow.axaml.cs +++ b/StarterNG/MainWindow.axaml.cs @@ -20,7 +20,7 @@ namespace StarterNG; public partial class MainWindow : Window { - // Height of the caption strip the OS buttons live in. + private const int TitleBarHeight = 32; private DateTime _openedUtc = DateTime.UtcNow; @@ -30,24 +30,15 @@ public partial class MainWindow : Window Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); InitializeComponent(); - // Extend the client area over the title bar so the logo + navigation sit - // on the top bar, while the OS caption buttons (min / max / close) stay in - // the top-right corner. The default chrome hints already keep the system - // caption buttons, so only the two supported hints are set here (this - // Avalonia build no longer exposes ExtendClientAreaChromeHints). ExtendClientAreaToDecorationsHint = true; ExtendClientAreaTitleBarHeightHint = TitleBarHeight; - // Windows draws the min / max / close buttons over the top of the extended - // client area, on top of the toolbar row. Push the bar down by the caption - // height so they get a strip of their own. if (OperatingSystem.IsWindows()) TopBar.Padding = new Thickness(0, TitleBarHeight, 0, 0); PopulateLangCombo(); App.Loc.LanguageChanged += OnLanguageChanged; - // Keep Start enabled/caption in sync with selection (Pascal actStartUpdate). AppState.Instance.Changed += UpdateStartButton; Opened += (_, _) => { @@ -57,20 +48,17 @@ public partial class MainWindow : Window Activated += (_, _) => CheckExternalSettings(); } - // Lets the user drag the window by the top caption strip. private void TitleBar_OnPointerPressed(object? sender, PointerPressedEventArgs e) { if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) BeginMoveDrag(e); } - // ── Navigation: show the page whose RadioButton was just checked ────────── private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e) { if (sender is not RadioButton { IsChecked: true } rb) return; - // Guard against the controls not being created yet during initial load. if (ScenariosView is null || DepotView is null || SettingsView is null) return; @@ -80,9 +68,6 @@ public partial class MainWindow : Window SettingsView.IsVisible = page == "settings"; } - // One entry per file in lang/. The entries are language names, which are not - // translated, so this runs once - never from OnLanguageChanged, where it - // would clear the combo from inside its own SelectionChanged and throw. private void PopulateLangCombo() { LangCombo.Items.Clear(); @@ -94,8 +79,6 @@ public partial class MainWindow : Window SelectActiveLanguage(); } - // Points the combo at the loaded language; a no-op when it already shows it, - // which is the case for a pick made here. private void SelectActiveLanguage() { var item = LangCombo.Items.OfType() @@ -112,18 +95,12 @@ public partial class MainWindow : Window if ((LangCombo.SelectedItem as ComboBoxItem)?.Tag is not string code) return; - // Filling the combo in selects the language that is already loaded, which - // lands here as well - comparing against the loaded one is what tells the - // two apart, and it keeps the handler idempotent. if (string.Equals(code, App.Loc.CurrentLangCode, StringComparison.OrdinalIgnoreCase)) return; App.ApplyLanguage(code); } - // Captions built in code (train stats, panel headers, combo items) are read - // from App.Loc once, so they have to be built again. Runs for a pick made - // here or in the settings tab, hence hanging off Loc rather off the combo. private void OnLanguageChanged() { SelectActiveLanguage(); @@ -204,8 +181,6 @@ public partial class MainWindow : Window } } - // Pascal actStartUpdate: Start needs a scenery and a trainset. A consist with - // nobody aboard still starts, after the confirmation in StartButton_OnClick. private void UpdateStartButton() { if (startButton is null) return; @@ -225,8 +200,7 @@ public partial class MainWindow : Window startButton.IsEnabled = false; return; } - // No driver and no passenger: still startable, but say so on the button - // and ask before launching. + bool staffed = HasStartableVehicle(trainset); startButton.Content = staffed ? App.Loc["Start"] : App.Loc["StartNoStaff"]; ToolTip.SetTip(startButton, staffed ? null : App.Loc["StartNoStaffConfirm"]); @@ -249,8 +223,6 @@ public partial class MainWindow : Window return trainset.Vehicles.Any(CanStart); } - // ── START: export the (possibly depot-modified) scenery and launch the sim. - // Global on the main window, like the original Starter's bottom START button. private async void StartButton_OnClick(object? sender, RoutedEventArgs e) { var scenery = AppState.Instance.CurrentScenery; @@ -260,8 +232,6 @@ public partial class MainWindow : Window if (trainset is null) return; - // Launching a consist with no driver and no passenger is legal (a scenery - // can be watched from the outside), so confirm instead of blocking it. if (!HasStartableVehicle(trainset) && !await MessageBox.Show(this, App.Loc["StartNoStaffConfirm"], App.Loc["StartNoStaffTitle"], MessageBoxButtons.YesNo)) @@ -277,7 +247,6 @@ public partial class MainWindow : Window }; } - // Pascal UniqueVehicleName / UniqueVehiclesName before writing $scn. string? vehicle = TrainsetDisplay.UniquifyForLaunch( trainset, scenery, AppState.Instance.StartingVehicleName); if (string.IsNullOrEmpty(vehicle)) @@ -286,11 +255,8 @@ public partial class MainWindow : Window LoadingScreen.Prepare(trainset.Logo, Path.GetFileNameWithoutExtension(scenery.Path)); - // Always inject the Weather-tab values (incl. start-time override), like the - // Pascal starter's ChangeConfig on every launch — not only after an edit. scenery.WeatherDirty = true; - // write scenery/$.scn with the replaced trainsets string dir = Path.GetDirectoryName(scenery.Path) ?? "scenery"; string exportName = "$" + Path.GetFileName(scenery.Path); string exportPath = Path.Combine(dir, exportName); @@ -300,16 +266,11 @@ public partial class MainWindow : Window } catch { - return; // couldn't write the scenery file + return; } - // make sure the latest settings are on disk before the sim reads them SettingsModel.Instance.CaptureAndSave(); - // launch the game: -s $.scn -v . Keep the handle so we can - // watch for the simulator exiting. UseShellExecute = false runs the binary - // directly with its arguments, which works the same on Windows and Linux - // (the executable is resolved to eu07.exe / eu07 via ResolveExecutable). Process? sim; try { @@ -324,29 +285,23 @@ public partial class MainWindow : Window } catch { - return; // executable not found / not configured yet + return; } - // honour the "close starter automatically" preference if (SettingsModel.Instance.AutoCloseStarter) { Close(); return; } - // Otherwise keep the starter running but hidden while the sim is in the - // foreground, and bring it back once the simulator process exits. Hide(); - // reduce ram usage while the sim runs GC.Collect(); GC.WaitForPendingFinalizers(); WatchSimulator(sim); } - // Pascal: RunInfo.Vehicle := Train.Vehicles[SelVehicle].Name after actStartUpdate - // requires headdriver/reardriver/passenger on that car. private static string? ResolveStartVehicle(Trainset? trainset) { if (trainset is null || trainset.Vehicles.Count == 0) @@ -368,9 +323,6 @@ public partial class MainWindow : Window ?? trainset.Vehicles.FirstOrDefault()?.Name; } - // Waits (off the UI thread) for the simulator to exit, then restores the - // starter window. Uses the launch handle when available, otherwise locates the - // process by name, and never leaves the UI hidden if it cannot be tracked. private void WatchSimulator(Process? sim) { if (sim is null) @@ -381,7 +333,7 @@ public partial class MainWindow : Window if (sim is null) { - // no handle to wait on - restore immediately rather than hide forever + Dispatcher.UIThread.Post(RestoreFromSimulator); return; } @@ -389,7 +341,7 @@ public partial class MainWindow : Window var watcher = new Thread(() => { try { sim.WaitForExit(); } - catch { /* already exited / access denied */ } + catch { } Dispatcher.UIThread.Post(RestoreFromSimulator); }) { @@ -399,7 +351,6 @@ public partial class MainWindow : Window watcher.Start(); } - // Pascal CheckSettingsFile on AppActivate (skip the first few seconds). private async void CheckExternalSettings() { if ((DateTime.UtcNow - _openedUtc).TotalSeconds < 3) @@ -421,7 +372,6 @@ public partial class MainWindow : Window SettingsModel.Instance.TouchConfigAge(); } - // Brings the starter back to the foreground after the simulator closes. private void RestoreFromSimulator() { Show(); diff --git a/StarterNG/Program.cs b/StarterNG/Program.cs index 1434544..a65de69 100644 --- a/StarterNG/Program.cs +++ b/StarterNG/Program.cs @@ -5,17 +5,14 @@ namespace StarterNG; class Program { - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. + [STAThread] public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); - // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() .LogToTrace(); -} \ No newline at end of file +} diff --git a/StarterNG/Services/LocalizationService.cs b/StarterNG/Services/LocalizationService.cs index da8ec71..a41924e 100644 --- a/StarterNG/Services/LocalizationService.cs +++ b/StarterNG/Services/LocalizationService.cs @@ -6,22 +6,8 @@ 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 { - /// Folder holding the language files, resolved as starter/lang next to the executable. public static string LangDirectory => Path.Combine(AppContext.BaseDirectory, "startercfg", "lang"); @@ -29,32 +15,17 @@ public class LocalizationService : INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; - /// - /// Raised after a language file is loaded. XAML bindings refresh themselves - /// through ; this is for captions that code - /// builds once and has to build again. - /// public event Action? LanguageChanged; - /// Display name of the active language (e.g. "English", "Polski"). public string CurrentLanguage { get; private set; } = "English"; - /// Short ISO code of the active language (e.g. "en", "pl"). public string CurrentLangCode { get; private set; } = "en"; - /// 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() { var list = new List(); @@ -71,17 +42,12 @@ public class LocalizationService : INotifyPropertyChanged } catch { - // Skip malformed files rather than crashing the launcher. + } } return list; } - /// - /// 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) { var languages = AvailableLanguages(); @@ -97,7 +63,6 @@ public class LocalizationService : INotifyPropertyChanged } } - // Fall back to English, then to whatever is available first. if (match is null) { foreach (var lang in languages) @@ -114,14 +79,12 @@ public class LocalizationService : INotifyPropertyChanged 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"; } - // "Item[]" is the indexer name bindings listen for; the empty name asks - // every binding on this object to re-read. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); LanguageChanged?.Invoke(); @@ -135,16 +98,12 @@ public class LocalizationService : INotifyPropertyChanged 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 + var value = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(key)) dict[key] = value; } diff --git a/StarterNG/SplashWindow.axaml.cs b/StarterNG/SplashWindow.axaml.cs index 028444e..7e8273d 100644 --- a/StarterNG/SplashWindow.axaml.cs +++ b/StarterNG/SplashWindow.axaml.cs @@ -10,7 +10,6 @@ public partial class SplashWindow : Window InitializeComponent(); } - /// Updates the bar and status line. Must be called on the UI thread. public void Report(LoadStatus status) { progressBar.Value = status.Fraction; diff --git a/StarterNG/Views/ConsistContextMenu.cs b/StarterNG/Views/ConsistContextMenu.cs index e226352..2e8fa34 100644 --- a/StarterNG/Views/ConsistContextMenu.cs +++ b/StarterNG/Views/ConsistContextMenu.cs @@ -11,18 +11,9 @@ using StarterNG.Classes; namespace StarterNG.Views; -/// -/// Right-click menu shared by the consist lists (the "Vehicles" list in Scenarios -/// and the "Map consists" list in the depot). Offers: save to warehouse, load from -/// warehouse (each entry deletable via an "✕"), copy/paste via the clipboard, and - -/// when removeAll is supplied - clearing the consist (Shift+Delete). -/// public static class ConsistContextMenu { - /// Control the menu/flyouts anchor to (for clipboard + popups). - /// Supplies the trainset to act on when the menu opens. - /// Replaces the target's vehicles and refreshes the view. - /// When non-null, adds a "remove all vehicles" entry. + public static ContextMenu Build( Control owner, Func getTarget, @@ -30,7 +21,7 @@ public static class ConsistContextMenu Action? removeAll = null) { var menu = new ContextMenu(); - // Rebuild the entries each time it opens so the warehouse list stays fresh. + menu.Opening += (_, _) => Populate(menu, owner, getTarget, applyVehicles, removeAll); return menu; } @@ -42,12 +33,10 @@ public static class ConsistContextMenu var target = getTarget(); bool hasVehicles = target is { Vehicles.Count: > 0 }; - // ── Save current consist to the warehouse ────────────────────────────── var save = new MenuItem { Header = App.Loc["PresetSave"], IsEnabled = hasVehicles }; save.Click += (_, _) => ShowSaveFlyout(owner, target); menu.Items.Add(save); - // ── Load from the warehouse (submenu; each entry deletable) ──────────── var load = new MenuItem { Header = App.Loc["PresetLoad"] }; var presets = PresetStore.All(); if (presets.Count == 0) @@ -82,7 +71,6 @@ public static class ConsistContextMenu menu.Items.Add(new Separator()); - // ── Clipboard (full trainset text) ───────────────────────────────────── var copy = new MenuItem { Header = App.Loc["ConsistCopy"], IsEnabled = hasVehicles }; copy.Click += async (_, _) => { @@ -103,7 +91,6 @@ public static class ConsistContextMenu }; menu.Items.Add(paste); - // ── Remove all vehicles (depot only) ─────────────────────────────────── if (removeAll != null) { menu.Items.Add(new Separator()); @@ -118,8 +105,6 @@ public static class ConsistContextMenu } } - // A warehouse entry: the name loads the preset on click, the trailing "✕" - // deletes it from the warehouse. private static MenuItem BuildPresetItem(ContextMenu menu, TrainsetPreset preset, Action> applyVehicles) { @@ -134,9 +119,9 @@ public static class ConsistContextMenu ToolTip.SetTip(del, App.Loc["PresetDelete"]); del.Click += (_, e) => { - e.Handled = true; // don't let the click also load the preset + e.Handled = true; PresetStore.Delete(preset.Name); - menu.Close(); // reopen to see the updated warehouse + menu.Close(); }; var label = new TextBlock { Text = preset.Name, VerticalAlignment = VerticalAlignment.Center }; @@ -156,7 +141,6 @@ public static class ConsistContextMenu return item; } - // Small popup to name the preset before saving it to the warehouse. private static void ShowSaveFlyout(Control owner, Trainset? target) { if (target is null || target.Vehicles.Count == 0) diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs index 5f780b0..4b1cff6 100644 --- a/StarterNG/Views/Depot.axaml.cs +++ b/StarterNG/Views/Depot.axaml.cs @@ -21,25 +21,19 @@ namespace StarterNG.Views; public partial class Depot : UserControl { - // Data is parsed once at startup (behind the splash) and shared here. + private readonly VehicleDatabase _db = GameData.Instance.Vehicles; private readonly List _sceneries = GameData.Instance.Sceneries; - // The consist being edited: a list of units (each unit = one or more cars). private readonly List _consist = new(); private ConsistItem? _selected; - // The scenery trainset this consist is bound to; edits are written back to it - // so the scenery is modified. Null when building an unattached consist. private Trainset? _editingTrainset; - // Shared, size-limited thumbnail cache (decoded down to display height). private readonly Dictionary _miniCache = new(); private int ConsistThumbHeight => StarterNG.Classes.Settings.Instance.LargeThumbnails ? 68 : 34; - // Display height of the mini under the vehicle list; the class combo uses - // the same size. private const int MiniPreviewHeight = 54; private const int CompactThumbHeight = 24; @@ -47,35 +41,25 @@ public partial class Depot : UserControl private readonly Cursor _hand = new(StandardCursorType.Hand); private int _nameCounter; - // shared RNG for the "random load / random amount" consist tools private static readonly Random _rng = new(); - // suppresses combo SelectionChanged handlers while we populate them private bool _suppress; - private bool _syncingCombos; // set while the combos mirror a pick + private bool _syncingCombos; - // debounces search input so we don't rebuild on every keystroke private DispatcherTimer? _searchTimer; - // the scenery whose consists are listed on the right (from the Scenarios tab) private Scenery? _listScenery; - // active database filters - private Func? _categoryFilter; // matches a category letter, null = none chosen - private string? _classFilter; // group mini (e.g. EU07), null = all in category + private Func? _categoryFilter; + private string? _classFilter; - // the vehicle currently highlighted in the flat list (drives the mini preview - // and the single Add button) private VehicleTexture? _browserSelected; - // drag-drop payload while a pointer drag is in progress private VehicleTexture? _dragTexture; private int _dragConsistIndex = -1; private const string DragVehicleFormat = "starterng/vehicle"; private const string DragConsistFormat = "starterng/consist-index"; - // Armed on press, started only after the pointer moves past DragThreshold. - // Starting DoDragDropAsync on every click freezes Avalonia (pointer capture). private PointerPressedEventArgs? _pendingDragPress; private Point _pendingDragOrigin; private VehicleTexture? _pendingDragTexture; @@ -84,17 +68,13 @@ public partial class Depot : UserControl private bool _dragSessionActive; private const double DragThreshold = 6; - // hard cap on how many entries the flat list shows at once private const int MaxListRows = 2000; - // card highlight brushes (theme-neutral overlays) private static readonly IBrush CardBorder = new SolidColorBrush(Color.Parse("#33888888")); private static readonly IBrush CardBorderSel = new SolidColorBrush(Color.Parse("#AA41C400")); private static readonly IBrush CardBgSel = new SolidColorBrush(Color.Parse("#2241C400")); private static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080")); - // Vehicle types, keyed by the JSON "category" letter. Powered/technical - // vehicles use lowercase letters; wagons use uppercase letters. private static readonly (string LocKey, Func Match)[] CategoryDefs = { ("CatElectricLoco", c => c == "e"), @@ -136,22 +116,18 @@ public partial class Depot : UserControl ("CatWagonsX", c => c == "X"), ("CatWagonsY", c => c == "Y"), ("CatWagonsZ", c => c == "Z"), - + ("CatOther", IsOtherCat), }; - // lowercase letters that already have a named type (so they aren't "other") private static readonly HashSet NamedLowerCats = new(StringComparer.Ordinal) { "a", "b", "c", "d", "e", "f", "h", "n", "o", "p", "r", "s", "t", "x", "z" }; private static bool IsOtherCat(string? c) => c is { Length: 1 } && char.IsLower(c[0]) && !NamedLowerCats.Contains(c); - // Car-type suffixes (longest first) used only for the consist unit label. private static readonly string[] CarSuffixes = { "sa", "sb", "ra", "rb", "s" }; - // Coupling-bit localisation keys in mask-bit order (bit i = value 1< { hideArchivalCheck.IsChecked = StarterNG.Classes.Settings.Instance.HideArchivalVehicles; InitClassComboTemplates(); - PopulateCategoryCombo(); // -> populates class combo -> builds browser - PopulateSceneryConsists(); // -> lists the current scenery's consists + PopulateCategoryCombo(); + PopulateSceneryConsists(); RebuildConsist(); }, DispatcherPriority.Background); - // Consist strip accepts drops from the browser and reorders via card drag. DragDrop.SetAllowDrop(consistDropTarget, true); consistDropTarget.AddHandler(DragDrop.DragOverEvent, Consist_OnDragOver); consistDropTarget.AddHandler(DragDrop.DropEvent, Consist_OnDrop); @@ -190,9 +164,6 @@ public partial class Depot : UserControl miniPreviewPanel.AddHandler(InputElement.PointerMovedEvent, PendingDrag_OnPointerMoved, handledEventsToo: true); miniPreviewPanel.AddHandler(InputElement.PointerReleasedEvent, PendingDrag_OnPointerReleased, handledEventsToo: true); - // Switching tabs only toggles IsVisible (the view stays in the visual - // tree), so refresh whenever the depot becomes visible - this picks up a - // scenery changed in the Scenarios tab in the meantime. AttachedToVisualTree += (_, _) => SyncFromSelection(); PropertyChanged += (_, e) => { @@ -203,7 +174,7 @@ public partial class Depot : UserControl private void SyncFromSelection() { - // pick up Settings-tab changes to the archival-vehicle filter + bool hideArchival = StarterNG.Classes.Settings.Instance.HideArchivalVehicles; if (hideArchivalCheck.IsChecked != hideArchival) { @@ -213,19 +184,15 @@ public partial class Depot : UserControl BuildList(); } - // refresh the on-map consists list to the scenery picked in the Scenarios tab PopulateSceneryConsists(); var trainset = AppState.Instance.CurrentTrainset; if (trainset != null && !ReferenceEquals(trainset, _editingTrainset)) LoadTrainset(trainset); else if (_editingTrainset != null) - RebuildConsist(); // pick up LargeThumbnails / filter-driven list changes + RebuildConsist(); } - // Loads a scenery trainset into the editor and binds edits back to it. - // Consecutive cars that belong to the same auto-consist set are regrouped - // into one locked unit so a scenery EN57 shows as a single vehicle. private void LoadTrainset(Trainset trainset) { _editingTrainset = trainset; @@ -234,7 +201,7 @@ public partial class Depot : UserControl var cars = trainset.Vehicles.Select(v => { var c = v.Clone(); - c.MiniName = _db.MiniForSkin(c.SkinFile) ?? c.MiniName; // mini from texture_mini + c.MiniName = _db.MiniForSkin(c.SkinFile) ?? c.MiniName; return c; }).ToList(); @@ -267,8 +234,7 @@ public partial class Depot : UserControl { Cars = group, Grouped = true, - // FirstOrDefault(predicate) defaults the enum to Headdriver(0) when - // every car is Nobody — that re-staffed decorative consists (#12). + Driver = UnitDriver(group) }); i = j; @@ -276,7 +242,6 @@ public partial class Depot : UserControl } } - // fallback: group consecutive cars sharing a unit number (e.g. EN57-001ra/s/rb) if (IsUnitCar(cars[i])) { string key = UnitKey(cars[i]); @@ -312,7 +277,6 @@ public partial class Depot : UserControl RebuildConsist(); } - // First staffed occupancy on the unit, or Nobody when the whole unit is unstaffed. private static eDriverType UnitDriver(IReadOnlyList group) => group.Select(c => c.DriverType) .Where(d => d != eDriverType.Nobody) @@ -325,10 +289,6 @@ public partial class Depot : UserControl AppState.Instance.StartingVehicleName = _selected.Cars[0].Name; } - // ----------------------------------------------------- series / unit helpers - - // Removes a trailing car-type suffix that directly follows a digit - // ("EN57-001ra" -> "EN57-001", "EN57ra" -> "EN57", "EP07-332" -> unchanged). private static string StripCarSuffix(string name) { foreach (string suf in CarSuffixes) @@ -345,23 +305,15 @@ public partial class Depot : UserControl private static string UnitKey(Dynamic d) => StripCarSuffix(Base(d.SkinFile)); - // True when the skin has a car-type suffix (so it is part of a multi-car unit). private static bool IsUnitCar(Dynamic d) => UnitKey(d) != Base(d.SkinFile); - // Vehicle class = the "mini" of the group the texture belongs to (e.g. EU07). - // Resolved once at load (see VehicleDatabase.BuildTextureIndex); read the cache - // here so search/filter never repeats the group lookup per keystroke. private static string ClassOf(VehicleTexture t) => t.ResolvedClass; - // Vehicle type = the group's "category" letter (also resolved once at load). private static string? CategoryOf(VehicleTexture t) => t.ResolvedCategory; - // ------------------------------------------------------------ category combo - private void PopulateCategoryCombo() { - // No "All" entry: nothing is selected by default, so the list starts empty - // (matching the original Starter's depot). + _suppress = true; categoryCombo.Items.Clear(); foreach (var (locKey, match) in CategoryDefs) @@ -384,9 +336,6 @@ public partial class Depot : UserControl BuildList(); } - // The dropdown shows the big stacked miniature, the closed combo a one-line - // row. Plain "=": SelectionBoxItemTemplate coerces to ItemTemplate while - // unset, so "??=" would never assign. private void InitClassComboTemplates() { classCombo.ItemTemplate = new FuncDataTemplate( @@ -395,9 +344,6 @@ public partial class Depot : UserControl (cls, _) => ClassComboContent(cls, large: false), false); } - // Fills the class combo for the current category filter; the caller owns the - // selection and _classFilter. Runs under _suppress. - // No "All" entry: an unselected class means "every class in the category". private void FillClassCombo() { classCombo.Items.Clear(); @@ -405,8 +351,6 @@ public partial class Depot : UserControl classCombo.Items.Add(cls); } - // Points both combos at the given category/class pair without firing their - // handlers. Returns false when they already matched. private bool ApplyFilters(string? category, string? cls) { var catItem = categoryCombo.Items.OfType() @@ -433,7 +377,6 @@ public partial class Depot : UserControl return true; } - // Every texture of a class shares its category. private string? CategoryOfClass(string cls) => _db.Textures.FirstOrDefault(t => string.Equals(ClassOf(t), cls, StringComparison.OrdinalIgnoreCase)) is { } t ? CategoryOf(t) : null; @@ -491,7 +434,6 @@ public partial class Depot : UserControl string? cls = classCombo.SelectedItem as string; _classFilter = cls; - // ApplyFilters refills this very combo, so it cannot run from here. if (cls != null && _categoryFilter == null && PostSyncing(() => { ApplyFilters(CategoryOfClass(cls), cls); BuildList(); })) return; @@ -499,9 +441,6 @@ public partial class Depot : UserControl BuildList(); } - // Hovering over the Type/Class combos and scrolling cycles the selection - // without having to open the dropdown (scroll up = previous, down = next). - // When the dropdown is open the wheel scrolls the list as usual. private void Combo_OnPointerWheel(object? sender, PointerWheelEventArgs e) { if (sender is not ComboBox combo || combo.IsDropDownOpen) @@ -512,9 +451,9 @@ public partial class Depot : UserControl return; int index = combo.SelectedIndex; - if (e.Delta.Y > 0) // scroll up -> previous + if (e.Delta.Y > 0) index = index <= 0 ? 0 : index - 1; - else if (e.Delta.Y < 0) // scroll down -> next + else if (e.Delta.Y < 0) index = index < 0 ? 0 : Math.Min(index + 1, count - 1); else return; @@ -523,15 +462,9 @@ public partial class Depot : UserControl e.Handled = true; } - // ----------------------------------------------------------- vehicle browser - - // Flat list of plain-text vehicle entries (no expanders, like the original - // Starter). Empty by default: an entry only appears once a category or class - // is chosen or a search is typed. Picking one drives the mini preview and the - // Add button. private void BuildList() { - // reset selection-dependent UI + _browserSelected = null; miniPreview.Source = null; addVehicleButton.IsEnabled = false; @@ -542,8 +475,6 @@ public partial class Depot : UserControl string search = searchBox.Text?.Trim() ?? ""; bool hasSearch = search.Length > 0; - // no filter and nothing searched -> keep the list empty (a class counts - // as a filter: it can be picked without a category) if (_categoryFilter == null && _classFilter == null && !hasSearch) return; @@ -572,7 +503,6 @@ public partial class Depot : UserControl }); } - // Selecting an entry shows its mini preview and enables the Add button. private void VehicleListBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { if (vehicleListBox.SelectedItem is ListBoxItem { Tag: VehicleTexture texture }) @@ -630,7 +560,7 @@ public partial class Depot : UserControl private void ShowTextureInfo(VehicleTexture texture) { textureInfoPanel.Children.Clear(); - // The rows are trimmed, so the tooltip carries the full text. + var full = new List(); void Row(string label, string? value) { @@ -659,29 +589,24 @@ public partial class Depot : UserControl ToolTip.SetTip(textureInfoPanel, string.Join("\n", full)); } - // Double-click an entry to replace the selected consist vehicle. private void VehicleListBox_OnDoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e) { if (_browserSelected != null) ReplaceSelected(_browserSelected); } - // The single Add button inserts the highlighted vehicle after the selected - // consist vehicle (or at the end when nothing is selected). private void AddVehicleButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { if (_browserSelected != null) AddTexture(_browserSelected); } - // Pascal actAddVehiclesCategory — every skin currently listed in the browser. private void AddAllCategoryButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { foreach (var t in CurrentBrowserTextures()) InsertTextureAt(t, _consist.Count); } - // Pascal actAddVehiclesMMD — first skin per distinct model/MMD. private void AddUniqueMmdButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -713,7 +638,6 @@ public partial class Depot : UserControl !string.Equals(ClassOf(t), _classFilter, StringComparison.OrdinalIgnoreCase)) return false; - // Hide subsequent multi-unit cars; adding the lead expands the whole set. if (_db.IsSetFollower(t)) return false; @@ -734,8 +658,6 @@ public partial class Depot : UserControl || C(t.Meta?.Vehicle) || C(t.Meta?.Operator) || C(ClassOf(t)); } - // Replaces the selected consist unit with this vehicle/unit (keeping its - // slot, crew and orientation). Falls back to appending if nothing is selected. private void ReplaceSelected(VehicleTexture texture) { if (_selected is null) @@ -767,8 +689,7 @@ public partial class Depot : UserControl private string BrowserLabel(VehicleTexture texture, IReadOnlyList? set) { - // When texture_mini is missing or only the class/category mini, use the - // skin basename so the list doesn't show the category name as the vehicle. + string name; if (!string.IsNullOrEmpty(texture.TextureMini) && !string.Equals(texture.TextureMini, texture.ResolvedClass, StringComparison.OrdinalIgnoreCase)) @@ -783,8 +704,6 @@ public partial class Depot : UserControl return name; } - // Clicking "Add" inserts after the selected vehicle (or appends). If the - // texture belongs to an auto-consist set, the whole set is one locked unit. private void AddTexture(VehicleTexture texture) { int at = _consist.Count; @@ -817,8 +736,6 @@ public partial class Depot : UserControl RebuildConsist(); } - // Like the Pascal MatchOccupancy: only powered units get a driver, and only - // when the consist has no crew yet (or the unit is placed at the front). private void MatchOccupancy(ConsistItem item, int position) { item.Driver = eDriverType.Nobody; @@ -839,20 +756,14 @@ public partial class Depot : UserControl private static bool IsPoweredCategory(string? c) => c is "e" or "s" or "p" or "z" or "a"; - // Every caption here is read from App.Loc when it is built, so a language - // switch has to build them again. public void RebuildAfterLanguageChange() { - PopulateCategoryCombo(); // -> class combo -> browser list + PopulateCategoryCombo(); PopulateSceneryConsists(); - RebuildConsist(); // -> train stats + RebuildConsist(); UpdateDetails(); } - // ------------------------------------------------------------- scenery combos - - // Lists the consists of the scenery currently selected in the Scenarios tab, - // so they can be picked and edited here. private void PopulateSceneryConsists() { _suppress = true; @@ -878,7 +789,7 @@ public partial class Depot : UserControl Content = ConsistListContent(trainset), Tag = trainset }; - // right-click: warehouse save/load, clipboard copy/paste, clear-all + var ts = trainset; entry.ContextMenu = ConsistContextMenu.Build( entry, @@ -894,7 +805,6 @@ public partial class Depot : UserControl _suppress = false; } - // Selecting a consist on the map loads it into the editor below. private void SceneryConsistList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { if (_suppress) return; @@ -911,8 +821,6 @@ public partial class Depot : UserControl !string.IsNullOrEmpty(trainset.Description) && trainset.Description.TrimStart().StartsWith("-", StringComparison.Ordinal); - // Shift+Delete clears the selected scenery consist (same as the context-menu - // "remove all vehicles" entry). private void SceneryConsistList_OnKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Delete && e.KeyModifiers.HasFlag(KeyModifiers.Shift) && @@ -924,9 +832,6 @@ public partial class Depot : UserControl } } - // Replaces a scenery consist's vehicles (from a warehouse preset or a pasted - // clipboard consist), keeping the consist's map slot (name/track/offset), then - // loads it into the editor and refreshes the list. private void ApplyConsist(Trainset target, List vehicles) { target.Vehicles = vehicles; @@ -936,7 +841,6 @@ public partial class Depot : UserControl PopulateSceneryConsists(); } - // Clears every vehicle from a scenery consist. private void RemoveAllVehicles(Trainset target) { target.Vehicles = new List(); @@ -946,7 +850,6 @@ public partial class Depot : UserControl PopulateSceneryConsists(); } - // List caption for a scenery consist — grows with the list width (ellipsis). private static Control ConsistListContent(Trainset trainset) => new TextBlock { @@ -972,8 +875,6 @@ public partial class Depot : UserControl } } - // ------------------------------------------------------------ consist edits - private Dynamic MakeDynamic(VehicleTexture texture) { string skin = Base(texture.Skinfile); @@ -987,8 +888,8 @@ public partial class Depot : UserControl MmdFile = string.IsNullOrEmpty(texture.Model) ? skin : texture.Model!, Offset = 0f, DriverType = eDriverType.Nobody, - Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe }, // 3 - HasVelocity = true, // emit a canonical "0" velocity in the trainset entry + Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe }, + HasVelocity = true, MiniName = _db.ResolveMiniName(texture) }; } @@ -1079,10 +980,9 @@ public partial class Depot : UserControl if (ReferenceEquals(_selected, item)) SyncStartingVehicle(); RebuildConsist(); - AppState.Instance.NotifyChanged(); // crew change may enable/disable Start + AppState.Instance.NotifyChanged(); } - // Breaks a grouped unit into individual single-car items. private void SplitItem(ConsistItem item) { int i = _consist.IndexOf(item); @@ -1103,8 +1003,6 @@ public partial class Depot : UserControl RebuildConsist(); } - // ----------------------------------------------------------- consist render - private void RebuildConsist() { consistStack.Children.Clear(); @@ -1112,8 +1010,7 @@ public partial class Depot : UserControl { var item = _consist[i]; consistStack.Children.Add(BuildCard(item, i)); - // Coupler between units, plus a trailing coupler after the last unit - // so end-of-train coupling / lights can be set (#10). + if (i < _consist.Count - 1) consistStack.Children.Add(BuildCoupler(item)); else @@ -1126,9 +1023,6 @@ public partial class Depot : UserControl WriteBackToScenery(); } - // Flattens the edited consist back onto the bound scenery trainset so the - // change is reflected on the scenery. Driver goes on each unit's lead car; - // a flipped unit writes its cars in reverse order. private void WriteBackToScenery() { if (_editingTrainset is null) @@ -1148,9 +1042,6 @@ public partial class Depot : UserControl RefreshConsistListLabels(); } - // Resolves the .fiz physics for a consist car: the .fiz is named after the - // model, so try the database model first, then the scenery mmd token, then the - // skin file name. private Physics? PhysicsFor(Dynamic car) { string? dbModel = _db.TextureForSkin(car.SkinFile)?.Model; @@ -1159,10 +1050,6 @@ public partial class Depot : UserControl ?? Physics.For(car.DataFolder, car.SkinFile); } - // Recomputes every coupling from the vehicles' .fiz AllowedFlag, like the - // original Starter's AutoCoupler: the coupling is the bits common to the - // facing ends, minus the multiple-unit (control) bit when the control types - // differ. Called when a vehicle is added or replaced. private void AutoConnectAll() { var flat = new List<(Dynamic car, bool flipped)>(); @@ -1181,10 +1068,9 @@ public partial class Depot : UserControl var lp = PhysicsFor(left); var rp = PhysicsFor(right); - // GetMaxCoupler: the end facing the neighbour (swapped when flipped) int leftMax = lp == null ? 3 : (lf ? lp.AllowedFlagA : lp.AllowedFlagB); int rightMax = rp == null ? 3 : (rf ? rp.AllowedFlagB : rp.AllowedFlagA); - int common = leftMax & rightMax; // CommonCoupler = shared flag bits + int common = leftMax & rightMax; string lct = lp == null ? "" : (lf ? lp.ControlTypeA : lp.ControlTypeB); string rct = rp == null ? "" : (rf ? rp.ControlTypeB : rp.ControlTypeA); @@ -1196,7 +1082,6 @@ public partial class Depot : UserControl } } - // Train totals — same Pascal mass rules as Scenarios (IncludeVehicleToMass + 200 t retry). private void UpdateTrainStats() { if (_editingTrainset is null) @@ -1315,16 +1200,13 @@ public partial class Depot : UserControl }; card.PointerPressed += (_, e) => { - // left button only - right-click is reserved for the context popup + if (!e.GetCurrentPoint(card).Properties.IsLeftButtonPressed) return; - // Buttons on the card (split / remove) handle their own clicks. + if (e.Source is Control src && src.GetVisualAncestors().OfType