Physics and linux compilation support

This commit is contained in:
2026-06-24 00:33:24 +02:00
parent d932761786
commit 60f61a122d
14 changed files with 706 additions and 48 deletions

View File

@@ -86,6 +86,11 @@
<StackPanel Name="loadsPanel" Margin="4" Spacing="8" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="damagePanel" Margin="4" Spacing="8" />
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</HeaderedContentControl>
@@ -93,15 +98,20 @@
<!-- Consist (full-width bottom, horizontally scrollable) -->
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<Panel>
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel Name="consistStack" Orientation="Horizontal"
VerticalAlignment="Center" Spacing="0" />
</ScrollViewer>
</Panel>
<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>
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel Name="consistStack" Orientation="Horizontal"
VerticalAlignment="Center" Spacing="0" />
</ScrollViewer>
</Panel>
</DockPanel>
</HeaderedContentControl>
</Grid>
</UserControl>

View File

@@ -118,11 +118,11 @@ public partial class Depot : UserControl
// Car-type suffixes (longest first) used only for the consist unit label.
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:
// https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
private static readonly string[] CouplingBits =
{ "Mechanical", "Brake pipe", "Control (MU)", "High voltage",
"Gangway", "Aux pneumatic", "Heating", "Workshop lock" };
// Coupling-bit localisation keys in mask-bit order (bit i = value 1<<i), per
// the wiki: https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
private static readonly string[] CouplingBitKeys =
{ "CplMechanical", "CplBrake", "CplControl", "CplHighVoltage",
"CplGangway", "CplAuxPneumatic", "CplHeating", "CplWorkshop" };
public Depot()
{
@@ -463,6 +463,7 @@ public partial class Depot : UserControl
};
_consist[i] = item;
_selected = item;
AutoConnectAll();
RebuildConsist();
}
@@ -503,6 +504,7 @@ public partial class Depot : UserControl
}
_consist.Insert(at, item);
_selected = item;
AutoConnectAll();
RebuildConsist();
}
@@ -686,6 +688,7 @@ public partial class Depot : UserControl
emptyHint.IsVisible = _consist.Count == 0;
UpdateDetails();
UpdateTrainStats();
WriteBackToScenery();
}
@@ -710,6 +713,89 @@ public partial class Depot : UserControl
_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)
{
bool selected = ReferenceEquals(_selected, item);
@@ -961,12 +1047,12 @@ public partial class Depot : UserControl
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;
var check = new CheckBox
{
Content = CouplingBits[i],
Content = App.Loc[CouplingBitKeys[i]],
IsChecked = d.Coupling.Has(bit),
FontSize = 11
};
@@ -1016,16 +1102,19 @@ public partial class Depot : UserControl
{
brakesPanel.Children.Clear();
loadsPanel.Children.Clear();
damagePanel.Children.Clear();
if (_selected is null)
{
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
damagePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
return;
}
BuildBrakesEditor(_selected);
BuildLoadsEditor(_selected);
BuildDamageEditor(_selected);
}
private static TextBlock DetailHint(string text) => new()
@@ -1060,20 +1149,20 @@ public partial class Depot : UserControl
{
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);
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
? Array.IndexOf(BrakeSetting.Modes, mode) + 1 : 0;
string?[] loads = { null, "T", "H", "F", "A" };
var loadCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
foreach (var l in loads) AddOption(loadCombo, l ?? App.Loc["None"], l);
var loadCombo = new ComboBox { FontSize = 12, MinWidth = 280 };
foreach (var l in loads) AddOption(loadCombo, LoadLabel(l), l);
loadCombo.SelectedIndex = Math.Max(0, Array.IndexOf(loads, brake?.Load));
string?[] switches = { null, "0", "1", "A" };
var switchCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
foreach (var s in switches) AddOption(switchCombo, s ?? App.Loc["None"], s);
var switchCombo = new ComboBox { FontSize = 12, MinWidth = 280 };
foreach (var s in switches) AddOption(switchCombo, SwitchLabel(s), s);
switchCombo.SelectedIndex = Math.Max(0, Array.IndexOf(switches, brake?.Switch));
void Apply()
@@ -1103,25 +1192,105 @@ public partial class Depot : UserControl
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.
private void BuildLoadsEditor(ConsistItem item)
{
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
{
FontSize = 12,
MinWidth = 200,
Text = lead.LoadType ?? "",
ItemsSource = LoadTypes(),
ItemsSource = accepted ?? (IEnumerable<string>)LoadTypes(),
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
{
FontSize = 12, Minimum = 0, Maximum = 1000, Increment = 1,
Value = lead.LoadCount, MinWidth = 120, FormatString = "0"
FontSize = 12, Minimum = 0, Maximum = maxLoad, Increment = 1,
Value = Math.Min(lead.LoadCount, maxLoad), MinWidth = 120, FormatString = "0"
};
void Apply()
@@ -1149,16 +1318,19 @@ public partial class Depot : UserControl
});
}
// Cargo names parsed once from data/load_weights.txt (the file the simulator
// and the original Starter use), for the load-type suggestions. Each line is
// "<name>: <weight>"; only the name (before the colon) is used here.
// Cargo names + per-unit weights parsed once from data/load_weights.txt (the
// file the simulator and the original Starter use). Each line is
// "<name>: <weight>".
private static List<string>? _loadTypes;
private static IReadOnlyList<string> LoadTypes()
private static Dictionary<string, int>? _loadWeights;
private static void EnsureLoads()
{
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
{
string path = Path.Combine("data", "load_weights.txt");
@@ -1171,17 +1343,32 @@ public partial class Depot : UserControl
continue;
int colon = line.IndexOf(':');
string name = (colon >= 0 ? line[..colon] : line).Trim();
if (name.Length > 0)
list.Add(name);
if (name.Length == 0) continue;
names.Add(name);
if (colon >= 0 && int.TryParse(line[(colon + 1)..].Trim(), out int w))
weights[name] = w;
}
}
}
catch { /* missing / unreadable - just no suggestions */ }
_loadTypes = list.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
.ToList();
return _loadTypes;
_loadTypes = names.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
.ToList();
_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