From 905d795f545a0127f57d4645e26958f3ef1a8523 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 22 Aug 2026 23:15:18 +0200 Subject: [PATCH 01/63] Move new domain/infra modules out of Classes into Domain/ and Infrastructure/ Consist/scenery/vehicle logic (TrainsetDisplay, TexRandomizer, SceneryI18n) goes under Domain/; the loading-screen bitmap writer under Infrastructure/, instead of the flat Classes/ bucket. --- StarterNG/Domain/SceneryI18n.cs | 92 ++++++++ StarterNG/Domain/TexRandomizer.cs | 241 +++++++++++++++++++ StarterNG/Domain/TrainsetDisplay.cs | 272 ++++++++++++++++++++++ StarterNG/Infrastructure/LoadingScreen.cs | 127 ++++++++++ 4 files changed, 732 insertions(+) create mode 100644 StarterNG/Domain/SceneryI18n.cs create mode 100644 StarterNG/Domain/TexRandomizer.cs create mode 100644 StarterNG/Domain/TrainsetDisplay.cs create mode 100644 StarterNG/Infrastructure/LoadingScreen.cs diff --git a/StarterNG/Domain/SceneryI18n.cs b/StarterNG/Domain/SceneryI18n.cs new file mode 100644 index 0000000..898dc80 --- /dev/null +++ b/StarterNG/Domain/SceneryI18n.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +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 = + new(StringComparer.OrdinalIgnoreCase); + + 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(); + string scnDir = Path.GetDirectoryName(Path.GetFullPath(scenery.Path)) ?? "scenery"; + string id = Path.GetFileNameWithoutExtension(scenery.Path); + string lang = NormalizeLang(langCode); + string i18nDir = Path.Combine(scnDir, "i18n"); + + // 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 ?? ""; + string t = text.TrimStart(); + if (t.Length == 0 || t[0] != '@') return text; + string key = t[1..].Trim(); + return Map.TryGetValue(key, out var v) ? v : text; + } + + private static void TryLoad(string path) + { + if (!File.Exists(path)) return; + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(path)); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + Flatten("", doc.RootElement); + } + catch { /* ignore bad i18n files */ } + } + + private static void Flatten(string prefix, JsonElement obj) + { + foreach (var prop in obj.EnumerateObject()) + { + string key = string.IsNullOrEmpty(prefix) ? prop.Name : prefix + "." + prop.Name; + switch (prop.Value.ValueKind) + { + case JsonValueKind.String: + Map[key] = prop.Value.GetString() ?? ""; + break; + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + Map[key] = prop.Value.ToString(); + break; + case JsonValueKind.Object: + Flatten(key, prop.Value); + break; + } + } + } + + private static string NormalizeLang(string language) + { + string l = language.Trim().ToLowerInvariant(); + return l switch + { + "polski" or "pl" => "pl", + "english" or "en" => "en", + "čeština" or "cesky" or "czech" or "cs" or "cz" => "cz", + "magyar" or "hungarian" or "hu" => "hu", + "русский" or "russian" or "ru" => "ru", + _ => l.Length >= 2 ? l[..2] : "en" + }; + } +} diff --git a/StarterNG/Domain/TexRandomizer.cs b/StarterNG/Domain/TexRandomizer.cs new file mode 100644 index 0000000..7925fd6 --- /dev/null +++ b/StarterNG/Domain/TexRandomizer.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +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; + private readonly Random _rng; + private readonly List _stock = new(); + + public int RevisionTolerance { get; set; } = -1; + public int RevYear { get; set; } = -1; + public bool WithoutArchival { get; set; } = true; + + public static string RulesPath + { + get + { + string cwd = Directory.GetCurrentDirectory(); + string primary = Path.Combine(cwd, "starter", "reguly.txt"); + if (File.Exists(primary)) return primary; + string bundled = Path.Combine(AppContext.BaseDirectory, "startercfg", "reguly.txt"); + if (File.Exists(bundled)) return bundled; + return primary; + } + } + + public TexRandomizer(VehicleDatabase db, Random? rng = null) + { + _db = db; + _rng = rng ?? Random.Shared; + LoadRules(); + } + + public void LoadRules() + { + _stock.Clear(); + string path = RulesPath; + if (!File.Exists(path)) return; + + try + { + foreach (string line in File.ReadAllLines(path)) + { + string trimmed = line.Trim(); + if (trimmed.Length == 0 || trimmed[0] == '#') continue; + string[] parts = trimmed.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length > 0) + _stock.Add(parts); + } + } + 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; + for (int i = 0; i < consist.Count; i++) + { + var item = consist[i]; + if (item.Cars.Count == 0) continue; + + var current = _db.TextureForSkin(item.Cars[0].SkinFile); + if (current != null && _db.IsSetFollower(current)) + continue; + + 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) + { + revYear = RevYear > 0 ? RevYear : ParseRevisionYear(current?.Meta?.RevisionDate); + if (revYear is null) continue; + } + + 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; + + var aliases = ExpandMini(mini); + var pool = FindCandidates(aliases, owner, revYear, current); + if (pool.Count < 2) continue; + + var pick = pool[_rng.Next(pool.Count)]; + if (ReferenceEquals(pick, current) || + string.Equals(pick.Skinfile, current?.Skinfile, StringComparison.OrdinalIgnoreCase)) + continue; + + var set = _db.ResolveSet(pick); + var unit = set ?? new List { pick }; + consist[i] = new ConsistItem + { + Cars = unit.Select(makeCar).ToList(), + Grouped = unit.Count > 1, + Driver = item.Driver, + Flipped = item.Flipped + }; + changed++; + } + return changed; + } + + private string? ResolveFilterMini(ConsistItem item, int index, IList consist, VehicleTexture? current) + { + if (current != null) + { + string skin = current.Skinfile ?? ""; + if (skin.Contains("112E", StringComparison.OrdinalIgnoreCase)) + return null; + if (current.Wreck) return null; + + string mini = current.ResolvedClass; + if (string.IsNullOrEmpty(mini)) + mini = _db.ResolveMiniName(current) ?? ""; + 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); + if (prev != null && !string.IsNullOrEmpty(prev.ResolvedClass)) + return prev.ResolvedClass; + } + if (index + 1 < consist.Count) + { + var next = _db.TextureForSkin(consist[index + 1].Cars[0].SkinFile); + if (next != null && !string.IsNullOrEmpty(next.ResolvedClass)) + return next.ResolvedClass; + } + + bool driven = item.Driver is eDriverType.Headdriver or eDriverType.Reardriver; + return driven ? "st44" : "A_112A"; + } + + private List ExpandMini(string mini) + { + var list = new List { mini }; + string upper = mini.ToUpperInvariant(); + foreach (var stock in _stock) + { + bool hit = stock.Any(s => s.ToUpperInvariant().Contains(upper) || upper.Contains(s.ToUpperInvariant())); + if (!hit) continue; + foreach (string s in stock) + { + if (!list.Contains(s, StringComparer.OrdinalIgnoreCase)) + list.Add(s); + } + break; + } + return list; + } + + private List FindCandidates( + IReadOnlyList minis, + string? owner, + int? revYear, + VehicleTexture? current) + { + var result = new List(); + foreach (var t in _db.Textures) + { + if (t.Wreck) continue; + if (WithoutArchival && t.ResolvedArchived) continue; + if (_db.IsSetFollower(t)) continue; + + string candidateMini = t.ResolvedClass; + if (string.IsNullOrEmpty(candidateMini)) + candidateMini = _db.ResolveMiniName(t) ?? ""; + if (string.IsNullOrEmpty(candidateMini)) continue; + + bool miniOk = minis.Any(m => + string.Equals(m, candidateMini, StringComparison.OrdinalIgnoreCase) || + string.Equals(m, t.TextureMini, StringComparison.OrdinalIgnoreCase) || + string.Equals(m, t.MiniRef, StringComparison.OrdinalIgnoreCase)); + if (!miniOk) continue; + + if (!string.IsNullOrEmpty(owner) && + !string.Equals(owner, t.Meta?.Operator, StringComparison.OrdinalIgnoreCase)) + continue; + + if (revYear is int year && RevisionTolerance > 0) + { + int? y = ParseRevisionYear(t.Meta?.RevisionDate); + if (y is null || Math.Abs(y.Value - year) > RevisionTolerance) + continue; + } + + 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; + + for (int i = 0; i + 3 < revision.Length; i++) + { + if (char.IsDigit(revision[i]) && char.IsDigit(revision[i + 1]) && + char.IsDigit(revision[i + 2]) && char.IsDigit(revision[i + 3])) + { + int y = (revision[i] - '0') * 1000 + (revision[i + 1] - '0') * 100 + + (revision[i + 2] - '0') * 10 + (revision[i + 3] - '0'); + if (y is >= 1900 and <= 2100) return y; + } + } + return null; + } +} diff --git a/StarterNG/Domain/TrainsetDisplay.cs b/StarterNG/Domain/TrainsetDisplay.cs new file mode 100644 index 0000000..1e0d345 --- /dev/null +++ b/StarterNG/Domain/TrainsetDisplay.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +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)) + return null; + + var db = GameData.Instance.Vehicles; + var sb = new StringBuilder(); + string? prevKey = null; + int run = 0; + + void Flush() + { + if (prevKey is null || run == 0) return; + if (sb.Length > 0) sb.Append(" + "); + sb.Append(run > 1 ? $"{prevKey}({run})" : prevKey); + prevKey = null; + run = 0; + } + + for (int i = 0; i < trainset.Vehicles.Count; i++) + { + var v = trainset.Vehicles[i]; + var tex = db.TextureForSkin(v.SkinFile); + string cat = tex?.ResolvedCategory ?? ""; + bool powered = cat is "e" or "s" or "p" or "z" or "a"; + + if (i == 0 && powered) + { + string head = !string.IsNullOrWhiteSpace(v.Name) ? v.Name! : + (!string.IsNullOrEmpty(tex?.ResolvedClass) ? tex!.ResolvedClass : BaseSkin(v)); + sb.Append(head); + prevKey = null; + run = 0; + continue; + } + + string key = !string.IsNullOrEmpty(tex?.ResolvedClass) ? tex!.ResolvedClass + : (!string.IsNullOrWhiteSpace(v.Name) ? v.Name! : BaseSkin(v)); + + if (string.Equals(key, prevKey, StringComparison.OrdinalIgnoreCase)) + { + run++; + } + else + { + Flush(); + prevKey = key; + run = 1; + } + } + Flush(); + return sb.Length > 0 ? sb.ToString() : string.Join(" + ", trainset.Vehicles.Select(v => + !string.IsNullOrWhiteSpace(v.Name) ? v.Name! : BaseSkin(v))); + } + + private static string BaseSkin(Dynamic v) => + 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) + return null; + + var last = trainset.Vehicles[^1]; + if (last.DriverType is eDriverType.Headdriver or eDriverType.Reardriver) + return last.Name; + + var staffed = trainset.Vehicles.FirstOrDefault(v => + v.DriverType is eDriverType.Headdriver or eDriverType.Reardriver or eDriverType.Passenger); + 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, + Func? loadWeightKg = null) + { + if (trainset is null) + return ""; + + var (lengthM, massKg, loadKg) = RecalcParams(trainset, physicsFor, categoryOf, loadWeightKg, allVehicles: false); + if (massKg < 200_000) + (lengthM, massKg, loadKg) = RecalcParams(trainset, physicsFor, categoryOf, loadWeightKg, allVehicles: true); + + var inv = CultureInfo.InvariantCulture; + string track = trainset.Track ?? ""; + if (trainset.Velocity > 0) + track = $"{track} ({trainset.Velocity.ToString("0.#", inv)} km/h)"; + + string massPart = $"{massLabel}: {(massKg / 1000.0).ToString("0.#", inv)} t"; + if (loadKg > 0) + massPart += $" / {((massKg + loadKg) / 1000.0).ToString("0.#", inv)} t"; + + return $"{lengthLabel}: {lengthM.ToString("0.#", inv)} m · " + + $"{massPart} · " + + $"{vehiclesLabel}: {trainset.Vehicles.Count} · " + + $"{trackLabel}: {track}"; + } + + private static (double lengthM, double massKg, double loadKg) RecalcParams( + Trainset trainset, + Func physicsFor, + Func? categoryOf, + Func? loadWeightKg, + bool allVehicles) + { + double lengthM = 0, massKg = 0, loadKg = 0; + foreach (var car in trainset.Vehicles) + { + var p = physicsFor(car); + if (p != null) + lengthM += p.Length; + + if (!IncludeVehicleToMass(car, p, categoryOf?.Invoke(car), allVehicles)) + continue; + + if (p != null) + massKg += p.Mass; + + if (loadWeightKg != null && + car.LoadCount > 0 && + !string.IsNullOrEmpty(car.LoadType) && + LoadAccepted(p, car.LoadType)) + loadKg += (double)car.LoadCount * loadWeightKg(car.LoadType); + } + return (lengthM, massKg, loadKg); + } + + private static bool IncludeVehicleToMass(Dynamic car, Physics? p, string? category, bool allVehicles) + { + if (p == null) return false; + if (allVehicles) return true; + if (category is "z" or "a") return true; + return car.DriverType is eDriverType.Passenger or eDriverType.Nobody; + } + + private static bool LoadAccepted(Physics? p, string loadType) + { + if (p == null || string.IsNullOrWhiteSpace(p.LoadAccepted)) + return true; + 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); + var db = GameData.Instance.Vehicles; + + bool mu = trainset.Vehicles.Any(v => + { + string? cat = db.TextureForSkin(v.SkinFile)?.ResolvedCategory; + return cat is "z" or "a"; + }); + + Dynamic? target = null; + if (!string.IsNullOrEmpty(startName)) + target = trainset.Vehicles.FirstOrDefault(v => + string.Equals(v.Name, startName, StringComparison.OrdinalIgnoreCase)); + target ??= trainset.Vehicles.FirstOrDefault(v => + v.DriverType is eDriverType.Headdriver or eDriverType.Reardriver or eDriverType.Passenger); + target ??= trainset.Vehicles.FirstOrDefault(); + + if (mu) + { + foreach (var v in trainset.Vehicles) + { + if (!string.IsNullOrEmpty(v.Name)) + used.Remove(v.Name!); + v.Name = EnsureUnique(PreferredBaseName(v), used); + } + return target?.Name; + } + + if (target != null) + { + if (!string.IsNullOrEmpty(target.Name)) + used.Remove(target.Name!); + target.Name = EnsureUnique(PreferredBaseName(target), used); + } + return target?.Name; + } + + public static string PreferredBaseName(Dynamic v) + { + 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!); + if (!string.IsNullOrEmpty(tex.Skinfile)) + return SanitizeName(Path.GetFileNameWithoutExtension(tex.Skinfile)); + } + if (!string.IsNullOrWhiteSpace(v.SkinFile)) + return SanitizeName(Path.GetFileNameWithoutExtension(v.SkinFile)); + return SanitizeName(string.IsNullOrWhiteSpace(v.Name) ? "vehicle" : v.Name!); + } + + public static string EnsureUnique(string baseName, HashSet used) + { + baseName = SanitizeName(baseName); + if (!used.Contains(baseName)) + { + used.Add(baseName); + return baseName; + } + int n = 1; + string candidate; + do + candidate = $"{baseName}_{n++}"; + while (used.Contains(candidate)); + used.Add(candidate); + return candidate; + } + + public static HashSet CollectNames(Scenery scenery, Trainset? except = null) + { + var used = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string n in scenery.LooseVehicleNames) + used.Add(n); + foreach (var d in scenery.LooseVehicles) + if (!string.IsNullOrEmpty(d.Name)) + used.Add(d.Name!); + foreach (var ts in scenery.Trainsets) + { + if (ReferenceEquals(ts, except)) continue; + foreach (var v in ts.Vehicles) + if (!string.IsNullOrEmpty(v.Name)) + used.Add(v.Name!); + } + return used; + } + + private static string SanitizeName(string name) => + string.IsNullOrWhiteSpace(name) ? "vehicle" : name.Replace(' ', '_'); +} diff --git a/StarterNG/Infrastructure/LoadingScreen.cs b/StarterNG/Infrastructure/LoadingScreen.cs new file mode 100644 index 0000000..47e662c --- /dev/null +++ b/StarterNG/Infrastructure/LoadingScreen.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Media.Imaging; +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) + { + try + { + string? src = ResolveSource(logoKey) ?? ResolveSource(sceneryName) ?? PickRandom(); + if (src is null) return; + + string destDir = Path.Combine(Directory.GetCurrentDirectory(), "textures"); + Directory.CreateDirectory(destDir); + string dest = Path.Combine(destDir, "logo.bmp"); + + if (src.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)) + { + File.Copy(src, dest, overwrite: true); + return; + } + + WriteBmp(src, dest); + } + catch + { + // never block launch on a bad logo + } + } + + private static string? ResolveSource(string? key) + { + if (string.IsNullOrWhiteSpace(key)) return null; + key = Path.GetFileNameWithoutExtension(key.Trim()); + string dir = Path.Combine(Directory.GetCurrentDirectory(), "textures", "logo"); + foreach (string ext in new[] { ".bmp", ".jpg", ".jpeg", ".png" }) + { + string path = Path.Combine(dir, key + ext); + if (File.Exists(path)) return path; + } + return null; + } + + private static string? PickRandom() + { + string dir = Path.Combine(Directory.GetCurrentDirectory(), "textures", "logo"); + if (!Directory.Exists(dir)) return null; + var files = Directory.GetFiles(dir, "logo*.*") + .Where(f => + { + string e = Path.GetExtension(f).ToLowerInvariant(); + return e is ".bmp" or ".jpg" or ".jpeg" or ".png"; + }) + .ToArray(); + if (files.Length == 0) return null; + return files[Random.Shared.Next(files.Length)]; + } + + private static void WriteBmp(string sourcePath, string destPath) + { + using var src = new Bitmap(sourcePath); + int w = src.PixelSize.Width; + int h = src.PixelSize.Height; + if (w <= 0 || h <= 0) return; + + var wb = new WriteableBitmap(src.PixelSize, src.Dpi, PixelFormat.Bgra8888, AlphaFormat.Opaque); + using var fb = wb.Lock(); + src.CopyPixels(new PixelRect(0, 0, w, h), fb.Address, fb.RowBytes * h, fb.RowBytes); + + int srcStride = fb.RowBytes; + var pixels = new byte[srcStride * h]; + Marshal.Copy(fb.Address, pixels, 0, pixels.Length); + + int rowStride = ((w * 3 + 3) / 4) * 4; + int pixelBytes = rowStride * h; + var file = new byte[54 + pixelBytes]; + + file[0] = (byte)'B'; file[1] = (byte)'M'; + WriteInt32(file, 2, file.Length); + WriteInt32(file, 10, 54); + WriteInt32(file, 14, 40); + WriteInt32(file, 18, w); + WriteInt32(file, 22, h); + WriteInt16(file, 26, 1); + WriteInt16(file, 28, 24); + WriteInt32(file, 34, pixelBytes); + + for (int y = 0; y < h; y++) + { + int destY = h - 1 - y; + int destOff = 54 + destY * rowStride; + int srcOff = y * srcStride; + for (int x = 0; x < w; x++) + { + int si = srcOff + x * 4; + int di = destOff + x * 3; + file[di] = pixels[si]; + file[di + 1] = pixels[si + 1]; + file[di + 2] = pixels[si + 2]; + } + } + + File.WriteAllBytes(destPath, file); + } + + private static void WriteInt32(byte[] b, int o, int v) + { + b[o] = (byte)v; b[o + 1] = (byte)(v >> 8); + b[o + 2] = (byte)(v >> 16); b[o + 3] = (byte)(v >> 24); + } + + private static void WriteInt16(byte[] b, int o, int v) + { + b[o] = (byte)v; b[o + 1] = (byte)(v >> 8); + } +} From 2298d6c803e9379ebd7555618a793bea5f27e52c Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 22 Aug 2026 23:15:18 +0200 Subject: [PATCH 02/63] Add scenery i18n and Czech/Hungarian/Russian translations Scenery @token strings resolve via SceneryI18n; add cz/hu/ru language files and the new UI strings for the depot and scenarios features. --- StarterNG/startercfg/lang/cz.xml | 424 +++++++++++++++++++++++++++++++ StarterNG/startercfg/lang/en.xml | 56 ++++ StarterNG/startercfg/lang/hu.xml | 424 +++++++++++++++++++++++++++++++ StarterNG/startercfg/lang/pl.xml | 56 ++++ StarterNG/startercfg/lang/ru.xml | 424 +++++++++++++++++++++++++++++++ 5 files changed, 1384 insertions(+) create mode 100644 StarterNG/startercfg/lang/cz.xml create mode 100644 StarterNG/startercfg/lang/hu.xml create mode 100644 StarterNG/startercfg/lang/ru.xml diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml new file mode 100644 index 0000000..50e8279 --- /dev/null +++ b/StarterNG/startercfg/lang/cz.xml @@ -0,0 +1,424 @@ + + + + + + + Scénáře + Depo + Nastavení + Controls + Nápověda + Wiki MaSzyna + Readme + Řadit sklad + Podle názvu + Podle koleje + Importovat magazyn.ini + Verze: + + + Scénáře + Vozidla + AI vehicles + Drivable only + Archival sceneries + Scenario description + Scenery description + Description + Počasí + Jízdní řád + No weather data in the scenery. + No timetable for the selected consist. + Time + Now + Day of year + Sky + Fog / visibility + Season + Temperature + Visibility (fog) + Overcast + Today + Spring + Summer + Autumn + Winter + Clear + Fair + Light clouds + Moderate clouds + Heavy clouds + Cloudy + Storm + Weather changes are written to the scenery on launch. + Souprava + Spustit + Select a scenery + Select a consist + No crew / passenger (hold Ctrl to start) + Restore weather + Reload + Open folder + Optional elements + Open texture folder + Copy name + Copy coupler from previous + Coolant at ambient temperature + Texture + Operator + Depot + Revision + Author + Photos + Open latest simulation log + + + Vehicle database + Rolling stock database + Stock database… + Hledat... + Load consist from scenery + New consist + Selected vehicle + Vehicle + with crew + reversed + No vehicles found in databases/vehicles/. + Add vehicles from the database on the left, or load an existing consist from a scenery file. + Move left + Move right + Reverse vehicle + Remove from consist + Change crew (cab 1 / cab 2 / passenger / none) + Scenery + Category + Class + All + Diesel + Electric + EMU + DMU + Carriages A + Carriages B + Electric locomotive + Diesel locomotive + Steam locomotive + Railbus + Draisine + Work vehicles + Prototype + Tram + Car + Bus + Truck + People + Animals + Wagons A + Wagons B + Wagons C + Wagons D + Wagons E + Wagons F + Wagons G + Wagons H + Wagons I + Wagons J + Wagons K + Wagons L + Wagons M + Wagons N + Wagons O + Wagons P + Wagons Q + Wagons R + Wagons S + Wagons T + Wagons U + Wagons V + Wagons W + Wagons X + Wagons Y + Wagons Z + Other + Brakes + Loads + Coupling + Consists on the scenery + Select a scenery in the Scenarios tab. + Brake mode + Load setting + Brake switch + — + + + Mechanical + Pneumatic 5 atm (brakes) + Multiple-unit control + High voltage + Gangway between vehicles + Auxiliary pneumatic 8 atm + Heating + Workshop coupling (uncouple lock) + + + Freight (G) + Passenger (P) + Express (R) + Express + magnetic rail (R+Mg) + Start without air (Q) + Brakes disconnected (O) + Automatic (A) + + + Empty (T) + Loaded I — medium (H) + Loaded II (F) + Automatic (A) + + + Off (0) + Off with 10% chance (1) + On (A) + + + Damage + Sway — probability (%) + Flat spot (mm) + Random flat spot (0–x mm) + Flat-spot probability (%) + Load type + Amount + Set an amount above 0 to load the vehicle. + Wagon number + Copy load from previous vehicle + Maximum possible load + Whole-consist load + Random load + Maximum amount + Random amount + Set + Length + Mass + Load + Vehicles + Track + No vehicles. (Track: {0}) + Hide archival vehicles + Remove vehicle + Randomize textures + Pick similar textures by class / rules + Change textures + Match + Revision difference at most + relative to the current texture revision + relative to year: + Do not pick archival textures + Edit randomization rules + One rule per line, mini names separated by commas. Lines starting with # are comments. + Save + Zrušit + Ano + Ne + Info + HDR tone mapping + Settings + eu07.ini was changed outside the starter. Reload it? + Add whole category + Add every texture currently listed in the browser + Add unique MMDs + Add one texture per distinct model (MMD) + Randomize order + Randomize orientation + Shuffle wagon order from the selected vehicle to the end + Randomly reverse vehicles from the selected one to the end + Select a vehicle in the consist to edit it. + Editing options coming soon. + Split + Split the unit into individual cars + Add + Add this vehicle to the consist + Add the whole unit to the consist + + + Save consist + Load consist from warehouse + (warehouse empty) + Remove from warehouse + Consist name + Copy consist to clipboard + Paste consist from clipboard + Remove all vehicles + + + Loading vehicle database + Loading sceneries + Starting… + + + General + + Language + Fullscreen + Pause simulation when window is inactive + Pause on start + Cursor sensitivity: + Invert mouse horizontally + Invert mouse vertically + + Communication + + Ignore gamepad input + + Feedback + Off + Caps: CA/SHP; ScrollLock: starting resistances + Caps: CA; ScrollLock: SHP + LPT + PoKeys55 + Serial port (COM) + + LPT port address + + UART configuration + Enable UART communication + Serial port + Tuning parameter + Print debug data to the console + Master controller + Secondary/shunting controller + Train brake + Locomotive brake + Radio volume + Radio channel + + Other + Select exe: + Select automatically + Debug mode (test mode) + Virtual shunting + Log missing vehicle files during export + + Graphics + Render engine + Full + Old + Simplified shader + Simplified + Disable visual layer + Experimental + + Resolution: + Buffer resolution: + Maximum texture resolution: + Maximum cabin texture resolution: + Texture filtering quality: + Multisampling: + Dynamic light sources: + Rendering range: + Limit frame rate + Shadow length limit + Automatic resolution (borderless fullscreen) + Use ANGLE Vulkan renderer + VSync + Smoke display + Smoke particles multiplier + + Chromatic aberration + Motion blur + Additional shader effects + Reflections as cubemap + Render VBO + Render shadows + Reflections refresh rate + Shadows resolution + Shadow range + Cabin shadow sources range + + Display shadows + Only important + Limited + All + + Reflections detail level + Terrain geometry only + Terrain geometry and static models + Terrain geometry, static models and vehicles + + View angle + Render onboard computer screens + Render screens in a separate thread + + Physics + + Track curves quality + Increased physics accuracy + Possibility of pantograph breaking + Powered only under overhead line + Save speedometer tapes + Save simulation log + Keep previous logs + Display simulation + Collision damage + Friction multiplier + Brake valve sensitivity + FV4a valve sensitivity + + Sound + Enable sound + Volume + Radio volume + Vehicles volume + Positional sounds volume + Ambient volume + + Advanced + Compress TGA textures + Scale specular highlights (old-model compatibility) + Use OpenGL ES + Gamma correction in shaders + Generate mipmaps for in-cab (Python) screen textures + Extended E3D model conversion (experimental) + Conservative texture memory management + Purge unused textures from GPU memory + Vehicle physics threads (0 = main thread) + Skip decorative consists when starting + + Starter + Settings profile: + Apply + Save as… + Profile name + Automatically close starter + Large thumbnails + Automatically expand scenery tree + Battery on launch: + Scenario default + Always off + Always on + + + Controls + Click a binding, then press the key (hold Shift / Ctrl for a combination): + Restore defaults + Press a key… + Clear binding + Conflict: this shortcut is used by more than one command + Unassigned + Assigned + With Shift + With Ctrl + Reserved by the simulator + Key: + Assign a command to each modifier combination for this key. + — none — + No modifier + Shift + Ctrl + Shift + Ctrl + + + Save + Reset + Settings saved + diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index ea6acdf..afb1081 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -9,6 +9,12 @@ Settings Controls Help + MaSzyna wiki + Readme + Sort warehouse + By name + By track + Import magazyn.ini Version: @@ -48,10 +54,29 @@ Weather changes are written to the scenery on launch. Consist Start + Select a scenery + Select a consist + No crew / passenger (hold Ctrl to start) + Restore weather + Reload + Open folder + Optional elements + Open texture folder + Copy name + Copy coupler from previous + Coolant at ambient temperature + Texture + Operator + Depot + Revision + Author + Photos Open latest simulation log Vehicle database + Rolling stock database + Stock database… Search... Load consist from scenery New consist @@ -176,6 +201,37 @@ Length Mass Load + Vehicles + Track + No vehicles. (Track: {0}) + Hide archival vehicles + Remove vehicle + Randomize textures + Pick similar textures by class / rules + Change textures + Match + Revision difference at most + relative to the current texture revision + relative to year: + Do not pick archival textures + Edit randomization rules + One rule per line, mini names separated by commas. Lines starting with # are comments. + Save + Cancel + Yes + No + Info + HDR tone mapping + Settings + eu07.ini was changed outside the starter. Reload it? + Add whole category + Add every texture currently listed in the browser + Add unique MMDs + Add one texture per distinct model (MMD) + Randomize order + Randomize orientation + Shuffle wagon order from the selected vehicle to the end + Randomly reverse vehicles from the selected one to the end Select a vehicle in the consist to edit it. Editing options coming soon. Split diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml new file mode 100644 index 0000000..437e1f8 --- /dev/null +++ b/StarterNG/startercfg/lang/hu.xml @@ -0,0 +1,424 @@ + + + + + + + Forgatókönyvek + Fűtőház + Beállítások + Controls + Súgó + MaSzyna wiki + Readme + Raktár rendezése + Név szerint + Vágány szerint + magazyn.ini import + Verzió: + + + Pályák + Járművek + AI vehicles + Drivable only + Archival sceneries + Scenario description + Scenery description + Description + Időjárás + Menetrend + No weather data in the scenery. + No timetable for the selected consist. + Time + Now + Day of year + Sky + Fog / visibility + Season + Temperature + Visibility (fog) + Overcast + Today + Spring + Summer + Autumn + Winter + Clear + Fair + Light clouds + Moderate clouds + Heavy clouds + Cloudy + Storm + Weather changes are written to the scenery on launch. + Szerelvény + Indítás + Select a scenery + Select a consist + No crew / passenger (hold Ctrl to start) + Restore weather + Reload + Open folder + Optional elements + Open texture folder + Copy name + Copy coupler from previous + Coolant at ambient temperature + Texture + Operator + Depot + Revision + Author + Photos + Open latest simulation log + + + Vehicle database + Rolling stock database + Stock database… + Keresés... + Load consist from scenery + New consist + Selected vehicle + Vehicle + with crew + reversed + No vehicles found in databases/vehicles/. + Add vehicles from the database on the left, or load an existing consist from a scenery file. + Move left + Move right + Reverse vehicle + Remove from consist + Change crew (cab 1 / cab 2 / passenger / none) + Scenery + Category + Class + All + Diesel + Electric + EMU + DMU + Carriages A + Carriages B + Electric locomotive + Diesel locomotive + Steam locomotive + Railbus + Draisine + Work vehicles + Prototype + Tram + Car + Bus + Truck + People + Animals + Wagons A + Wagons B + Wagons C + Wagons D + Wagons E + Wagons F + Wagons G + Wagons H + Wagons I + Wagons J + Wagons K + Wagons L + Wagons M + Wagons N + Wagons O + Wagons P + Wagons Q + Wagons R + Wagons S + Wagons T + Wagons U + Wagons V + Wagons W + Wagons X + Wagons Y + Wagons Z + Other + Brakes + Loads + Coupling + Consists on the scenery + Select a scenery in the Scenarios tab. + Brake mode + Load setting + Brake switch + — + + + Mechanical + Pneumatic 5 atm (brakes) + Multiple-unit control + High voltage + Gangway between vehicles + Auxiliary pneumatic 8 atm + Heating + Workshop coupling (uncouple lock) + + + Freight (G) + Passenger (P) + Express (R) + Express + magnetic rail (R+Mg) + Start without air (Q) + Brakes disconnected (O) + Automatic (A) + + + Empty (T) + Loaded I — medium (H) + Loaded II (F) + Automatic (A) + + + Off (0) + Off with 10% chance (1) + On (A) + + + Damage + Sway — probability (%) + Flat spot (mm) + Random flat spot (0–x mm) + Flat-spot probability (%) + Load type + Amount + Set an amount above 0 to load the vehicle. + Wagon number + Copy load from previous vehicle + Maximum possible load + Whole-consist load + Random load + Maximum amount + Random amount + Set + Length + Mass + Load + Vehicles + Track + No vehicles. (Track: {0}) + Hide archival vehicles + Remove vehicle + Randomize textures + Pick similar textures by class / rules + Change textures + Match + Revision difference at most + relative to the current texture revision + relative to year: + Do not pick archival textures + Edit randomization rules + One rule per line, mini names separated by commas. Lines starting with # are comments. + Save + Mégse + Igen + Nem + Info + HDR tone mapping + Settings + eu07.ini was changed outside the starter. Reload it? + Add whole category + Add every texture currently listed in the browser + Add unique MMDs + Add one texture per distinct model (MMD) + Randomize order + Randomize orientation + Shuffle wagon order from the selected vehicle to the end + Randomly reverse vehicles from the selected one to the end + Select a vehicle in the consist to edit it. + Editing options coming soon. + Split + Split the unit into individual cars + Add + Add this vehicle to the consist + Add the whole unit to the consist + + + Save consist + Load consist from warehouse + (warehouse empty) + Remove from warehouse + Consist name + Copy consist to clipboard + Paste consist from clipboard + Remove all vehicles + + + Loading vehicle database + Loading sceneries + Starting… + + + General + + Language + Fullscreen + Pause simulation when window is inactive + Pause on start + Cursor sensitivity: + Invert mouse horizontally + Invert mouse vertically + + Communication + + Ignore gamepad input + + Feedback + Off + Caps: CA/SHP; ScrollLock: starting resistances + Caps: CA; ScrollLock: SHP + LPT + PoKeys55 + Serial port (COM) + + LPT port address + + UART configuration + Enable UART communication + Serial port + Tuning parameter + Print debug data to the console + Master controller + Secondary/shunting controller + Train brake + Locomotive brake + Radio volume + Radio channel + + Other + Select exe: + Select automatically + Debug mode (test mode) + Virtual shunting + Log missing vehicle files during export + + Graphics + Render engine + Full + Old + Simplified shader + Simplified + Disable visual layer + Experimental + + Resolution: + Buffer resolution: + Maximum texture resolution: + Maximum cabin texture resolution: + Texture filtering quality: + Multisampling: + Dynamic light sources: + Rendering range: + Limit frame rate + Shadow length limit + Automatic resolution (borderless fullscreen) + Use ANGLE Vulkan renderer + VSync + Smoke display + Smoke particles multiplier + + Chromatic aberration + Motion blur + Additional shader effects + Reflections as cubemap + Render VBO + Render shadows + Reflections refresh rate + Shadows resolution + Shadow range + Cabin shadow sources range + + Display shadows + Only important + Limited + All + + Reflections detail level + Terrain geometry only + Terrain geometry and static models + Terrain geometry, static models and vehicles + + View angle + Render onboard computer screens + Render screens in a separate thread + + Physics + + Track curves quality + Increased physics accuracy + Possibility of pantograph breaking + Powered only under overhead line + Save speedometer tapes + Save simulation log + Keep previous logs + Display simulation + Collision damage + Friction multiplier + Brake valve sensitivity + FV4a valve sensitivity + + Sound + Enable sound + Volume + Radio volume + Vehicles volume + Positional sounds volume + Ambient volume + + Advanced + Compress TGA textures + Scale specular highlights (old-model compatibility) + Use OpenGL ES + Gamma correction in shaders + Generate mipmaps for in-cab (Python) screen textures + Extended E3D model conversion (experimental) + Conservative texture memory management + Purge unused textures from GPU memory + Vehicle physics threads (0 = main thread) + Skip decorative consists when starting + + Starter + Settings profile: + Apply + Save as… + Profile name + Automatically close starter + Large thumbnails + Automatically expand scenery tree + Battery on launch: + Scenario default + Always off + Always on + + + Controls + Click a binding, then press the key (hold Shift / Ctrl for a combination): + Restore defaults + Press a key… + Clear binding + Conflict: this shortcut is used by more than one command + Unassigned + Assigned + With Shift + With Ctrl + Reserved by the simulator + Key: + Assign a command to each modifier combination for this key. + — none — + No modifier + Shift + Ctrl + Shift + Ctrl + + + Save + Reset + Settings saved + diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index afc328a..c4c3420 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -9,6 +9,12 @@ Ustawienia Sterowanie Pomoc + Wiki MaSzyna + Instrukcja (readme) + Sortuj magazyn + Według nazwy + Według toru + Importuj magazyn.ini Wersja: @@ -48,10 +54,29 @@ Zmiany pogody zostaną zapisane do scenerii przy uruchomieniu. Zestawienie Uruchom + Wybierz scenerię + Wybierz skład + Brak obsady / pasażera (Ctrl = start) + Przywróć pogodę + Przeładuj + Otwórz folder + Elementy opcjonalne + Otwórz folder tekstury + Kopiuj nazwę + Kopiuj sprzęg z poprzedniego + Chłodziwo w temp. otoczenia + Tekstura + Przewoźnik + Stacja + Data rew. + Autor + Zdjęcia Otwórz ostatni log symulatora Baza pojazdów + Baza taboru + Baza taboru… Szukaj... Wczytaj skład ze scenerii Nowy skład @@ -176,6 +201,37 @@ Długość Masa Ładunek + Pojazdy + Tor + Wpis bez pojazdów. (Tor: {0}) + Ukryj pojazdy archiwalne + Usuń pojazd + Losuj tekstury + Dobierz podobne tekstury według klasy / reguł + Zmiana tekstur + Dopasuj + Różnica rewizji nie większa niż + względem daty rewizji obecnej tekstury + względem roku: + Nie losuj tekstur oznaczonych jako archiwalne + Ustaw reguły losowania + Jedna reguła w linii, nazwy mini rozdzielone przecinkami. Linie z # są komentarzem. + Zapisz + Anuluj + Tak + Nie + Informacje + Tone mapping HDR + Ustawienia + Plik eu07.ini został zmieniony poza starterem. Wczytać ponownie? + Dodaj całą kategorię + Dodaj wszystkie tekstury z bieżącej listy przeglądarki + Dodaj unikalne MMD + Dodaj po jednej teksturze na każdy model (MMD) + Losuj kolejność + Losuj obrócenie + Przetasuj kolejność wagonów od wybranego pojazdu do końca + Losowo obróć pojazdy od wybranego do końca Wybierz pojazd w składzie, aby go edytować. Opcje edycji wkrótce. Rozdziel diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml new file mode 100644 index 0000000..44cbb1f --- /dev/null +++ b/StarterNG/startercfg/lang/ru.xml @@ -0,0 +1,424 @@ + + + + + + + Сценарии + Депо + Настройки + Controls + Справка + Wiki MaSzyna + Readme + Сортировать склад + По имени + По пути + Импорт magazyn.ini + Версия: + + + Сцены + Составы + AI vehicles + Drivable only + Archival sceneries + Scenario description + Scenery description + Description + Погода + Расписание + No weather data in the scenery. + No timetable for the selected consist. + Time + Now + Day of year + Sky + Fog / visibility + Season + Temperature + Visibility (fog) + Overcast + Today + Spring + Summer + Autumn + Winter + Clear + Fair + Light clouds + Moderate clouds + Heavy clouds + Cloudy + Storm + Weather changes are written to the scenery on launch. + Состав + Запуск + Select a scenery + Select a consist + No crew / passenger (hold Ctrl to start) + Restore weather + Reload + Open folder + Optional elements + Open texture folder + Copy name + Copy coupler from previous + Coolant at ambient temperature + Texture + Operator + Depot + Revision + Author + Photos + Open latest simulation log + + + Vehicle database + Rolling stock database + Stock database… + Поиск... + Load consist from scenery + New consist + Selected vehicle + Vehicle + with crew + reversed + No vehicles found in databases/vehicles/. + Add vehicles from the database on the left, or load an existing consist from a scenery file. + Move left + Move right + Reverse vehicle + Remove from consist + Change crew (cab 1 / cab 2 / passenger / none) + Scenery + Category + Class + All + Diesel + Electric + EMU + DMU + Carriages A + Carriages B + Electric locomotive + Diesel locomotive + Steam locomotive + Railbus + Draisine + Work vehicles + Prototype + Tram + Car + Bus + Truck + People + Animals + Wagons A + Wagons B + Wagons C + Wagons D + Wagons E + Wagons F + Wagons G + Wagons H + Wagons I + Wagons J + Wagons K + Wagons L + Wagons M + Wagons N + Wagons O + Wagons P + Wagons Q + Wagons R + Wagons S + Wagons T + Wagons U + Wagons V + Wagons W + Wagons X + Wagons Y + Wagons Z + Other + Brakes + Loads + Coupling + Consists on the scenery + Select a scenery in the Scenarios tab. + Brake mode + Load setting + Brake switch + — + + + Mechanical + Pneumatic 5 atm (brakes) + Multiple-unit control + High voltage + Gangway between vehicles + Auxiliary pneumatic 8 atm + Heating + Workshop coupling (uncouple lock) + + + Freight (G) + Passenger (P) + Express (R) + Express + magnetic rail (R+Mg) + Start without air (Q) + Brakes disconnected (O) + Automatic (A) + + + Empty (T) + Loaded I — medium (H) + Loaded II (F) + Automatic (A) + + + Off (0) + Off with 10% chance (1) + On (A) + + + Damage + Sway — probability (%) + Flat spot (mm) + Random flat spot (0–x mm) + Flat-spot probability (%) + Load type + Amount + Set an amount above 0 to load the vehicle. + Wagon number + Copy load from previous vehicle + Maximum possible load + Whole-consist load + Random load + Maximum amount + Random amount + Set + Length + Mass + Load + Vehicles + Track + No vehicles. (Track: {0}) + Hide archival vehicles + Remove vehicle + Randomize textures + Pick similar textures by class / rules + Change textures + Match + Revision difference at most + relative to the current texture revision + relative to year: + Do not pick archival textures + Edit randomization rules + One rule per line, mini names separated by commas. Lines starting with # are comments. + Save + Отмена + Да + Нет + Info + HDR tone mapping + Settings + eu07.ini was changed outside the starter. Reload it? + Add whole category + Add every texture currently listed in the browser + Add unique MMDs + Add one texture per distinct model (MMD) + Randomize order + Randomize orientation + Shuffle wagon order from the selected vehicle to the end + Randomly reverse vehicles from the selected one to the end + Select a vehicle in the consist to edit it. + Editing options coming soon. + Split + Split the unit into individual cars + Add + Add this vehicle to the consist + Add the whole unit to the consist + + + Save consist + Load consist from warehouse + (warehouse empty) + Remove from warehouse + Consist name + Copy consist to clipboard + Paste consist from clipboard + Remove all vehicles + + + Loading vehicle database + Loading sceneries + Starting… + + + General + + Language + Fullscreen + Pause simulation when window is inactive + Pause on start + Cursor sensitivity: + Invert mouse horizontally + Invert mouse vertically + + Communication + + Ignore gamepad input + + Feedback + Off + Caps: CA/SHP; ScrollLock: starting resistances + Caps: CA; ScrollLock: SHP + LPT + PoKeys55 + Serial port (COM) + + LPT port address + + UART configuration + Enable UART communication + Serial port + Tuning parameter + Print debug data to the console + Master controller + Secondary/shunting controller + Train brake + Locomotive brake + Radio volume + Radio channel + + Other + Select exe: + Select automatically + Debug mode (test mode) + Virtual shunting + Log missing vehicle files during export + + Graphics + Render engine + Full + Old + Simplified shader + Simplified + Disable visual layer + Experimental + + Resolution: + Buffer resolution: + Maximum texture resolution: + Maximum cabin texture resolution: + Texture filtering quality: + Multisampling: + Dynamic light sources: + Rendering range: + Limit frame rate + Shadow length limit + Automatic resolution (borderless fullscreen) + Use ANGLE Vulkan renderer + VSync + Smoke display + Smoke particles multiplier + + Chromatic aberration + Motion blur + Additional shader effects + Reflections as cubemap + Render VBO + Render shadows + Reflections refresh rate + Shadows resolution + Shadow range + Cabin shadow sources range + + Display shadows + Only important + Limited + All + + Reflections detail level + Terrain geometry only + Terrain geometry and static models + Terrain geometry, static models and vehicles + + View angle + Render onboard computer screens + Render screens in a separate thread + + Physics + + Track curves quality + Increased physics accuracy + Possibility of pantograph breaking + Powered only under overhead line + Save speedometer tapes + Save simulation log + Keep previous logs + Display simulation + Collision damage + Friction multiplier + Brake valve sensitivity + FV4a valve sensitivity + + Sound + Enable sound + Volume + Radio volume + Vehicles volume + Positional sounds volume + Ambient volume + + Advanced + Compress TGA textures + Scale specular highlights (old-model compatibility) + Use OpenGL ES + Gamma correction in shaders + Generate mipmaps for in-cab (Python) screen textures + Extended E3D model conversion (experimental) + Conservative texture memory management + Purge unused textures from GPU memory + Vehicle physics threads (0 = main thread) + Skip decorative consists when starting + + Starter + Settings profile: + Apply + Save as… + Profile name + Automatically close starter + Large thumbnails + Automatically expand scenery tree + Battery on launch: + Scenario default + Always off + Always on + + + Controls + Click a binding, then press the key (hold Shift / Ctrl for a combination): + Restore defaults + Press a key… + Clear binding + Conflict: this shortcut is used by more than one command + Unassigned + Assigned + With Shift + With Ctrl + Reserved by the simulator + Key: + Assign a command to each modifier combination for this key. + — none — + No modifier + Shift + Ctrl + Shift + Ctrl + + + Save + Reset + Settings saved + From 8fad37c6e8c5d06a44ec6e96c5576f456b937d67 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 22 Aug 2026 23:15:18 +0200 Subject: [PATCH 03/63] Parse loose vehicles, optional includes, attachments and dual weather clocks #17 keep empty/loose consists; #19 scenery display name; two weather clocks (time + scenario.time.override) with atmo fog randomisation; scenery fault list; reload sceneries. --- StarterNG/Classes/GameData.cs | 11 ++ StarterNG/Classes/Scenery.cs | 306 ++++++++++++++++++++++++++++++++-- StarterNG/Classes/Trainset.cs | 81 ++++++++- 3 files changed, 375 insertions(+), 23 deletions(-) diff --git a/StarterNG/Classes/GameData.cs b/StarterNG/Classes/GameData.cs index ae17654..e14b37d 100644 --- a/StarterNG/Classes/GameData.cs +++ b/StarterNG/Classes/GameData.cs @@ -80,6 +80,17 @@ public sealed class GameData Loaded = true; } + /// Re-reads scenery/*.scn (Pascal actReloadScenarios). + public void ReloadSceneries(string sceneryDir = "scenery/") + { + Sceneries.Clear(); + foreach (string file in EnumerateScenery(sceneryDir)) + { + try { Sceneries.Add(new Scenery(file)); } + catch { /* skip */ } + } + } + private static List EnumerateScenery(string sceneryDir) { if (!Directory.Exists(sceneryDir)) diff --git a/StarterNG/Classes/Scenery.cs b/StarterNG/Classes/Scenery.cs index bcf3583..c2b238f 100644 --- a/StarterNG/Classes/Scenery.cs +++ b/StarterNG/Classes/Scenery.cs @@ -1,15 +1,43 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using StarterNG.Domain; namespace StarterNG.Classes; +public sealed class SceneryAttachment +{ + public string FilePath = ""; + public string Label = ""; +} + +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; +} + public class Scenery { public List Lines; 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; @@ -24,10 +52,16 @@ public class Scenery // Weather / environment. Like the original Starter, these are editable and are // written into the scenery's "config" block on launch (see RewriteWeather). - // Defaults mirror the original (15 °C, day 0, 10:30, clear sky). - public string WeatherTime = "10:30"; // h:mm -> "time"/"scenario.time.override" + // + // 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) @@ -37,10 +71,34 @@ public class Scenery /// 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; + private double _origTemperature = 15; + 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 + { + if (!string.IsNullOrWhiteSpace(Name)) + return SceneryI18n.T(Name); + return System.IO.Path.GetFileNameWithoutExtension(Path); + } + } + + /// //$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; @@ -68,7 +126,10 @@ public class Scenery // 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(); @@ -82,12 +143,73 @@ public class Scenery trainsetEntries.Add(match.Value); return $"{{{{{idx++}}}}}"; }); + 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) - Trainsets.Add(new Trainset(trainsetEntry)); + { + var ts = new Trainset(trainsetEntry); + Trainsets.Add(ts); + if (!ts.Parsed) + Faults.Add($"# trainset parse fault: {ts.Name} ({ts.Track})"); + } + } + + // 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(); + LooseVehicleNames.Clear(); + var inv = System.Globalization.CultureInfo.InvariantCulture; + + return Regex.Replace( + content, + @"(?is)\bnode\s+(\S+)\s+(\S+)\s+(\S+)\s+dynamic\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)((?:\s+(?!enddynamic)\S+)*)\s*enddynamic\b", + m => + { + string name = m.Groups[3].Value; + if (name.StartsWith("{{", StringComparison.Ordinal)) + return m.Value; + try + { + var d = new Dynamic + { + RangeMax = float.Parse(m.Groups[1].Value, inv), + RangeMin = float.Parse(m.Groups[2].Value, inv), + Name = name, + DataFolder = m.Groups[4].Value, + SkinFile = m.Groups[5].Value, + MmdFile = m.Groups[6].Value, + PathName = m.Groups[7].Value, + Offset = float.Parse(m.Groups[8].Value, inv), + DriverType = m.Groups[9].Value.ToLowerInvariant() switch + { + "headdriver" => eDriverType.Headdriver, + "reardriver" => eDriverType.Reardriver, + "passenger" => eDriverType.Passenger, + _ => eDriverType.Nobody + }, + HasVelocity = true, + Velocity = float.Parse(m.Groups[10].Value, inv) + }; + var trailing = m.Groups[11].Value + .Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) + .ToList(); + d.ReadTrailing(trailing); + LooseVehicles.Add(d); + LooseVehicleNames.Add(d.Name); + return "\n"; + } + catch + { + Faults.Add($"# loose dynamic parse fault: {name}"); + LooseVehicleNames.Add(name); + return m.Value; + } + }); } /// @@ -98,12 +220,11 @@ public class Scenery { string result = _template; - // Inject the (edited) weather into the config block before substituting - // trainsets, so the {{i}} placeholders shield the trainset text from the - // weather rewrite. Only done when the user actually changed the weather. if (WeatherDirty) result = RewriteWeather(result); + result = InjectIncludes(result); + for (int i = 0; i < Trainsets.Count; i++) { string entry = skipDecorTrainsets && Trainsets[i].Decor @@ -111,9 +232,120 @@ public class Scenery : Trainsets[i].ToSceneryEntry(); result = result.Replace("{{" + i + "}}", entry); } + + if (LooseVehicles.Count > 0) + result = AppendLooseVehicles(result); + return result; } + private string AppendLooseVehicles(string text) + { + var sb = new StringBuilder(); + foreach (var v in LooseVehicles) + sb.Append(v.ToLooseNode()); + if (sb.Length == 0) return text; + + var fi = Regex.Match(text, @"\bFirstInit\b", RegexOptions.IgnoreCase); + if (fi.Success) + return text[..fi.Index] + sb + text[fi.Index..]; + return text + sb; + } + + private void ParseAttachments(string content) + { + Attachments.Clear(); + foreach (Match m in Regex.Matches(content, @"^//\$f\b[ \t]*([^\r\n]*)", RegexOptions.Multiline)) + { + 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 + { + FilePath = parts[1], + Label = string.Join(" ", parts.Skip(2)) + }); + else if (parts.Length >= 2) + Attachments.Add(new SceneryAttachment + { + FilePath = parts[0], + Label = string.Join(" ", parts.Skip(1)) + }); + else + Attachments.Add(new SceneryAttachment + { + FilePath = parts[0], + Label = System.IO.Path.GetFileName(parts[0]) + }); + } + } + + // 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(); + return Regex.Replace( + content, + @"include\s+(\S+(?:\s+\S+)*?)\s+end\s*//[^\r\n]*\$optional([^\r\n]*)\r?\n([\s\S]*?)endoptional\b", + m => + { + string incPath = Regex.Replace(m.Groups[1].Value.Trim(), @"\s+", " "); + string raw = m.Groups[2].Value.Trim().TrimStart(',', ' '); + string[] par = raw.Split(new[] { ',', '|', ';' }, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + int kind = 1; + string desc = incPath; + if (par.Length >= 1 && int.TryParse(par[0], out int k)) + kind = k; + if (par.Length >= 2) + desc = par[1]; + + Includes.Add(new SceneryInclude + { + FilePath = incPath, + Desc = desc, + Kind = kind, + Selected = kind == 1 + }); + return " "; + }, + RegexOptions.IgnoreCase); + } + + private string InjectIncludes(string text) + { + if (Includes.Count == 0) + return text; + + var sb = new StringBuilder(); + string sbt = System.IO.Path.ChangeExtension(Path, ".sbt"); + bool hasSbt = File.Exists(sbt); + + foreach (var inc in Includes) + { + if (inc.Kind == 2) + { + if (!hasSbt) + sb.Append("include ").Append(inc.FilePath).Append(" end\r\n"); + continue; + } + if (inc.Selected) + sb.Append("include ").Append(inc.FilePath).Append(" end\r\n"); + } + + if (sb.Length == 0) + return text; + + var fi = Regex.Match(text, @"\bFirstInit\b", RegexOptions.IgnoreCase); + if (fi.Success) + return text[..fi.Index] + sb + text[fi.Index..]; + return sb + text; + } + /// /// Reads the scenery's environment commands into the editable weather fields. /// Mirrors the original Starter (config: movelight / scenario.weather.temperature @@ -121,16 +353,28 @@ public class Scenery /// private void ParseWeather(string content) { - // start time: "scenario.time.override h:mm" wins, else top-level "time h:mm" - var ovr = Regex.Match(content, @"(?i)scenario\.time\.override\s+(\d{1,2})[:.](\d{2})"); - var time = ovr.Success - ? ovr - : Regex.Match(content, @"(?im)^\s*time\s+(\d{1,2})[:.](\d{2})\b"); + // 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) { - WeatherTime = $"{time.Groups[1].Value.PadLeft(2, '0')}:{time.Groups[2].Value}"; + Time = $"{time.Groups[1].Value.PadLeft(2, '0')}:{time.Groups[2].Value}"; + ScenarioTimeOverride = Time; + } + + var ovr = Regex.Match(content, @"(?i)scenario\.time\.override\s+(\d{1,2})[:.](\d{2})"); + if (ovr.Success) + { + ScenarioTimeOverride = $"{ovr.Groups[1].Value.PadLeft(2, '0')}:{ovr.Groups[2].Value}"; HasWeather = true; } + 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+)"); @@ -151,14 +395,21 @@ public class Scenery } // "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) { var nums = Regex.Matches(atmo.Groups[1].Value, @"-?\d+(?:\.\d+)?") .Select(m => m.Value).ToList(); - if (nums.Count >= 5 && int.TryParse(nums[4], out int fog)) + if (nums.Count >= 5 && + int.TryParse(nums[3], out int fogStart) && + int.TryParse(nums[4], out int fogEnd)) { - FogEnd = fog; + FogStart = Math.Clamp(fogStart, 10, 2500); + fogEnd = Math.Clamp(fogEnd, FogStart, 2500); + FogEnd = fogEnd > FogStart + ? Random.Shared.Next(FogStart, fogEnd + 1) + : fogEnd; HasWeather = true; } if (nums.Count >= 6 && @@ -168,6 +419,28 @@ public class Scenery } } + private void SnapshotWeather() + { + _origTime = Time; + _origOverride = ScenarioTimeOverride; + _origDay = Day; + _origTemperature = Temperature; + _origFogEnd = FogEnd; + _origOvercast = Overcast; + } + + /// Restores weather fields to the values parsed from the SCN file. + public void RestoreWeather() + { + Time = _origTime; + ScenarioTimeOverride = _origOverride; + Day = _origDay; + Temperature = _origTemperature; + FogEnd = _origFogEnd; + Overcast = _origOvercast; + 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 @@ -188,12 +461,13 @@ 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" + $"scenario.weather.temperature {Temperature.ToString(inv)}\r\n" + - $"scenario.time.override {WeatherTime}\r\n" + - $"time {WeatherTime} 0 0 endtime\r\n" + + $"scenario.time.override {ScenarioTimeOverride}\r\n" + + $"time {Time} 0 0 endtime\r\n" + $"atmo 0 0 0 {FogEnd} {FogEnd} 0 0 0 {Overcast.ToString(inv)} endatmo\r\n" + "endconfig\r\n"; diff --git a/StarterNG/Classes/Trainset.cs b/StarterNG/Classes/Trainset.cs index f857168..2733bad 100644 --- a/StarterNG/Classes/Trainset.cs +++ b/StarterNG/Classes/Trainset.cs @@ -34,6 +34,12 @@ public class Trainset /// 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) { RawEntry = trainsetEntry; @@ -68,6 +74,14 @@ public class Trainset ); if (match.Success) this.Description = match.Groups[1].Value.Trim(); + + var logo = Regex.Match(trainsetEntry, @"^\s*//\$il\b[ \t]*([^\r\n]*)", RegexOptions.Multiline); + if (logo.Success) + Logo = logo.Groups[1].Value.Trim(); + + var mini = Regex.Match(trainsetEntry, @"^\s*//\$it\b[ \t]*([^\r\n]*)", RegexOptions.Multiline); + if (mini.Success) + Mini = mini.Groups[1].Value.Trim(); // parse trainset @@ -145,14 +159,27 @@ public class Trainset public string GetTrainsetEntry() { - string entry = ""; + // 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"); + if (!string.IsNullOrEmpty(Description)) + sb.Append("//$o ").Append(Description).Append('\n'); + if (!string.IsNullOrEmpty(Logo)) + sb.Append("//$il ").Append(Logo).Append('\n'); + if (!string.IsNullOrEmpty(Mini)) + sb.Append("//$it ").Append(Mini).Append('\n'); - entry += "trainset "; - entry += this.Name + " "; - entry += this.Track + " "; - entry += this.Offset.ToString(CultureInfo.InvariantCulture) + " "; - entry += this.Velocity.ToString(CultureInfo.InvariantCulture) + " "; - entry += "\n"; + sb.Append("trainset "); + sb.Append(Name).Append(' '); + sb.Append(Track).Append(' '); + sb.Append(Offset.ToString(CultureInfo.InvariantCulture)).Append(' '); + sb.Append(Velocity.ToString(CultureInfo.InvariantCulture)); + sb.Append('\n'); + + string entry = sb.ToString(); foreach (Dynamic vehicle in Vehicles) { string driverType = ""; @@ -323,6 +350,35 @@ public class Dynamic MiniName = MiniName, Flipped = Flipped }; + + /// Pascal PrepareNode(..., TrainSet=False). + public string ToLooseNode() + { + string driver = DriverType switch + { + eDriverType.Headdriver => "headdriver", + eDriverType.Reardriver => "reardriver", + eDriverType.Passenger => "passenger", + _ => "nobody" + }; + string path = string.IsNullOrEmpty(PathName) ? "none" : PathName; + string vel = (HasVelocity ? Velocity : 0f).ToString(CultureInfo.InvariantCulture); + return + $"node {RangeMax.ToString(CultureInfo.InvariantCulture)} " + + $"{RangeMin.ToString(CultureInfo.InvariantCulture)} {Name} dynamic " + + $"{DataFolder} {SkinFile} {MmdFile} {path} " + + $"{Offset.ToString(CultureInfo.InvariantCulture)} {driver} {vel}" + + $"{WriteTrailing()} enddynamic\n"; + } +} + +/// 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(); + public bool Grouped { get; set; } + public bool Flipped { get; set; } + public eDriverType Driver { get; set; } = eDriverType.Nobody; } /// @@ -414,6 +470,17 @@ 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)); + set + { + Parameters.RemoveAll(p => p.Equals("TA", StringComparison.OrdinalIgnoreCase)); + if (value) Parameters.Add("TA"); + } + } + public Coupling Clone() => new() { Flags = Flags, From 2e21d416ef5799ef580416daa7b259d885784a1f Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 22 Aug 2026 23:15:18 +0200 Subject: [PATCH 04/63] Track archival vehicles and multi-unit set followers in the vehicle database #13 ResolvedArchived flag; #18 IsSetFollower hides trailing multi-unit cars; missing-asset log lines. --- StarterNG/Classes/VehicleDatabase.cs | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/StarterNG/Classes/VehicleDatabase.cs b/StarterNG/Classes/VehicleDatabase.cs index e9dd68b..34e8b3d 100644 --- a/StarterNG/Classes/VehicleDatabase.cs +++ b/StarterNG/Classes/VehicleDatabase.cs @@ -66,6 +66,9 @@ public class VehicleTexture /// 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. @@ -297,9 +300,26 @@ public class VehicleDatabase t.ResolvedCategory = category; t.ResolvedClass = mini; + t.ResolvedArchived = g?.Archived ?? false; } } + /// + /// 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; + if (!SetByTextureUuid.TryGetValue(texture.Uuid!, out var set) || + set.TextureRefs is null || set.TextureRefs.Count < 2) + return false; + + string? lead = set.TextureRefs.FirstOrDefault(r => !string.IsNullOrEmpty(r)); + return lead != null && + !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. @@ -332,6 +352,31 @@ 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(); + foreach (var t in Textures) + { + string dir = Path.Combine(Directory.GetCurrentDirectory(), "dynamic", + t.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/')); + string skin = Path.Combine(dir, t.Skinfile); + if (!string.IsNullOrEmpty(t.Skinfile) && !File.Exists(skin) && + !File.Exists(Path.ChangeExtension(skin, ".mat")) && + !File.Exists(Path.ChangeExtension(skin, ".bmp"))) + lines.Add($"# no file: {t.Directory}{t.Skinfile}"); + + if (!string.IsNullOrEmpty(t.Model)) + { + string model = Path.Combine(dir, t.Model!); + if (!File.Exists(model) && !File.Exists(Path.ChangeExtension(model, ".t3d")) && + !File.Exists(Path.ChangeExtension(model, ".e3d"))) + lines.Add($"# no model: {t.Directory}{t.Model}"); + } + } + return lines; + } + /// Header label for the group a texture belongs to. public string GroupHeader(string? groupId) { From ce806130f7ef33789037648a96304390c8bf694b Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 22 Aug 2026 23:15:34 +0200 Subject: [PATCH 05/63] Add hide-archival, last-scenery, HDR tone map and external-change settings #13 HideArchivalVehicles; #19 LastScenery; HDR tonemap + framebuffer fidelity; reload eu07.ini when it changes on disk; dump missing-vehicle log on exit. --- StarterNG/App.axaml.cs | 1 + StarterNG/Classes/Settings.cs | 120 +++++++++++++++++++++++++++++- StarterNG/Views/Settings.axaml | 8 ++ StarterNG/Views/Settings.axaml.cs | 6 ++ 4 files changed, 133 insertions(+), 2 deletions(-) diff --git a/StarterNG/App.axaml.cs b/StarterNG/App.axaml.cs index fa255b6..176313c 100644 --- a/StarterNG/App.axaml.cs +++ b/StarterNG/App.axaml.cs @@ -70,6 +70,7 @@ public partial class App : Application desktop.ShutdownRequested += (_, _) => { Settings.Instance.CaptureAndSave(); + Settings.Instance.DumpMissingVehicleLog(); if (KeyboardConfig.Instance.Dirty) KeyboardConfig.Instance.Save(); }; diff --git a/StarterNG/Classes/Settings.cs b/StarterNG/Classes/Settings.cs index 1275cb9..c90d8a2 100644 --- a/StarterNG/Classes/Settings.cs +++ b/StarterNG/Classes/Settings.cs @@ -65,13 +65,15 @@ public sealed class Settings 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 (launcher-only; not yet wired to any load/export codepath) + public bool LogMissingVehicleFiles; // starter.logmissingvehicles → bledy.txt dump // ── Graphics ────────────────────────────────────────────────────────── public int RenderEngine; // gfxrenderer (index, see RenderEngines) public int Width = 1280; // width public int Height = 720; // height - public int BufferScalePercent = 100; // starter.bufferscale (NOTE: launcher-only) + public int BufferScalePercent = 100; // starter.bufferscale → gfx.framebuffer.fidelity + /// 0 = Reinhard, 1 = ACESFilm (Pascal ChangeHDR). + public int HdrToneMap; // starter.hdr public int MaxTextureSize = 4096; // maxtexturesize public int MaxCabTextureSize = 4096; // maxcabtexturesize public int TextureFiltering = 8; // anisotropicfiltering (1/2/4/8/16) @@ -146,6 +148,9 @@ public sealed class Settings 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 /// gfxrenderer tokens in the order shown by the render-engine combo. public static readonly string[] RenderEngines = @@ -160,6 +165,7 @@ public sealed class Settings 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() @@ -308,6 +314,7 @@ public sealed class Settings } ReadFromConfig(); + TouchConfigAge(); } /// Persists settings to the per-user eu07.ini, keeping unknown keys. @@ -323,6 +330,8 @@ public sealed class Settings public void SaveTo(string path) { WriteToConfig(); + ApplyFramebufferFidelity(); + ApplyHdrShader(); try { @@ -330,6 +339,7 @@ public sealed class Settings if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); File.WriteAllText(path, _config.ToText(), FileEncoding); + TouchConfigAge(); } catch { @@ -337,6 +347,103 @@ public sealed class Settings } } + public void TouchConfigAge() + { + string path = string.IsNullOrEmpty(_savePath) ? UserConfigPath() : _savePath; + try + { + _configAgeUtc = File.Exists(path) + ? File.GetLastWriteTimeUtc(path) + : DateTime.UtcNow; + } + catch + { + _configAgeUtc = DateTime.UtcNow; + } + } + + /// Pascal CheckSettingsFile: true when eu07.ini is newer than last load/save. + public bool IsConfigNewerOnDisk() + { + string path = string.IsNullOrEmpty(_savePath) ? UserConfigPath() : _savePath; + try + { + if (!File.Exists(path) || _configAgeUtc == DateTime.MinValue) + return false; + return File.GetLastWriteTimeUtc(path) > _configAgeUtc + TimeSpan.FromSeconds(1); + } + catch + { + return false; + } + } + + // Map BufferScalePercent → gfx.framebuffer.fidelity like Pascal's 720/1080/1440/Custom. + private void ApplyFramebufferFidelity() + { + int fidelity = BufferScalePercent switch + { + <= 75 => 1, + <= 125 => 2, + <= 175 => 3, + _ => 0 + }; + if (fidelity > 0) + _config.SetInt("gfx.framebuffer.fidelity", fidelity); + else + _config.Remove("gfx.framebuffer.fidelity"); + } + + /// Copy Reinhard/ACESFilm 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; + + string name = HdrToneMap == 0 ? "Reinhard.glsl" : "ACESFilm.glsl"; + string? src = FindBundled(name); + if (src is null) return; + + try + { + string destDir = Path.Combine(Directory.GetCurrentDirectory(), "shaders"); + Directory.CreateDirectory(destDir); + File.Copy(src, Path.Combine(destDir, "tonemapping.glsl"), overwrite: true); + } + catch { /* missing shaders dir / permissions */ } + } + + private static string? FindBundled(string fileName) + { + foreach (string dir in new[] + { + Path.Combine(Directory.GetCurrentDirectory(), "starter"), + Path.Combine(AppContext.BaseDirectory, "startercfg"), + Path.Combine(Directory.GetCurrentDirectory(), "startercfg") + }) + { + string path = Path.Combine(dir, fileName); + if (File.Exists(path)) return path; + } + return null; + } + + /// Pascal chLogExt on exit: dump missing vehicle assets to starter/bledy.txt. + public void DumpMissingVehicleLog() + { + if (!LogMissingVehicleFiles) return; + try + { + var lines = GameData.Instance.Vehicles.CollectMissingAssetLines(); + if (lines.Count == 0) return; + string dir = Path.Combine(Directory.GetCurrentDirectory(), "starter"); + Directory.CreateDirectory(dir); + File.WriteAllLines(Path.Combine(dir, "bledy.txt"), lines); + } + catch { } + } + // ── config → fields ─────────────────────────────────────────────────── private void ReadFromConfig() { @@ -388,6 +495,7 @@ public sealed class Settings DebugMode = c.GetBool("debugmode", false); VirtualShunting = c.GetBool("ai.trainman", true); LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false); + HdrToneMap = Clamp(c.GetInt("starter.hdr", 0), 0, 1); // Graphics RenderEngine = IndexOf(RenderEngines, c.GetString("gfxrenderer", "full"), 0); @@ -470,6 +578,8 @@ public sealed class Settings ShowAiVehicles = c.GetBool("starter.show.ai", true); DrivableOnly = c.GetBool("starter.drivableonly", true); ShowArchivalSceneries = c.GetBool("starter.show.archival", true); + HideArchivalVehicles = c.GetBool("starter.hide.archivalvehicles", true); + LastScenery = c.GetString("starter.last.scenery", ""); } // ── fields → config ─────────────────────────────────────────────────── @@ -511,6 +621,7 @@ public sealed class Settings c.SetBool("debugmode", DebugMode); c.SetBool("ai.trainman", VirtualShunting); c.SetBool("starter.logmissingvehicles", LogMissingVehicleFiles); + c.SetInt("starter.hdr", Clamp(HdrToneMap, 0, 1)); // Graphics c.Set("gfxrenderer", RenderEngines[Clamp(RenderEngine, 0, RenderEngines.Length - 1)]); @@ -590,6 +701,11 @@ public sealed class Settings c.SetBool("starter.show.ai", ShowAiVehicles); c.SetBool("starter.drivableonly", DrivableOnly); c.SetBool("starter.show.archival", ShowArchivalSceneries); + c.SetBool("starter.hide.archivalvehicles", HideArchivalVehicles); + if (!string.IsNullOrWhiteSpace(LastScenery)) + c.Set("starter.last.scenery", LastScenery); + else + c.Remove("starter.last.scenery"); } // ── small helpers ────────────────────────────────────────────────────── diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index 38d51fc..82f8a45 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -130,6 +130,13 @@ + + - - + + + + - @@ -270,11 +267,6 @@ - - - - diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs index 156b423..9eb884d 100644 --- a/StarterNG/Views/Settings.axaml.cs +++ b/StarterNG/Views/Settings.axaml.cs @@ -12,6 +12,7 @@ using Avalonia.Layout; using Avalonia.Markup.Xaml.Styling; using Avalonia.Media; using StarterNG.Classes; +using StarterNG.Services; // Disambiguate from Avalonia.Input.KeyBinding (a command-input binding) imported above. using KeyBinding = StarterNG.Classes.KeyBinding; @@ -34,11 +35,10 @@ public partial class Settings : UserControl shaderResolutionSlider_OnValueChanged(null, null); }; - ChangeLanguageCb.SelectedIndex = App.Loc.CurrentLanguage switch - { - "Polski" => 0, - _ => 1 - }; + FillLanguageCombo(); + // The other picker is in the main window's top bar; both follow whatever + // language ends up loaded. + App.Loc.LanguageChanged += SelectActiveLanguage; FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged; FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb); @@ -143,7 +143,7 @@ public partial class Settings : UserControl PopulateExeCombo(); // General - ChangeLanguageCb.SelectedIndex = s.Language == "Polski" ? 0 : 1; + SelectActiveLanguage(); FullscreenCb.IsChecked = s.Fullscreen; PauseInactiveCb.IsChecked = s.PauseWhenInactive; PauseStartCb.IsChecked = s.PauseOnStart; @@ -177,7 +177,6 @@ public partial class Settings : UserControl // Graphics RenderEngineCb.SelectedIndex = s.RenderEngine; - HdrToneMapCb.SelectedIndex = Clamp(s.HdrToneMap, 0, 1); SelectResolution(s.Width, s.Height); bufferScale.Value = s.BufferScalePercent; textureResolutionSlider.Value = Log2(s.MaxTextureSize, 12); @@ -264,7 +263,10 @@ public partial class Settings : UserControl var s = StarterNG.Classes.Settings.Instance; // General - s.Language = ChangeLanguageCb.SelectedIndex == 0 ? "Polski" : "English"; + // Keep the loaded language rather than writing a wrong one over it when + // the combo has no valid pick. + s.Language = (ChangeLanguageCb.SelectedItem as ComboBoxItem)?.Content?.ToString() + ?? App.Loc.CurrentLanguage; s.Fullscreen = IsChecked(FullscreenCb); s.PauseWhenInactive = IsChecked(PauseInactiveCb); s.PauseOnStart = IsChecked(PauseStartCb); @@ -297,7 +299,6 @@ public partial class Settings : UserControl // Graphics s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex); - s.HdrToneMap = Clamp(HdrToneMapCb.SelectedIndex, 0, 1); ReadResolution(s); s.BufferScalePercent = (int)bufferScale.Value; s.MaxTextureSize = 1 << (int)textureResolutionSlider.Value; @@ -454,14 +455,39 @@ public partial class Settings : UserControl shaderResolution.Text = $"{resolution} px"; } + // One entry per file in the lang/ folder, so every translation is reachable + // (the list used to be a hard-coded Polski / English pair). + private void FillLanguageCombo() + { + ChangeLanguageCb.Items.Clear(); + foreach (var lang in LocalizationService.AvailableLanguages()) + ChangeLanguageCb.Items.Add(new ComboBoxItem { Content = lang.Name, Tag = lang.Code }); + if (ChangeLanguageCb.ItemCount == 0) + ChangeLanguageCb.Items.Add(new ComboBoxItem { Content = "English", Tag = "en" }); + SelectActiveLanguage(); + } + + // Points the combo at the language currently loaded. The selection change + // lands in the handler below, which sees it is already loaded and stops. + private void SelectActiveLanguage() + { + var item = ChangeLanguageCb.Items.OfType() + .FirstOrDefault(it => string.Equals(it.Tag as string, App.Loc.CurrentLangCode, + StringComparison.OrdinalIgnoreCase)) + ?? ChangeLanguageCb.Items.OfType().FirstOrDefault(); + + if (item != null && !ReferenceEquals(ChangeLanguageCb.SelectedItem, item)) + ChangeLanguageCb.SelectedItem = item; + } + private void LanguageComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { - if (sender is not ComboBox { SelectedItem: ComboBoxItem item }) return; - if (!item.IsKeyboardFocusWithin) return; // ignore programmatic (ApplyToUi) changes - var lang = item.Content?.ToString(); - if (string.IsNullOrEmpty(lang)) return; + if ((ChangeLanguageCb.SelectedItem as ComboBoxItem)?.Tag is not string code) + return; + if (string.Equals(code, App.Loc.CurrentLangCode, StringComparison.OrdinalIgnoreCase)) + return; - App.ApplyLanguage(lang); + App.ApplyLanguage(code); } private void FeedbackCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) => diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml index 23191c3..8cc2e7a 100644 --- a/StarterNG/startercfg/lang/cz.xml +++ b/StarterNG/startercfg/lang/cz.xml @@ -223,7 +223,6 @@ Ano Ne Informace - Mapování tónů HDR Nastavení Soubor eu07.ini byl změněn mimo starter. Načíst jej znovu? Přidat celou kategorii diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index 747eeea..fc8ec32 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -223,7 +223,6 @@ Yes No Info - HDR tone mapping Settings eu07.ini was changed outside the starter. Reload it? Add whole category diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index 9149d40..0ff8c56 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -223,7 +223,6 @@ Igen Nem Információ - HDR tónusleképezés Beállítások Az eu07.ini fájl a starteren kívül módosult. Újratölti? Teljes kategória hozzáadása diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index e2215eb..3bc69cc 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -223,7 +223,6 @@ Tak Nie Informacje - Tone mapping HDR Ustawienia Plik eu07.ini został zmieniony poza starterem. Wczytać ponownie? Dodaj całą kategorię diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index a226348..8fff66c 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -223,7 +223,6 @@ Да Нет Информация - Тональная компрессия HDR Настройки Файл eu07.ini изменён вне стартера. Загрузить заново? Добавить всю категорию From caf1c6d0b3d38d274ea680bd16b720e1e5242f18 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 25 Aug 2026 23:41:42 +0200 Subject: [PATCH 17/63] Bring the build workflow over from main The file did not exist on this branch (it was cut before the workflow landed on main), so a merge would have shown it as new. It is copied verbatim: this branch is not in the push triggers and builds nothing on its own. --- .github/workflows/dotnet.yml | 152 +++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..b262753 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,152 @@ +name: Build NativeAOT + +on: + push: + branches: + - main + - staging + + pull_request: + branches: + - main + - staging + + workflow_dispatch: + +permissions: + contents: read + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: true + PROJECT_PATH: StarterNG/StarterNG.csproj + +jobs: + publish: + name: ${{ matrix.name }} + + strategy: + fail-fast: false + + matrix: + include: + - name: Windows x64 + os: windows-2022 + rid: win-x64 + artifact: StarterNG-windows-x64 + package: artifacts/StarterNG-windows-x64.zip + + - name: Linux x64 + os: ubuntu-22.04 + rid: linux-x64 + artifact: StarterNG-linux-x64 + package: artifacts/StarterNG-linux-x64.tar.gz + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Setup .NET 9 + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 9.0.x + + # NativeAOT on Linux needs a native compiler/linker toolchain. + - name: Install NativeAOT dependencies + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y clang zlib1g-dev + + - name: Restore + run: > + dotnet restore + "${{ env.PROJECT_PATH }}" + -r "${{ matrix.rid }}" + + - name: Publish NativeAOT + run: > + dotnet publish + "${{ env.PROJECT_PATH }}" + -c Release + -r "${{ matrix.rid }}" + --self-contained true + --no-restore + -p:PublishAot=true + -p:PublishTrimmed=true + -o "publish/${{ matrix.rid }}" + + - name: Package Windows build + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path "artifacts" | Out-Null + + Compress-Archive ` + -Path "publish/${{ matrix.rid }}/*" ` + -DestinationPath "${{ matrix.package }}" + + - name: Package Linux build + if: runner.os == 'Linux' + shell: bash + run: | + mkdir -p artifacts + + tar \ + -C "publish/${{ matrix.rid }}" \ + -czf "${{ matrix.package }}" \ + . + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact }} + path: ${{ matrix.package }} + if-no-files-found: error + archive: false + + # Workflow artifacts are only reachable for signed-in users with repository + # access, so every build of a release branch also lands in a rolling + # pre-release, whose assets anyone can download: + # https://github.com/${{ github.repository }}/releases/download// + # main -> "latest", staging -> "staging"; the two never overwrite each other. + release: + name: Rolling pre-release + needs: publish + if: github.event_name == 'push' + runs-on: ubuntu-latest + + permissions: + contents: write + + env: + RELEASE_TAG: ${{ github.ref_name == 'main' && 'latest' || 'staging' }} + RELEASE_NAME: ${{ github.ref_name == 'main' && 'Latest build' || 'Staging build' }} + + steps: + - name: Collect the packages + uses: actions/download-artifact@v7 + with: + path: dist + merge-multiple: true + + # Recreated rather than edited, so the tag follows its branch and no asset + # from an older build is left behind. + - name: Replace the rolling pre-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + ls -l dist + + gh release delete "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag || true + gh release create "$RELEASE_TAG" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --prerelease \ + --title "$RELEASE_NAME" \ + --notes "Automatic build of $GITHUB_REF_NAME (${GITHUB_SHA::7}), $(date -u '+%Y-%m-%d %H:%M UTC')." From 969a5b94943f9b7146e477863c15c92634ad2488 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 28 Aug 2026 22:08:38 +0200 Subject: [PATCH 18/63] scenario time override fix --- StarterNG/Classes/Scenery.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StarterNG/Classes/Scenery.cs b/StarterNG/Classes/Scenery.cs index c2b238f..f42f300 100644 --- a/StarterNG/Classes/Scenery.cs +++ b/StarterNG/Classes/Scenery.cs @@ -467,9 +467,9 @@ public class Scenery $"movelight {Day}\r\n" + $"scenario.weather.temperature {Temperature.ToString(inv)}\r\n" + $"scenario.time.override {ScenarioTimeOverride}\r\n" + - $"time {Time} 0 0 endtime\r\n" + + "endconfig\r\n" + $"atmo 0 0 0 {FogEnd} {FogEnd} 0 0 0 {Overcast.ToString(inv)} endatmo\r\n" + - "endconfig\r\n"; + $"time {Time} 0 0 endtime\r\n"; return config + s; } From 3b7a8135eaf567ef978a7386a20736d0584a08ad Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 29 Aug 2026 16:41:22 +0200 Subject: [PATCH 19/63] 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 @@ -92,18 +73,22 @@ + + - - - - - + + - + public bool LanguageWasSet { get; internal set; } + /// + /// Display name of the starter's language, as the picker shows it ("Polski", + /// "English", "中文"). Resolved from at startup. + /// public string Language = "English"; + + /// + /// The language's code, as its startercfg/lang/*.xml declares it and as + /// eu07.ini stores it. This is what the simulator reads, so it has to carry + /// whatever language the user actually picked rather than being folded into + /// one of two known ones. + /// + public string LanguageCode = ""; public bool Fullscreen; public bool PauseWhenInactive = true; public bool PauseOnStart; diff --git a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs index 2dd05e7..916e159 100644 --- a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs +++ b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs @@ -23,7 +23,13 @@ public sealed class SettingsSerializer { s.LanguageWasSet = c.Has("lang"); - s.Language = c.GetString("lang", "en").ToLowerInvariant() == "pl" ? "Polski" : "English"; + // Kept as the code the file holds. Startup hands it to the localisation + // service, which resolves it to a display name and writes that back; the + // starter must not decide here that an unrecognised code means English, + // because the same key is what tells the simulator which language to run + // in. + s.LanguageCode = c.GetString("lang", ""); + s.Language = s.LanguageCode; s.Fullscreen = c.GetBool("fullscreen", false); s.PauseWhenInactive = c.GetBool("inactivepause", true); s.PauseOnStart = c.GetBool("pause", false); @@ -165,7 +171,7 @@ public sealed class SettingsSerializer foreach (string key in ObsoleteKeys) c.Remove(key); - c.Set("lang", s.Language == "Polski" ? "pl" : "en"); + c.Set("lang", string.IsNullOrWhiteSpace(s.LanguageCode) ? "en" : s.LanguageCode); c.SetBool("fullscreen", s.Fullscreen); c.SetBool("inactivepause", s.PauseWhenInactive); c.SetBool("pause", s.PauseOnStart); diff --git a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs index 8871522..09c6ba4 100644 --- a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs +++ b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using StarterNG.Application.Abstractions; @@ -18,26 +19,45 @@ public sealed class MiniTextureIndex : IMiniTextureIndex /// Thumbnail shown when a vehicle names one we do not have. private const string FallbackName = "other"; + /// + /// Set to 1-100 to make that percentage of thumbnails behave as if the + /// installation did not have their file - the vehicle then falls back to + /// other.bmp exactly as one naming an unknown thumbnail would. For checking + /// how a consist looks when some of its stock has no picture of its own. + /// Off unless the variable is set. + /// + public const string MissRateVariable = "STARTER_DEBUG_MISSING_MINI"; + private readonly IFileSystem _files; private readonly IGamePaths _paths; + private readonly int _missRate; private readonly object _gate = new(); private Dictionary? _index; - public MiniTextureIndex(IFileSystem files, IGamePaths paths) + public MiniTextureIndex(IFileSystem files, IGamePaths paths, IEnvironment? environment = null) { _files = files; _paths = paths; + _missRate = ReadMissRate(environment); } + /// + /// Percentage of thumbnails currently being hidden on purpose; 0 in a normal + /// run. + /// + public int SimulatedMissRate => _missRate; + public void Preload() => Index(); public bool Has(string? miniName) => - !string.IsNullOrEmpty(miniName) && Index().ContainsKey(miniName.ToLowerInvariant()); + !string.IsNullOrEmpty(miniName) && + !IsSimulatedMiss(miniName) && + Index().ContainsKey(miniName.ToLowerInvariant()); public string? PathFor(string? miniName) { - if (!string.IsNullOrEmpty(miniName) && + if (!IsSimulatedMiss(miniName) && !string.IsNullOrEmpty(miniName) && Index().TryGetValue(miniName.ToLowerInvariant(), out string? path)) return path; @@ -47,6 +67,35 @@ public sealed class MiniTextureIndex : IMiniTextureIndex public string? FallbackPath => Index().TryGetValue(FallbackName, out string? path) ? path : null; + /// + /// Whether this name is one of the thumbnails the debug switch is hiding. + /// Decided from the name itself rather than a random draw, so a vehicle keeps + /// the same answer every time the strip is rebuilt and can actually be looked + /// at. + /// + private bool IsSimulatedMiss(string? miniName) => + _missRate > 0 && !string.IsNullOrEmpty(miniName) && Bucket(miniName) < _missRate; + + /// FNV-1a over the lower-cased name, folded into 0-99. + private static int Bucket(string name) + { + uint hash = 2166136261; + foreach (char c in name.ToLowerInvariant()) + { + hash ^= c; + hash *= 16777619; + } + return (int)(hash % 100); + } + + private static int ReadMissRate(IEnvironment? environment) + { + string? raw = environment?.GetVariable(MissRateVariable); + return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int rate) + ? Math.Clamp(rate, 0, 100) + : 0; + } + private Dictionary Index() { lock (_gate) diff --git a/StarterNG/Views/Depot/MiniTextures.cs b/StarterNG/Views/Depot/MiniTextures.cs index 86a6862..787ddc4 100644 --- a/StarterNG/Views/Depot/MiniTextures.cs +++ b/StarterNG/Views/Depot/MiniTextures.cs @@ -1,6 +1,10 @@ +using System; using System.Collections.Generic; using System.IO; +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -25,6 +29,75 @@ public sealed class MiniTextures return image; } + /// Body of the drawn stand-in; the black wagon of the old starter. + private static readonly IBrush MissingBody = new SolidColorBrush(Color.Parse("#0B0C0D")); + + private static readonly IBrush MissingEdge = new SolidColorBrush(Color.Parse("#3A424A")); + + private static readonly IBrush MissingMark = new SolidColorBrush(Color.Parse("#E6E8EA")); + + /// Real minis are about 80x30, and the stand-in keeps that shape. + private const double MiniAspect = 80.0 / 30.0; + + /// + /// The stand-in for a vehicle with no thumbnail: a dark wagon marked with + /// question marks, the way the old starter drew textures/mini/other.bmp. + /// + /// + /// Reached only when the installation has no other.bmp either - a vehicle + /// naming a thumbnail we do not have already falls back to that file. Drawn + /// rather than loaded so the consist keeps its length whatever is missing; + /// a vehicle that is simply absent from the strip reads as a shorter train. + /// + public static Control MissingVisual(string? label, int height) + { + double width = height * MiniAspect; + double bodyTop = height * 0.23; + double bodyHeight = height * 0.47; + double wheelRadius = height * 0.13; + + var canvas = new Canvas { Width = width, Height = height }; + + var body = new Border + { + Width = width - 4, + Height = bodyHeight, + Background = MissingBody, + BorderBrush = MissingEdge, + BorderThickness = new Thickness(1), + Child = new TextBlock + { + Text = "? ?", + FontSize = Math.Max(9, height * 0.34), + FontWeight = FontWeight.SemiBold, + Foreground = MissingMark, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + Canvas.SetLeft(body, 2); + Canvas.SetTop(body, bodyTop); + canvas.Children.Add(body); + + foreach (double centre in new[] { width * 0.22, width * 0.78 }) + { + var wheel = new Ellipse + { + Width = wheelRadius * 2, + Height = wheelRadius * 2, + Fill = MissingBody + }; + Canvas.SetLeft(wheel, centre - wheelRadius); + Canvas.SetTop(wheel, bodyTop + bodyHeight - wheelRadius); + canvas.Children.Add(wheel); + } + + if (!string.IsNullOrWhiteSpace(label)) + ToolTip.SetTip(canvas, label); + + return canvas; + } + public Bitmap? Get(string? name, int height, bool strict = false) { if (string.IsNullOrEmpty(name)) diff --git a/StarterNG/Views/Depot/VehicleCards.cs b/StarterNG/Views/Depot/VehicleCards.cs index a4a84de..231c59c 100644 --- a/StarterNG/Views/Depot/VehicleCards.cs +++ b/StarterNG/Views/Depot/VehicleCards.cs @@ -92,18 +92,9 @@ public sealed class VehicleCards } else { - minis.Children.Add(MemberFrame(new Border - { - Height = VehicleCardStyle.ThumbHeight, Width = 90, Background = VehicleCardStyle.Placeholder, - CornerRadius = new CornerRadius(4), - Child = new TextBlock - { - Text = car.SkinFile, FontSize = 10, TextWrapping = TextWrapping.Wrap, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center - } - }, item, car, CardCaption(item, car, index))); + minis.Children.Add(MemberFrame( + MiniTextures.MissingVisual(car.SkinFile, VehicleCardStyle.ThumbHeight), + item, car, CardCaption(item, car, index))); } } diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs index 84bf2ee..3339d08 100644 --- a/StarterNG/Views/Scenarios.axaml.cs +++ b/StarterNG/Views/Scenarios.axaml.cs @@ -427,26 +427,6 @@ public partial class Scenarios : UserControl private static readonly Avalonia.Media.IBrush SelectedVehicleBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#41C400")); - private static readonly Avalonia.Media.IBrush MissingMiniBrush = - new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#22808080")); - - private static Control MissingMiniBox(string? label, int height) => new Border - { - Height = height, - Width = height * 2, - Background = MissingMiniBrush, - CornerRadius = new Avalonia.CornerRadius(4), - Child = new TextBlock - { - Text = label, - FontSize = 10, - TextWrapping = Avalonia.Media.TextWrapping.Wrap, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = Avalonia.Media.TextAlignment.Center - } - }; - private readonly Dictionary _miniCache = new(); private Bitmap? LoadMini(string? name, int height) @@ -834,7 +814,7 @@ public partial class Scenarios : UserControl Stretch = Avalonia.Media.Stretch.Uniform, Margin = new Thickness(0) }) - : MissingMiniBox(train.SkinFile, thumbH); + : MiniTextures.MissingVisual(train.SkinFile, thumbH); bool selected = !string.IsNullOrEmpty(train.Name) && string.Equals(train.Name, AppServices.Current.State.StartingVehicleName, @@ -1025,14 +1005,11 @@ public partial class Scenarios : UserControl for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++) { var source = consistStack.Children[i]; + // The ghost mirrors what the slot shows, stand-in wagons included, so + // dragging a vehicle with no thumbnail still carries something visible. Control copy = source is Border { Child: Image image } ? new Image { Source = image.Source, Height = image.Height, Stretch = image.Stretch } - : new Border - { - Width = source.Bounds.Width, - Height = source.Bounds.Height, - Background = MissingMiniBrush - }; + : MiniTextures.MissingVisual(null, (int)source.Bounds.Height); panel.Children.Add(copy); } diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs index b7a6ea2..5580b24 100644 --- a/StarterNG/Views/Settings.axaml.cs +++ b/StarterNG/Views/Settings.axaml.cs @@ -277,8 +277,9 @@ public partial class Settings : UserControl, ISettingsCapture public void CaptureInto(SimulatorSettings s) { - s.Language = (ChangeLanguageCb.SelectedItem as ComboBoxItem)?.Content?.ToString() - ?? App.Loc.CurrentLanguage; + var language = ChangeLanguageCb.SelectedItem as ComboBoxItem; + s.Language = language?.Content?.ToString() ?? App.Loc.CurrentLanguage; + s.LanguageCode = language?.Tag as string ?? App.Loc.CurrentLangCode; s.Fullscreen = IsChecked(FullscreenCb); s.PauseWhenInactive = IsChecked(PauseInactiveCb); s.PauseOnStart = IsChecked(PauseStartCb); From 673cda57d737bba880e736b3ef754bbfee8813c7 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 21:57:08 +0200 Subject: [PATCH 45/63] Keep a language code the starter cannot display in eu07.ini Matches the old starter: lower case, and an unknown code is left alone. --- StarterNG.Tests/SettingsStoreTests.cs | 12 ++++++++++++ StarterNG/App.axaml.cs | 11 +++++++++++ .../Infrastructure/Settings/SettingsSerializer.cs | 5 ++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/StarterNG.Tests/SettingsStoreTests.cs b/StarterNG.Tests/SettingsStoreTests.cs index 2403a0d..9b4c2b3 100644 --- a/StarterNG.Tests/SettingsStoreTests.cs +++ b/StarterNG.Tests/SettingsStoreTests.cs @@ -70,6 +70,18 @@ public class SettingsStoreTests Assert.Contains("lang en", installation.Files.ReadAllText(UserIni)); } + [Fact] + public void The_language_code_is_written_in_lower_case() + { + var installation = WithHome(); + installation.SettingsStore.Load(); + installation.SettingsStore.Settings.LanguageCode = "ZH"; + + installation.SettingsStore.Save(); + + Assert.Contains("lang zh", installation.Files.ReadAllText(UserIni)); + } + [Fact] public void A_first_run_falls_back_to_the_ini_shipped_with_the_installation() { diff --git a/StarterNG/App.axaml.cs b/StarterNG/App.axaml.cs index 6b363b2..52a2cad 100644 --- a/StarterNG/App.axaml.cs +++ b/StarterNG/App.axaml.cs @@ -44,7 +44,18 @@ public partial class App : Avalonia.Application }; AppServices.Current.Settings.Language = langName; + // What eu07.ini asked for, before the starter picks a language it can + // actually display. + string requestedCode = AppServices.Current.Settings.LanguageCode; + ApplyLanguage(langName); + + // The starter ships no translation for this code, so its own interface + // falls back - but the simulator has one and keeps it. Overwriting the key + // here would silently move eu07.exe to English on the next save. + if (!string.IsNullOrWhiteSpace(requestedCode) && + !string.Equals(requestedCode, Loc.CurrentLangCode, StringComparison.OrdinalIgnoreCase)) + AppServices.Current.Settings.LanguageCode = requestedCode; } public static void ApplyLanguage(string langNameOrCode) diff --git a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs index 916e159..3ad6ccd 100644 --- a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs +++ b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs @@ -171,7 +171,10 @@ public sealed class SettingsSerializer foreach (string key in ObsoleteKeys) c.Remove(key); - c.Set("lang", string.IsNullOrWhiteSpace(s.LanguageCode) ? "en" : s.LanguageCode); + // Lower case, as the simulator expects and as the old starter wrote it. + c.Set("lang", string.IsNullOrWhiteSpace(s.LanguageCode) + ? "en" + : s.LanguageCode.Trim().ToLowerInvariant()); c.SetBool("fullscreen", s.Fullscreen); c.SetBool("inactivepause", s.PauseWhenInactive); c.SetBool("pause", s.PauseOnStart); From b33d7697a01649527f216352e6698f2ccba32a1c Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:06:59 +0200 Subject: [PATCH 46/63] Fix cp1250 decoding, log unsupported loads The repositories resolved GetEncoding(1250) in their constructors, before anything registered CodePagesEncodingProvider - reads fell back to Latin-1 and Polish characters came out as mojibake. --- StarterNG.Tests/LegacyTextTests.cs | 38 ++++++++++++++++ StarterNG/Application/SettingsStore.cs | 19 +------- StarterNG/Application/StartSimulation.cs | 14 +----- StarterNG/Classes/KeyboardConfig.cs | 4 +- StarterNG/Classes/PresetStore.cs | 3 +- StarterNG/Domain/TrainsetDisplay.cs | 16 ++++--- StarterNG/Domain/UDepot.cs | 5 ++- .../Infrastructure/Adapters/LegacyText.cs | 43 +++++++++++++++++++ .../Sceneries/SceneryRepository.cs | 18 +------- .../Vehicles/FizPhysicsRepository.cs | 14 +----- .../Vehicles/LoadWeightsRepository.cs | 14 +----- .../Vehicles/TexturesTxtVehicleRepository.cs | 14 +----- StarterNG/Views/Scenarios.axaml.cs | 3 +- 13 files changed, 113 insertions(+), 92 deletions(-) create mode 100644 StarterNG.Tests/LegacyTextTests.cs create mode 100644 StarterNG/Infrastructure/Adapters/LegacyText.cs diff --git a/StarterNG.Tests/LegacyTextTests.cs b/StarterNG.Tests/LegacyTextTests.cs new file mode 100644 index 0000000..9e48eee --- /dev/null +++ b/StarterNG.Tests/LegacyTextTests.cs @@ -0,0 +1,38 @@ +using StarterNG.Infrastructure.Adapters; + +namespace StarterNG.Tests; + +public class LegacyTextTests +{ + [Fact] + public void The_game_code_page_resolves_without_anyone_registering_it_first() + { + // Regression guard: when this falls back, every Polish character in a + // scenery description comes out as Latin-1 mojibake ("Skład" as "Sk³ad"). + Assert.False(LegacyText.IsFallback); + Assert.Equal(1250, LegacyText.CodePage1250.CodePage); + } + + [Fact] + public void Polish_characters_survive_a_round_trip_through_it() + { + const string text = "Skład wygaszony pod wjazdowym. Strzebiń - Woźniki Śl."; + + byte[] bytes = LegacyText.CodePage1250.GetBytes(text); + + Assert.Equal(text, LegacyText.CodePage1250.GetString(bytes)); + } + + [Fact] + public void A_scenery_written_in_the_game_code_page_reads_back_intact() + { + var files = new StarterNG.Tests.Fakes.InMemoryFileSystem() + .WithLegacyFile(StarterNG.Tests.Fakes.TestInstallation.At("scenery", "td.scn"), + "//$n Strzebiń - Woźniki Śl. '92\n//$d Skład wygaszony pod wjazdowym."); + + var scenery = Assert.Single(new StarterNG.Tests.Fakes.TestInstallation(files).Sceneries.LoadAll()); + + Assert.Equal("Strzebiń - Woźniki Śl. '92", scenery.Name); + Assert.Equal("Skład wygaszony pod wjazdowym.", scenery.Description); + } +} diff --git a/StarterNG/Application/SettingsStore.cs b/StarterNG/Application/SettingsStore.cs index 9edb3d0..bdd40c9 100644 --- a/StarterNG/Application/SettingsStore.cs +++ b/StarterNG/Application/SettingsStore.cs @@ -5,6 +5,7 @@ using StarterNG.Application.Abstractions; using StarterNG.Classes; using StarterNG.Domain.Settings; using StarterNG.Infrastructure.Settings; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Application; @@ -50,7 +51,7 @@ public sealed class SettingsStore _paths = paths; _serializer = serializer; _executables = executables; - _encoding = LegacyEncoding(); + _encoding = LegacyText.CodePage1250; } /// The settings the application is running with. @@ -172,20 +173,4 @@ public sealed class SettingsStore public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem); - /// - /// The .ini keeps its own code page. UTF-8 is a safe fallback if the code - /// page provider is unavailable. - /// - private static Encoding LegacyEncoding() - { - try - { - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.UTF8; - } - } } diff --git a/StarterNG/Application/StartSimulation.cs b/StarterNG/Application/StartSimulation.cs index fd28b53..a6b75dd 100644 --- a/StarterNG/Application/StartSimulation.cs +++ b/StarterNG/Application/StartSimulation.cs @@ -6,6 +6,7 @@ using StarterNG.Application.Abstractions; using StarterNG.Classes; using StarterNG.Domain; using StarterNG.Domain.Settings; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Application; @@ -125,7 +126,7 @@ public sealed class StartSimulation { _files.WriteAllText(exportPath, scenery.BuildExportContent(_settings.Settings.IgnoreIrrelevantTrains, _random), - LegacyEncoding()); + LegacyText.CodePage1250); } catch (Exception ex) { @@ -174,15 +175,4 @@ public sealed class StartSimulation }; } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Classes/KeyboardConfig.cs b/StarterNG/Classes/KeyboardConfig.cs index b3fd6cd..b717853 100644 --- a/StarterNG/Classes/KeyboardConfig.cs +++ b/StarterNG/Classes/KeyboardConfig.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Classes; @@ -43,8 +44,7 @@ public sealed class KeyboardConfig try { - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - return Encoding.GetEncoding(1250); + return LegacyText.CodePage1250; } catch { diff --git a/StarterNG/Classes/PresetStore.cs b/StarterNG/Classes/PresetStore.cs index c024e57..b03a748 100644 --- a/StarterNG/Classes/PresetStore.cs +++ b/StarterNG/Classes/PresetStore.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Classes; @@ -100,7 +101,7 @@ public static class PresetStore int added = 0; try { - string text = File.ReadAllText(path, Encoding.GetEncoding(1250)); + string text = File.ReadAllText(path, LegacyText.CodePage1250); var section = Regex.Matches(text, @"\[TRAINSET\d+\s*=\s*([^\]]*)\](.*?)(?=\[TRAINSET|\z)", RegexOptions.IgnoreCase | RegexOptions.Singleline); diff --git a/StarterNG/Domain/TrainsetDisplay.cs b/StarterNG/Domain/TrainsetDisplay.cs index 040add4..10037af 100644 --- a/StarterNG/Domain/TrainsetDisplay.cs +++ b/StarterNG/Domain/TrainsetDisplay.cs @@ -169,11 +169,17 @@ public static class TrainsetDisplay if (p != null) massKg += p.Mass; - if (loadWeightKg != null && - car.LoadCount > 0 && - !string.IsNullOrEmpty(car.LoadType) && - LoadAccepted(p, car.LoadType)) - loadKg += (double)car.LoadCount * loadWeightKg(car.LoadType); + if (loadWeightKg != null && car.LoadCount > 0 && !string.IsNullOrEmpty(car.LoadType)) + { + if (LoadAccepted(p, car.LoadType)) + loadKg += (double)car.LoadCount * loadWeightKg(car.LoadType); + else + // The scenery loads something this vehicle's .fiz does not + // accept. It is left out of the gross mass, and saying so beats + // leaving the reading quietly short. + AppServices.Current.Log.Log( + $"{car.Name}: nieobslugiwany ladunek \"{car.LoadType}\""); + } } return (lengthM, massKg, loadKg); } diff --git a/StarterNG/Domain/UDepot.cs b/StarterNG/Domain/UDepot.cs index 25beae8..36ee242 100644 --- a/StarterNG/Domain/UDepot.cs +++ b/StarterNG/Domain/UDepot.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using StarterNG.Classes; using StarterNG.Infrastructure; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Domain; @@ -40,7 +41,7 @@ public static class UDepot string text; try { - text = File.ReadAllText(file, Encoding.GetEncoding(1250)); + text = File.ReadAllText(file, LegacyText.CodePage1250); } catch (Exception ex) { @@ -141,7 +142,7 @@ public static class UDepot string? dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - File.WriteAllText(path, sb.ToString(), Encoding.GetEncoding(1250)); + File.WriteAllText(path, sb.ToString(), LegacyText.CodePage1250); return true; } catch (Exception ex) diff --git a/StarterNG/Infrastructure/Adapters/LegacyText.cs b/StarterNG/Infrastructure/Adapters/LegacyText.cs new file mode 100644 index 0000000..aa48b34 --- /dev/null +++ b/StarterNG/Infrastructure/Adapters/LegacyText.cs @@ -0,0 +1,43 @@ +using System; +using System.Text; + +namespace StarterNG.Infrastructure.Adapters; + +/// +/// The code page every file the simulator owns is written in: sceneries, +/// textures.txt, .fiz, timetables, eu07.ini. +/// +/// +/// Code page 1250 is not built into .NET; it arrives with +/// , which has to be registered before the +/// first GetEncoding call. Doing that registration at each call site is how +/// Polish characters end up as Latin-1 mojibake ("Skład" as "Sk³ad"): whichever +/// site runs first wins, and the composition root builds its repositories before +/// any of them. One property, resolved once, removes the ordering question. +/// +public static class LegacyText +{ + /// The .scn / .fiz / .ini code page, or Latin-1 if it is unavailable. + public static Encoding CodePage1250 { get; } = Resolve(); + + /// + /// True when the code page could not be loaded and text is being read as + /// Latin-1 instead. Polish characters will be wrong; the startup check + /// reports it rather than leaving it to be discovered on screen. + /// + public static bool IsFallback { get; private set; } + + private static Encoding Resolve() + { + try + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Encoding.GetEncoding(1250); + } + catch (Exception) + { + IsFallback = true; + return Encoding.Latin1; + } + } +} diff --git a/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs b/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs index afcb536..e8308a2 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using StarterNG.Application.Abstractions; using StarterNG.Domain.Sceneries; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Sceneries; @@ -36,7 +37,7 @@ public sealed class SceneryRepository : ISceneryRepository _paths = paths; _log = log; _parser = parser; - _encoding = CodePage1250(); + _encoding = LegacyText.CodePage1250; } public IReadOnlyList LoadAll(IProgress? progress = null) @@ -83,19 +84,4 @@ public sealed class SceneryRepository : ISceneryRepository _files.GetFiles(_paths.Scenery, "*.scn") .Where(path => Path.GetFileName(path).FirstOrDefault() != ExcludedPrefix); - /// - /// The .scn dialect predates Unicode. The provider is registered by the entry - /// point; fall back to Latin-1 if that ever fails so loading still works. - /// - private static Encoding CodePage1250() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs b/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs index 1856982..b530a59 100644 --- a/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using StarterNG.Application.Abstractions; using StarterNG.Domain.Vehicles; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; @@ -51,7 +52,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository _files = files; _paths = paths; _log = log; - _encoding = LegacyEncoding(); + _encoding = LegacyText.CodePage1250; } public void Preload() => Index(); @@ -270,15 +271,4 @@ public sealed class FizPhysicsRepository : IPhysicsRepository .ToList(); } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs index 486c991..8639771 100644 --- a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using StarterNG.Application.Abstractions; using StarterNG.Domain.Vehicles; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; @@ -51,7 +52,7 @@ public sealed class LoadWeightsRepository try { - return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyEncoding()))); + return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyText.CodePage1250))); } catch (Exception ex) { @@ -100,15 +101,4 @@ public sealed class LoadWeightsRepository return weights; } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs b/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs index 6c959ab..d2f709c 100644 --- a/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text; using StarterNG.Application.Abstractions; using StarterNG.Domain.Vehicles; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; @@ -28,7 +29,7 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository _paths = paths; _log = log; _parser = parser; - _encoding = LegacyEncoding(); + _encoding = LegacyText.CodePage1250; } public int Load(VehicleCatalog catalog) @@ -98,15 +99,4 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository return relative.Replace('\\', '/').TrimEnd('/') + "/"; } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs index 3339d08..a55b984 100644 --- a/StarterNG/Views/Scenarios.axaml.cs +++ b/StarterNG/Views/Scenarios.axaml.cs @@ -20,6 +20,7 @@ using StarterNG.Domain; using StarterNG.Application; using StarterNG.Domain.Sceneries; using StarterNG.Presentation.Scenarios; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Views; @@ -708,7 +709,7 @@ public partial class Scenarios : UserControl try { - timetableContent.Text = File.ReadAllText(path, Encoding.GetEncoding(1250)); + timetableContent.Text = File.ReadAllText(path, LegacyText.CodePage1250); UpdateTimetableTab(true); } catch From 6a21a2fd27e50f7c268e5b590bb5dec9013cf357 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:10:25 +0200 Subject: [PATCH 47/63] Temporarily make half the thumbnails act as missing --- StarterNG.Tests/MiniTextureIndexTests.cs | 20 +++++++++++++---- .../Vehicles/MiniTextureIndex.cs | 22 ++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/StarterNG.Tests/MiniTextureIndexTests.cs b/StarterNG.Tests/MiniTextureIndexTests.cs index a4b61d0..294c292 100644 --- a/StarterNG.Tests/MiniTextureIndexTests.cs +++ b/StarterNG.Tests/MiniTextureIndexTests.cs @@ -12,7 +12,7 @@ public class MiniTextureIndexTests "406r", "424z", "621z", "z1kd", "gags", "eaos", "falns", "uacs", "sgs", "kbs" }; - private static MiniTextureIndex Build(int missRate) + private static MiniTextureIndex Build(int? missRate) { var files = new InMemoryFileSystem(); foreach (string name in Names) @@ -20,14 +20,14 @@ public class MiniTextureIndexTests files.WithFile(TestInstallation.At("textures", "mini", "other.bmp"), "bitmap"); var environment = new FakeEnvironment(); - if (missRate > 0) - environment.With(MiniTextureIndex.MissRateVariable, missRate.ToString()); + if (missRate is { } rate) + environment.With(MiniTextureIndex.MissRateVariable, rate.ToString()); return new MiniTextureIndex(files, new GamePaths(TestInstallation.Root), environment); } [Fact] - public void Without_the_debug_variable_every_thumbnail_loads() + public void Setting_the_variable_to_zero_shows_the_installation_as_it_is() { var index = Build(missRate: 0); @@ -35,6 +35,18 @@ public class MiniTextureIndexTests Assert.All(Names, name => Assert.True(index.Has(name))); } + [Fact] + public void The_temporary_default_still_hides_half_the_thumbnails() + { + // Guards the testing default in MiniTextureIndex.DefaultMissRate. When + // that goes back to 0 for release, this test is what says so - change it + // together with the constant, do not delete it quietly. + var index = Build(missRate: null); + + Assert.Equal(50, index.SimulatedMissRate); + Assert.Contains(Names, name => !index.Has(name)); + } + [Fact] public void The_debug_variable_hides_roughly_the_share_it_names() { diff --git a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs index 09c6ba4..0e91337 100644 --- a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs +++ b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs @@ -20,14 +20,24 @@ public sealed class MiniTextureIndex : IMiniTextureIndex private const string FallbackName = "other"; /// - /// Set to 1-100 to make that percentage of thumbnails behave as if the - /// installation did not have their file - the vehicle then falls back to - /// other.bmp exactly as one naming an unknown thumbnail would. For checking - /// how a consist looks when some of its stock has no picture of its own. - /// Off unless the variable is set. + /// Set to 0-100 to override : the percentage of + /// thumbnails that behave as if the installation did not have their file, so + /// the vehicle falls back to other.bmp exactly as one naming an unknown + /// thumbnail would. Set it to 0 to see the installation as it really is. /// public const string MissRateVariable = "STARTER_DEBUG_MISSING_MINI"; + /// + /// TEMPORARY, FOR TESTING THE MISSING-THUMBNAIL DRAWING. Half the vehicles in + /// every consist will show the other.bmp stand-in instead of their own + /// picture, whether or not the installation is complete. + /// + /// Set this back to 0 before release - it is the whole of the switch, and + /// MiniTextureIndexTests.The_temporary_default_still_hides_half_the_thumbnails + /// fails the moment it changes, so nothing ships it by accident. + /// + private const int DefaultMissRate = 50; + private readonly IFileSystem _files; private readonly IGamePaths _paths; private readonly int _missRate; @@ -93,7 +103,7 @@ public sealed class MiniTextureIndex : IMiniTextureIndex string? raw = environment?.GetVariable(MissRateVariable); return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int rate) ? Math.Clamp(rate, 0, 100) - : 0; + : DefaultMissRate; } private Dictionary Index() From 7849ef9729efd071c788b445fea1f7cb74f3e41d Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:19:02 +0200 Subject: [PATCH 48/63] Remove the missing-thumbnail mocking Leaves the plain chain: own thumbnail, other.bmp, drawn wagon. --- StarterNG.Tests/MiniTextureIndexTests.cs | 93 ++++++------------- StarterNG/Application/AppServices.cs | 8 +- .../Vehicles/MiniTextureIndex.cs | 65 +------------ 3 files changed, 34 insertions(+), 132 deletions(-) diff --git a/StarterNG.Tests/MiniTextureIndexTests.cs b/StarterNG.Tests/MiniTextureIndexTests.cs index 294c292..9133c66 100644 --- a/StarterNG.Tests/MiniTextureIndexTests.cs +++ b/StarterNG.Tests/MiniTextureIndexTests.cs @@ -6,83 +6,50 @@ namespace StarterNG.Tests; public class MiniTextureIndexTests { - private static readonly string[] Names = - { - "ep07", "et22", "111a", "112a", "203v", "401k", "en57", "sm42", "st44", "412w", - "406r", "424z", "621z", "z1kd", "gags", "eaos", "falns", "uacs", "sgs", "kbs" - }; - - private static MiniTextureIndex Build(int? missRate) + private static MiniTextureIndex Build(params string[] thumbnails) { var files = new InMemoryFileSystem(); - foreach (string name in Names) + foreach (string name in thumbnails) files.WithFile(TestInstallation.At("textures", "mini", name + ".bmp"), "bitmap"); - files.WithFile(TestInstallation.At("textures", "mini", "other.bmp"), "bitmap"); - var environment = new FakeEnvironment(); - if (missRate is { } rate) - environment.With(MiniTextureIndex.MissRateVariable, rate.ToString()); - - return new MiniTextureIndex(files, new GamePaths(TestInstallation.Root), environment); + return new MiniTextureIndex(files, new GamePaths(TestInstallation.Root)); } [Fact] - public void Setting_the_variable_to_zero_shows_the_installation_as_it_is() + public void A_thumbnail_is_found_by_name_whatever_its_case() { - var index = Build(missRate: 0); + var index = Build("ep07", "other"); - Assert.Equal(0, index.SimulatedMissRate); - Assert.All(Names, name => Assert.True(index.Has(name))); + Assert.True(index.Has("EP07")); + Assert.EndsWith("ep07.bmp", index.PathFor("ep07")); } [Fact] - public void The_temporary_default_still_hides_half_the_thumbnails() + public void A_vehicle_naming_an_unknown_thumbnail_gets_other_bmp() { - // Guards the testing default in MiniTextureIndex.DefaultMissRate. When - // that goes back to 0 for release, this test is what says so - change it - // together with the constant, do not delete it quietly. - var index = Build(missRate: null); - - Assert.Equal(50, index.SimulatedMissRate); - Assert.Contains(Names, name => !index.Has(name)); - } - - [Fact] - public void The_debug_variable_hides_roughly_the_share_it_names() - { - var index = Build(missRate: 50); - - int hidden = Names.Count(name => !index.Has(name)); - - // Twenty names is a small sample, so this only asserts that the switch - // bites and does not take everything with it. - Assert.InRange(hidden, 3, Names.Length - 3); - } - - [Fact] - public void A_hidden_thumbnail_stays_hidden_rather_than_flickering() - { - var index = Build(missRate: 50); - string hidden = Names.First(name => !index.Has(name)); - - for (int i = 0; i < 20; i++) - Assert.False(index.Has(hidden)); - } - - [Fact] - public void A_hidden_thumbnail_falls_back_to_other_bmp_like_an_unknown_one() - { - var index = Build(missRate: 50); - string hidden = Names.First(name => !index.Has(name)); - - Assert.EndsWith("other.bmp", index.PathFor(hidden)); - } - - [Fact] - public void An_unknown_name_still_falls_back_to_other_bmp_in_a_normal_run() - { - var index = Build(missRate: 0); + var index = Build("ep07", "other"); + Assert.False(index.Has("nie-ma-takiej")); Assert.EndsWith("other.bmp", index.PathFor("nie-ma-takiej")); } + + [Fact] + public void Without_other_bmp_there_is_nothing_to_fall_back_to() + { + var index = Build("ep07"); + + // The views draw their own stand-in wagon from here on; see + // MiniTextures.MissingVisual. + Assert.Null(index.PathFor("nie-ma-takiej")); + Assert.Null(index.FallbackPath); + } + + [Fact] + public void An_installation_with_no_thumbnails_at_all_resolves_nothing() + { + var index = Build(); + + Assert.False(index.Has("ep07")); + Assert.Null(index.PathFor("ep07")); + } } diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs index dd65ffb..d12d0f9 100644 --- a/StarterNG/Application/AppServices.cs +++ b/StarterNG/Application/AppServices.cs @@ -43,19 +43,13 @@ public sealed class AppServices Localization = new LocalizationService(log); Sceneries = new SceneryRepository(files, paths, log, new SceneryParser(clock, Random)); SceneryImages = new SceneryImageLocator(files); - MiniTextures = new MiniTextureIndex(files, paths, environment); + MiniTextures = new MiniTextureIndex(files, paths); Vehicles = new TexturesTxtVehicleRepository(files, paths, log, new TexturesTxtParser()); Physics = new FizPhysicsRepository(files, paths, log); MissingAssets = new MissingAssetScanner(files, paths); SceneryTexts = new SceneryTranslations(files, log); Timetables = new TimetableLocator(files); LoadWeights = new LoadWeightsRepository(files, paths, log); - // Leaves a trail when the thumbnail debug switch is on, so a run with - // missing minis is never mistaken for a broken installation. - if (MiniTextures is MiniTextureIndex { SimulatedMissRate: > 0 } debugMinis) - log.Log($"{MiniTextureIndex.MissRateVariable}={debugMinis.SimulatedMissRate}: " + - "ukrywam ten procent miniatur pojazdow"); - Library = new GameLibrary(Vehicles, Sceneries, MiniTextures, Physics, log); State = new AppState(); diff --git a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs index 0e91337..8871522 100644 --- a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs +++ b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using StarterNG.Application.Abstractions; @@ -19,55 +18,26 @@ public sealed class MiniTextureIndex : IMiniTextureIndex /// Thumbnail shown when a vehicle names one we do not have. private const string FallbackName = "other"; - /// - /// Set to 0-100 to override : the percentage of - /// thumbnails that behave as if the installation did not have their file, so - /// the vehicle falls back to other.bmp exactly as one naming an unknown - /// thumbnail would. Set it to 0 to see the installation as it really is. - /// - public const string MissRateVariable = "STARTER_DEBUG_MISSING_MINI"; - - /// - /// TEMPORARY, FOR TESTING THE MISSING-THUMBNAIL DRAWING. Half the vehicles in - /// every consist will show the other.bmp stand-in instead of their own - /// picture, whether or not the installation is complete. - /// - /// Set this back to 0 before release - it is the whole of the switch, and - /// MiniTextureIndexTests.The_temporary_default_still_hides_half_the_thumbnails - /// fails the moment it changes, so nothing ships it by accident. - /// - private const int DefaultMissRate = 50; - private readonly IFileSystem _files; private readonly IGamePaths _paths; - private readonly int _missRate; private readonly object _gate = new(); private Dictionary? _index; - public MiniTextureIndex(IFileSystem files, IGamePaths paths, IEnvironment? environment = null) + public MiniTextureIndex(IFileSystem files, IGamePaths paths) { _files = files; _paths = paths; - _missRate = ReadMissRate(environment); } - /// - /// Percentage of thumbnails currently being hidden on purpose; 0 in a normal - /// run. - /// - public int SimulatedMissRate => _missRate; - public void Preload() => Index(); public bool Has(string? miniName) => - !string.IsNullOrEmpty(miniName) && - !IsSimulatedMiss(miniName) && - Index().ContainsKey(miniName.ToLowerInvariant()); + !string.IsNullOrEmpty(miniName) && Index().ContainsKey(miniName.ToLowerInvariant()); public string? PathFor(string? miniName) { - if (!IsSimulatedMiss(miniName) && !string.IsNullOrEmpty(miniName) && + if (!string.IsNullOrEmpty(miniName) && Index().TryGetValue(miniName.ToLowerInvariant(), out string? path)) return path; @@ -77,35 +47,6 @@ public sealed class MiniTextureIndex : IMiniTextureIndex public string? FallbackPath => Index().TryGetValue(FallbackName, out string? path) ? path : null; - /// - /// Whether this name is one of the thumbnails the debug switch is hiding. - /// Decided from the name itself rather than a random draw, so a vehicle keeps - /// the same answer every time the strip is rebuilt and can actually be looked - /// at. - /// - private bool IsSimulatedMiss(string? miniName) => - _missRate > 0 && !string.IsNullOrEmpty(miniName) && Bucket(miniName) < _missRate; - - /// FNV-1a over the lower-cased name, folded into 0-99. - private static int Bucket(string name) - { - uint hash = 2166136261; - foreach (char c in name.ToLowerInvariant()) - { - hash ^= c; - hash *= 16777619; - } - return (int)(hash % 100); - } - - private static int ReadMissRate(IEnvironment? environment) - { - string? raw = environment?.GetVariable(MissRateVariable); - return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int rate) - ? Math.Clamp(rate, 0, 100) - : DefaultMissRate; - } - private Dictionary Index() { lock (_gate) From df21ec104fac7f024e090a806598b0ff7f5cef43 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:24:54 +0200 Subject: [PATCH 49/63] Cut the comments from the refactored code Three survive: why AppServices is hand-wired, the cp1250 registration trap, and the IsDieselEngine note carried over from Physics.cs. --- StarterNG/Application/Abstractions/IClock.cs | 5 -- .../Abstractions/IDiagnosticsLog.cs | 6 --- .../Application/Abstractions/IEnvironment.cs | 6 --- .../Application/Abstractions/IFileSystem.cs | 22 --------- .../Application/Abstractions/IGamePaths.cs | 14 ------ .../Abstractions/ILocalizedStrings.cs | 10 ---- .../Abstractions/IMiniTextureIndex.cs | 7 --- .../Abstractions/IPhysicsRepository.cs | 9 ---- .../Abstractions/IProcessLauncher.cs | 20 -------- .../Application/Abstractions/IRandomSource.cs | 7 --- .../Abstractions/ISceneryRepository.cs | 10 ---- .../Abstractions/ISceneryTranslations.cs | 13 ----- .../Abstractions/IVehicleRepository.cs | 9 ---- StarterNG/Application/AppServices.cs | 35 ++------------ StarterNG/Application/AppState.cs | 10 ---- StarterNG/Application/GameLibrary.cs | 18 ------- StarterNG/Application/MissingVehicleLog.cs | 9 ---- StarterNG/Application/SettingsStore.cs | 33 ------------- StarterNG/Application/StartSimulation.cs | 38 --------------- StarterNG/Controls/DetailRows.cs | 23 --------- StarterNG/Controls/SettingRow.cs | 20 -------- StarterNG/Controls/SettingsSection.cs | 5 -- StarterNG/Domain/Sceneries/Scenery.cs | 48 ------------------- StarterNG/Domain/Sceneries/SceneryWeather.cs | 24 ---------- .../Domain/Sceneries/SceneryWeatherWriter.cs | 17 ------- .../Domain/Settings/SimulatorSettings.cs | 45 ----------------- StarterNG/Domain/Vehicles/LoadWeightsTable.cs | 11 ----- StarterNG/Domain/Vehicles/VehicleCatalog.cs | 30 ------------ StarterNG/Domain/Vehicles/VehiclePhysics.cs | 14 ------ StarterNG/Domain/Vehicles/VehicleTexture.cs | 23 --------- .../Adapters/FileDiagnosticsLog.cs | 16 ------- .../Infrastructure/Adapters/GamePaths.cs | 5 -- .../Infrastructure/Adapters/LegacyText.cs | 6 --- .../Adapters/PhysicalFileSystem.cs | 4 -- .../Infrastructure/Adapters/SystemClock.cs | 1 - .../Adapters/SystemEnvironment.cs | 1 - .../Adapters/SystemProcessLauncher.cs | 4 -- .../Adapters/SystemRandomSource.cs | 1 - StarterNG/Infrastructure/Diagnostics.cs | 13 ----- StarterNG/Infrastructure/InstallationCheck.cs | 8 ---- .../Sceneries/SceneryImageLocator.cs | 10 ---- .../Infrastructure/Sceneries/SceneryParser.cs | 25 ---------- .../Sceneries/SceneryRepository.cs | 10 ---- .../Sceneries/SceneryTranslations.cs | 14 ------ .../Sceneries/TimetableLocator.cs | 9 ---- .../Settings/ExecutableLocator.cs | 22 --------- .../Infrastructure/Settings/SettingsPaths.cs | 6 --- .../Settings/SettingsSerializer.cs | 17 ------- StarterNG/Infrastructure/Shortcuts.cs | 14 ------ StarterNG/Infrastructure/TextMatch.cs | 8 ---- .../Vehicles/FizPhysicsRepository.cs | 23 --------- .../Vehicles/LoadWeightsRepository.cs | 11 ----- .../Vehicles/MiniTextureIndex.cs | 9 ---- .../Vehicles/MissingAssetScanner.cs | 8 ---- .../Vehicles/TexturesTxtParser.cs | 26 ---------- .../Vehicles/TexturesTxtVehicleRepository.cs | 15 ------ StarterNG/Presentation/Scenarios/FogScale.cs | 12 ----- .../Presentation/Scenarios/SceneryTree.cs | 21 -------- .../Presentation/Scenarios/SeasonDates.cs | 14 ------ StarterNG/Views/ShortcutsWindow.cs | 4 -- 60 files changed, 3 insertions(+), 875 deletions(-) diff --git a/StarterNG/Application/Abstractions/IClock.cs b/StarterNG/Application/Abstractions/IClock.cs index b3fa40d..97f0c2c 100644 --- a/StarterNG/Application/Abstractions/IClock.cs +++ b/StarterNG/Application/Abstractions/IClock.cs @@ -2,11 +2,6 @@ namespace StarterNG.Application.Abstractions; using System; -/// -/// Port over the system clock. Scenery weather defaults, timetable rendering and -/// log timestamps all depend on "now"; routing them through a clock keeps those -/// rules deterministic under test. -/// public interface IClock { DateTime Now { get; } diff --git a/StarterNG/Application/Abstractions/IDiagnosticsLog.cs b/StarterNG/Application/Abstractions/IDiagnosticsLog.cs index aa049b4..a5a0765 100644 --- a/StarterNG/Application/Abstractions/IDiagnosticsLog.cs +++ b/StarterNG/Application/Abstractions/IDiagnosticsLog.cs @@ -3,11 +3,6 @@ using System.Collections.Generic; namespace StarterNG.Application.Abstractions; -/// -/// Port over the starter's error log. Domain and application code depends on -/// this interface only; the file-backed implementation and the message box that -/// surfaces faults to the user live in Infrastructure and Presentation. -/// public interface IDiagnosticsLog { void Log(string message); @@ -16,7 +11,6 @@ public interface IDiagnosticsLog void Log(IEnumerable messages); - /// Everything logged so far, newline separated. string Text { get; } void Clear(); diff --git a/StarterNG/Application/Abstractions/IEnvironment.cs b/StarterNG/Application/Abstractions/IEnvironment.cs index aad2b85..bc614ab 100644 --- a/StarterNG/Application/Abstractions/IEnvironment.cs +++ b/StarterNG/Application/Abstractions/IEnvironment.cs @@ -1,9 +1,5 @@ namespace StarterNG.Application.Abstractions; -/// -/// The bits of the host machine the starter has to ask about: which operating -/// system it is on, and where that system keeps per-user configuration. -/// public interface IEnvironment { bool IsWindows { get; } @@ -12,9 +8,7 @@ public interface IEnvironment bool IsMacOS { get; } - /// An environment variable, or null when it is not set. string? GetVariable(string name); - /// The folder the starter's own executable lives in. string BaseDirectory { get; } } diff --git a/StarterNG/Application/Abstractions/IFileSystem.cs b/StarterNG/Application/Abstractions/IFileSystem.cs index fe381c5..93c1083 100644 --- a/StarterNG/Application/Abstractions/IFileSystem.cs +++ b/StarterNG/Application/Abstractions/IFileSystem.cs @@ -5,18 +5,6 @@ using System.Text; namespace StarterNG.Application.Abstractions; -/// -/// Port over the file system. Everything that reads or writes the MaSzyna -/// installation goes through this interface instead of and -/// , so parsers and use cases can be exercised against an -/// in-memory installation in tests. -/// -/// -/// Kept deliberately narrow: only the operations the application actually -/// performs are exposed. Paths are passed through verbatim to the adapter, which -/// resolves them relative to the process working directory just like the -/// framework methods it replaces. -/// public interface IFileSystem { bool FileExists(string path); @@ -45,23 +33,13 @@ public interface IFileSystem void CopyFile(string sourcePath, string destinationPath, bool overwrite); - /// Top-level files, empty when the directory does not exist. IReadOnlyList GetFiles(string path, string searchPattern = "*"); - /// Top-level directories, empty when the directory does not exist. IReadOnlyList GetDirectories(string path); - /// - /// Files matching the pattern anywhere below the path. Empty when the - /// directory does not exist. - /// IReadOnlyList GetFilesRecursive(string path, string searchPattern); DateTime GetLastWriteTimeUtc(string path); - /// - /// Whether the file carries an execute bit. Always true on Windows, where - /// executability is a property of the file's contents rather than its mode. - /// bool IsExecutable(string path); } diff --git a/StarterNG/Application/Abstractions/IGamePaths.cs b/StarterNG/Application/Abstractions/IGamePaths.cs index 464cf37..314647c 100644 --- a/StarterNG/Application/Abstractions/IGamePaths.cs +++ b/StarterNG/Application/Abstractions/IGamePaths.cs @@ -1,34 +1,20 @@ namespace StarterNG.Application.Abstractions; -/// -/// The layout of a MaSzyna installation. Replaces the -/// Directory.GetCurrentDirectory() calls and hard-coded "scenery/", -/// "dynamic", "textures/mini/" literals that were previously spread across -/// parsers, views and the loader. -/// public interface IGamePaths { - /// Installation root: the folder holding eu07.exe. string Root { get; } - /// Scenery definitions (*.scn). string Scenery { get; } - /// Rolling stock: dynamic/<maker>/<vehicle>/textures.txt. string Dynamic { get; } - /// Vehicle thumbnails used by the depot and the consist strip. string MiniTextures { get; } - /// Simulator data files (load_weights.txt and friends). string Data { get; } - /// Starter-owned configuration: presets, profiles, key bindings. string StarterConfig { get; } - /// The starter's own error log (starter/bledy.txt). string DiagnosticsLog { get; } - /// Resolves a path relative to . string FromRoot(params string[] segments); } diff --git a/StarterNG/Application/Abstractions/ILocalizedStrings.cs b/StarterNG/Application/Abstractions/ILocalizedStrings.cs index f72ce5b..826d732 100644 --- a/StarterNG/Application/Abstractions/ILocalizedStrings.cs +++ b/StarterNG/Application/Abstractions/ILocalizedStrings.cs @@ -1,19 +1,9 @@ namespace StarterNG.Application.Abstractions; -/// -/// Port over the translation table. Anything that needs a user-facing string -/// takes this, rather than reaching for the concrete localization service or a -/// static on App. -/// public interface ILocalizedStrings { - /// - /// Translation for a key. Missing keys return the key itself (and are logged - /// once), so an untranslated screen stays usable. - /// string this[string key] { get; } - /// Lookup for a key that may legitimately be absent; "" when missing. string Opt(string key); string CurrentLanguage { get; } diff --git a/StarterNG/Application/Abstractions/IMiniTextureIndex.cs b/StarterNG/Application/Abstractions/IMiniTextureIndex.cs index d876765..1edadd4 100644 --- a/StarterNG/Application/Abstractions/IMiniTextureIndex.cs +++ b/StarterNG/Application/Abstractions/IMiniTextureIndex.cs @@ -1,19 +1,12 @@ namespace StarterNG.Application.Abstractions; -/// -/// The thumbnails in textures/mini, looked up by name. Backed by one directory -/// scan; a name with no bitmap falls back to the generic "other" thumbnail. -/// public interface IMiniTextureIndex { - /// Builds the index up front, off the UI thread. void Preload(); bool Has(string? miniName); - /// Bitmap for the name, the fallback thumbnail, or null when neither exists. string? PathFor(string? miniName); - /// The generic thumbnail shown for unknown stock. string? FallbackPath { get; } } diff --git a/StarterNG/Application/Abstractions/IPhysicsRepository.cs b/StarterNG/Application/Abstractions/IPhysicsRepository.cs index 44e9a63..d479167 100644 --- a/StarterNG/Application/Abstractions/IPhysicsRepository.cs +++ b/StarterNG/Application/Abstractions/IPhysicsRepository.cs @@ -2,20 +2,11 @@ using StarterNG.Domain.Vehicles; namespace StarterNG.Application.Abstractions; -/// -/// Looks up the .fiz physics of a vehicle type by model name. -/// public interface IPhysicsRepository { - /// Indexes the installation's .fiz files up front, off the UI thread. void Preload(); - /// How many vehicle types were indexed; zero means a broken installation. int IndexedCount { get; } - /// - /// Physics for a model name (with or without extension), or null when the - /// installation has no .fiz for it. - /// VehiclePhysics? For(string? dataFolder, string? model); } diff --git a/StarterNG/Application/Abstractions/IProcessLauncher.cs b/StarterNG/Application/Abstractions/IProcessLauncher.cs index 7029f36..a8ecab1 100644 --- a/StarterNG/Application/Abstractions/IProcessLauncher.cs +++ b/StarterNG/Application/Abstractions/IProcessLauncher.cs @@ -4,12 +4,6 @@ using System.Threading.Tasks; namespace StarterNG.Application.Abstractions; -/// -/// A process the starter has launched or adopted. Exposes only what the starter -/// needs — whether the simulator is still up, and a way to await its exit — so -/// the presentation layer never handles -/// directly. -/// public interface IProcessHandle { bool HasExited { get; } @@ -17,26 +11,12 @@ public interface IProcessHandle Task WaitForExitAsync(CancellationToken cancellationToken = default); } -/// -/// Port over process creation: starting the simulator, opening a file, folder or -/// URL in the desktop shell, and adopting an already running simulator. -/// public interface IProcessLauncher { - /// - /// Starts an executable with the given arguments. Returns null when the - /// process could not be created; then carries the - /// reason for display. - /// IProcessHandle? Start(string executablePath, IReadOnlyList arguments, string? workingDirectory, out string? error); - /// - /// Hands a path or URL to the desktop shell (explorer, xdg-open, ...). - /// Returns false and logs when the shell refused it. - /// bool OpenInShell(string pathOrUrl); - /// The first running process with that name, or null. IProcessHandle? FindRunning(string processName); } diff --git a/StarterNG/Application/Abstractions/IRandomSource.cs b/StarterNG/Application/Abstractions/IRandomSource.cs index f5f57b2..b4403a2 100644 --- a/StarterNG/Application/Abstractions/IRandomSource.cs +++ b/StarterNG/Application/Abstractions/IRandomSource.cs @@ -1,15 +1,8 @@ namespace StarterNG.Application.Abstractions; -/// -/// Port over randomness. Scenery weather deliberately varies between runs (fog -/// distance, overcast presets, texture randomiser); routing it through a port -/// keeps those rules assertable instead of flaky. -/// public interface IRandomSource { - /// Inclusive lower bound, exclusive upper bound. int Next(int minInclusive, int maxExclusive); - /// Uniform value in [0, 1). double NextDouble(); } diff --git a/StarterNG/Application/Abstractions/ISceneryRepository.cs b/StarterNG/Application/Abstractions/ISceneryRepository.cs index f923fc7..031b05b 100644 --- a/StarterNG/Application/Abstractions/ISceneryRepository.cs +++ b/StarterNG/Application/Abstractions/ISceneryRepository.cs @@ -4,21 +4,11 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Application.Abstractions; -/// How far a scenery sweep has got, for the splash screen. public readonly record struct SceneryLoadProgress(int Loaded, int Total, string FileName); -/// -/// Reads the scenarios of an installation. The only route from a .scn on disk to -/// a ; nothing else constructs one from a path. -/// public interface ISceneryRepository { - /// - /// Every scenario in the installation, in no particular order. Files that - /// fail to parse are logged and skipped rather than failing the load. - /// IReadOnlyList LoadAll(IProgress? progress = null); - /// Reloads a single scenario; null when it is gone or unreadable. Scenery? Load(string path); } diff --git a/StarterNG/Application/Abstractions/ISceneryTranslations.cs b/StarterNG/Application/Abstractions/ISceneryTranslations.cs index 4ffdc17..de599ce 100644 --- a/StarterNG/Application/Abstractions/ISceneryTranslations.cs +++ b/StarterNG/Application/Abstractions/ISceneryTranslations.cs @@ -2,22 +2,9 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Application.Abstractions; -/// -/// The translations a scenery ships alongside itself, in scenery/i18n/*.json. -/// -/// -/// Scenery text may be an "@key" reference rather than literal prose, resolved -/// against the currently loaded scenery's table. The table is per scenery, so -/// replaces it rather than adding to it. -/// public interface ISceneryTranslations { - /// Loads the tables for one scenery, English first then the user's language. void LoadFor(Scenery scenery, string langCode); - /// - /// Resolves an "@key" reference against the loaded table. Text that is not a - /// reference, or a key with no translation, comes back unchanged. - /// string Translate(string? text); } diff --git a/StarterNG/Application/Abstractions/IVehicleRepository.cs b/StarterNG/Application/Abstractions/IVehicleRepository.cs index d32247b..5e285f2 100644 --- a/StarterNG/Application/Abstractions/IVehicleRepository.cs +++ b/StarterNG/Application/Abstractions/IVehicleRepository.cs @@ -2,16 +2,7 @@ using StarterNG.Domain.Vehicles; namespace StarterNG.Application.Abstractions; -/// -/// Reads the installation's rolling stock. The only route from the dynamic/ -/// folder to a . -/// public interface IVehicleRepository { - /// - /// Fills the catalogue from every textures.txt under dynamic/. Returns the - /// number of liveries read, which the caller uses to spot an installation - /// with no rolling stock at all. - /// int Load(VehicleCatalog catalog); } diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs index d12d0f9..e223746 100644 --- a/StarterNG/Application/AppServices.cs +++ b/StarterNG/Application/AppServices.cs @@ -9,22 +9,9 @@ using StarterNG.Services; namespace StarterNG.Application; -/// -/// The composition root: the single place where concrete adapters are chosen and -/// wired to the ports the rest of the application depends on. -/// -/// -/// Hand-written on purpose. The starter publishes with PublishAot and -/// PublishTrimmed, which rules out reflection-based containers; the object -/// graph is small enough that explicit construction is also the clearest -/// documentation of what depends on what. -/// -/// is a service locator and is used in exactly one kind of -/// place — the entry point and the view constructors Avalonia creates for us, -/// which cannot take arguments. Everything below that boundary receives its -/// dependencies through its constructor; new code must not reach for -/// . -/// +// Skladany recznie: PublishAot i PublishTrimmed wykluczaja kontener oparty na +// refleksji. Current jest service locatorem i wolno go czytac tylko z entry +// pointu i z konstruktorow widokow, ktorych Avalonia nie potrafi parametryzowac. public sealed class AppServices { private static AppServices? _current; @@ -61,16 +48,10 @@ public sealed class AppServices StartSimulation = new StartSimulation(State, SettingsStore, files, Processes, Random, log); } - /// - /// The graph built by the entry point. Throws when read before - /// , which would mean a static initializer somewhere - /// ran ahead of the composition root. - /// public static AppServices Current => _current ?? throw new InvalidOperationException( "AppServices.Current read before Initialize(); the composition root must be built first."); - /// Builds the production graph. Called once, from the entry point. public static AppServices Initialize(IGamePaths? paths = null, IFileSystem? files = null, IClock? clock = null, IEnvironment? environment = null) { @@ -114,16 +95,12 @@ public sealed class AppServices public LoadWeightsRepository LoadWeights { get; } - /// Everything read out of the installation: rolling stock and scenarios. public GameLibrary Library { get; } - /// What the user currently has selected. public AppState State { get; } - /// The settings the starter runs with, and their load/save lifecycle. public SettingsStore SettingsStore { get; } - /// Shorthand for the settings themselves. public SimulatorSettings Settings => SettingsStore.Settings; public ExecutableLocator Executables { get; } @@ -132,13 +109,7 @@ public sealed class AppServices public MissingVehicleLog MissingVehicleLog { get; } - /// Starts the simulator on the current selection. public StartSimulation StartSimulation { get; } - /// - /// Exposed as the concrete service rather than - /// because the XAML binds to it as a change-notifying source; code should take - /// the port. - /// public LocalizationService Localization { get; } } diff --git a/StarterNG/Application/AppState.cs b/StarterNG/Application/AppState.cs index 105e6c7..78ab716 100644 --- a/StarterNG/Application/AppState.cs +++ b/StarterNG/Application/AppState.cs @@ -5,18 +5,8 @@ using StarterNG.Classes; namespace StarterNG.Application; -/// -/// What the user currently has selected: which scenario, which consist in it and -/// which vehicle they will start in. -/// -/// -/// Session state rather than domain state, which is why it lives in the -/// application layer. Built by the composition root instead of being an ambient -/// singleton; screens observe to stay in step. -/// public sealed class AppState { - /// Raised whenever any of the selections changes. public event Action? Changed; private Scenery? _currentScenery; diff --git a/StarterNG/Application/GameLibrary.cs b/StarterNG/Application/GameLibrary.cs index 8783c09..1f94b67 100644 --- a/StarterNG/Application/GameLibrary.cs +++ b/StarterNG/Application/GameLibrary.cs @@ -7,7 +7,6 @@ using StarterNG.Domain.Vehicles; namespace StarterNG.Application; -/// Which part of the installation the loader is working through. public enum LoadPhase { Vehicles, @@ -15,19 +14,8 @@ public enum LoadPhase Done } -/// How far the load has got, for the splash screen. public readonly record struct LoadStatus(double Fraction, LoadPhase Phase, string? Detail); -/// -/// Everything the starter reads out of the installation once: the rolling stock -/// catalogue and the list of scenarios. -/// -/// -/// Was the GameData singleton, which also owned directory enumeration and -/// parallel parsing. It is now an ordinary object built by the composition root, -/// delegating to the repositories; the loading policy (what is read, in what -/// order, what counts as progress) is all that is left here. -/// public sealed class GameLibrary { private readonly IVehicleRepository _vehicles; @@ -53,11 +41,6 @@ public sealed class GameLibrary public bool Loaded { get; private set; } - /// - /// Reads the whole installation. Vehicles first, because the scenery list - /// resolves consists against them; the thumbnail and physics indexes are - /// built alongside since they are independent directory scans. - /// public void Load(IProgress? progress = null) { if (Loaded) @@ -78,7 +61,6 @@ public sealed class GameLibrary Loaded = true; } - /// Re-reads the scenery folder, picking up files added since startup. public void ReloadSceneries(IProgress? progress = null) { var reloaded = _sceneries.LoadAll(progress); diff --git a/StarterNG/Application/MissingVehicleLog.cs b/StarterNG/Application/MissingVehicleLog.cs index fa4de6c..5d3ff7e 100644 --- a/StarterNG/Application/MissingVehicleLog.cs +++ b/StarterNG/Application/MissingVehicleLog.cs @@ -5,15 +5,6 @@ using StarterNG.Infrastructure.Vehicles; namespace StarterNG.Application; -/// -/// Writes the "vehicle files that are not there" report into the diagnostics -/// log, when the user has asked for it. -/// -/// -/// Was a method on the settings object, which had no business scanning the -/// installation. The setting decides whether it runs; the scan itself belongs to -/// the vehicle side. -/// public sealed class MissingVehicleLog { private readonly SimulatorSettings _settings; diff --git a/StarterNG/Application/SettingsStore.cs b/StarterNG/Application/SettingsStore.cs index bdd40c9..bb56352 100644 --- a/StarterNG/Application/SettingsStore.cs +++ b/StarterNG/Application/SettingsStore.cs @@ -9,24 +9,11 @@ using StarterNG.Infrastructure.Adapters; namespace StarterNG.Application; -/// -/// A screen that holds settings the user is editing and can be asked to write -/// them back into the model before a save. -/// -/// -/// Replaces the mutable Action? CaptureFromUi the settings model used to -/// expose: the dependency now points the right way, from the store to an -/// interface the view implements. -/// public interface ISettingsCapture { void CaptureInto(SimulatorSettings settings); } -/// -/// Owns the settings the starter runs with: where they were read from, when to -/// write them back, and which executable they point at. -/// public sealed class SettingsStore { private readonly IFileSystem _files; @@ -54,25 +41,14 @@ public sealed class SettingsStore _encoding = LegacyText.CodePage1250; } - /// The settings the application is running with. public SimulatorSettings Settings { get; } = new(); - /// The file the current settings were read from. public string LoadedFrom { get; private set; } = string.Empty; - /// The file a save will write to. public string SavePath => _savePath; - /// - /// Registers the screen that holds pending edits, so a save can collect them - /// first. Called by the settings view when it is created. - /// public void RegisterCapture(ISettingsCapture capture) => _capture = capture; - /// - /// Reads the user's settings, falling back to the copy shipped with the - /// installation on a first run. - /// public void Load() { _savePath = _paths.UserConfigPath(); @@ -91,7 +67,6 @@ public sealed class SettingsStore } catch (Exception ex) { - // An unreadable ini means defaults, not a failed start. _log.Log($"reading {path}", ex); _config = new ConfigFile(); } @@ -100,7 +75,6 @@ public sealed class SettingsStore TouchConfigAge(); } - /// Collects pending edits from the settings screen, then saves. public void CaptureAndSave() { _capture?.CaptureInto(Settings); @@ -134,7 +108,6 @@ public sealed class SettingsStore } } - /// Records the ini's timestamp, so an outside edit can be noticed. public void TouchConfigAge() { string path = string.IsNullOrEmpty(_savePath) ? _paths.UserConfigPath() : _savePath; @@ -148,10 +121,6 @@ public sealed class SettingsStore } } - /// - /// True when something else has written the ini since we last touched it - - /// the simulator itself, usually, which the starter must not clobber. - /// public bool IsConfigNewerOnDisk() { string path = string.IsNullOrEmpty(_savePath) ? _paths.UserConfigPath() : _savePath; @@ -160,7 +129,6 @@ public sealed class SettingsStore if (!_files.FileExists(path) || _configAgeUtc == DateTime.MinValue) return false; - // A second of slack: writing our own file must not count as an edit. return _files.GetLastWriteTimeUtc(path) > _configAgeUtc + TimeSpan.FromSeconds(1); } catch (Exception) @@ -172,5 +140,4 @@ public sealed class SettingsStore public string ResolveExecutable() => _executables.Resolve(Settings, out _); public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem); - } diff --git a/StarterNG/Application/StartSimulation.cs b/StarterNG/Application/StartSimulation.cs index a6b75dd..fcfe53c 100644 --- a/StarterNG/Application/StartSimulation.cs +++ b/StarterNG/Application/StartSimulation.cs @@ -10,28 +10,19 @@ using StarterNG.Infrastructure.Adapters; namespace StarterNG.Application; -/// Why a start attempt ended the way it did. public enum SimulationStartOutcome { Started, - /// Nothing is selected to run. NothingSelected, - /// The scenario could not be written for the simulator to read. ExportFailed, - /// The executable is missing, not executable, or built for another platform. ExecutableProblem, - /// The executable is fine but the process would not start. LaunchFailed } -/// -/// What happened, and what the caller needs in order to say so or to watch the -/// simulator it just started. -/// public readonly record struct SimulationStartResult( SimulationStartOutcome Outcome, IProcessHandle? Process = null, @@ -39,25 +30,10 @@ public readonly record struct SimulationStartResult( ExeProblem Problem = ExeProblem.None, string? Detail = null); -/// -/// Starts the simulator on the scenario and consist the user has selected. -/// -/// -/// This was the middle of a 100-line click handler that also drove dialogs and -/// the loading screen. The sequence itself - settle the consist's starting -/// state, write the scenario the simulator will read, save the settings, launch - -/// is application logic and belongs away from the window; what is left in the -/// view is asking the user things and showing what came back. -/// public sealed class StartSimulation { - /// - /// A scenario the starter has rewritten is saved beside the original under a - /// '$' name, which the simulator loads and the scenery list ignores. - /// private const char ExportPrefix = '$'; - /// Battery voltage stood in for by a token velocity, as the format demands. private const float BatteryOnVelocity = 0.1f; private readonly AppState _state; @@ -78,10 +54,6 @@ public sealed class StartSimulation _log = log; } - /// - /// The vehicle the player would start in, or null when the consist has no one - /// aboard - which is what the caller warns about before committing. - /// public static string? StartableVehicle(Trainset? trainset, string? preferred) { if (trainset is null || trainset.Vehicles.Count == 0) @@ -101,8 +73,6 @@ public sealed class StartSimulation return trainset.Vehicles.FirstOrDefault(CanStart)?.Name; } - /// Start as a free camera rather than in a vehicle. - /// Write the settings before launching. public SimulationStartResult Execute(bool freeFly, bool saveSettings) { var scenery = _state.CurrentScenery; @@ -116,8 +86,6 @@ public sealed class StartSimulation ?? StartableVehicle(trainset, _state.StartingVehicleName); _state.StartingVehicleName = vehicle; - // The export always rewrites the weather block, so a run reflects what the - // weather tab shows even when the user never touched it. scenery.Weather.Dirty = true; string exportName = ExportPrefix + Path.GetFileName(scenery.Path); @@ -155,11 +123,6 @@ public sealed class StartSimulation return new SimulationStartResult(SimulationStartOutcome.Started, process, executable); } - /// - /// Applies the user's battery preference to the consist. The scenery format - /// carries the battery state in the trainset's velocity field, so "always on" - /// means a token non-zero value when the consist is standing still. - /// private void ApplyBatteryDefault(Trainset trainset) { if (trainset.Vehicles.Count == 0) @@ -174,5 +137,4 @@ public sealed class StartSimulation _ => trainset.OriginalVelocity }; } - } diff --git a/StarterNG/Controls/DetailRows.cs b/StarterNG/Controls/DetailRows.cs index 4d325e7..706a895 100644 --- a/StarterNG/Controls/DetailRows.cs +++ b/StarterNG/Controls/DetailRows.cs @@ -2,28 +2,10 @@ using Avalonia.Controls; namespace StarterNG.Controls; -/// -/// The read-only label/value lines the depot's detail tabs are built from. -/// -/// -/// Shared so every tab reads the same way: label in the left column, value in -/// the right, aligned down the panel. Previously each tab built its own rows, -/// and the texture tab glued "label: value" into a single line, which lined up -/// with nothing. -/// public static class DetailRows { - /// Shown in place of a reading a vehicle does not have. public const string NoValue = "–"; - /// - /// The one row shape every detail tab uses. The label column negotiates its - /// width through the shared-size group on the host panel, so all rows in a - /// panel line up without anyone naming a pixel count - see the Compact styles - /// in Settings.axaml. Omitting makes it a - /// read-only line; omitting indents a bare widget to - /// the control column. - /// public static SettingRow Row(string? label = null, Control? content = null, string? value = null, string? description = null, string? tip = null) @@ -43,11 +25,6 @@ public static class DetailRows return row; } - /// - /// A read-only label/value line. The dash keeps the row from collapsing when a - /// vehicle has no reading for the field; the full value is on the tooltip, so - /// a trimmed one is still legible. - /// public static SettingRow Reading(string label, string? value) { bool empty = string.IsNullOrWhiteSpace(value); diff --git a/StarterNG/Controls/SettingRow.cs b/StarterNG/Controls/SettingRow.cs index 3135314..bee10d4 100644 --- a/StarterNG/Controls/SettingRow.cs +++ b/StarterNG/Controls/SettingRow.cs @@ -3,13 +3,6 @@ using Avalonia.Controls; namespace StarterNG.Controls; -/// -/// One line of the settings panel: label on the left, the control itself in the -/// middle, a value read-out on the right, and underneath the eu07.ini key this -/// row writes plus an optional description. -/// Replaces the hand-rolled StackPanel/DockPanel pattern that used to be repeated -/// for every setting. -/// public class SettingRow : ContentControl { public static readonly StyledProperty LabelProperty = @@ -18,12 +11,6 @@ public class SettingRow : ContentControl public static readonly StyledProperty DescriptionProperty = AvaloniaProperty.Register(nameof(Description)); - /// - /// The eu07.ini key this row reads and writes, shown verbatim so a user can - /// match what they see here against the file, the wiki or a forum post. Rows - /// that are starter-only preferences carry their "starter.*" key just the - /// same; rows that write no key at all leave this empty. - /// public static readonly StyledProperty ConfigKeyProperty = AvaloniaProperty.Register(nameof(ConfigKey)); @@ -71,19 +58,12 @@ public class SettingRow : ContentControl UpdatePseudoClasses(); } - /// - /// Drives the template: an empty label lets the control span the label column, - /// an empty description or value collapses that part of the row entirely. - /// private void UpdatePseudoClasses() { PseudoClasses.Set(":no-label", string.IsNullOrEmpty(Label)); PseudoClasses.Set(":no-description", string.IsNullOrEmpty(Description)); PseudoClasses.Set(":no-key", string.IsNullOrEmpty(ConfigKey)); PseudoClasses.Set(":no-value", string.IsNullOrEmpty(ValueText)); - // A read-only row is a label and a reading with no widget between them. The - // reading then takes the middle column too, so a long path ellipsises instead - // of demanding its full width from an Auto column and getting cut off. PseudoClasses.Set(":no-content", Content is null); } } diff --git a/StarterNG/Controls/SettingsSection.cs b/StarterNG/Controls/SettingsSection.cs index cf5fa23..7cb092d 100644 --- a/StarterNG/Controls/SettingsSection.cs +++ b/StarterNG/Controls/SettingsSection.cs @@ -3,11 +3,6 @@ using Avalonia.Controls.Primitives; namespace StarterNG.Controls; -/// -/// A group of s under a flat title with an accent rule. -/// holds an optional master switch shown on the right of the -/// title line; the section content gates itself off it. -/// public class SettingsSection : HeaderedContentControl { public static readonly StyledProperty ToggleProperty = diff --git a/StarterNG/Domain/Sceneries/Scenery.cs b/StarterNG/Domain/Sceneries/Scenery.cs index df9a079..243ab2f 100644 --- a/StarterNG/Domain/Sceneries/Scenery.cs +++ b/StarterNG/Domain/Sceneries/Scenery.cs @@ -9,43 +9,22 @@ using StarterNG.Domain; namespace StarterNG.Domain.Sceneries; -/// A file the scenery offers to open alongside it (map, manual, ...). public sealed class SceneryAttachment { public string FilePath = ""; public string Label = ""; } -/// -/// An include the scenery marks as optional, so the user can decide -/// whether it ends up in the exported scenario. -/// public sealed class SceneryInclude { public string FilePath = ""; public string Desc = ""; - /// 1 = on by default, 2 = only when no companion timetable exists. public int Kind; public bool Selected; } -/// -/// A scenario the user can start: its consists, loose vehicles, optional -/// includes and weather, plus the ability to render all of that back into a .scn -/// the simulator can load. -/// -/// -/// The aggregate is pure: it never reads or writes files. Parsing lives in -/// Infrastructure.Sceneries.SceneryParser and loading in -/// SceneryRepository, which is what lets the export rules be tested from -/// a string rather than an installation. -/// -/// is the .scn text with every trainset replaced by a -/// {{index}} placeholder and the optional includes stripped, so -/// only has to substitute rather than re-parse. -/// public sealed class Scenery { public Scenery(string path, string template) @@ -54,13 +33,10 @@ public sealed class Scenery Template = template; } - /// Path of the .scn this was parsed from; the aggregate's identity. public string Path { get; } - /// The .scn body with trainset placeholders, used for export. public string Template { get; } - /// True when a companion .sbt timetable sits next to the .scn. public bool HasCompanionTimetable { get; set; } public string? Group { get; set; } @@ -71,7 +47,6 @@ public sealed class Scenery public string? ImageName { get; set; } - /// Scenery kept for historical interest; hidden unless asked for. public bool Archival { get; set; } public List Trainsets { get; } = new(); @@ -80,38 +55,21 @@ public sealed class Scenery public List Includes { get; } = new(); - /// Vehicles placed directly as nodes rather than inside a trainset. public List LooseVehicles { get; } = new(); - /// - /// Names of every loose vehicle, including those whose node failed to parse - /// and therefore has no . - /// public List LooseVehicleNames { get; } = new(); - /// Parse problems worth showing on the scenario's fault list. public List Faults { get; } = new(); public SceneryWeather Weather { get; } = new(); public string DisplayName => System.IO.Path.GetFileNameWithoutExtension(Path); - /// - /// The description with any "@key" references resolved against the scenery's - /// own translation files. - /// public string LocalizedDescription(ISceneryTranslations translations) => string.IsNullOrEmpty(Description) ? "" : string.Join("\n", Description.Split('\n').Select(translations.Translate)); - /// - /// Renders the scenario the simulator should load: the original file with the - /// user's consists, weather and optional includes applied. - /// - /// - /// Drops AI trainsets the user marked irrelevant, which shortens loading. - /// public string BuildExportContent(bool skipDecorTrainsets, IRandomSource random) { string result = Template; @@ -154,8 +112,6 @@ public sealed class Scenery var sb = new StringBuilder(); foreach (var include in Includes) { - // Kind 2 is the scenery's own timetable stand-in: it is only wanted - // when the user has not supplied a companion .sbt. if (include.Kind == 2) { if (!HasCompanionTimetable) @@ -172,10 +128,6 @@ public sealed class Scenery return InsertBeforeFirstInit(text, sb.ToString(), fallbackAtEnd: false); } - /// - /// Places generated content ahead of FirstInit, which the simulator - /// treats as the end of the definition section. - /// private static string InsertBeforeFirstInit(string text, string addition, bool fallbackAtEnd) { var firstInit = Regex.Match(text, @"\bFirstInit\b", RegexOptions.IgnoreCase); diff --git a/StarterNG/Domain/Sceneries/SceneryWeather.cs b/StarterNG/Domain/Sceneries/SceneryWeather.cs index 36f7054..ebe9b78 100644 --- a/StarterNG/Domain/Sceneries/SceneryWeather.cs +++ b/StarterNG/Domain/Sceneries/SceneryWeather.cs @@ -3,22 +3,11 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Domain.Sceneries; -/// -/// The weather and time-of-day a scenery starts with: what the .scn file -/// declared, what the user has changed it to, and whether that differs. -/// -/// -/// Split out of Scenery so the "is this still the authored weather?" -/// rule lives next to the values it compares, instead of being spread over the -/// aggregate and the weather tab. The aggregate owns one of these; the parser -/// fills it and calls once. -/// public sealed class SceneryWeather { public const int FogMin = 10; public const int FogMax = 10000; - /// Overcast values the "random" option picks between. private static readonly double[] OvercastPresets = { -1.5, 0, 0.3, 0.7, 1, 1.1, 1.5 }; private string _authoredTime = "10:30"; @@ -33,7 +22,6 @@ public sealed class SceneryWeather public string ScenarioTimeOverride { get; set; } = "10:30"; - /// Day of the year driving the sun position (movelight). public int Day { get; set; } public double Temperature { get; set; } = 15; @@ -44,16 +32,12 @@ public sealed class SceneryWeather public double Overcast { get; set; } - /// When set, each export draws a fresh overcast from the presets. public bool OvercastRandom { get; set; } - /// True when the .scn actually declared weather, rather than us defaulting it. public bool IsAuthored { get; set; } - /// Set once the user has touched the weather, so export rewrites the config block. public bool Dirty { get; set; } - /// Takes the current values as the baseline "what the file said". public void AcceptAsAuthored() { _authoredTime = Time; @@ -65,7 +49,6 @@ public sealed class SceneryWeather _authoredOvercastRandom = OvercastRandom; } - /// True when the user's weather differs from what the .scn declared. public bool Changed => Day != _authoredDay || ScenarioTimeOverride != _authoredTimeOverride || @@ -74,11 +57,6 @@ public sealed class SceneryWeather OvercastRandom != _authoredOvercastRandom || (!OvercastRandom && Math.Abs(Overcast - _authoredOvercast) > 0.001); - /// - /// Puts back the authored weather. Stays : the export must - /// still rewrite the config block, because the file on disk may already carry - /// an edit from an earlier run. - /// public void Restore() { Time = _authoredTime; @@ -91,7 +69,6 @@ public sealed class SceneryWeather Dirty = true; } - /// Clamps a parsed fog range and picks the distance to run with. public void SetFogRange(int start, int end, IRandomSource random) { FogStart = Math.Clamp(start, FogMin, FogMax); @@ -99,7 +76,6 @@ public sealed class SceneryWeather FogEnd = end > FogStart ? random.Next(FogStart, end + 1) : end; } - /// The overcast to export, drawing a preset when randomisation is on. public double EffectiveOvercast(IRandomSource random) => OvercastRandom ? OvercastPresets[random.Next(0, OvercastPresets.Length)] : Overcast; } diff --git a/StarterNG/Domain/Sceneries/SceneryWeatherWriter.cs b/StarterNG/Domain/Sceneries/SceneryWeatherWriter.cs index 2b9d069..bf4270d 100644 --- a/StarterNG/Domain/Sceneries/SceneryWeatherWriter.cs +++ b/StarterNG/Domain/Sceneries/SceneryWeatherWriter.cs @@ -6,17 +6,8 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Domain.Sceneries; -/// -/// Renders a back into the .scn dialect: strips the -/// blocks the starter owns and prepends freshly written ones. -/// -/// -/// A pure text transformation, kept out of the aggregate because it is entirely -/// about the file format rather than about the scenario. -/// public static class SceneryWeatherWriter { - /// Config lines the starter writes itself and must not carry over. private static readonly Regex ManagedConfigLine = new( @"^\s*(movelight|scenario\.weather\.temperature|scenario\.time\.override)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -51,14 +42,10 @@ public static class SceneryWeatherWriter } catch { - // A scenery we cannot rewrite is still worth starting unmodified. return text; } } - /// - /// The simulator's parser expects an unpadded hour ("7:30", not "07:30"). - /// private static string DelphiHourMinute(string hhmm) { int colon = hhmm.IndexOf(':'); @@ -70,10 +57,6 @@ public static class SceneryWeatherWriter : "0" + hhmm[colon..]; } - /// - /// Config entries the scenery author wrote that are none of the starter's - /// business, preserved verbatim across a rewrite. - /// private static string CarriedConfigLines(string text) { var block = Regex.Match(text, @"(?is)\bconfig\b(.*?)\bendconfig\b"); diff --git a/StarterNG/Domain/Settings/SimulatorSettings.cs b/StarterNG/Domain/Settings/SimulatorSettings.cs index f326691..2ba822b 100644 --- a/StarterNG/Domain/Settings/SimulatorSettings.cs +++ b/StarterNG/Domain/Settings/SimulatorSettings.cs @@ -2,43 +2,16 @@ using System; namespace StarterNG.Domain.Settings; -/// How the simulator should treat the battery switch when a run starts. public enum BatteryDefault { Default = 0, AlwaysOff = 1, AlwaysOn = 2 } -/// What is wrong with the executable the starter would launch. public enum ExeProblem { None, NotFound, NotExecutable, WrongPlatform } -/// -/// Everything the starter writes into eu07.ini, plus the handful of preferences -/// it keeps for itself. -/// -/// -/// Plain state with sensible defaults, and nothing else: reading and writing the -/// ini is Infrastructure.Settings.SettingsSerializer, locating the file -/// and the executable is the rest of that folder, and the load/save lifecycle is -/// Application.SettingsStore. Splitting those out is what makes it -/// possible to test the ini mapping without a disk. -/// public sealed class SimulatorSettings { - /// - /// False until the ini names a language, which is how the first run knows to - /// follow the operating system instead. - /// public bool LanguageWasSet { get; internal set; } - /// - /// Display name of the starter's language, as the picker shows it ("Polski", - /// "English", "中文"). Resolved from at startup. - /// public string Language = "English"; - /// - /// The language's code, as its startercfg/lang/*.xml declares it and as - /// eu07.ini stores it. This is what the simulator reads, so it has to carry - /// whatever language the user actually picked rather than being folded into - /// one of two known ones. - /// public string LanguageCode = ""; public bool Fullscreen; public bool PauseWhenInactive = true; @@ -118,10 +91,6 @@ public sealed class SimulatorSettings public bool SkipPipeline; public int PythonScreenUpdateRate = 200; public bool DebugLogVisible; - /// - /// Debug log bits beyond the two the UI exposes, carried through a round trip - /// so a hand-configured log level is not silently narrowed. - /// internal int DebugLogExtraBits { get; set; } public bool CompressTextures = true; @@ -146,11 +115,6 @@ public sealed class SimulatorSettings public bool HideArchivalVehicles = true; public string LastScenery = ""; - /// - /// Starter's own window state, the way the old one kept WindowMaximized in - /// starter.ini. Only the maximised flag is remembered - a normal window - /// always opens at its design size, centred on the screen. - /// public bool WindowMaximized; public static readonly string[] RenderEngines = @@ -158,16 +122,7 @@ public sealed class SimulatorSettings public static readonly int[] AnisotropySteps = { 1, 2, 4, 8, 16 }; - /// - /// Vertical mouse scale as the ini stored it. The UI only exposes one - /// sensitivity, so the authored vertical magnitude is carried through a - /// round trip rather than being overwritten. - /// internal double MouseScaleYMagnitude { get; set; } = 0.2; - /// - /// Shadow tuning values beyond the two the UI edits, carried through a round - /// trip unchanged. - /// internal string[] ShadowTuneExtra { get; set; } = { "400", "300" }; } diff --git a/StarterNG/Domain/Vehicles/LoadWeightsTable.cs b/StarterNG/Domain/Vehicles/LoadWeightsTable.cs index 52d8501..2422545 100644 --- a/StarterNG/Domain/Vehicles/LoadWeightsTable.cs +++ b/StarterNG/Domain/Vehicles/LoadWeightsTable.cs @@ -4,18 +4,8 @@ using StarterNG.Classes; namespace StarterNG.Domain.Vehicles; -/// -/// How heavy a tonne of each cargo type is, as declared by -/// data/load_weights.txt, used to turn a load into a mass. -/// -/// -/// Was a static class that also read the file. The table is now a value the -/// repository builds; an unknown cargo weighs , which -/// is what the simulator assumes. -/// public sealed class LoadWeightsTable { - /// Weight assumed for a cargo the file does not mention. public const int DefaultWeight = 1000; private readonly Dictionary _weights; @@ -24,7 +14,6 @@ public sealed class LoadWeightsTable { _weights = new Dictionary(StringComparer.OrdinalIgnoreCase) { - // The pantograph state is carried as a pseudo load and weighs nothing. [Dynamic.PantState] = 0 }; diff --git a/StarterNG/Domain/Vehicles/VehicleCatalog.cs b/StarterNG/Domain/Vehicles/VehicleCatalog.cs index 96bf7c4..095837e 100644 --- a/StarterNG/Domain/Vehicles/VehicleCatalog.cs +++ b/StarterNG/Domain/Vehicles/VehicleCatalog.cs @@ -6,17 +6,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Domain.Vehicles; -/// -/// Every livery the installation offers, indexed for the lookups the depot and -/// the consist editor make: by uuid, by skin file, and by the set a vehicle -/// belongs to. -/// -/// -/// Was VehicleCatalog, which also owned file enumeration, textures.txt -/// parsing, a static thumbnail dictionary and a missing-asset scan. Those moved -/// to Infrastructure; what is left is the model and its resolution rules, filled -/// by a repository through . -/// public sealed class VehicleCatalog { private readonly IMiniTextureIndex _minis; @@ -26,7 +15,6 @@ public sealed class VehicleCatalog _minis = minis; } - /// Every livery worth offering (wrecks excluded). public List Textures { get; } = new(); public Dictionary GroupsById { get; } = new(); @@ -37,7 +25,6 @@ public sealed class VehicleCatalog public Dictionary TextureByUuid { get; } = new(); - /// Liveries by skin file name without extension, lower cased. public Dictionary TextureBySkin { get; } = new(); public void BeginLoad() @@ -50,7 +37,6 @@ public sealed class VehicleCatalog TextureBySkin.Clear(); } - /// Adds one vehicle folder's liveries to the catalogue. public void Ingest(VehicleEntry entry) { foreach (var group in entry.Groups) @@ -74,14 +60,12 @@ public sealed class VehicleCatalog Sets.AddRange(entry.Sets); } - /// Builds the indexes that depend on the catalogue being complete. public void EndLoad() { BuildSetIndex(); ResolveGroupInheritance(); } - /// The vehicles of the fixed set this livery leads or belongs to. public List? ResolveSet(VehicleTexture texture) { if (string.IsNullOrEmpty(texture.Uuid)) @@ -97,10 +81,6 @@ public sealed class VehicleCatalog return cars.Count > 0 ? cars : null; } - /// - /// True when the livery is a non-leading member of a set, and so is added - /// along with its leader rather than on its own. - /// public bool IsSetFollower(VehicleTexture texture) { if (string.IsNullOrEmpty(texture.Uuid)) @@ -113,10 +93,6 @@ public sealed class VehicleCatalog return lead is not null && !string.Equals(lead, texture.Uuid, StringComparison.OrdinalIgnoreCase); } - /// - /// The thumbnail to draw for a livery: its own if we have that bitmap, - /// otherwise the one its group declares. - /// public string? ResolveMiniName(VehicleTexture texture) { if (_minis.Has(texture.TextureMini)) @@ -141,7 +117,6 @@ public sealed class VehicleCatalog return TextureBySkin.TryGetValue(key, out var texture) ? texture : null; } - /// Heading shown above a group in the depot browser. public string GroupHeader(string? groupId) { if (string.IsNullOrEmpty(groupId) || !GroupsById.TryGetValue(groupId, out var group)) @@ -164,10 +139,6 @@ public sealed class VehicleCatalog } } - /// - /// Copies thumbnail, category and archived status down from each group onto - /// its liveries, so the browser can sort and filter without walking back up. - /// private void ResolveGroupInheritance() { foreach (var texture in Textures) @@ -182,7 +153,6 @@ public sealed class VehicleCatalog string? category = group?.Category; - // "*" means "categorise by the first letter of the thumbnail name". if (category == "*") { string source = !string.IsNullOrEmpty(mini) ? mini : texture.TextureMini ?? ""; diff --git a/StarterNG/Domain/Vehicles/VehiclePhysics.cs b/StarterNG/Domain/Vehicles/VehiclePhysics.cs index 22c62bd..742bed8 100644 --- a/StarterNG/Domain/Vehicles/VehiclePhysics.cs +++ b/StarterNG/Domain/Vehicles/VehiclePhysics.cs @@ -2,24 +2,12 @@ using System; namespace StarterNG.Domain.Vehicles; -/// -/// The physical properties of a vehicle type, as declared by its .fiz file: -/// what it weighs, how fast it goes, what it can carry and what it couples to. -/// -/// -/// Was Classes.Physics, which also owned the .fiz index, the parser and a -/// static cache. Those are now Infrastructure.Vehicles.FizPhysicsRepository; -/// this is the value the rest of the application reasons about. -/// public sealed class VehiclePhysics { - /// Tare mass in kilograms. public double Mass { get; set; } - /// Design speed in km/h. public double VMax { get; set; } - /// Length over buffers in metres. public double Length { get; set; } public string LoadAccepted { get; set; } = ""; @@ -28,10 +16,8 @@ public sealed class VehiclePhysics public int MaxLoad { get; set; } - /// Coupling capabilities of the front end. public int AllowedFlagA { get; set; } = 3; - /// Coupling capabilities of the rear end. public int AllowedFlagB { get; set; } = 3; public string ControlTypeA { get; set; } = ""; diff --git a/StarterNG/Domain/Vehicles/VehicleTexture.cs b/StarterNG/Domain/Vehicles/VehicleTexture.cs index 2c4131c..820b08a 100644 --- a/StarterNG/Domain/Vehicles/VehicleTexture.cs +++ b/StarterNG/Domain/Vehicles/VehicleTexture.cs @@ -2,10 +2,6 @@ using System.Collections.Generic; namespace StarterNG.Domain.Vehicles; -/// -/// One vehicle folder's contribution to the catalogue: the liveries it defines, -/// the groups they belong to and any fixed sets they form. -/// public sealed class VehicleEntry { public string? Uuid { get; set; } @@ -14,21 +10,15 @@ public sealed class VehicleEntry public List Sets { get; } = new(); } -/// -/// A family of liveries sharing a thumbnail and a category letter, as declared -/// by one textures.txt block. -/// public sealed class VehicleGroup { public string Id { get; set; } = ""; public string? Category { get; set; } public string? Mini { get; set; } - /// Withdrawn stock, hidden from the depot unless asked for. public bool Archived { get; set; } } -/// A single livery: one skin file on one model. public sealed class VehicleTexture { public string? Uuid { get; set; } @@ -39,7 +29,6 @@ public sealed class VehicleTexture public string? MiniRef { get; set; } public string? TextureMini { get; set; } - /// Wreck liveries exist in the files but are not offered in the depot. public bool Wreck { get; set; } public List Aliases { get; } = new(); @@ -47,17 +36,13 @@ public sealed class VehicleTexture public string FullPath => Directory + Skinfile; - /// Thumbnail name after group inheritance; filled by the catalogue. public string ResolvedClass { get; internal set; } = ""; - /// Category letter after group inheritance; filled by the catalogue. public string? ResolvedCategory { get; internal set; } - /// Whether the owning group is archived; filled by the catalogue. public bool ResolvedArchived { get; internal set; } } -/// An alternative model this same skin can be worn by. public sealed class VehicleAlias { public string? Model { get; set; } @@ -66,10 +51,6 @@ public sealed class VehicleAlias public string? TextureMini { get; set; } } -/// -/// The comma separated description a livery carries after "//": who made it, for -/// which operator and depot, and when it was last revised. -/// public sealed class VehicleMeta { public string? Raw { get; set; } @@ -84,10 +65,6 @@ public sealed class VehicleMeta public List Extra { get; } = new(); } -/// -/// Vehicles that belong together and are added to a consist as a unit, such as a -/// multiple unit or a fixed rake. -/// public sealed class VehicleSet { public string? Uuid { get; set; } diff --git a/StarterNG/Infrastructure/Adapters/FileDiagnosticsLog.cs b/StarterNG/Infrastructure/Adapters/FileDiagnosticsLog.cs index f8b1c18..e1c8e5c 100644 --- a/StarterNG/Infrastructure/Adapters/FileDiagnosticsLog.cs +++ b/StarterNG/Infrastructure/Adapters/FileDiagnosticsLog.cs @@ -6,17 +6,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// -/// that mirrors every line into starter/bledy.txt. -/// Writing is best-effort: a read-only or full installation directory degrades to -/// an in-memory log rather than taking the starter down, and a single retry is -/// made at time. -/// -/// -/// Instance state replaces the previous static Diagnostics accumulator, so -/// tests can assert on what a use case logged without touching disk (pair it with -/// an in-memory ). -/// public sealed class FileDiagnosticsLog : IDiagnosticsLog { private readonly IFileSystem _files; @@ -75,7 +64,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog } catch { - // A log we cannot delete is not worth failing a user action over. } } } @@ -100,8 +88,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog if (!_writerFailed && _writer is not null) return; - // The streaming writer never came up (or died mid-session): fall back - // to one bulk write so the session is not lost entirely. try { _files.CreateDirectory(Path.GetDirectoryName(_logPath)!); @@ -109,7 +95,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog } catch { - // Nothing left to try; the in-memory log still backs the UI. } } } @@ -143,7 +128,6 @@ public sealed class FileDiagnosticsLog : IDiagnosticsLog } catch { - // Disposing a broken writer is allowed to fail. } _writer = null; } diff --git a/StarterNG/Infrastructure/Adapters/GamePaths.cs b/StarterNG/Infrastructure/Adapters/GamePaths.cs index fe5d38e..3dd5c67 100644 --- a/StarterNG/Infrastructure/Adapters/GamePaths.cs +++ b/StarterNG/Infrastructure/Adapters/GamePaths.cs @@ -3,10 +3,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// -/// The stock MaSzyna layout, rooted at the process working directory (the -/// starter ships inside the installation folder, next to eu07.exe). -/// public sealed class GamePaths : IGamePaths { public GamePaths(string root) @@ -14,7 +10,6 @@ public sealed class GamePaths : IGamePaths Root = root; } - /// The installation the starter was launched from. public static GamePaths ForCurrentDirectory() => new(Directory.GetCurrentDirectory()); public string Root { get; } diff --git a/StarterNG/Infrastructure/Adapters/LegacyText.cs b/StarterNG/Infrastructure/Adapters/LegacyText.cs index aa48b34..991b023 100644 --- a/StarterNG/Infrastructure/Adapters/LegacyText.cs +++ b/StarterNG/Infrastructure/Adapters/LegacyText.cs @@ -17,14 +17,8 @@ namespace StarterNG.Infrastructure.Adapters; /// public static class LegacyText { - /// The .scn / .fiz / .ini code page, or Latin-1 if it is unavailable. public static Encoding CodePage1250 { get; } = Resolve(); - /// - /// True when the code page could not be loaded and text is being read as - /// Latin-1 instead. Polish characters will be wrong; the startup check - /// reports it rather than leaving it to be discovered on screen. - /// public static bool IsFallback { get; private set; } private static Encoding Resolve() diff --git a/StarterNG/Infrastructure/Adapters/PhysicalFileSystem.cs b/StarterNG/Infrastructure/Adapters/PhysicalFileSystem.cs index 2a3c295..3c3effe 100644 --- a/StarterNG/Infrastructure/Adapters/PhysicalFileSystem.cs +++ b/StarterNG/Infrastructure/Adapters/PhysicalFileSystem.cs @@ -6,10 +6,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// -/// backed by the real disk. The only place in the -/// application allowed to touch and . -/// public sealed class PhysicalFileSystem : IFileSystem { public bool FileExists(string path) => File.Exists(path); diff --git a/StarterNG/Infrastructure/Adapters/SystemClock.cs b/StarterNG/Infrastructure/Adapters/SystemClock.cs index 6f3e514..225e73b 100644 --- a/StarterNG/Infrastructure/Adapters/SystemClock.cs +++ b/StarterNG/Infrastructure/Adapters/SystemClock.cs @@ -3,7 +3,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// backed by the machine clock. public sealed class SystemClock : IClock { public DateTime Now => DateTime.Now; diff --git a/StarterNG/Infrastructure/Adapters/SystemEnvironment.cs b/StarterNG/Infrastructure/Adapters/SystemEnvironment.cs index e4b4f7e..24e9ded 100644 --- a/StarterNG/Infrastructure/Adapters/SystemEnvironment.cs +++ b/StarterNG/Infrastructure/Adapters/SystemEnvironment.cs @@ -3,7 +3,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// backed by the running process. public sealed class SystemEnvironment : IEnvironment { public bool IsWindows => OperatingSystem.IsWindows(); diff --git a/StarterNG/Infrastructure/Adapters/SystemProcessLauncher.cs b/StarterNG/Infrastructure/Adapters/SystemProcessLauncher.cs index ae99789..c72eae0 100644 --- a/StarterNG/Infrastructure/Adapters/SystemProcessLauncher.cs +++ b/StarterNG/Infrastructure/Adapters/SystemProcessLauncher.cs @@ -8,10 +8,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// -/// backed by . The only place -/// in the application that starts an operating-system process. -/// public sealed class SystemProcessLauncher : IProcessLauncher { private readonly IDiagnosticsLog _log; diff --git a/StarterNG/Infrastructure/Adapters/SystemRandomSource.cs b/StarterNG/Infrastructure/Adapters/SystemRandomSource.cs index debbfb8..b5300dd 100644 --- a/StarterNG/Infrastructure/Adapters/SystemRandomSource.cs +++ b/StarterNG/Infrastructure/Adapters/SystemRandomSource.cs @@ -3,7 +3,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Adapters; -/// backed by . public sealed class SystemRandomSource : IRandomSource { public int Next(int minInclusive, int maxExclusive) => Random.Shared.Next(minInclusive, maxExclusive); diff --git a/StarterNG/Infrastructure/Diagnostics.cs b/StarterNG/Infrastructure/Diagnostics.cs index e5bbc61..2696797 100644 --- a/StarterNG/Infrastructure/Diagnostics.cs +++ b/StarterNG/Infrastructure/Diagnostics.cs @@ -9,16 +9,6 @@ using StarterNG.Views; namespace StarterNG.Infrastructure; -/// -/// Compatibility facade over -/// plus the fault dialog. -/// -/// -/// The log itself now lives in Infrastructure.Adapters.FileDiagnosticsLog -/// and is reached through the composition root. This static shim keeps the -/// existing call sites compiling while they are migrated to constructor-injected -/// logging; new code should take IDiagnosticsLog instead. -/// public static class Diagnostics { private static Application.Abstractions.IDiagnosticsLog Log_ => AppServices.Current.Log; @@ -63,9 +53,6 @@ public static class Diagnostics Dispatcher.UIThread.Post(() => _ = ReportAsync(message, title)); } - /// - /// Sanity check of the installation the starter was launched from. - /// public static List CheckInstallation() => new InstallationCheck( AppServices.Current.Paths, diff --git a/StarterNG/Infrastructure/InstallationCheck.cs b/StarterNG/Infrastructure/InstallationCheck.cs index e6d1824..0c0bb0c 100644 --- a/StarterNG/Infrastructure/InstallationCheck.cs +++ b/StarterNG/Infrastructure/InstallationCheck.cs @@ -7,14 +7,6 @@ using StarterNG.Domain.Settings; namespace StarterNG.Infrastructure; -/// -/// Verifies that the folder the starter runs from actually looks like a MaSzyna -/// installation, and reports what is missing in the user's language. -/// -/// -/// Depends only on ports, so a broken installation can be described in a test by -/// handing it an empty in-memory file system. -/// public sealed class InstallationCheck { private static readonly string[] RequiredFolders = { "dynamic", "sounds", "models", "scenery", "textures" }; diff --git a/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs b/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs index 42b95a5..dba90fe 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryImageLocator.cs @@ -5,15 +5,6 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Infrastructure.Sceneries; -/// -/// Finds the preview image a scenery names, which authors place in any of -/// several conventional spots relative to the .scn. -/// -/// -/// Was a lazy property on the aggregate; moved out because it is a lookup on -/// disk. Results are cached per scenery path, which is what the lazy field -/// bought us before. -/// public sealed class SceneryImageLocator { private readonly IFileSystem _files; @@ -24,7 +15,6 @@ public sealed class SceneryImageLocator _files = files; } - /// The image file for this scenery, or null when there is none. public string? Resolve(Scenery scenery) { lock (_cache) diff --git a/StarterNG/Infrastructure/Sceneries/SceneryParser.cs b/StarterNG/Infrastructure/Sceneries/SceneryParser.cs index 7897255..788062a 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryParser.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryParser.cs @@ -10,15 +10,6 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Infrastructure.Sceneries; -/// -/// Turns the text of a .scn file into a . -/// -/// -/// All knowledge of the .scn dialect lives here, so the aggregate stays free of -/// regular expressions and the parser can be exercised on a string. Anything -/// malformed is recorded as a fault rather than thrown: a scenery with one bad -/// consist is still worth listing. -/// public sealed class SceneryParser { private readonly IClock _clock; @@ -32,8 +23,6 @@ public sealed class SceneryParser public Scenery Parse(string path, string content) { - // Directives, attachments and weather are read from the file as written; - // the placeholder rewriting below only shapes the export template. string original = content; var faults = new List(); @@ -42,8 +31,6 @@ public sealed class SceneryParser var includes = new List(); content = StripOptionalIncludes(content, includes); - // Trainsets are lifted out first so the placeholders survive the loose - // vehicle sweep below, which would otherwise match nodes inside them. var trainsetEntries = new List(); content = Regex.Replace(content, @"trainset\b[\s\S]*?\bendtrainset\b", match => @@ -98,7 +85,6 @@ public sealed class SceneryParser string[] parts = rest.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - // A leading one- or two-character token is a legacy ordering hint. if (parts.Length >= 3 && parts[0].Length <= 2) attachments.Add(new SceneryAttachment { @@ -122,10 +108,6 @@ public sealed class SceneryParser return attachments; } - /// - /// Removes the includes the scenery marked $optional from the text and - /// records them, so the user can pick which ones the export puts back. - /// private static string StripOptionalIncludes(string content, List includes) { if (content.IndexOf("$optional", StringComparison.OrdinalIgnoreCase) < 0) @@ -160,10 +142,6 @@ public sealed class SceneryParser RegexOptions.IgnoreCase); } - /// - /// Lifts node ... dynamic ... enddynamic entries that sit outside any - /// trainset, so the depot can show and reorder them like a consist. - /// private static string ExtractLooseVehicles(string content, List vehicles, List names, List faults) { @@ -176,7 +154,6 @@ public sealed class SceneryParser { string name = m.Groups[3].Value; - // A placeholder means the node belongs to a trainset already lifted out. if (name.StartsWith("{{", StringComparison.Ordinal)) return m.Value; @@ -214,7 +191,6 @@ public sealed class SceneryParser } catch { - // Leave the node in the template so the export stays faithful. faults.Add($"# loose dynamic parse fault: {name}"); names.Add(name); return m.Value; @@ -239,7 +215,6 @@ public sealed class SceneryParser } else if (!time.Success) { - // Nothing authored: start the scenario at the player's wall clock. var now = _clock.Now; weather.ScenarioTimeOverride = $"{now.Hour:D2}:{now.Minute:D2}"; } diff --git a/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs b/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs index e8308a2..5da5dee 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs @@ -12,17 +12,8 @@ using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Sceneries; -/// -/// Loads scenarios from the installation's scenery folder. -/// -/// -/// Sceneries are code page 1250 text, and a large installation holds hundreds of -/// them, so the sweep is parallel; each file that throws is logged and dropped -/// rather than taking the whole load down. -/// public sealed class SceneryRepository : ISceneryRepository { - /// Files starting with '$' are authoring scratch, not scenarios. private const char ExcludedPrefix = '$'; private readonly IFileSystem _files; @@ -83,5 +74,4 @@ public sealed class SceneryRepository : ISceneryRepository private IEnumerable SceneryFiles() => _files.GetFiles(_paths.Scenery, "*.scn") .Where(path => Path.GetFileName(path).FirstOrDefault() != ExcludedPrefix); - } diff --git a/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs b/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs index 593e425..efcc73d 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryTranslations.cs @@ -7,18 +7,8 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Infrastructure.Sceneries; -/// -/// reading the JSON files a scenery may ship -/// in its i18n folder. -/// -/// -/// English is loaded first and the user's language layered over it, so a partial -/// translation falls back per key rather than per file. Nested objects are -/// flattened to dotted keys, which is how the scenery text references them. -/// public sealed class SceneryTranslations : ISceneryTranslations { - /// Marks a scenery string as a key rather than literal text. private const char ReferencePrefix = '@'; private const string BaseLanguage = "en"; @@ -99,10 +89,6 @@ public sealed class SceneryTranslations : ISceneryTranslations } } - /// - /// Sceneries name their files by two-letter code; the starter's language - /// setting may carry a display name instead. - /// private static string NormalizeLanguage(string language) { string lower = language.Trim().ToLowerInvariant(); diff --git a/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs b/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs index 0fe1dcc..75496d4 100644 --- a/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs +++ b/StarterNG/Infrastructure/Sceneries/TimetableLocator.cs @@ -6,15 +6,6 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Infrastructure.Sceneries; -/// -/// Finds the timetable file a consist names. -/// -/// -/// Timetables live in the shared timetables/ folder, next to the scenery, or in -/// scenario/ depending on the installation's vintage, and the name may or may -/// not carry its .txt extension. "none" is the format's way of saying a consist -/// runs without one. -/// public sealed class TimetableLocator { private const string NoTimetable = "none"; diff --git a/StarterNG/Infrastructure/Settings/ExecutableLocator.cs b/StarterNG/Infrastructure/Settings/ExecutableLocator.cs index 7521cff..2283eee 100644 --- a/StarterNG/Infrastructure/Settings/ExecutableLocator.cs +++ b/StarterNG/Infrastructure/Settings/ExecutableLocator.cs @@ -7,18 +7,8 @@ using StarterNG.Domain.Settings; namespace StarterNG.Infrastructure.Settings; -/// -/// Finds and vets the simulator executable the starter will launch. -/// -/// -/// Installations ship the binary under several names (eu07.exe, eu07, -/// eu07-vulkan, ...), and a folder shared between platforms may hold both a PE -/// and an ELF build. So a candidate is not just found by name: it is checked for -/// the execute bit and for a magic number matching the platform we are on. -/// public sealed class ExecutableLocator { - /// Extensions that are never the simulator, whatever they are named. private static readonly string[] NeverExecutable = { ".ini", ".log", ".txt", ".cfg", ".config", ".json", ".dll", ".so", ".dat", ".bak", ".csv", ".xml" @@ -37,13 +27,8 @@ public sealed class ExecutableLocator _log = log; } - /// The canonical name for the platform, used when nothing is found. public string CanonicalName => _environment.IsWindows ? "eu07.exe" : "eu07"; - /// - /// The executable to launch for these settings, along with what is wrong with - /// it. A path is always returned so the UI can show what it was looking for. - /// public string Resolve(SimulatorSettings settings, out ExeProblem problem) { if (!settings.SelectExeAutomatically && !string.IsNullOrWhiteSpace(settings.ExecutablePath)) @@ -55,7 +40,6 @@ public sealed class ExecutableLocator string? fallback = null; try { - // Prefer the canonical name, then the shortest: "eu07" over "eu07-dev". var candidates = _files.GetFiles(_paths.Root, "eu07*") .Where(IsCandidate) .OrderBy(path => string.Equals(Path.GetFileName(path), CanonicalName, @@ -87,7 +71,6 @@ public sealed class ExecutableLocator return CanonicalName; } - /// Executables the user can pick from, most recently built first. public List ListCandidates(string? directory = null) { try @@ -122,11 +105,6 @@ public sealed class ExecutableLocator } } - /// - /// Reads the file's magic number: "MZ" for a Windows binary, 0x7F "ELF" for a - /// Linux one. A file we cannot read four bytes from is given the benefit of - /// the doubt. - /// private bool MatchesPlatform(string path) { byte[] head = new byte[4]; diff --git a/StarterNG/Infrastructure/Settings/SettingsPaths.cs b/StarterNG/Infrastructure/Settings/SettingsPaths.cs index f8e62b1..d9eba18 100644 --- a/StarterNG/Infrastructure/Settings/SettingsPaths.cs +++ b/StarterNG/Infrastructure/Settings/SettingsPaths.cs @@ -3,10 +3,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Settings; -/// -/// Where the simulator's eu07.ini lives: per user on each platform, with the -/// installation's own copy as the template for a first run. -/// public sealed class SettingsPaths { private const string ConfigFileName = "eu07.ini"; @@ -21,7 +17,6 @@ public sealed class SettingsPaths _paths = paths; } - /// The user's own settings file, which is what the starter writes. public string UserConfigPath() { if (_environment.IsWindows) @@ -46,7 +41,6 @@ public sealed class SettingsPaths return Path.Combine(_environment.BaseDirectory, ConfigFileName); } - /// The ini shipped with the installation, read when the user has none. public string InstallationConfigPath() => _paths.FromRoot(ConfigFileName); public string UserConfigDirectory() => diff --git a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs index 3ad6ccd..de02a80 100644 --- a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs +++ b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs @@ -8,26 +8,11 @@ using StarterNG.Domain.Settings; namespace StarterNG.Infrastructure.Settings; -/// -/// Maps onto the simulator's eu07.ini and back. -/// -/// -/// Every key the starter understands is named exactly once, here. Reads clamp -/// and default so a hand-edited ini cannot put the UI into an impossible state, -/// and writes preserve the keys the starter does not manage - which is why a -/// round trip through this class is worth testing on its own. -/// public sealed class SettingsSerializer { public void Read(SimulatorSettings s, ConfigFile c) { - s.LanguageWasSet = c.Has("lang"); - // Kept as the code the file holds. Startup hands it to the localisation - // service, which resolves it to a display name and writes that back; the - // starter must not decide here that an unrecognised code means English, - // because the same key is what tells the simulator which language to run - // in. s.LanguageCode = c.GetString("lang", ""); s.Language = s.LanguageCode; s.Fullscreen = c.GetBool("fullscreen", false); @@ -167,11 +152,9 @@ public sealed class SettingsSerializer public void Write(SimulatorSettings s, ConfigFile c) { - foreach (string key in ObsoleteKeys) c.Remove(key); - // Lower case, as the simulator expects and as the old starter wrote it. c.Set("lang", string.IsNullOrWhiteSpace(s.LanguageCode) ? "en" : s.LanguageCode.Trim().ToLowerInvariant()); diff --git a/StarterNG/Infrastructure/Shortcuts.cs b/StarterNG/Infrastructure/Shortcuts.cs index 03cabaf..2791347 100644 --- a/StarterNG/Infrastructure/Shortcuts.cs +++ b/StarterNG/Infrastructure/Shortcuts.cs @@ -7,21 +7,13 @@ namespace StarterNG.Infrastructure; public enum ShortcutScope { - /// Works anywhere in the window. Global, - /// Only while the depot page is on screen. Depot, - /// Handled by the consist strip itself, once a vehicle card has focus. Consist } -/// -/// One row of the shortcut table. is -/// for rows that only document a gesture handled elsewhere (the consist strip reads the -/// arrow keys itself, they are never dispatched from the window). -/// public sealed record Shortcut( string Id, string Display, @@ -36,12 +28,6 @@ public sealed record Shortcut( IsDispatched && Key == key && Modifiers == modifiers; } -/// -/// The single source of truth for the starter's own shortcuts: the window dispatches from -/// this table and the cheat sheet is printed from it, so the two cannot drift apart. These -/// are deliberately hardcoded - the configurable key list in Settings belongs to the -/// simulator's cab controls and mixing the two only confuses people. -/// public static class Shortcuts { public const string Start = "start"; diff --git a/StarterNG/Infrastructure/TextMatch.cs b/StarterNG/Infrastructure/TextMatch.cs index e851791..ffb0651 100644 --- a/StarterNG/Infrastructure/TextMatch.cs +++ b/StarterNG/Infrastructure/TextMatch.cs @@ -4,10 +4,6 @@ using System.Text; namespace StarterNG.Infrastructure; -/// -/// Case- and diacritic-insensitive substring matching, so that typing -/// "rozdzielczosc" in a search box still finds "Rozdzielczość". -/// public static class TextMatch { public static bool Contains(string? haystack, string? needle) @@ -20,10 +16,6 @@ public static class TextMatch return Fold(haystack).Contains(Fold(needle), StringComparison.Ordinal); } - /// - /// Lower-cases and strips combining marks, so "Ż" and "z" compare equal. - /// Polish "ł" has no decomposed form, so it is mapped by hand. - /// public static string Fold(string text) { if (string.IsNullOrEmpty(text)) diff --git a/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs b/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs index b530a59..52f52eb 100644 --- a/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs @@ -11,24 +11,10 @@ using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; -/// -/// Reads vehicle physics from the .fiz files under dynamic/. -/// -/// -/// The format is a flat token stream of "section" headings and "key value" -/// pairs, with an include file p1 p2 end directive whose parameters are -/// substituted for (1)-style placeholders in the included file. Only the -/// handful of keys the starter shows are decoded; everything else is skipped. -/// -/// Both the name index and the parsed results are cached, because the depot asks -/// for the same model repeatedly while the user scrolls. -/// public sealed class FizPhysicsRepository : IPhysicsRepository { - /// Guard against an include cycle in hand written files. private const int MaxIncludeDepth = 6; - /// Sentinel meaning "the rear coupler was never declared". private const int UndeclaredCoupler = -1; private static readonly HashSet KnownKeys = new(StringComparer.OrdinalIgnoreCase) @@ -80,7 +66,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository { var index = Index(); - // Unpowered variants are shipped as "dumb.fiz". if (!index.TryGetValue(name, out string? path) && !index.TryGetValue(name + "dumb", out path)) return null; @@ -95,7 +80,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository _log.Log($"physics/{Path.GetFileName(path)}", ex); } - // A vehicle that only declares one coupler has the same at both ends. if (physics.AllowedFlagB == UndeclaredCoupler) { physics.AllowedFlagB = physics.AllowedFlagA; @@ -189,7 +173,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository } else { - // Anything else at this level opens a section, e.g. "Param:" or "BuffCoupl1:". section = new string(bare.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant(); } } @@ -228,10 +211,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository private static bool Is(string key, string name) => key.Equals(name, StringComparison.OrdinalIgnoreCase); - /// - /// A negative flag means the coupling is present but disabled; the simulator - /// encodes that as its absolute value plus 128. - /// private static int ParseFlag(string value, int fallback) => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int flag) ? flag < 0 ? -flag + 128 : flag @@ -242,7 +221,6 @@ public sealed class FizPhysicsRepository : IPhysicsRepository ? parsed : fallback; - /// Substitutes an include's "(n)" placeholder with its n-th parameter. private static string Resolve(string token, string[]? parameters) { if (parameters is null || token.Length < 3 || token[0] != '(' || token[^1] != ')') @@ -270,5 +248,4 @@ public sealed class FizPhysicsRepository : IPhysicsRepository .Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .ToList(); } - } diff --git a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs index 8639771..53a4f1f 100644 --- a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs @@ -9,14 +9,6 @@ using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; -/// -/// Reads data/load_weights.txt into a . -/// -/// -/// The file is a brace-delimited list of "name : weight" pairs with '#', ';' and -/// '//' comments. It is read once and kept, since it does not change while the -/// starter runs. -/// public sealed class LoadWeightsRepository { private const string FileName = "load_weights.txt"; @@ -79,8 +71,6 @@ public sealed class LoadWeightsRepository body.Append(line).Append(' '); } - // Colons may be written tight against their neighbours, so space them out - // before tokenising. string[] tokens = body.Replace(":", " : ").ToString() .Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries); @@ -100,5 +90,4 @@ public sealed class LoadWeightsRepository return weights; } - } diff --git a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs index 8871522..c42293c 100644 --- a/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs +++ b/StarterNG/Infrastructure/Vehicles/MiniTextureIndex.cs @@ -5,17 +5,8 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Infrastructure.Vehicles; -/// -/// over the installation's textures/mini folder. -/// -/// -/// Replaces the static thumbnail dictionary that used to live on the vehicle -/// database: an installation-scoped lookup with its own lifetime, which is what -/// lets a test point the depot at thumbnails that only exist in memory. -/// public sealed class MiniTextureIndex : IMiniTextureIndex { - /// Thumbnail shown when a vehicle names one we do not have. private const string FallbackName = "other"; private readonly IFileSystem _files; diff --git a/StarterNG/Infrastructure/Vehicles/MissingAssetScanner.cs b/StarterNG/Infrastructure/Vehicles/MissingAssetScanner.cs index d44bdc1..d303e69 100644 --- a/StarterNG/Infrastructure/Vehicles/MissingAssetScanner.cs +++ b/StarterNG/Infrastructure/Vehicles/MissingAssetScanner.cs @@ -5,14 +5,6 @@ using StarterNG.Domain.Vehicles; namespace StarterNG.Infrastructure.Vehicles; -/// -/// Reports liveries whose skin or model file is not actually present, for the -/// optional "log missing vehicle files" diagnostic. -/// -/// -/// Both assets are looked up under several extensions because the simulator -/// accepts a texture as .mat or .bmp and a model as .t3d or .e3d. -/// public sealed class MissingAssetScanner { private static readonly string[] TextureExtensions = { ".mat", ".bmp" }; diff --git a/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs b/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs index d3e0591..c99a88e 100644 --- a/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs +++ b/StarterNG/Infrastructure/Vehicles/TexturesTxtParser.cs @@ -4,30 +4,10 @@ using StarterNG.Domain.Vehicles; namespace StarterNG.Infrastructure.Vehicles; -/// -/// Reads one vehicle folder's textures.txt into a . -/// -/// -/// The format is line oriented and stateful: '$a' marks the rest of the file as -/// archived, '!=' sets the category letter, and '^n' opens a fixed set of n -/// vehicles. Everything else with an '=' is a livery. Unknown lines are skipped -/// rather than rejected, because the files are hand written. -/// -/// Pure text in, model out: the repository does the reading, so the format can -/// be tested from a string array. -/// public sealed class TexturesTxtParser { - /// Category letter meaning "derive it from the thumbnail name". private const string DeriveCategory = "*"; - /// - /// Parses the file's lines. Returns null when the folder declares no liveries. - /// - /// - /// The folder path relative to dynamic/, with a trailing slash; it forms the - /// livery ids and is what the scenery format writes as the data folder. - /// public VehicleEntry? Parse(string directory, IReadOnlyList lines) { var entry = new VehicleEntry { Uuid = "legacy:" + directory }; @@ -120,8 +100,6 @@ public sealed class TexturesTxtParser if (skin.Length == 0) return null; - // Every "=" segment is "model, thumbnail[, per-texture thumbnail]"; the - // first is the livery proper and the rest are alternative models. var specs = new List<(string Model, string Mini, string TextureMini)>(); for (int i = 1; i < parts.Length; i++) { @@ -170,10 +148,6 @@ public sealed class TexturesTxtParser return texture; } - /// - /// The trailing comment is a fixed, comma separated credit line. Underscores - /// stand in for spaces, a convention from the era the format comes from. - /// private static VehicleMeta? ParseDescription(string description) { if (string.IsNullOrWhiteSpace(description)) diff --git a/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs b/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs index d2f709c..4a9a30d 100644 --- a/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs @@ -8,10 +8,6 @@ using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; -/// -/// Loads the catalogue from the dynamic/<maker>/<vehicle>/textures.txt -/// files an installation ships. -/// public sealed class TexturesTxtVehicleRepository : IVehicleRepository { private const string TexturesFileName = "textures.txt"; @@ -55,8 +51,6 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository { try { - // Read as text so the code page survives: the credit lines carry - // Polish characters that ReadAllLines' default encoding would mangle. string[] lines = _files.ReadAllText(path, _encoding).Split('\n'); return _parser.Parse(RelativeDirectory(path), lines); } @@ -67,10 +61,6 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository } } - /// - /// Rolling stock sits exactly two levels below dynamic/: a maker folder and a - /// vehicle folder. - /// private IEnumerable TexturesFiles() { foreach (string maker in _files.GetDirectories(_paths.Dynamic)) @@ -82,10 +72,6 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository } } - /// - /// The folder as the scenery format spells it: relative to dynamic/, forward - /// slashes, trailing slash. - /// private string RelativeDirectory(string texturesPath) { string directory = Path.GetDirectoryName(texturesPath) ?? ""; @@ -98,5 +84,4 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository return relative.Replace('\\', '/').TrimEnd('/') + "/"; } - } diff --git a/StarterNG/Presentation/Scenarios/FogScale.cs b/StarterNG/Presentation/Scenarios/FogScale.cs index b95c82c..0b76c13 100644 --- a/StarterNG/Presentation/Scenarios/FogScale.cs +++ b/StarterNG/Presentation/Scenarios/FogScale.cs @@ -4,19 +4,8 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Presentation.Scenarios; -/// -/// Converts between the weather tab's fog slider and a distance in metres. -/// -/// -/// Visibility runs from 10 m to 10 km, a range no linear slider handles well: -/// the interesting part - thick fog, where a few metres matter - would be a -/// couple of pixels wide. The slider is therefore logarithmic over its 0-1000 -/// travel, and the result snaps to a step that grows with the distance, so the -/// read-out lands on round numbers a driver would recognise. -/// public static class FogScale { - /// Travel of the slider the scale is spread over. public const double SliderRange = 1000.0; public static int ToMetres(double sliderPosition) @@ -37,7 +26,6 @@ public static class FogScale Math.Log((double)SceneryWeather.FogMax / SceneryWeather.FogMin); } - /// Metres below a kilometre, kilometres above it. public static string Format(int metres, CultureInfo culture) => metres < 1000 ? $"{metres} m" diff --git a/StarterNG/Presentation/Scenarios/SceneryTree.cs b/StarterNG/Presentation/Scenarios/SceneryTree.cs index 12569c7..507809a 100644 --- a/StarterNG/Presentation/Scenarios/SceneryTree.cs +++ b/StarterNG/Presentation/Scenarios/SceneryTree.cs @@ -6,27 +6,11 @@ using StarterNG.Domain.Sceneries; namespace StarterNG.Presentation.Scenarios; -/// -/// A node of the scenery list: either a scenario, or a group holding scenarios. -/// -/// Text to show. -/// Index into the scenery list, or -1 for a group. -/// Scenarios under a group; empty for a scenario. public sealed record SceneryTreeNode(string Label, int SceneryIndex, IReadOnlyList Children) { public bool IsGroup => SceneryIndex < 0; } -/// -/// Turns the flat scenery list into the tree the scenario picker shows: -/// scenarios that declare a group sit under it, the rest stay at the top, and -/// everything is sorted by name. -/// -/// -/// Pulled out of the view so the grouping and the archival filter can be tested -/// without a TreeView. The view's job is reduced to turning these nodes into -/// controls. -/// public sealed class SceneryTreeBuilder { private readonly ISceneryTranslations _translations; @@ -36,9 +20,6 @@ public sealed class SceneryTreeBuilder _translations = translations; } - /// The list the node indices refer to. - /// Whether scenarios marked archival are listed. - /// Language for resolving each scenery's own translations. public IReadOnlyList Build(IReadOnlyList sceneries, bool includeArchival, string langCode) { @@ -52,8 +33,6 @@ public sealed class SceneryTreeBuilder if (scenery.Archival && !includeArchival) continue; - // The group name may itself be an "@key" reference into the scenery's - // own translation files, so the table has to be loaded per scenery. _translations.LoadFor(scenery, langCode); var node = new SceneryTreeNode(scenery.DisplayName, i, Array.Empty()); diff --git a/StarterNG/Presentation/Scenarios/SeasonDates.cs b/StarterNG/Presentation/Scenarios/SeasonDates.cs index 5a938a4..959d952 100644 --- a/StarterNG/Presentation/Scenarios/SeasonDates.cs +++ b/StarterNG/Presentation/Scenarios/SeasonDates.cs @@ -3,15 +3,6 @@ using StarterNG.Application.Abstractions; namespace StarterNG.Presentation.Scenarios; -/// -/// Translates between the scenery's day-of-year (movelight) and the date the -/// weather tab shows. -/// -/// -/// Day 0 is the scenery saying "whatever today is", which the tab presents as -/// the current date; the season presets are ordinary days of the year. Leap -/// years are honoured so 366 stays selectable in one. -/// public sealed class SeasonDates { private readonly IClock _clock; @@ -21,13 +12,8 @@ public sealed class SeasonDates _clock = clock; } - /// The day-of-year a scenery means by 0: today. public int Today => _clock.Now.DayOfYear; - /// - /// The date to show for a stored day-of-year, in the current year. Day 0 and - /// out-of-range values fall back to today, clamped to the year's length. - /// public DateTimeOffset DateOf(int dayOfYear) { int year = _clock.Now.Year; diff --git a/StarterNG/Views/ShortcutsWindow.cs b/StarterNG/Views/ShortcutsWindow.cs index 421284f..d7bd5be 100644 --- a/StarterNG/Views/ShortcutsWindow.cs +++ b/StarterNG/Views/ShortcutsWindow.cs @@ -9,10 +9,6 @@ using StarterNG.Infrastructure; namespace StarterNG.Views; -/// -/// The cheat sheet, printed straight from so it can never -/// list a gesture the window does not actually dispatch. -/// public static class ShortcutsWindow { public static Window Create() From b9f455e33de2d79e0ea691200c34348a69f23652 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:29:04 +0200 Subject: [PATCH 50/63] Show vehicle and texture info when a browser entry is picked The General and About texture tabs only ever described the selected consist car, so clicking a vehicle in the list told the user nothing about it. --- StarterNG/Domain/VehicleInfo.cs | 4 ++++ StarterNG/Views/Depot.axaml.cs | 2 ++ StarterNG/Views/Depot/VehicleBrowser.cs | 7 +++++++ StarterNG/Views/Depot/VehicleDetails.cs | 20 ++++++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/StarterNG/Domain/VehicleInfo.cs b/StarterNG/Domain/VehicleInfo.cs index d83e68d..1193e58 100644 --- a/StarterNG/Domain/VehicleInfo.cs +++ b/StarterNG/Domain/VehicleInfo.cs @@ -22,6 +22,10 @@ public sealed class VehicleInfo public string? CategoryOf(Dynamic car) => TextureFor(car) is { } t ? CategoryOf(t) : null; + public VehiclePhysics? PhysicsFor(VehicleTexture texture) => + AppServices.Current.Physics.For(texture.Directory, texture.Model) + ?? AppServices.Current.Physics.For(texture.Directory, texture.Skinfile); + public VehiclePhysics? PhysicsFor(Dynamic car) { string? dbModel = TextureFor(car)?.Model; diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs index 2bf6957..f703b78 100644 --- a/StarterNG/Views/Depot.axaml.cs +++ b/StarterNG/Views/Depot.axaml.cs @@ -89,6 +89,8 @@ public partial class Depot : UserControl _db, _info, _consist, _cargo, _hand, RebuildConsist, _cards.RefreshMemberChrome, _browser.ShowTextureInfo); + _browser.TextureSelected = _details.ShowTexture; + Dispatcher.UIThread.Post(() => { hideArchivalCheck.IsChecked = AppServices.Current.Settings.HideArchivalVehicles; diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs index e67c8b2..1fc4c2a 100644 --- a/StarterNG/Views/Depot/VehicleBrowser.cs +++ b/StarterNG/Views/Depot/VehicleBrowser.cs @@ -49,6 +49,8 @@ public sealed class VehicleBrowser public VehicleTexture? Selected { get; private set; } + public Action? TextureSelected { get; set; } + private bool _suppress; private bool _syncingCombos; @@ -342,6 +344,11 @@ public sealed class VehicleBrowser miniPreview.Source = _minis.Get(_db.ResolveMiniName(texture), MiniPreviewHeight); addVehicleButton.IsEnabled = true; SyncCombosTo(texture); + + // Clicking a card in the consist also selects its texture here. The + // detail panels then belong to that car, not to the catalogue entry. + if (!_syncingCombos) + TextureSelected?.Invoke(texture); } else { diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs index e420df5..5b8f0c9 100644 --- a/StarterNG/Views/Depot/VehicleDetails.cs +++ b/StarterNG/Views/Depot/VehicleDetails.cs @@ -122,6 +122,26 @@ public sealed class VehicleDetails AddConsistLoadSection(); } + public void ShowTexture(VehicleTexture texture) + { + generalPanel.Children.Clear(); + consistTexturePanel.Children.Clear(); + + var inv = CultureInfo.InvariantCulture; + var physics = _info.PhysicsFor(texture); + + generalPanel.Children.Add(Reading($"{App.Loc["Length"]} [m]", + physics?.Length.ToString("0.##", inv))); + generalPanel.Children.Add(Reading($"{App.Loc["Mass"]} [t]", + physics is null ? null : (physics.Mass / 1000.0).ToString("0.#", inv))); + generalPanel.Children.Add(Reading($"{App.Loc["VMax"]} [km/h]", + physics?.VMax.ToString("0", inv))); + generalPanel.Children.Add(Reading(App.Loc["Model"], texture.Model)); + generalPanel.Children.Add(Reading(App.Loc["Texture"], texture.Skinfile)); + + _showTextureInfo(texture, consistTexturePanel); + } + private void BuildGeneralInfo(ConsistItem item) { var car = _consist.ActiveCar(item); From 027dff17d914414909fe794821e69154c1edbc4a Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:48:43 +0200 Subject: [PATCH 51/63] Let read-only detail values wrap instead of ellipsising In a 250-390 px panel the label column leaves little room, so operator, depot and author names lost their ends to the ellipsis. --- StarterNG/Styles/Settings.axaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 268d6ba..ba614d7 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -232,6 +232,14 @@ + + From f626f47ab1b164436b4eb819d2bf732e52e88fa3 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:51:59 +0200 Subject: [PATCH 52/63] Give the depot detail panel room for the whole texture tab At the default 768 px window the row capped at 192 px cut off the last credit line once values started wrapping. --- StarterNG/Styles/Settings.axaml | 2 +- StarterNG/Views/Depot.axaml | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index ba614d7..1ce8c82 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -212,7 +212,7 @@ longest label in whichever panel it lands in, so no pixel count is picked here and it follows the translation. --> + whether the thumbnails are the small ones or the large ones. + Row 1's cap is set by the texture tab: skin, operator, depot, revision + date and place, texture and photo author, some of which wrap onto a + second line in this width. --> - + @@ -155,16 +158,16 @@ + BorderThickness="1" CornerRadius="2" Padding="4"> - + - + From 88fa5a29789fc9e8276a3d114e31ab384b213585 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:54:15 +0200 Subject: [PATCH 53/63] Drop value wrapping, raise the detail row by one line 192 to 207 px, the height of one more reading. Values go back to a single ellipsised line. --- StarterNG/Styles/Settings.axaml | 10 +--------- StarterNG/Views/Depot.axaml | 14 ++++++-------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 1ce8c82..268d6ba 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -212,7 +212,7 @@ longest label in whichever panel it lands in, so no pixel count is picked here and it follows the translation. --> - diff --git a/StarterNG/Views/Depot.axaml b/StarterNG/Views/Depot.axaml index 1729263..9c9c923 100644 --- a/StarterNG/Views/Depot.axaml +++ b/StarterNG/Views/Depot.axaml @@ -19,13 +19,11 @@ + whether the thumbnails are the small ones or the large ones. Row 1's + cap fits the seven lines of the texture tab. --> - + @@ -158,16 +156,16 @@ + BorderThickness="1" CornerRadius="2" Padding="6"> - + - + From c072898e4f89f1133e8eb5793a612589fa6a517f Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:04:09 +0200 Subject: [PATCH 54/63] Depot: per-vehicle wagon number, model path, activation after removal The number editor was pinned to the unit's first car, so every vehicle in a multiple unit shared one number. Removing a vehicle only moved the highlight, leaving the starting vehicle and the panels on the deleted one. Drops the tab strip around the single configuration panel and shortens the consist row when the thumbnails are small. --- StarterNG/Views/Depot.axaml | 17 +++++++---------- StarterNG/Views/Depot.axaml.cs | 21 +++++++++++++++++++++ StarterNG/Views/Depot/VehicleBrowser.cs | 1 + StarterNG/Views/Depot/VehicleCards.cs | 7 +++++++ StarterNG/Views/Depot/VehicleDetails.cs | 8 +++++--- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/StarterNG/Views/Depot.axaml b/StarterNG/Views/Depot.axaml index 9c9c923..2990ac0 100644 --- a/StarterNG/Views/Depot.axaml +++ b/StarterNG/Views/Depot.axaml @@ -10,7 +10,7 @@ x:Class="StarterNG.Views.Depot"> - + @@ -23,7 +23,7 @@ cap fits the seven lines of the texture tab. --> - + @@ -127,7 +127,8 @@ + the consist. No tab strip: it was a single tab naming the only thing + the column could hold. --> - - - - - - - + + + diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs index f703b78..ffce1a8 100644 --- a/StarterNG/Views/Depot.axaml.cs +++ b/StarterNG/Views/Depot.axaml.cs @@ -147,6 +147,19 @@ public partial class Depot : UserControl private void VehicleListBox_OnPointerPressed(object? sender, PointerPressedEventArgs e) => _drag.VehicleListPressed(sender, e); + /// + /// Floor of the consist row with the small thumbnails, one thumbnail height + /// below the roomy one. + /// + private const double SmallConsistRowHeight = 106; + + private const double LargeConsistRowHeight = 140; + + private void SyncConsistRowHeight() => + depotGrid.RowDefinitions[2].MinHeight = AppServices.Current.Settings.LargeThumbnails + ? LargeConsistRowHeight + : SmallConsistRowHeight; + private Dynamic NewVehicle(VehicleTexture texture) => _consist.MakeDynamic(texture, _listScenery ?? AppServices.Current.State.CurrentScenery); @@ -177,6 +190,7 @@ public partial class Depot : UserControl private void RebuildConsist() { + SyncConsistRowHeight(); consistStack.Children.Clear(); _cards.Forget(); _drag.Reset(); @@ -192,6 +206,13 @@ public partial class Depot : UserControl } emptyHint.IsVisible = _consist.Count == 0; + + // A selection the consist moved on its own - after a removal, say - is only + // highlighted; activating it runs what a click would have run. + if (_consist.Selected is { } moved && + (_consist.SelectedCar is null || !moved.Cars.Contains(_consist.SelectedCar))) + _cards.Activate(moved); + _details.Refresh(); UpdateTrainStats(); WriteBackToScenery(); diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs index 1fc4c2a..bcecf32 100644 --- a/StarterNG/Views/Depot/VehicleBrowser.cs +++ b/StarterNG/Views/Depot/VehicleBrowser.cs @@ -445,6 +445,7 @@ public sealed class VehicleBrowser } Reading(App.Loc["TextureInfo"], Consist.Base(texture.Skinfile)); + Reading(App.Loc["Model"], texture.Directory + texture.Model); if (texture.Meta is { } meta) { diff --git a/StarterNG/Views/Depot/VehicleCards.cs b/StarterNG/Views/Depot/VehicleCards.cs index 231c59c..00a2701 100644 --- a/StarterNG/Views/Depot/VehicleCards.cs +++ b/StarterNG/Views/Depot/VehicleCards.cs @@ -234,6 +234,13 @@ public sealed class VehicleCards /// Selects an item without going through a redraw. picks the /// active member of a multi-vehicle unit; null keeps the current one when possible. /// + /// + /// Gives an item the selection a click would give it. Removing a vehicle only + /// moves the highlight, which left the starting vehicle, the detail panels and + /// the browser pointing at what was deleted. + /// + public void Activate(ConsistItem item) => Select(item, null); + private void Select(ConsistItem item, Dynamic? car) { var target = car ?? (ReferenceEquals(_consist.Selected, item) diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs index 5b8f0c9..97d5c03 100644 --- a/StarterNG/Views/Depot/VehicleDetails.cs +++ b/StarterNG/Views/Depot/VehicleDetails.cs @@ -304,19 +304,21 @@ public sealed class VehicleDetails if (IsPassengerVehicle(item)) { - var lead = item.Cars[0]; + // The number rides in each vehicle's own name, so it belongs to the + // member the user picked inside the unit, not to its first car. + var numbered = _consist.ActiveCar(item); var numBox = new NumericUpDown { FontSize = 12, Minimum = 0, Maximum = 99999, Increment = 1, - Value = Consist.GetWagonNumber(lead), + Value = Consist.GetWagonNumber(numbered), FormatString = "0" }; numBox.ValueChanged += (_, _) => { - _consist.SetWagonNumber(lead, (int)(numBox.Value ?? 0)); + _consist.SetWagonNumber(numbered, (int)(numBox.Value ?? 0)); _redraw(); }; rows.Children.Add(Row(App.Loc["WagonNumber"], numBox)); From da1b54cf1fd4c71e2e00bbaae8c4684832038d79 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:05:50 +0200 Subject: [PATCH 55/63] Sort the browser list by the name its rows show It was ordered on TextureMini while the row prints the skin name whenever the thumbnail matches the class, so the list looked unsorted. --- StarterNG/Views/Depot/VehicleBrowser.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs index bcecf32..56352d3 100644 --- a/StarterNG/Views/Depot/VehicleBrowser.cs +++ b/StarterNG/Views/Depot/VehicleBrowser.cs @@ -314,7 +314,8 @@ public sealed class VehicleBrowser var matched = _db.Textures .Where(t => PassesFilters(t, search, hasSearch)) - .OrderBy(t => t.TextureMini ?? t.Skinfile, StringComparer.OrdinalIgnoreCase) + .OrderBy(BrowserName, StringComparer.OrdinalIgnoreCase) + .ThenBy(t => t.Skinfile, StringComparer.OrdinalIgnoreCase) .Take(MaxListRows) .ToList(); @@ -470,7 +471,8 @@ public sealed class VehicleBrowser return _db.Textures .Where(t => PassesFilters(t, search, hasSearch)) - .OrderBy(t => t.TextureMini ?? t.Skinfile, StringComparer.OrdinalIgnoreCase) + .OrderBy(BrowserName, StringComparer.OrdinalIgnoreCase) + .ThenBy(t => t.Skinfile, StringComparer.OrdinalIgnoreCase) .ToList(); } @@ -503,6 +505,10 @@ public sealed class VehicleBrowser || C(t.Meta?.Vehicle) || C(t.Meta?.Operator) || C(VehicleInfo.ClassOf(t)); } + /// + /// The name the list row carries, and the key it is sorted on - the two have + /// to be the same string or the list reads as unordered. + /// private static string BrowserName(VehicleTexture texture) { if (!string.IsNullOrEmpty(texture.TextureMini) && From 1939493b479146cd2d3cb06c2211c637b14a567a Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:09:42 +0200 Subject: [PATCH 56/63] Tighten the row spacing in the depot detail panels 5 px between readings to 2, which fits three more lines in the same panel. --- StarterNG/Styles/Settings.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 268d6ba..7d5f14f 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -212,7 +212,7 @@ longest label in whichever panel it lands in, so no pixel count is picked here and it follows the translation. --> Ovládání diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index 300d28d..4199795 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -214,6 +214,8 @@ To following Maximum Whole-consist load + Consist ready to go + The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. Random type Maximum amount Random amount @@ -474,11 +476,6 @@ Automatically close starter Large thumbnails Automatically expand scenery tree - Battery on launch - Battery state when the simulation starts, unless the scenario decides otherwise. - Scenario default - Always off - Always on Controls diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index f58fd46..a8a22f4 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -214,6 +214,8 @@ A következőkre Legnagyobb A teljes szerelvény rakománya + Consist ready to go + The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. Véletlen típus Legnagyobb mennyiség Véletlen mennyiség @@ -440,10 +442,6 @@ A starter automatikus bezárása Nagy előnézetek Pályafa automatikus kibontása - Akkumulátor indításkor - Forgatókönyv szerint - Mindig ki - Mindig be Irányítás diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index 9e6a098..b78f82c 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -214,6 +214,8 @@ Do następnych Maksymalny Ładunki zestawienia + Skład gotowy do jazdy + Skład startuje przygotowany: przewód główny napełniony, kran maszynisty ustawiony, akumulatory załączone. Odznaczony startuje zimny i zahamowany. Losowy rodzaj Maksymalna ilość Losowa ilość @@ -474,11 +476,6 @@ Zamknij starter automatycznie Duże miniaturki Automatycznie rozwijanie drzewka scenerii - Akumulator przy starcie - Stan akumulatora przy starcie symulacji, o ile scenariusz nie zdecyduje inaczej. - Domyślnie ze scenerii - Zawsze wyłączony - Zawsze włączony Sterowanie diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index 21d2a8a..1ecee1c 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -214,6 +214,8 @@ На следующие Максимум Груз всего состава + Consist ready to go + The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. Случайный тип Максимальное количество Случайное количество @@ -440,10 +442,6 @@ Автоматически закрывать стартер Крупные миниатюры Автоматически разворачивать дерево сценерий - Батарея при запуске - По сценарию - Всегда выключена - Всегда включена Управление From c0cc696dc91818f5b00d231a8a153e9239ebe268 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:24:45 +0200 Subject: [PATCH 58/63] Restore the three-state consist override in settings Keeps the old starter's starter.batterydefault key and its three states. The setting wins over the depot checkbox, which greys out and says so while an override is in force. --- StarterNG.Tests/SettingsStoreTests.cs | 26 +++++++++++++++++++ StarterNG/Application/StartSimulation.cs | 12 +++++++++ .../Domain/Settings/SimulatorSettings.cs | 5 ++++ .../Settings/SettingsSerializer.cs | 9 +++++++ StarterNG/Views/Depot/VehicleDetails.cs | 14 +++++++--- StarterNG/Views/Settings.axaml | 9 +++++++ StarterNG/Views/Settings.axaml.cs | 2 ++ StarterNG/startercfg/lang/cz.xml | 6 +++++ StarterNG/startercfg/lang/en.xml | 6 +++++ StarterNG/startercfg/lang/hu.xml | 6 +++++ StarterNG/startercfg/lang/pl.xml | 6 +++++ StarterNG/startercfg/lang/ru.xml | 6 +++++ 12 files changed, 104 insertions(+), 3 deletions(-) diff --git a/StarterNG.Tests/SettingsStoreTests.cs b/StarterNG.Tests/SettingsStoreTests.cs index 9b4c2b3..ba870a9 100644 --- a/StarterNG.Tests/SettingsStoreTests.cs +++ b/StarterNG.Tests/SettingsStoreTests.cs @@ -82,6 +82,32 @@ public class SettingsStoreTests Assert.Contains("lang zh", installation.Files.ReadAllText(UserIni)); } + [Fact] + public void The_consist_state_override_keeps_the_key_the_old_starter_used() + { + var installation = WithHome(new InMemoryFileSystem() + .WithFile(UserIni, "starter.batterydefault 2\n")); + + installation.SettingsStore.Load(); + Assert.Equal(ConsistReadyOverride.AlwaysReady, installation.SettingsStore.Settings.ReadyOverride); + + installation.SettingsStore.Settings.ReadyOverride = ConsistReadyOverride.AlwaysCold; + installation.SettingsStore.Save(); + + Assert.Contains("starter.batterydefault 1", installation.Files.ReadAllText(UserIni)); + } + + [Fact] + public void An_out_of_range_override_falls_back_to_leaving_the_scenery_alone() + { + var installation = WithHome(new InMemoryFileSystem() + .WithFile(UserIni, "starter.batterydefault 9\n")); + + installation.SettingsStore.Load(); + + Assert.Equal(ConsistReadyOverride.Scenery, installation.SettingsStore.Settings.ReadyOverride); + } + [Fact] public void A_first_run_falls_back_to_the_ini_shipped_with_the_installation() { diff --git a/StarterNG/Application/StartSimulation.cs b/StarterNG/Application/StartSimulation.cs index 18697c5..8e05d29 100644 --- a/StarterNG/Application/StartSimulation.cs +++ b/StarterNG/Application/StartSimulation.cs @@ -79,6 +79,18 @@ public sealed class StartSimulation if (scenery is null || trainset is null) return new SimulationStartResult(SimulationStartOutcome.NothingSelected); + // The depot checkbox is the consist's own state; this setting is a blanket + // override for people who always want the same thing, and it wins. + switch (_settings.Settings.ReadyOverride) + { + case ConsistReadyOverride.AlwaysCold: + trainset.ReadyToGo = false; + break; + case ConsistReadyOverride.AlwaysReady: + trainset.ReadyToGo = true; + break; + } + string? vehicle = TrainsetDisplay.UniquifyForLaunch(trainset, scenery, _state.StartingVehicleName) ?? StartableVehicle(trainset, _state.StartingVehicleName); _state.StartingVehicleName = vehicle; diff --git a/StarterNG/Domain/Settings/SimulatorSettings.cs b/StarterNG/Domain/Settings/SimulatorSettings.cs index 4aa48c2..a93169e 100644 --- a/StarterNG/Domain/Settings/SimulatorSettings.cs +++ b/StarterNG/Domain/Settings/SimulatorSettings.cs @@ -2,6 +2,9 @@ using System; namespace StarterNG.Domain.Settings; +// Values match the key eu07.ini has carried since the old starter. +public enum ConsistReadyOverride { Scenery = 0, AlwaysCold = 1, AlwaysReady = 2 } + public enum ExeProblem { None, NotFound, NotExecutable, WrongPlatform } public sealed class SimulatorSettings @@ -106,6 +109,8 @@ public sealed class SimulatorSettings public bool LargeThumbnails; public bool AutoExpandSceneryTree; + public ConsistReadyOverride ReadyOverride = ConsistReadyOverride.Scenery; + public bool ShowAiVehicles = true; public bool DrivableOnly = true; public bool ShowArchivalSceneries = true; diff --git a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs index 38561aa..498a50f 100644 --- a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs +++ b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs @@ -136,6 +136,14 @@ public sealed class SettingsSerializer s.AutoCloseStarter = c.GetBool("starter.autoclose", false); s.LargeThumbnails = c.GetBool("starter.largethumbnails", false); s.AutoExpandSceneryTree = c.GetBool("starter.expandtree", false); + // An unrecognised value means "do not interfere" rather than the nearest + // state: clamping upwards would silently force every consist ready. + s.ReadyOverride = c.GetInt("starter.batterydefault", 0) switch + { + 1 => ConsistReadyOverride.AlwaysCold, + 2 => ConsistReadyOverride.AlwaysReady, + _ => ConsistReadyOverride.Scenery + }; s.ShowAiVehicles = c.GetBool("starter.show.ai", true); s.DrivableOnly = c.GetBool("starter.drivableonly", true); s.ShowArchivalSceneries = c.GetBool("starter.show.archival", true); @@ -264,6 +272,7 @@ public sealed class SettingsSerializer c.SetBool("starter.autoclose", s.AutoCloseStarter); c.SetBool("starter.largethumbnails", s.LargeThumbnails); c.SetBool("starter.expandtree", s.AutoExpandSceneryTree); + c.SetInt("starter.batterydefault", (int)s.ReadyOverride); c.SetBool("starter.show.ai", s.ShowAiVehicles); c.SetBool("starter.drivableonly", s.DrivableOnly); c.SetBool("starter.show.archival", s.ShowArchivalSceneries); diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs index 7fe8475..4527826 100644 --- a/StarterNG/Views/Depot/VehicleDetails.cs +++ b/StarterNG/Views/Depot/VehicleDetails.cs @@ -12,6 +12,7 @@ using Avalonia.Media; using StarterNG.Classes; using StarterNG.Controls; using StarterNG.Domain; +using StarterNG.Domain.Settings; using StarterNG.Domain.Vehicles; using StarterNG.Application; @@ -579,14 +580,21 @@ public sealed class VehicleDetails private Control ReadyToGoRow() { var trainset = _consist.EditingTrainset; + var over = AppServices.Current.Settings.ReadyOverride; + bool overridden = over != ConsistReadyOverride.Scenery; + var ready = new CheckBox { Content = App.Loc["ConsistReady"], FontSize = 12, - IsEnabled = trainset is not null, - IsChecked = trainset?.ReadyToGo ?? false + IsEnabled = trainset is not null && !overridden, + IsChecked = overridden + ? over == ConsistReadyOverride.AlwaysReady + : trainset?.ReadyToGo ?? false }; - ToolTip.SetTip(ready, App.Loc["ConsistReadyDesc"]); + ToolTip.SetTip(ready, overridden + ? App.Loc["ConsistReadyOverridden"] + : App.Loc["ConsistReadyDesc"]); ready.IsCheckedChanged += (_, _) => { diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index cf176fe..d441d5a 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -118,6 +118,15 @@ + + + + + + + diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs index 8e480cf..0676034 100644 --- a/StarterNG/Views/Settings.axaml.cs +++ b/StarterNG/Views/Settings.axaml.cs @@ -260,6 +260,7 @@ public partial class Settings : UserControl, ISettingsCapture AutoCloseStarterCb.IsChecked = s.AutoCloseStarter; LargeThumbnailsCb.IsChecked = s.LargeThumbnails; AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree; + ReadyOverrideCb.SelectedIndex = (int)s.ReadyOverride; } finally { @@ -357,6 +358,7 @@ public partial class Settings : UserControl, ISettingsCapture s.AutoCloseStarter = IsChecked(AutoCloseStarterCb); s.LargeThumbnails = IsChecked(LargeThumbnailsCb); s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb); + s.ReadyOverride = (ConsistReadyOverride)Math.Clamp(ReadyOverrideCb.SelectedIndex, 0, 2); } private void ComButton_OnClick(object? sender, RoutedEventArgs e) diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml index 109f347..9332368 100644 --- a/StarterNG/startercfg/lang/cz.xml +++ b/StarterNG/startercfg/lang/cz.xml @@ -216,6 +216,12 @@ Náklad celé soupravy Consist ready to go The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. + Force the consist state on launch + Overrides the depot’s "consist ready to go" box for every scenario you start. + As authored + Always cold + Always ready + Overridden by "Force the consist state on launch" in Settings. Náhodný druh Maximální množství Náhodné množství diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index 4199795..2886e96 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -216,6 +216,12 @@ Whole-consist load Consist ready to go The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. + Force the consist state on launch + Overrides the depot’s "consist ready to go" box for every scenario you start. + As authored + Always cold + Always ready + Overridden by "Force the consist state on launch" in Settings. Random type Maximum amount Random amount diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index a8a22f4..d25ded6 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -216,6 +216,12 @@ A teljes szerelvény rakománya Consist ready to go The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. + Force the consist state on launch + Overrides the depot’s "consist ready to go" box for every scenario you start. + As authored + Always cold + Always ready + Overridden by "Force the consist state on launch" in Settings. Véletlen típus Legnagyobb mennyiség Véletlen mennyiség diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index b78f82c..80405e7 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -216,6 +216,12 @@ Ładunki zestawienia Skład gotowy do jazdy Skład startuje przygotowany: przewód główny napełniony, kran maszynisty ustawiony, akumulatory załączone. Odznaczony startuje zimny i zahamowany. + Wymuś stan składu przy starcie + Nadpisuje pole „Skład gotowy do jazdy” w zajezdni dla każdego uruchamianego scenariusza. + Jak w scenerii + Zawsze zimny + Zawsze gotowy + Nadpisane opcją „Wymuś stan składu przy starcie” w Ustawieniach. Losowy rodzaj Maksymalna ilość Losowa ilość diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index 1ecee1c..fbda204 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -216,6 +216,12 @@ Груз всего состава Consist ready to go The consist starts prepared: brake pipe charged, driver’s brake valve set, batteries closed. Unchecked it starts cold and braked. + Force the consist state on launch + Overrides the depot’s "consist ready to go" box for every scenario you start. + As authored + Always cold + Always ready + Overridden by "Force the consist state on launch" in Settings. Случайный тип Максимальное количество Случайное количество From 4047e39456fdb3cac9364680077996cf0361a406 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:28:20 +0200 Subject: [PATCH 59/63] Line the settings key and description up with the row above They were laid out as one run, so every explanation started wherever its key happened to end. Key now sits under the label, explanation under the control. --- StarterNG/Styles/Settings.axaml | 40 +++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 7d5f14f..730dc43 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -53,25 +53,27 @@ VerticalAlignment="Center" Margin="12,0,0,0" /> - - - - - + + + + + From 4840740f313c0672ce98bdd68a1ea89b936b4ecb Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:32:16 +0200 Subject: [PATCH 60/63] Pin the settings key to the label column width A row with no label hides PART_Label, collapsing the column onto the key, so the explanations went ragged again on every checkbox row. --- StarterNG/Styles/Settings.axaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 730dc43..c5a8ca4 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -57,6 +57,10 @@ key under the label, the explanation under the control. Laying them out as one run instead made every explanation start wherever its key happened to end. --> + + From 392d9e7b6aad95a6caae000449ef1726480fb420 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:34:29 +0200 Subject: [PATCH 61/63] Give the consist state its own section, drop the maximum amount button The ready-to-go box was sitting under a heading about cargo. --- StarterNG/Domain/Cargo.cs | 6 ------ StarterNG/Views/Depot/VehicleDetails.cs | 7 +++---- StarterNG/startercfg/lang/cz.xml | 1 - StarterNG/startercfg/lang/en.xml | 1 - StarterNG/startercfg/lang/hu.xml | 1 - StarterNG/startercfg/lang/pl.xml | 1 - StarterNG/startercfg/lang/ru.xml | 1 - 7 files changed, 3 insertions(+), 15 deletions(-) diff --git a/StarterNG/Domain/Cargo.cs b/StarterNG/Domain/Cargo.cs index 0b2c76a..4584bc5 100644 --- a/StarterNG/Domain/Cargo.cs +++ b/StarterNG/Domain/Cargo.cs @@ -117,12 +117,6 @@ public sealed class Cargo } } - public void MaxAmounts(IReadOnlyList consist) - { - foreach (var c in consist.SelectMany(u => u.Cars)) - FillToMax(c); - } - public void RandomAmounts(IReadOnlyList consist) { foreach (var c in consist.SelectMany(u => u.Cars)) diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs index 4527826..f621207 100644 --- a/StarterNG/Views/Depot/VehicleDetails.cs +++ b/StarterNG/Views/Depot/VehicleDetails.cs @@ -559,12 +559,10 @@ public sealed class VehicleDetails var tools = Stack( LoadToolButton(App.Loc["ConsistRandomType"], () => { _cargo.RandomTypes(_consist); _redraw(); }, secondary: true), - LoadToolButton(App.Loc["ConsistMaxAmount"], - () => { _cargo.MaxAmounts(_consist); _redraw(); }, secondary: true), LoadToolButton(App.Loc["ConsistRandomAmount"], () => { _cargo.RandomAmounts(_consist); _redraw(); }, secondary: true)); - var buttons = Stack(ReadyToGoRow(), tools); + var buttons = tools; var count = new TextBlock { @@ -574,7 +572,8 @@ public sealed class VehicleDetails VerticalAlignment = VerticalAlignment.Center }; - loadsPanel.Children.Add(Section(App.Loc["ConsistLoadTools"], buttons, count)); + loadsPanel.Children.Add(Section(App.Loc["Consist"], ReadyToGoRow(), count)); + loadsPanel.Children.Add(Section(App.Loc["ConsistLoadTools"], buttons)); } private Control ReadyToGoRow() diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml index 9332368..5c95d84 100644 --- a/StarterNG/startercfg/lang/cz.xml +++ b/StarterNG/startercfg/lang/cz.xml @@ -223,7 +223,6 @@ Always ready Overridden by "Force the consist state on launch" in Settings. Náhodný druh - Maximální množství Náhodné množství Nastavit Délka diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index 2886e96..36f7335 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -223,7 +223,6 @@ Always ready Overridden by "Force the consist state on launch" in Settings. Random type - Maximum amount Random amount Set Length diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index d25ded6..b05ebd4 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -223,7 +223,6 @@ Always ready Overridden by "Force the consist state on launch" in Settings. Véletlen típus - Legnagyobb mennyiség Véletlen mennyiség Beállítás Hossz diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index 80405e7..77e280b 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -223,7 +223,6 @@ Zawsze gotowy Nadpisane opcją „Wymuś stan składu przy starcie” w Ustawieniach. Losowy rodzaj - Maksymalna ilość Losowa ilość Ustaw Długość diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index fbda204..9c4aed0 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -223,7 +223,6 @@ Always ready Overridden by "Force the consist state on launch" in Settings. Случайный тип - Максимальное количество Случайное количество Задать Длина From 10523a500a5b82c3f6fd810c9f534e9327e114b6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:36:00 +0200 Subject: [PATCH 62/63] Order the vehicle list the way the old starter did It listed textures by file name and left the ordering to Delphi's Sorted, which is AnsiCompareText - case-insensitive and locale aware, not ordinal. --- StarterNG/Views/Depot/VehicleBrowser.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs index 56352d3..6fc0ac8 100644 --- a/StarterNG/Views/Depot/VehicleBrowser.cs +++ b/StarterNG/Views/Depot/VehicleBrowser.cs @@ -314,8 +314,7 @@ public sealed class VehicleBrowser var matched = _db.Textures .Where(t => PassesFilters(t, search, hasSearch)) - .OrderBy(BrowserName, StringComparer.OrdinalIgnoreCase) - .ThenBy(t => t.Skinfile, StringComparer.OrdinalIgnoreCase) + .OrderBy(t => Consist.Base(t.Skinfile), TextureOrder) .Take(MaxListRows) .ToList(); @@ -471,8 +470,7 @@ public sealed class VehicleBrowser return _db.Textures .Where(t => PassesFilters(t, search, hasSearch)) - .OrderBy(BrowserName, StringComparer.OrdinalIgnoreCase) - .ThenBy(t => t.Skinfile, StringComparer.OrdinalIgnoreCase) + .OrderBy(t => Consist.Base(t.Skinfile), TextureOrder) .ToList(); } @@ -505,10 +503,12 @@ public sealed class VehicleBrowser || C(t.Meta?.Vehicle) || C(t.Meta?.Operator) || C(VehicleInfo.ClassOf(t)); } - /// - /// The name the list row carries, and the key it is sorted on - the two have - /// to be the same string or the list reads as unordered. - /// + // The old starter listed textures by file name and left the ordering to + // Delphi's Sorted, which is AnsiCompareText: case-insensitive and locale + // aware. Ordinal comparison puts digits, underscores and accented letters + // somewhere else entirely, which is why the list looked reshuffled. + private static readonly StringComparer TextureOrder = StringComparer.CurrentCultureIgnoreCase; + private static string BrowserName(VehicleTexture texture) { if (!string.IsNullOrEmpty(texture.TextureMini) && From 5d01c8d381bbee002badaaac5a3870345e0ba345 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 23:40:29 +0200 Subject: [PATCH 63/63] List vehicles by texture name, not thumbnail name The list called a vehicle something no other panel used: the detail tab, the old starter and the sort key all name it by its texture file. --- StarterNG/Views/Depot/VehicleBrowser.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs index 6fc0ac8..96210d9 100644 --- a/StarterNG/Views/Depot/VehicleBrowser.cs +++ b/StarterNG/Views/Depot/VehicleBrowser.cs @@ -509,13 +509,10 @@ public sealed class VehicleBrowser // somewhere else entirely, which is why the list looked reshuffled. private static readonly StringComparer TextureOrder = StringComparer.CurrentCultureIgnoreCase; - private static string BrowserName(VehicleTexture texture) - { - if (!string.IsNullOrEmpty(texture.TextureMini) && - !string.Equals(texture.TextureMini, texture.ResolvedClass, StringComparison.OrdinalIgnoreCase)) - return texture.TextureMini!; - return Consist.Base(texture.Skinfile); - } + // The texture's own file name, as the old starter listed it and as the detail + // panel names it. The thumbnail name went here before, which meant the list + // called a vehicle something none of the other panels did. + private static string BrowserName(VehicleTexture texture) => Consist.Base(texture.Skinfile); private string BrowserLabel(VehicleTexture texture, IReadOnlyList? set) {