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