mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-07-18 01:19:19 +02:00
Physics and linux compilation support
This commit is contained in:
@@ -25,4 +25,8 @@ Global
|
|||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(RiderSharedRunConfigurations) = postSolution
|
||||||
|
File = linuix.run\StarterNG (linux64).run.xml
|
||||||
|
File = windows.run\StarterNG (win64).run.xml
|
||||||
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ public sealed class GameData
|
|||||||
// first thumbnail render doesn't scan textures/mini/ on the UI thread.
|
// first thumbnail render doesn't scan textures/mini/ on the UI thread.
|
||||||
VehicleDatabase.PreloadMiniIndex(miniDir);
|
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
|
// Phase 2 - sceneries
|
||||||
foreach (string file in scnFiles)
|
foreach (string file in scnFiles)
|
||||||
{
|
{
|
||||||
|
|||||||
230
StarterNG/Classes/Physics.cs
Normal file
230
StarterNG/Classes/Physics.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace StarterNG.Classes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Physics
|
||||||
|
{
|
||||||
|
public double Mass; // kg, as written in the .fiz
|
||||||
|
public double VMax; // km/h
|
||||||
|
public double Length; // metres
|
||||||
|
public string LoadAccepted = "";
|
||||||
|
public int MaxLoad;
|
||||||
|
public int AllowedFlagA = 3;
|
||||||
|
public int AllowedFlagB = 3;
|
||||||
|
public string ControlTypeA = "";
|
||||||
|
public string ControlTypeB = "";
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, Physics?> 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<string, string>? _fizIndex;
|
||||||
|
|
||||||
|
/// <summary>Builds the .fiz index up front (call from the startup load).</summary>
|
||||||
|
public static void PreloadIndex(string dynamicRoot = "dynamic") => FizIndex(dynamicRoot);
|
||||||
|
|
||||||
|
private static Dictionary<string, string> FizIndex(string dynamicRoot)
|
||||||
|
{
|
||||||
|
if (_fizIndex != null)
|
||||||
|
return _fizIndex;
|
||||||
|
|
||||||
|
var index = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(dynamicRoot))
|
||||||
|
foreach (string f in Directory.EnumerateFiles(dynamicRoot, "*.fiz", SearchOption.AllDirectories))
|
||||||
|
index[Path.GetFileNameWithoutExtension(f)] = f;
|
||||||
|
}
|
||||||
|
catch { /* unreadable dynamic/ - no physics */ }
|
||||||
|
|
||||||
|
_fizIndex = index;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads (and caches) the physics for a model name (the .fiz is located by name
|
||||||
|
/// anywhere under dynamic/). The <paramref name="dataFolder"/> is unused now but
|
||||||
|
/// kept for call-site clarity. Returns null when nothing matches. Never throws.
|
||||||
|
/// </summary>
|
||||||
|
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.
|
||||||
|
if (m.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (Cache.TryGetValue(m, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var index = FizIndex(dynamicRoot);
|
||||||
|
string? path = null;
|
||||||
|
if (index.TryGetValue(m, out var p1)) path = p1;
|
||||||
|
else if (index.TryGetValue(m + "dumb", out var p2)) path = p2;
|
||||||
|
|
||||||
|
Physics? phys = null;
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
phys = new Physics { AllowedFlagB = -1 }; // -1 = "B end not declared yet"
|
||||||
|
try { Parse(phys, path, Path.GetDirectoryName(path) ?? "", null, 0); }
|
||||||
|
catch { /* partial data still useful */ }
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if (string.IsNullOrEmpty(phys.ControlTypeB))
|
||||||
|
phys.ControlTypeB = phys.ControlTypeA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache[m] = phys;
|
||||||
|
return phys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<string> KnownKeys = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"M", "Vmax", "L", "LoadAccepted", "MaxLoad", "AllowedFlag", "ControlType"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void Parse(Physics p, string path, string dir, string[]? prms, int depth)
|
||||||
|
{
|
||||||
|
if (depth > 6 || !File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tokens = Tokenize(File.ReadAllText(path, Encoding.GetEncoding(1250)));
|
||||||
|
|
||||||
|
string section = "";
|
||||||
|
string? pendingKey = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < tokens.Count; i++)
|
||||||
|
{
|
||||||
|
string tok = tokens[i];
|
||||||
|
if (tok == "=")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// include <file> <params...> end -> parse the included physics
|
||||||
|
if (tok.Equals("include", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (++i >= tokens.Count) break;
|
||||||
|
string incFile = Resolve(tokens[i], prms);
|
||||||
|
var incParams = new List<string>();
|
||||||
|
i++;
|
||||||
|
while (i < tokens.Count && !tokens[i].Equals("end", StringComparison.OrdinalIgnoreCase))
|
||||||
|
incParams.Add(Resolve(tokens[i++], prms));
|
||||||
|
Parse(p, Path.Combine(dir, incFile), dir, incParams.ToArray(), depth + 1);
|
||||||
|
pendingKey = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int eq = tok.IndexOf('=');
|
||||||
|
if (eq >= 0)
|
||||||
|
{
|
||||||
|
string k = tok[..eq].TrimStart('.').Trim();
|
||||||
|
string v = tok[(eq + 1)..].Trim();
|
||||||
|
if (v.Length == 0) { pendingKey = k; continue; }
|
||||||
|
Apply(p, section, k, Resolve(v, prms));
|
||||||
|
pendingKey = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string t = tok.TrimStart('.').Trim();
|
||||||
|
if (pendingKey != null)
|
||||||
|
{
|
||||||
|
Apply(p, section, pendingKey, Resolve(t, prms));
|
||||||
|
pendingKey = null;
|
||||||
|
}
|
||||||
|
else if (KnownKeys.Contains(t))
|
||||||
|
{
|
||||||
|
pendingKey = t; // value comes in a following token (spaced "Key = value")
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Apply(Physics p, string section, string key, string value)
|
||||||
|
{
|
||||||
|
switch (section)
|
||||||
|
{
|
||||||
|
case "param":
|
||||||
|
if (key.Equals("M", StringComparison.OrdinalIgnoreCase)) p.Mass = ParseD(value, p.Mass);
|
||||||
|
else if (key.Equals("Vmax", StringComparison.OrdinalIgnoreCase)) p.VMax = ParseD(value, p.VMax);
|
||||||
|
break;
|
||||||
|
case "load":
|
||||||
|
if (key.Equals("LoadAccepted", StringComparison.OrdinalIgnoreCase)) p.LoadAccepted = value;
|
||||||
|
else if (key.Equals("MaxLoad", StringComparison.OrdinalIgnoreCase)) p.MaxLoad = (int)ParseD(value, p.MaxLoad);
|
||||||
|
break;
|
||||||
|
case "dimensions":
|
||||||
|
if (key.Equals("L", StringComparison.OrdinalIgnoreCase)) p.Length = ParseD(value, p.Length);
|
||||||
|
break;
|
||||||
|
case "buffcoupl":
|
||||||
|
case "buffcoupl1":
|
||||||
|
if (key.Equals("AllowedFlag", StringComparison.OrdinalIgnoreCase)) p.AllowedFlagA = Flag(value, p.AllowedFlagA);
|
||||||
|
else if (key.Equals("ControlType", StringComparison.OrdinalIgnoreCase)) p.ControlTypeA = value;
|
||||||
|
break;
|
||||||
|
case "buffcoupl2":
|
||||||
|
if (key.Equals("AllowedFlag", StringComparison.OrdinalIgnoreCase)) p.AllowedFlagB = Flag(value, p.AllowedFlagB);
|
||||||
|
else if (key.Equals("ControlType", StringComparison.OrdinalIgnoreCase)) p.ControlTypeB = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
? (f < 0 ? -f + 128 : f)
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
private static double ParseD(string v, double fallback)
|
||||||
|
=> double.TryParse(v, System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out double d)
|
||||||
|
? 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] != ')')
|
||||||
|
return token;
|
||||||
|
string digits = new string(token.Where(char.IsDigit).ToArray());
|
||||||
|
return int.TryParse(digits, out int idx) && idx >= 1 && idx <= prms.Length
|
||||||
|
? prms[idx - 1]
|
||||||
|
: token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> Tokenize(string text)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var line in text.Split('\n'))
|
||||||
|
{
|
||||||
|
string l = line;
|
||||||
|
int hash = l.IndexOf('#');
|
||||||
|
if (hash >= 0) l = l[..hash];
|
||||||
|
sb.Append(l).Append(' ');
|
||||||
|
}
|
||||||
|
return sb.ToString()
|
||||||
|
.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,6 +153,53 @@ public sealed class Settings
|
|||||||
private static string DefaultConfigPath() =>
|
private static string DefaultConfigPath() =>
|
||||||
Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini");
|
Path.Combine(Directory.GetCurrentDirectory(), "eu07.ini");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The simulator executable to launch. With auto-selection on, the working
|
||||||
|
/// directory is scanned for an <c>eu07*</c> binary - <c>eu07</c> on Linux/macOS,
|
||||||
|
/// <c>eu07.exe</c> on Windows - so the same procedure works on every OS. With it
|
||||||
|
/// off, the explicitly chosen path is used.
|
||||||
|
/// </summary>
|
||||||
|
public string ResolveExecutable()
|
||||||
|
{
|
||||||
|
if (!SelectExeAutomatically && !string.IsNullOrWhiteSpace(ExecutablePath))
|
||||||
|
return ExecutablePath;
|
||||||
|
|
||||||
|
string canonical = OperatingSystem.IsWindows() ? "eu07.exe" : "eu07";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? best = null;
|
||||||
|
foreach (string path in Directory.GetFiles(Directory.GetCurrentDirectory(), "eu07*"))
|
||||||
|
{
|
||||||
|
if (!IsExecutableCandidate(path))
|
||||||
|
continue;
|
||||||
|
string file = Path.GetFileName(path);
|
||||||
|
if (string.Equals(file, canonical, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return path; // exact canonical name wins
|
||||||
|
if (best == null || file.Length < Path.GetFileName(best).Length)
|
||||||
|
best = path; // otherwise prefer the shortest eu07* name
|
||||||
|
}
|
||||||
|
if (best != null)
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
catch { /* fall through to the canonical name */ }
|
||||||
|
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
if (ext is ".ini" or ".log" or ".txt" or ".cfg" or ".config" or ".json"
|
||||||
|
or ".dll" or ".so" or ".dat" or ".bak" or ".csv" or ".xml")
|
||||||
|
return false;
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
return ext == ".exe";
|
||||||
|
// Linux / macOS: no extension (eu07) or a known binary suffix
|
||||||
|
return ext.Length == 0 || ext is ".x86_64" or ".run" or ".appimage";
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly Encoding FileEncoding = ResolveEncoding();
|
private static readonly Encoding FileEncoding = ResolveEncoding();
|
||||||
|
|
||||||
private static Encoding ResolveEncoding()
|
private static Encoding ResolveEncoding()
|
||||||
|
|||||||
@@ -395,6 +395,19 @@ public sealed class Coupling
|
|||||||
Parameters.Insert(0, brake.ToParameter());
|
Parameters.Insert(0, brake.ToParameter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The wheel/damage setting carried in the "W" parameter, or null.</summary>
|
||||||
|
public WheelSettings? GetWheels() =>
|
||||||
|
WheelSettings.FromParameter(
|
||||||
|
Parameters.FirstOrDefault(p => p.StartsWith("W", StringComparison.Ordinal)));
|
||||||
|
|
||||||
|
/// <summary>Replaces (or clears, when empty/null) the "W" wheel parameter.</summary>
|
||||||
|
public void SetWheels(WheelSettings? wheels)
|
||||||
|
{
|
||||||
|
Parameters.RemoveAll(p => p.StartsWith("W", StringComparison.Ordinal));
|
||||||
|
if (wheels != null && !wheels.IsEmpty)
|
||||||
|
Parameters.Add(wheels.ToParameter());
|
||||||
|
}
|
||||||
|
|
||||||
public Coupling Clone() => new()
|
public Coupling Clone() => new()
|
||||||
{
|
{
|
||||||
Flags = Flags,
|
Flags = Flags,
|
||||||
@@ -452,3 +465,60 @@ public sealed class BrakeSetting
|
|||||||
|
|
||||||
public string ToParameter() => "B" + Mode + (Load ?? "") + (Switch ?? "");
|
public string ToParameter() => "B" + Mode + (Load ?? "") + (Switch ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wheel-damage settings carried in the "W" parameter of a vehicle's coupling
|
||||||
|
/// field: <c>W[H<sway%>][F<flat mm>][R<random flat mm>][P<flat prob%>]</c>
|
||||||
|
/// (e.g. "WH25F5R10P8"). See https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WheelSettings
|
||||||
|
{
|
||||||
|
/// <summary>H — sway ("wężykowanie") probability, %.</summary>
|
||||||
|
public int Sway;
|
||||||
|
|
||||||
|
/// <summary>F — flat spot ("podkucie") of this size, mm.</summary>
|
||||||
|
public int Flatness;
|
||||||
|
|
||||||
|
/// <summary>R — additional random flat spot, 0..x mm.</summary>
|
||||||
|
public int FlatnessRand;
|
||||||
|
|
||||||
|
/// <summary>P — flat-spot probability, % (guaranteed when omitted).</summary>
|
||||||
|
public int FlatnessProb;
|
||||||
|
|
||||||
|
public bool IsEmpty => Sway <= 0 && Flatness <= 0 && FlatnessRand <= 0 && FlatnessProb <= 0;
|
||||||
|
|
||||||
|
public static WheelSettings? FromParameter(string? param)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(param) || param![0] != 'W')
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var w = new WheelSettings();
|
||||||
|
string body = param.Substring(1);
|
||||||
|
int i = 0;
|
||||||
|
while (i < body.Length)
|
||||||
|
{
|
||||||
|
char code = body[i++];
|
||||||
|
int start = i;
|
||||||
|
while (i < body.Length && char.IsDigit(body[i])) i++;
|
||||||
|
if (!int.TryParse(body[start..i], out int val)) continue;
|
||||||
|
switch (char.ToUpperInvariant(code))
|
||||||
|
{
|
||||||
|
case 'H': w.Sway = val; break;
|
||||||
|
case 'F': w.Flatness = val; break;
|
||||||
|
case 'R': w.FlatnessRand = val; break;
|
||||||
|
case 'P': w.FlatnessProb = val; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToParameter()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder("W");
|
||||||
|
if (Sway > 0) sb.Append('H').Append(Sway);
|
||||||
|
if (Flatness > 0) sb.Append('F').Append(Flatness);
|
||||||
|
if (FlatnessRand > 0) sb.Append('R').Append(FlatnessRand);
|
||||||
|
if (FlatnessProb > 0) sb.Append('P').Append(FlatnessProb);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,14 +122,19 @@ public partial class MainWindow : Window
|
|||||||
Settings.Instance.CaptureAndSave();
|
Settings.Instance.CaptureAndSave();
|
||||||
|
|
||||||
// launch the game: -s $<name>.scn -v <vehicle>. Keep the handle so we can
|
// launch the game: -s $<name>.scn -v <vehicle>. Keep the handle so we can
|
||||||
// watch for the simulator exiting.
|
// 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;
|
Process? sim;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sim = Process.Start(new ProcessStartInfo(Settings.Instance.ExecutablePath)
|
string exe = Path.GetFullPath(Settings.Instance.ResolveExecutable());
|
||||||
|
sim = Process.Start(new ProcessStartInfo
|
||||||
{
|
{
|
||||||
|
FileName = exe,
|
||||||
Arguments = $"-s {exportName} -v {vehicle}",
|
Arguments = $"-s {exportName} -v {vehicle}",
|
||||||
UseShellExecute = true
|
WorkingDirectory = Path.GetDirectoryName(exe) ?? Directory.GetCurrentDirectory(),
|
||||||
|
UseShellExecute = false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -162,7 +167,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
if (sim is null)
|
if (sim is null)
|
||||||
{
|
{
|
||||||
string name = Path.GetFileNameWithoutExtension(Settings.Instance.ExecutablePath);
|
string name = Path.GetFileNameWithoutExtension(Settings.Instance.ResolveExecutable());
|
||||||
sim = Process.GetProcessesByName(name).FirstOrDefault();
|
sim = Process.GetProcessesByName(name).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class LocalizationService : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
/// <summary>Folder holding the language files, resolved as starter/lang next to the executable.</summary>
|
/// <summary>Folder holding the language files, resolved as starter/lang next to the executable.</summary>
|
||||||
public static string LangDirectory =>
|
public static string LangDirectory =>
|
||||||
Path.Combine(AppContext.BaseDirectory, "starter", "lang");
|
Path.Combine(AppContext.BaseDirectory, "startercfg", "lang");
|
||||||
|
|
||||||
private Dictionary<string, string> _strings = new();
|
private Dictionary<string, string> _strings = new();
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
|
<PublishTrimmed>true</PublishTrimmed>
|
||||||
<IsAotCompatible>true</IsAotCompatible>
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<AssemblyVersion>1.0.0.1</AssemblyVersion>
|
<AssemblyVersion>1.1.0.3</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.1</FileVersion>
|
<FileVersion>1.1.0.3</FileVersion>
|
||||||
<AssemblyName>Starter</AssemblyName>
|
<AssemblyName>Starter</AssemblyName>
|
||||||
<Company>eu07.pl</Company>
|
<Company>eu07.pl</Company>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -54,9 +55,9 @@
|
|||||||
compiled into the assembly. They are copied verbatim into a starter/lang/
|
compiled into the assembly. They are copied verbatim into a starter/lang/
|
||||||
folder next to the executable so they can be edited or extended without a
|
folder next to the executable so they can be edited or extended without a
|
||||||
rebuild. XmlReader parsing is AOT-safe, unlike the Avalonia XAML loader. -->
|
rebuild. XmlReader parsing is AOT-safe, unlike the Avalonia XAML loader. -->
|
||||||
<Content Include="starter\lang\*.xml">
|
<Content Include="startercfg\lang\*.xml">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Link>starter\lang\%(Filename)%(Extension)</Link>
|
<Link>startercfg\lang\%(Filename)%(Extension)</Link>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -86,6 +86,11 @@
|
|||||||
<StackPanel Name="loadsPanel" Margin="4" Spacing="8" />
|
<StackPanel Name="loadsPanel" Margin="4" Spacing="8" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Name="damagePanel" Margin="4" Spacing="8" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
@@ -93,6 +98,10 @@
|
|||||||
<!-- Consist (full-width bottom, horizontally scrollable) -->
|
<!-- Consist (full-width bottom, horizontally scrollable) -->
|
||||||
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
|
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
|
||||||
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
|
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
|
||||||
|
<DockPanel LastChildFill="True">
|
||||||
|
<!-- Train totals computed from the .fiz files -->
|
||||||
|
<TextBlock Name="trainStats" DockPanel.Dock="Top" FontSize="12"
|
||||||
|
Opacity="0.85" Margin="0,0,0,6" />
|
||||||
<Panel>
|
<Panel>
|
||||||
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
|
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||||
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
|
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
|
||||||
@@ -102,6 +111,7 @@
|
|||||||
VerticalAlignment="Center" Spacing="0" />
|
VerticalAlignment="Center" Spacing="0" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
</DockPanel>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -118,11 +118,11 @@ public partial class Depot : UserControl
|
|||||||
// Car-type suffixes (longest first) used only for the consist unit label.
|
// Car-type suffixes (longest first) used only for the consist unit label.
|
||||||
private static readonly string[] CarSuffixes = { "sa", "sb", "ra", "rb", "s" };
|
private static readonly string[] CarSuffixes = { "sa", "sb", "ra", "rb", "s" };
|
||||||
|
|
||||||
// Coupling-bit labels in mask-bit order (bit i = value 1<<i), per the wiki:
|
// Coupling-bit localisation keys in mask-bit order (bit i = value 1<<i), per
|
||||||
// https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
|
// the wiki: https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
|
||||||
private static readonly string[] CouplingBits =
|
private static readonly string[] CouplingBitKeys =
|
||||||
{ "Mechanical", "Brake pipe", "Control (MU)", "High voltage",
|
{ "CplMechanical", "CplBrake", "CplControl", "CplHighVoltage",
|
||||||
"Gangway", "Aux pneumatic", "Heating", "Workshop lock" };
|
"CplGangway", "CplAuxPneumatic", "CplHeating", "CplWorkshop" };
|
||||||
|
|
||||||
public Depot()
|
public Depot()
|
||||||
{
|
{
|
||||||
@@ -463,6 +463,7 @@ public partial class Depot : UserControl
|
|||||||
};
|
};
|
||||||
_consist[i] = item;
|
_consist[i] = item;
|
||||||
_selected = item;
|
_selected = item;
|
||||||
|
AutoConnectAll();
|
||||||
RebuildConsist();
|
RebuildConsist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +504,7 @@ public partial class Depot : UserControl
|
|||||||
}
|
}
|
||||||
_consist.Insert(at, item);
|
_consist.Insert(at, item);
|
||||||
_selected = item;
|
_selected = item;
|
||||||
|
AutoConnectAll();
|
||||||
RebuildConsist();
|
RebuildConsist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +688,7 @@ public partial class Depot : UserControl
|
|||||||
|
|
||||||
emptyHint.IsVisible = _consist.Count == 0;
|
emptyHint.IsVisible = _consist.Count == 0;
|
||||||
UpdateDetails();
|
UpdateDetails();
|
||||||
|
UpdateTrainStats();
|
||||||
WriteBackToScenery();
|
WriteBackToScenery();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,6 +713,89 @@ public partial class Depot : UserControl
|
|||||||
_editingTrainset.Vehicles = flat;
|
_editingTrainset.Vehicles = flat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
return Physics.For(car.DataFolder, dbModel)
|
||||||
|
?? Physics.For(car.DataFolder, car.MmdFile)
|
||||||
|
?? 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)>();
|
||||||
|
foreach (var item in _consist)
|
||||||
|
{
|
||||||
|
var cars = item.Flipped ? item.Cars.AsEnumerable().Reverse() : item.Cars;
|
||||||
|
foreach (var c in cars)
|
||||||
|
flat.Add((c, item.Flipped));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < flat.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var (left, lf) = flat[i];
|
||||||
|
var (right, rf) = flat[i + 1];
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
string lct = lp == null ? "" : (lf ? lp.ControlTypeA : lp.ControlTypeB);
|
||||||
|
string rct = rp == null ? "" : (rf ? rp.ControlTypeB : rp.ControlTypeA);
|
||||||
|
if ((common & Coupling.ControlMU) != 0 &&
|
||||||
|
!string.Equals(lct, rct, StringComparison.OrdinalIgnoreCase))
|
||||||
|
common &= ~Coupling.ControlMU;
|
||||||
|
|
||||||
|
left.Coupling.Flags = left.Coupling.Locked ? -common : common;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Train totals (length / mass / Vmax / load) read from the .fiz files.
|
||||||
|
private void UpdateTrainStats()
|
||||||
|
{
|
||||||
|
if (_consist.Count == 0)
|
||||||
|
{
|
||||||
|
trainStats.Text = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double lengthM = 0, massKg = 0, loadKg = 0;
|
||||||
|
double vmax = double.PositiveInfinity;
|
||||||
|
|
||||||
|
foreach (var item in _consist)
|
||||||
|
foreach (var c in item.Cars)
|
||||||
|
{
|
||||||
|
var p = PhysicsFor(c);
|
||||||
|
if (p != null)
|
||||||
|
{
|
||||||
|
lengthM += p.Length;
|
||||||
|
massKg += p.Mass;
|
||||||
|
if (p.VMax > 0) vmax = Math.Min(vmax, p.VMax);
|
||||||
|
}
|
||||||
|
if (c.LoadCount > 0 && !string.IsNullOrEmpty(c.LoadType))
|
||||||
|
loadKg += (double)c.LoadCount * LoadWeight(c.LoadType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
string v = double.IsInfinity(vmax) ? "—" : $"{vmax.ToString("0", inv)} km/h";
|
||||||
|
trainStats.Text =
|
||||||
|
$"{App.Loc["Length"]}: {lengthM.ToString("0.#", inv)} m · " +
|
||||||
|
$"{App.Loc["Mass"]}: {(massKg / 1000.0).ToString("0.#", inv)} t · " +
|
||||||
|
$"Vmax: {v} · " +
|
||||||
|
$"{App.Loc["Load"]}: {(loadKg / 1000.0).ToString("0.#", inv)} t";
|
||||||
|
}
|
||||||
|
|
||||||
private Control BuildCard(ConsistItem item)
|
private Control BuildCard(ConsistItem item)
|
||||||
{
|
{
|
||||||
bool selected = ReferenceEquals(_selected, item);
|
bool selected = ReferenceEquals(_selected, item);
|
||||||
@@ -961,12 +1047,12 @@ public partial class Depot : UserControl
|
|||||||
Margin = new Thickness(0, 0, 0, 2)
|
Margin = new Thickness(0, 0, 0, 2)
|
||||||
});
|
});
|
||||||
|
|
||||||
for (int i = 0; i < CouplingBits.Length; i++)
|
for (int i = 0; i < CouplingBitKeys.Length; i++)
|
||||||
{
|
{
|
||||||
int bit = 1 << i;
|
int bit = 1 << i;
|
||||||
var check = new CheckBox
|
var check = new CheckBox
|
||||||
{
|
{
|
||||||
Content = CouplingBits[i],
|
Content = App.Loc[CouplingBitKeys[i]],
|
||||||
IsChecked = d.Coupling.Has(bit),
|
IsChecked = d.Coupling.Has(bit),
|
||||||
FontSize = 11
|
FontSize = 11
|
||||||
};
|
};
|
||||||
@@ -1016,16 +1102,19 @@ public partial class Depot : UserControl
|
|||||||
{
|
{
|
||||||
brakesPanel.Children.Clear();
|
brakesPanel.Children.Clear();
|
||||||
loadsPanel.Children.Clear();
|
loadsPanel.Children.Clear();
|
||||||
|
damagePanel.Children.Clear();
|
||||||
|
|
||||||
if (_selected is null)
|
if (_selected is null)
|
||||||
{
|
{
|
||||||
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||||
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||||
|
damagePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildBrakesEditor(_selected);
|
BuildBrakesEditor(_selected);
|
||||||
BuildLoadsEditor(_selected);
|
BuildLoadsEditor(_selected);
|
||||||
|
BuildDamageEditor(_selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TextBlock DetailHint(string text) => new()
|
private static TextBlock DetailHint(string text) => new()
|
||||||
@@ -1060,20 +1149,20 @@ public partial class Depot : UserControl
|
|||||||
{
|
{
|
||||||
var brake = item.Cars[0].Coupling.GetBrake();
|
var brake = item.Cars[0].Coupling.GetBrake();
|
||||||
|
|
||||||
var modeCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
|
var modeCombo = new ComboBox { FontSize = 12, MinWidth = 280 };
|
||||||
AddOption(modeCombo, App.Loc["None"], null);
|
AddOption(modeCombo, App.Loc["None"], null);
|
||||||
foreach (var m in BrakeSetting.Modes) AddOption(modeCombo, m, m);
|
foreach (var m in BrakeSetting.Modes) AddOption(modeCombo, BrakeModeLabel(m), m);
|
||||||
modeCombo.SelectedIndex = brake?.Mode is { } mode
|
modeCombo.SelectedIndex = brake?.Mode is { } mode
|
||||||
? Array.IndexOf(BrakeSetting.Modes, mode) + 1 : 0;
|
? Array.IndexOf(BrakeSetting.Modes, mode) + 1 : 0;
|
||||||
|
|
||||||
string?[] loads = { null, "T", "H", "F", "A" };
|
string?[] loads = { null, "T", "H", "F", "A" };
|
||||||
var loadCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
|
var loadCombo = new ComboBox { FontSize = 12, MinWidth = 280 };
|
||||||
foreach (var l in loads) AddOption(loadCombo, l ?? App.Loc["None"], l);
|
foreach (var l in loads) AddOption(loadCombo, LoadLabel(l), l);
|
||||||
loadCombo.SelectedIndex = Math.Max(0, Array.IndexOf(loads, brake?.Load));
|
loadCombo.SelectedIndex = Math.Max(0, Array.IndexOf(loads, brake?.Load));
|
||||||
|
|
||||||
string?[] switches = { null, "0", "1", "A" };
|
string?[] switches = { null, "0", "1", "A" };
|
||||||
var switchCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
|
var switchCombo = new ComboBox { FontSize = 12, MinWidth = 280 };
|
||||||
foreach (var s in switches) AddOption(switchCombo, s ?? App.Loc["None"], s);
|
foreach (var s in switches) AddOption(switchCombo, SwitchLabel(s), s);
|
||||||
switchCombo.SelectedIndex = Math.Max(0, Array.IndexOf(switches, brake?.Switch));
|
switchCombo.SelectedIndex = Math.Max(0, Array.IndexOf(switches, brake?.Switch));
|
||||||
|
|
||||||
void Apply()
|
void Apply()
|
||||||
@@ -1103,25 +1192,105 @@ public partial class Depot : UserControl
|
|||||||
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo));
|
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Descriptive labels (what the option does), per the wiki - not the raw code.
|
||||||
|
private static string BrakeModeLabel(string code) => code switch
|
||||||
|
{
|
||||||
|
"G" => App.Loc["BrakeFreight"],
|
||||||
|
"P" => App.Loc["BrakePassenger"],
|
||||||
|
"R" => App.Loc["BrakeExpress"],
|
||||||
|
"R+Mg" => App.Loc["BrakeExpressMg"],
|
||||||
|
"Q" => App.Loc["BrakeNoAir"],
|
||||||
|
"O" => App.Loc["BrakeOff"],
|
||||||
|
"A" => App.Loc["BrakeAuto"],
|
||||||
|
_ => code
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string LoadLabel(string? code) => code switch
|
||||||
|
{
|
||||||
|
"T" => App.Loc["LoadEmpty"],
|
||||||
|
"H" => App.Loc["LoadMedium"],
|
||||||
|
"F" => App.Loc["LoadFull"],
|
||||||
|
"A" => App.Loc["LoadAuto"],
|
||||||
|
_ => App.Loc["None"]
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string SwitchLabel(string? code) => code switch
|
||||||
|
{
|
||||||
|
"0" => App.Loc["SwitchOff"],
|
||||||
|
"1" => App.Loc["SwitchOff10"],
|
||||||
|
"A" => App.Loc["SwitchOn"],
|
||||||
|
_ => App.Loc["None"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wheel-damage editor: sway / flat-spot parameters (the "W" coupling prefix).
|
||||||
|
private void BuildDamageEditor(ConsistItem item)
|
||||||
|
{
|
||||||
|
var w = item.Cars[0].Coupling.GetWheels() ?? new WheelSettings();
|
||||||
|
|
||||||
|
NumericUpDown Spin(int value, int max) => new()
|
||||||
|
{
|
||||||
|
FontSize = 12, Minimum = 0, Maximum = max, Increment = 1,
|
||||||
|
Value = value, MinWidth = 120, FormatString = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sway = Spin(w.Sway, 100);
|
||||||
|
var flat = Spin(w.Flatness, 100);
|
||||||
|
var flatRand = Spin(w.FlatnessRand, 100);
|
||||||
|
var flatProb = Spin(w.FlatnessProb, 100);
|
||||||
|
|
||||||
|
void Apply()
|
||||||
|
{
|
||||||
|
var ws = new WheelSettings
|
||||||
|
{
|
||||||
|
Sway = (int)(sway.Value ?? 0),
|
||||||
|
Flatness = (int)(flat.Value ?? 0),
|
||||||
|
FlatnessRand = (int)(flatRand.Value ?? 0),
|
||||||
|
FlatnessProb = (int)(flatProb.Value ?? 0)
|
||||||
|
};
|
||||||
|
foreach (var c in item.Cars)
|
||||||
|
c.Coupling.SetWheels(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
sway.ValueChanged += (_, _) => Apply();
|
||||||
|
flat.ValueChanged += (_, _) => Apply();
|
||||||
|
flatRand.ValueChanged += (_, _) => Apply();
|
||||||
|
flatProb.ValueChanged += (_, _) => Apply();
|
||||||
|
|
||||||
|
damagePanel.Children.Add(UnitTitle(item));
|
||||||
|
damagePanel.Children.Add(LabeledRow(App.Loc["DamageSway"], sway));
|
||||||
|
damagePanel.Children.Add(LabeledRow(App.Loc["DamageFlatness"], flat));
|
||||||
|
damagePanel.Children.Add(LabeledRow(App.Loc["DamageFlatnessRand"], flatRand));
|
||||||
|
damagePanel.Children.Add(LabeledRow(App.Loc["DamageFlatnessProb"], flatProb));
|
||||||
|
}
|
||||||
|
|
||||||
// Cargo type + amount, written into each car's node::dynamic trailing params.
|
// Cargo type + amount, written into each car's node::dynamic trailing params.
|
||||||
private void BuildLoadsEditor(ConsistItem item)
|
private void BuildLoadsEditor(ConsistItem item)
|
||||||
{
|
{
|
||||||
var lead = item.Cars[0];
|
var lead = item.Cars[0];
|
||||||
|
var phys = PhysicsFor(lead);
|
||||||
|
|
||||||
|
// Prefer the cargo this vehicle actually accepts (.fiz LoadAccepted);
|
||||||
|
// otherwise suggest everything from data/load_weights.txt. Free text is
|
||||||
|
// still allowed.
|
||||||
|
var accepted = phys != null && !string.IsNullOrWhiteSpace(phys.LoadAccepted)
|
||||||
|
? phys.LoadAccepted.Split(',', ';').Select(s => s.Trim()).Where(s => s.Length > 0).ToList()
|
||||||
|
: null;
|
||||||
|
|
||||||
// Suggest cargo names from data/load_weights.txt (same source the original
|
|
||||||
// Starter uses), while still allowing a free-typed value.
|
|
||||||
var typeBox = new AutoCompleteBox
|
var typeBox = new AutoCompleteBox
|
||||||
{
|
{
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
MinWidth = 200,
|
MinWidth = 200,
|
||||||
Text = lead.LoadType ?? "",
|
Text = lead.LoadType ?? "",
|
||||||
ItemsSource = LoadTypes(),
|
ItemsSource = accepted ?? (IEnumerable<string>)LoadTypes(),
|
||||||
FilterMode = AutoCompleteFilterMode.ContainsOrdinal
|
FilterMode = AutoCompleteFilterMode.ContainsOrdinal
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cap the amount at the .fiz MaxLoad when it is defined.
|
||||||
|
int maxLoad = phys != null && phys.MaxLoad > 0 ? phys.MaxLoad : 1000;
|
||||||
var countBox = new NumericUpDown
|
var countBox = new NumericUpDown
|
||||||
{
|
{
|
||||||
FontSize = 12, Minimum = 0, Maximum = 1000, Increment = 1,
|
FontSize = 12, Minimum = 0, Maximum = maxLoad, Increment = 1,
|
||||||
Value = lead.LoadCount, MinWidth = 120, FormatString = "0"
|
Value = Math.Min(lead.LoadCount, maxLoad), MinWidth = 120, FormatString = "0"
|
||||||
};
|
};
|
||||||
|
|
||||||
void Apply()
|
void Apply()
|
||||||
@@ -1149,16 +1318,19 @@ public partial class Depot : UserControl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargo names parsed once from data/load_weights.txt (the file the simulator
|
// Cargo names + per-unit weights parsed once from data/load_weights.txt (the
|
||||||
// and the original Starter use), for the load-type suggestions. Each line is
|
// file the simulator and the original Starter use). Each line is
|
||||||
// "<name>: <weight>"; only the name (before the colon) is used here.
|
// "<name>: <weight>".
|
||||||
private static List<string>? _loadTypes;
|
private static List<string>? _loadTypes;
|
||||||
private static IReadOnlyList<string> LoadTypes()
|
private static Dictionary<string, int>? _loadWeights;
|
||||||
|
|
||||||
|
private static void EnsureLoads()
|
||||||
{
|
{
|
||||||
if (_loadTypes != null)
|
if (_loadTypes != null)
|
||||||
return _loadTypes;
|
return;
|
||||||
|
|
||||||
var list = new List<string>();
|
var names = new List<string>();
|
||||||
|
var weights = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string path = Path.Combine("data", "load_weights.txt");
|
string path = Path.Combine("data", "load_weights.txt");
|
||||||
@@ -1171,17 +1343,32 @@ public partial class Depot : UserControl
|
|||||||
continue;
|
continue;
|
||||||
int colon = line.IndexOf(':');
|
int colon = line.IndexOf(':');
|
||||||
string name = (colon >= 0 ? line[..colon] : line).Trim();
|
string name = (colon >= 0 ? line[..colon] : line).Trim();
|
||||||
if (name.Length > 0)
|
if (name.Length == 0) continue;
|
||||||
list.Add(name);
|
names.Add(name);
|
||||||
|
if (colon >= 0 && int.TryParse(line[(colon + 1)..].Trim(), out int w))
|
||||||
|
weights[name] = w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* missing / unreadable - just no suggestions */ }
|
catch { /* missing / unreadable - just no suggestions */ }
|
||||||
|
|
||||||
_loadTypes = list.Distinct(StringComparer.OrdinalIgnoreCase)
|
_loadTypes = names.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
return _loadTypes;
|
_loadWeights = weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> LoadTypes()
|
||||||
|
{
|
||||||
|
EnsureLoads();
|
||||||
|
return _loadTypes!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-unit weight (kg) of a cargo, defaulting to 1000 like the original Starter.
|
||||||
|
private static int LoadWeight(string? name)
|
||||||
|
{
|
||||||
|
EnsureLoads();
|
||||||
|
return !string.IsNullOrEmpty(name) && _loadWeights!.TryGetValue(name!, out int w) ? w : 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------- minis
|
// ------------------------------------------------------------------- minis
|
||||||
|
|||||||
@@ -121,11 +121,51 @@
|
|||||||
<String key="BrakeLoad">Load setting</String>
|
<String key="BrakeLoad">Load setting</String>
|
||||||
<String key="BrakeSwitch">Brake switch</String>
|
<String key="BrakeSwitch">Brake switch</String>
|
||||||
<String key="None">—</String>
|
<String key="None">—</String>
|
||||||
|
|
||||||
|
<!-- Coupling mask bits -->
|
||||||
|
<String key="CplMechanical">Mechanical</String>
|
||||||
|
<String key="CplBrake">Pneumatic 5 atm (brakes)</String>
|
||||||
|
<String key="CplControl">Multiple-unit control</String>
|
||||||
|
<String key="CplHighVoltage">High voltage</String>
|
||||||
|
<String key="CplGangway">Gangway between vehicles</String>
|
||||||
|
<String key="CplAuxPneumatic">Auxiliary pneumatic 8 atm</String>
|
||||||
|
<String key="CplHeating">Heating</String>
|
||||||
|
<String key="CplWorkshop">Workshop coupling (uncouple lock)</String>
|
||||||
|
|
||||||
|
<!-- Brake modes -->
|
||||||
|
<String key="BrakeFreight">Freight (G)</String>
|
||||||
|
<String key="BrakePassenger">Passenger (P)</String>
|
||||||
|
<String key="BrakeExpress">Express (R)</String>
|
||||||
|
<String key="BrakeExpressMg">Express + magnetic rail (R+Mg)</String>
|
||||||
|
<String key="BrakeNoAir">Start without air (Q)</String>
|
||||||
|
<String key="BrakeOff">Brakes disconnected (O)</String>
|
||||||
|
<String key="BrakeAuto">Automatic (A)</String>
|
||||||
|
|
||||||
|
<!-- Load adaptation -->
|
||||||
|
<String key="LoadEmpty">Empty (T)</String>
|
||||||
|
<String key="LoadMedium">Loaded I — medium (H)</String>
|
||||||
|
<String key="LoadFull">Loaded II (F)</String>
|
||||||
|
<String key="LoadAuto">Automatic (A)</String>
|
||||||
|
|
||||||
|
<!-- Brake switch -->
|
||||||
|
<String key="SwitchOff">Off (0)</String>
|
||||||
|
<String key="SwitchOff10">Off with 10% chance (1)</String>
|
||||||
|
<String key="SwitchOn">On (A)</String>
|
||||||
|
|
||||||
|
<!-- Damage (wheel parameters) -->
|
||||||
|
<String key="Damage">Damage</String>
|
||||||
|
<String key="DamageSway">Sway — probability (%)</String>
|
||||||
|
<String key="DamageFlatness">Flat spot (mm)</String>
|
||||||
|
<String key="DamageFlatnessRand">Random flat spot (0–x mm)</String>
|
||||||
|
<String key="DamageFlatnessProb">Flat-spot probability (%)</String>
|
||||||
<String key="LoadType">Load type</String>
|
<String key="LoadType">Load type</String>
|
||||||
<String key="LoadCount">Amount</String>
|
<String key="LoadCount">Amount</String>
|
||||||
<String key="LoadHint">Set an amount above 0 to load the vehicle.</String>
|
<String key="LoadHint">Set an amount above 0 to load the vehicle.</String>
|
||||||
<String key="WagonNumber">Wagon number</String>
|
<String key="WagonNumber">Wagon number</String>
|
||||||
<String key="Set">Set</String>
|
<String key="Set">Set</String>
|
||||||
|
<String key="Length">Length</String>
|
||||||
|
<String key="Mass">Mass</String>
|
||||||
|
<String key="Load">Load</String>
|
||||||
<String key="SelectVehicleHint">Select a vehicle in the consist to edit it.</String>
|
<String key="SelectVehicleHint">Select a vehicle in the consist to edit it.</String>
|
||||||
<String key="ComingSoon">Editing options coming soon.</String>
|
<String key="ComingSoon">Editing options coming soon.</String>
|
||||||
<String key="Split">Split</String>
|
<String key="Split">Split</String>
|
||||||
@@ -121,11 +121,51 @@
|
|||||||
<String key="BrakeLoad">Nastawa ładunkowa</String>
|
<String key="BrakeLoad">Nastawa ładunkowa</String>
|
||||||
<String key="BrakeSwitch">Włącznik hamulca</String>
|
<String key="BrakeSwitch">Włącznik hamulca</String>
|
||||||
<String key="None">—</String>
|
<String key="None">—</String>
|
||||||
|
|
||||||
|
<!-- Bity maski sprzęgu -->
|
||||||
|
<String key="CplMechanical">Mechaniczny</String>
|
||||||
|
<String key="CplBrake">Pneumatyczny 5 atm (hamulce)</String>
|
||||||
|
<String key="CplControl">Ukrotnienie (sterowanie wielokrotne)</String>
|
||||||
|
<String key="CplHighVoltage">Wysokie napięcie</String>
|
||||||
|
<String key="CplGangway">Przejście między pojazdami</String>
|
||||||
|
<String key="CplAuxPneumatic">Pneumatyczny pomocniczy 8 atm</String>
|
||||||
|
<String key="CplHeating">Ogrzewanie</String>
|
||||||
|
<String key="CplWorkshop">Sprzęg warsztatowy (blokada rozłączania)</String>
|
||||||
|
|
||||||
|
<!-- Tryby nastawy hamulca -->
|
||||||
|
<String key="BrakeFreight">Towarowa (G)</String>
|
||||||
|
<String key="BrakePassenger">Osobowa (P)</String>
|
||||||
|
<String key="BrakeExpress">Pospieszna (R)</String>
|
||||||
|
<String key="BrakeExpressMg">Pospieszna + szynowy magnetyczny (R+Mg)</String>
|
||||||
|
<String key="BrakeNoAir">Start bez powietrza (Q)</String>
|
||||||
|
<String key="BrakeOff">Odłączone hamulce (O)</String>
|
||||||
|
<String key="BrakeAuto">Wybór automatyczny (A)</String>
|
||||||
|
|
||||||
|
<!-- Nastawa ładunkowa -->
|
||||||
|
<String key="LoadEmpty">Próżny (T)</String>
|
||||||
|
<String key="LoadMedium">Ładowny I — średni (H)</String>
|
||||||
|
<String key="LoadFull">Ładowny II (F)</String>
|
||||||
|
<String key="LoadAuto">Wybór automatyczny (A)</String>
|
||||||
|
|
||||||
|
<!-- Włącznik hamulca -->
|
||||||
|
<String key="SwitchOff">Wyłączony (0)</String>
|
||||||
|
<String key="SwitchOff10">Wyłączony z prawd. 10% (1)</String>
|
||||||
|
<String key="SwitchOn">Włączony (A)</String>
|
||||||
|
|
||||||
|
<!-- Uszkodzenia (parametry kół) -->
|
||||||
|
<String key="Damage">Uszkodzenia</String>
|
||||||
|
<String key="DamageSway">Wężykowanie — prawdop. (%)</String>
|
||||||
|
<String key="DamageFlatness">Podkucie (mm)</String>
|
||||||
|
<String key="DamageFlatnessRand">Podkucie losowe (0–x mm)</String>
|
||||||
|
<String key="DamageFlatnessProb">Prawdop. podkucia (%)</String>
|
||||||
<String key="LoadType">Rodzaj ładunku</String>
|
<String key="LoadType">Rodzaj ładunku</String>
|
||||||
<String key="LoadCount">Ilość</String>
|
<String key="LoadCount">Ilość</String>
|
||||||
<String key="LoadHint">Ustaw ilość większą od 0, aby załadować pojazd.</String>
|
<String key="LoadHint">Ustaw ilość większą od 0, aby załadować pojazd.</String>
|
||||||
<String key="WagonNumber">Numer wagonu</String>
|
<String key="WagonNumber">Numer wagonu</String>
|
||||||
<String key="Set">Ustaw</String>
|
<String key="Set">Ustaw</String>
|
||||||
|
<String key="Length">Długość</String>
|
||||||
|
<String key="Mass">Masa</String>
|
||||||
|
<String key="Load">Ładunek</String>
|
||||||
<String key="SelectVehicleHint">Wybierz pojazd w składzie, aby go edytować.</String>
|
<String key="SelectVehicleHint">Wybierz pojazd w składzie, aby go edytować.</String>
|
||||||
<String key="ComingSoon">Opcje edycji wkrótce.</String>
|
<String key="ComingSoon">Opcje edycji wkrótce.</String>
|
||||||
<String key="Split">Rozdziel</String>
|
<String key="Split">Rozdziel</String>
|
||||||
10
linuix.run/StarterNG (linux64).run.xml
Normal file
10
linuix.run/StarterNG (linux64).run.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="StarterNG (linux64)" type="DotNetFolderPublish" factoryName="Publish to folder">
|
||||||
|
<riderPublish configuration="Release" delete_existing_files="true" platform="x64" produce_single_file="true" ready_to_run="true" self_contained="true" target_folder="$PROJECT_DIR$/StarterNG/bin/Release/net9.0/linux-x64/publish" target_framework="net9.0" trim_unused_assemblies="true" uuid_high="-3321224803864985254" uuid_low="-7733288644066679555">
|
||||||
|
<runtimes>
|
||||||
|
<item value="linux-x64" />
|
||||||
|
</runtimes>
|
||||||
|
</riderPublish>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
10
windows.run/StarterNG (win64).run.xml
Normal file
10
windows.run/StarterNG (win64).run.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="StarterNG (win64)" type="DotNetFolderPublish" factoryName="Publish to folder">
|
||||||
|
<riderPublish configuration="Release" delete_existing_files="true" platform="x64" produce_single_file="true" ready_to_run="true" self_contained="true" target_folder="$PROJECT_DIR$/StarterNG/bin/Release/net9.0/win-x64/publish" target_framework="net9.0" trim_unused_assemblies="true" uuid_high="-3321224803864985254" uuid_low="-7733288644066679555">
|
||||||
|
<runtimes>
|
||||||
|
<item value="win-x64" />
|
||||||
|
</runtimes>
|
||||||
|
</riderPublish>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
Reference in New Issue
Block a user