fixes for trainset generation

This commit is contained in:
2026-06-16 22:19:42 +02:00
parent fee1106cd4
commit 1cc0cd306a
5 changed files with 365 additions and 93 deletions

View File

@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Text.RegularExpressions;
namespace StarterNG.Classes;
@@ -92,7 +93,6 @@ public class Trainset
// load vehicles
if (tokens[i] == "node")
{
//i++; // node keyword
Dynamic nodeDynamic = new Dynamic();
nodeDynamic.RangeMax = float.Parse(tokens[++i], CultureInfo.InvariantCulture);
nodeDynamic.RangeMin = float.Parse(tokens[++i], CultureInfo.InvariantCulture);
@@ -102,30 +102,27 @@ public class Trainset
nodeDynamic.SkinFile = tokens[++i];
nodeDynamic.MmdFile = tokens[++i];
nodeDynamic.Offset = float.Parse(tokens[++i], CultureInfo.InvariantCulture);
string driverType = tokens[++i];
switch (driverType)
nodeDynamic.DriverType = tokens[++i] switch
{
case "headdriver":
nodeDynamic.DriverType = eDriverType.Headdriver;
break;
case "reardriver":
nodeDynamic.DriverType = eDriverType.Reardriver;
break;
case "passenger":
nodeDynamic.DriverType = eDriverType.Passenger;
break;
default:
nodeDynamic.DriverType = eDriverType.Nobody;
break;
}
"headdriver" => eDriverType.Headdriver,
"reardriver" => eDriverType.Reardriver,
"passenger" => eDriverType.Passenger,
_ => eDriverType.Nobody
};
nodeDynamic.couplingData = (byte)int.Parse(tokens[++i].Split('.')[0]);
// couplingdata: numeric bit-mask plus optional ".B/.W/.T" parameter
// codes (brake / wheel / thermo settings) kept verbatim.
nodeDynamic.Coupling = Coupling.Parse(tokens[++i]);
nodeDynamic.Tail = "";
// Everything before enddynamic is the optional, positional tail
// documented for node::dynamic:
// [Lx] velocity [loadcount [loadtype]] [destination [destination]]
var trailing = new List<string>();
while (i + 1 < tokens.Count && tokens[i + 1] != "enddynamic")
{
nodeDynamic.Tail += " " + tokens[++i];
}
trailing.Add(tokens[++i]);
nodeDynamic.ReadTrailing(trailing);
i++; // jump over enddynamic
Vehicles.Add(nodeDynamic);
@@ -173,7 +170,7 @@ public class Trainset
$"node {vehicle.RangeMax} {vehicle.RangeMin} {vehicle.Name} dynamic " +
$"{vehicle.DataFolder} {vehicle.SkinFile} {vehicle.MmdFile} " +
$"{vehicle.Offset.ToString(CultureInfo.InvariantCulture)} " +
$"{driverType} {vehicle.couplingData}{vehicle.Tail} enddynamic\n";
$"{driverType} {vehicle.Coupling}{vehicle.WriteTrailing()} enddynamic\n";
}
@@ -183,8 +180,8 @@ public class Trainset
public class Dynamic
{
public float RangeMax;
public float RangeMin;
public float RangeMax = -1;
public float RangeMin = 0f;
public string Name;
public string DataFolder;
public string SkinFile;
@@ -192,8 +189,42 @@ public class Dynamic
public string PathName;
public float Offset;
public eDriverType DriverType;
public byte couplingData;
public string Tail;
/// <summary>
/// Coupling with the next vehicle (the <c>couplingdata</c> field): the bit-mask
/// plus any ".B/.W/.T" parameter codes (brake / wheel / thermo settings).
/// </summary>
public Coupling Coupling = new();
/// <summary>
/// Low byte of the coupling mask, exposed for the depot's per-bit editor.
/// Backed by <see cref="Coupling"/> so the two never diverge.
/// </summary>
public byte couplingData
{
get => (byte)(Coupling.AbsFlags & 0xFF);
set => Coupling.Flags = value;
}
// --- optional node::dynamic trailing parameters (all positional, all optional) ---
/// <summary>"Lx" MaxLoad override token (e.g. "L0"), or null when absent.</summary>
public string? MaxLoad;
/// <summary>Whether a per-vehicle velocity token is present / should be written.</summary>
public bool HasVelocity;
/// <summary>Per-vehicle starting velocity.</summary>
public float Velocity;
/// <summary>Cargo amount (loadcount).</summary>
public int LoadCount;
/// <summary>Cargo type (loadtype) — only meaningful when <see cref="LoadCount"/> &gt; 0.</summary>
public string? LoadType;
/// <summary>Optional cargo destination tokens (rarely used; preserved verbatim).</summary>
public List<string> Destinations = new();
// --- depot / consist-builder extras (not part of the .scn syntax) ---
@@ -203,6 +234,67 @@ public class Dynamic
/// <summary>Whether the vehicle is visually reversed in the consist.</summary>
public bool Flipped;
/// <summary>
/// Parses the optional positional parameters that follow couplingdata:
/// <c>[Lx] velocity [loadcount [loadtype]] [destination [destination]]</c>.
/// See https://wiki.eu07.pl/index.php?title=Obiekt_node::dynamic
/// </summary>
internal void ReadTrailing(List<string> 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))
{
Velocity = v;
HasVelocity = true;
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))
{
LoadCount = lc;
p++;
if (lc > 0 && p < t.Count)
LoadType = t[p++];
}
// remaining tokens: optional destinations, kept verbatim
while (p < t.Count)
Destinations.Add(t[p++]);
}
/// <summary>
/// Regenerates the trailing parameter string (each present token prefixed with
/// a space), the inverse of <see cref="ReadTrailing"/>. A loadcount of 0 is
/// omitted, matching the canonical trainset entries the simulator expects.
/// </summary>
internal string WriteTrailing()
{
var sb = new StringBuilder();
if (MaxLoad != null)
sb.Append(' ').Append(MaxLoad);
if (HasVelocity)
sb.Append(' ').Append(Velocity.ToString(CultureInfo.InvariantCulture));
if (LoadCount > 0)
{
sb.Append(' ').Append(LoadCount.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrEmpty(LoadType))
sb.Append(' ').Append(LoadType);
}
foreach (string d in Destinations)
sb.Append(' ').Append(d);
return sb.ToString();
}
/// <summary>Deep copy, used when importing a trainset into the editable consist.</summary>
public Dynamic Clone() => new Dynamic
{
@@ -215,9 +307,148 @@ public class Dynamic
PathName = PathName,
Offset = Offset,
DriverType = DriverType,
couplingData = couplingData,
Tail = Tail,
Coupling = Coupling.Clone(),
MaxLoad = MaxLoad,
HasVelocity = HasVelocity,
Velocity = Velocity,
LoadCount = LoadCount,
LoadType = LoadType,
Destinations = new List<string>(Destinations),
MiniName = MiniName,
Flipped = Flipped
};
}
/// <summary>
/// The <c>couplingdata</c> 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
/// </summary>
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)
/// <summary>Signed mask exactly as written; negative = locked.</summary>
public int Flags;
/// <summary>
/// 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.
/// </summary>
public List<string> Parameters = new();
/// <summary>Mask with the lock sign removed (what the simulator tests bits on).</summary>
public int AbsFlags => Flags < 0 ? -Flags : Flags;
/// <summary>True when the coupling is marked permanent (negative mask).</summary>
public bool Locked
{
get => Flags < 0;
set => Flags = value ? -AbsFlags : AbsFlags;
}
public bool Has(int bit) => (AbsFlags & bit) != 0;
/// <summary>Sets or clears a mask bit, preserving the lock sign.</summary>
public void Set(int bit, bool on)
{
int abs = on ? (AbsFlags | bit) : (AbsFlags & ~bit);
Flags = Locked ? -abs : abs;
}
public static Coupling Parse(string token)
{
var c = new Coupling();
if (string.IsNullOrEmpty(token))
return c;
string[] parts = token.Split('.');
int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int flags);
c.Flags = flags;
for (int i = 1; i < parts.Length; i++)
if (parts[i].Length > 0)
c.Parameters.Add(parts[i]);
return c;
}
/// <summary>The brake-rack setting carried in the "B" parameter, or null.</summary>
public BrakeSetting? GetBrake() =>
BrakeSetting.FromParameter(
Parameters.FirstOrDefault(p => p.StartsWith("B", StringComparison.Ordinal)));
/// <summary>Replaces (or clears, when null) the "B" brake parameter.</summary>
public void SetBrake(BrakeSetting? brake)
{
Parameters.RemoveAll(p => p.StartsWith("B", StringComparison.Ordinal));
if (brake != null)
Parameters.Insert(0, brake.ToParameter());
}
public Coupling Clone() => new()
{
Flags = Flags,
Parameters = new List<string>(Parameters)
};
/// <summary>Serialises back to "&lt;flags&gt;[.param[.param…]]".</summary>
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(Flags.ToString(CultureInfo.InvariantCulture));
foreach (string p in Parameters)
sb.Append('.').Append(p);
return sb.ToString();
}
}
/// <summary>
/// A vehicle's brake-rack setting, carried in the "B" parameter of its coupling
/// field: <c>B&lt;mode&gt;[&lt;load&gt;][&lt;switch&gt;]</c> (e.g. "BP", "BRA", "BG1").
/// See https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
/// </summary>
public sealed class BrakeSetting
{
/// <summary>Brake modes, longest first so "R+Mg" wins over "R" while parsing.</summary>
public static readonly string[] Modes = { "R+Mg", "G", "P", "R", "Q", "O", "A" };
/// <summary>Brake mode: G (freight), P (passenger), R (express), R+Mg, Q, O (off), A (auto).</summary>
public string Mode = "P";
/// <summary>Load adaptation: T (empty), H (medium), F (loaded), A (auto), or null.</summary>
public string? Load;
/// <summary>Brake on/off switch: 0 (off), 1 (off ~10%), A (on), or null.</summary>
public string? Switch;
public static BrakeSetting? FromParameter(string? param)
{
if (string.IsNullOrEmpty(param) || param![0] != 'B' || param.Length < 2)
return null;
string body = param.Substring(1);
string? mode = Modes.FirstOrDefault(m => body.StartsWith(m, StringComparison.Ordinal));
if (mode is null)
return null;
var b = new BrakeSetting { Mode = mode };
int p = mode.Length;
if (p < body.Length && "THFA".IndexOf(body[p]) >= 0)
b.Load = body[p++].ToString();
if (p < body.Length && "01A".IndexOf(body[p]) >= 0)
b.Switch = body[p].ToString();
return b;
}
public string ToParameter() => "B" + Mode + (Load ?? "") + (Switch ?? "");
}

View File

@@ -2,8 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace StarterNG.Classes;
@@ -15,43 +14,43 @@ namespace StarterNG.Classes;
/// <summary>Root object of a single vehicle JSON file.</summary>
public class VehicleEntry
{
[JsonPropertyName("uuid")] public string? Uuid { get; set; }
[JsonPropertyName("schema_version")] public int SchemaVersion { get; set; }
[JsonPropertyName("groups")] public List<VehicleGroup> Groups { get; set; } = new();
[JsonPropertyName("textures")] public List<VehicleTexture> Textures { get; set; } = new();
[JsonPropertyName("sets")] public List<VehicleSet> Sets { get; set; } = new();
[JsonPropertyName("unknown")] public List<string> Unknown { get; set; } = new();
[JsonProperty("uuid")] public string? Uuid { get; set; }
[JsonProperty("schema_version")] public int SchemaVersion { get; set; }
[JsonProperty("groups")] public List<VehicleGroup> Groups { get; set; } = new();
[JsonProperty("textures")] public List<VehicleTexture> Textures { get; set; } = new();
[JsonProperty("sets")] public List<VehicleSet> Sets { get; set; } = new();
[JsonProperty("unknown")] public List<string> Unknown { get; set; } = new();
}
/// <summary>Merged database root (databases/vehicles/vehicles.json).</summary>
public class VehicleEntryCollection
{
[JsonPropertyName("vehicles")] public List<VehicleEntry> Vehicles { get; set; } = new();
[JsonProperty("vehicles")] public List<VehicleEntry> Vehicles { get; set; } = new();
}
/// <summary>Legacy header / vehicle category group.</summary>
public class VehicleGroup
{
[JsonPropertyName("id")] public string Id { get; set; } = "";
[JsonPropertyName("category")] public string? Category { get; set; }
[JsonPropertyName("mini")] public string? Mini { get; set; }
[JsonPropertyName("archived")] public bool Archived { get; set; }
[JsonPropertyName("implicit")] public bool Implicit { get; set; }
[JsonProperty("id")] public string Id { get; set; } = "";
[JsonProperty("category")] public string? Category { get; set; }
[JsonProperty("mini")] public string? Mini { get; set; }
[JsonProperty("archived")] public bool Archived { get; set; }
[JsonProperty("implicit")] public bool Implicit { get; set; }
}
/// <summary>A single .mat skin entry.</summary>
public class VehicleTexture
{
[JsonPropertyName("uuid")] public string? Uuid { get; set; }
[JsonPropertyName("directory")] public string Directory { get; set; } = "";
[JsonPropertyName("skinfile")] public string Skinfile { get; set; } = "";
[JsonPropertyName("model")] public string? Model { get; set; }
[JsonPropertyName("group")] public string? Group { get; set; }
[JsonPropertyName("mini_ref")] public string? MiniRef { get; set; }
[JsonPropertyName("texture_mini")] public string? TextureMini { get; set; }
[JsonPropertyName("wreck")] public bool Wreck { get; set; }
[JsonPropertyName("aliases")] public List<VehicleAlias> Aliases { get; set; } = new();
[JsonPropertyName("meta")] public VehicleMeta? Meta { get; set; }
[JsonProperty("uuid")] public string? Uuid { get; set; }
[JsonProperty("directory")] public string Directory { get; set; } = "";
[JsonProperty("skinfile")] public string Skinfile { get; set; } = "";
[JsonProperty("model")] public string? Model { get; set; }
[JsonProperty("group")] public string? Group { get; set; }
[JsonProperty("mini_ref")] public string? MiniRef { get; set; }
[JsonProperty("texture_mini")] public string? TextureMini { get; set; }
[JsonProperty("wreck")] public bool Wreck { get; set; }
[JsonProperty("aliases")] public List<VehicleAlias> Aliases { get; set; } = new();
[JsonProperty("meta")] public VehicleMeta? Meta { get; set; }
/// <summary>Full skin path = directory + skinfile.</summary>
[JsonIgnore] public string FullPath => Directory + Skinfile;
@@ -70,25 +69,25 @@ public class VehicleTexture
/// <summary>Alternate mapping from a malformed multi-= legacy line.</summary>
public class VehicleAlias
{
[JsonPropertyName("model")] public string? Model { get; set; }
[JsonPropertyName("group")] public string? Group { get; set; }
[JsonPropertyName("mini_ref")] public string? MiniRef { get; set; }
[JsonPropertyName("texture_mini")] public string? TextureMini { get; set; }
[JsonProperty("model")] public string? Model { get; set; }
[JsonProperty("group")] public string? Group { get; set; }
[JsonProperty("mini_ref")] public string? MiniRef { get; set; }
[JsonProperty("texture_mini")] public string? TextureMini { get; set; }
}
/// <summary>Parsed metadata from the legacy // comment section.</summary>
public class VehicleMeta
{
[JsonPropertyName("raw")] public string? Raw { get; set; }
[JsonPropertyName("version")] public string? Version { get; set; }
[JsonPropertyName("vehicle")] public string? Vehicle { get; set; }
[JsonPropertyName("operator")] public string? Operator { get; set; }
[JsonPropertyName("depot")] public string? Depot { get; set; }
[JsonPropertyName("revision_date")] public string? RevisionDate { get; set; }
[JsonPropertyName("revision_place")] public string? RevisionPlace { get; set; }
[JsonPropertyName("texture_author")] public string? TextureAuthor { get; set; }
[JsonPropertyName("photo_author")] public string? PhotoAuthor { get; set; }
[JsonPropertyName("extra")] public List<string> Extra { get; set; } = new();
[JsonProperty("raw")] public string? Raw { get; set; }
[JsonProperty("version")] public string? Version { get; set; }
[JsonProperty("vehicle")] public string? Vehicle { get; set; }
[JsonProperty("operator")] public string? Operator { get; set; }
[JsonProperty("depot")] public string? Depot { get; set; }
[JsonProperty("revision_date")] public string? RevisionDate { get; set; }
[JsonProperty("revision_place")] public string? RevisionPlace { get; set; }
[JsonProperty("texture_author")] public string? TextureAuthor { get; set; }
[JsonProperty("photo_author")] public string? PhotoAuthor { get; set; }
[JsonProperty("extra")] public List<string> Extra { get; set; } = new();
}
/// <summary>
@@ -110,11 +109,12 @@ public class VehicleDatabase
/// <summary>skinfile (without extension, lower-case) -> texture.</summary>
public Dictionary<string, VehicleTexture> TextureBySkin { get; } = new();
private static readonly JsonSerializerOptions JsonOpts = new()
// Newtonsoft.Json matches property names case-insensitively, allows trailing
// commas, and skips // and /* */ comments by default — the same leniency the
// previous System.Text.Json options provided. Unknown members are ignored.
private static readonly JsonSerializerSettings JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
MissingMemberHandling = MissingMemberHandling.Ignore
};
/// <summary>
@@ -177,7 +177,7 @@ public class VehicleDatabase
{
try
{
var collection = JsonSerializer.Deserialize<VehicleEntryCollection>(
var collection = JsonConvert.DeserializeObject<VehicleEntryCollection>(
File.ReadAllText(file), JsonOpts);
if (collection?.Vehicles is null) return;
foreach (var entry in collection.Vehicles)
@@ -193,7 +193,7 @@ public class VehicleDatabase
{
try
{
var entry = JsonSerializer.Deserialize<VehicleEntry>(
var entry = JsonConvert.DeserializeObject<VehicleEntry>(
File.ReadAllText(file), JsonOpts);
if (entry is not null)
Ingest(entry);
@@ -359,8 +359,8 @@ public class VehicleDatabase
/// <summary>Automatic consist definition (legacy ^x grouping).</summary>
public class VehicleSet
{
[JsonPropertyName("uuid")] public string? Uuid { get; set; }
[JsonPropertyName("mode")] public string? Mode { get; set; }
[JsonPropertyName("count")] public int Count { get; set; }
[JsonPropertyName("texture_refs")] public List<string> TextureRefs { get; set; } = new();
[JsonProperty("uuid")] public string? Uuid { get; set; }
[JsonProperty("mode")] public string? Mode { get; set; }
[JsonProperty("count")] public int Count { get; set; }
[JsonProperty("texture_refs")] public List<string> TextureRefs { get; set; } = new();
}

View File

@@ -27,6 +27,7 @@
</PackageReference>
<PackageReference Include="HotAvalonia" Version="3.0.2" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
<PackageReference Include="SukiUI" Version="6.0.3" />
</ItemGroup>

View File

@@ -91,9 +91,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" };
// Placeholder coupling-bit labels (couplingData byte). Finished later.
// 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 =
{ "Coupler", "Brake pipe", "Brake tank", "Control", "Gangway", "High voltage", "Heating", "Aux" };
{ "Mechanical", "Brake pipe", "Control (MU)", "High voltage",
"Gangway", "Aux pneumatic", "Heating", "Workshop lock" };
public Depot()
{
@@ -663,15 +665,15 @@ public partial class Depot : UserControl
return new Dynamic
{
RangeMax = -1,
RangeMin = -1,
RangeMin = 0,
Name = MakeUniqueName(skin),
DataFolder = StripDynamicPrefix(texture.Directory),
SkinFile = skin,
MmdFile = string.IsNullOrEmpty(texture.Model) ? skin : texture.Model!,
Offset = 0f,
DriverType = eDriverType.Nobody,
couplingData = 3,
Tail = "",
Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe }, // 3
HasVelocity = true, // emit a canonical "0" velocity in the trainset entry
MiniName = _db.ResolveMiniName(texture)
};
}
@@ -950,14 +952,52 @@ public partial class Depot : UserControl
for (int i = 0; i < CouplingBits.Length; i++)
{
panel.Children.Add(new CheckBox
int bit = 1 << i;
var check = new CheckBox
{
Content = CouplingBits[i],
IsChecked = (d.couplingData & (1 << i)) != 0,
IsChecked = d.Coupling.Has(bit),
FontSize = 11
});
};
// edits the shared Dynamic, so the change is written back on export
check.IsCheckedChanged += (_, _) => d.Coupling.Set(bit, check.IsChecked == true);
panel.Children.Add(check);
}
// Brake-rack setting (the "B" parameter of the coupling field).
panel.Children.Add(new TextBlock
{
Text = "Brake mode",
FontWeight = FontWeight.Bold,
FontSize = 11,
Margin = new Thickness(0, 6, 0, 2)
});
var brakeCombo = new ComboBox { FontSize = 11, MinWidth = 90 };
brakeCombo.Items.Add(new ComboBoxItem { Content = "(none)", Tag = null });
foreach (string mode in BrakeSetting.Modes)
brakeCombo.Items.Add(new ComboBoxItem { Content = mode, Tag = mode });
string? current = d.Coupling.GetBrake()?.Mode;
brakeCombo.SelectedIndex = current is null
? 0
: Array.IndexOf(BrakeSetting.Modes, current) + 1;
brakeCombo.SelectionChanged += (_, _) =>
{
if ((brakeCombo.SelectedItem as ComboBoxItem)?.Tag is not string mode)
{
d.Coupling.SetBrake(null);
}
else
{
var brake = d.Coupling.GetBrake() ?? new BrakeSetting();
brake.Mode = mode;
d.Coupling.SetBrake(brake);
}
};
panel.Children.Add(brakeCombo);
return new Border { Padding = new Thickness(8), Child = panel };
}

View File

@@ -14,20 +14,20 @@ namespace StarterNG.Views;
public partial class Scenarios : UserControl
{
public List<Scenery> sceneries;
public List<Scenery> Sceneries;
public Scenarios()
{
InitializeComponent();
// Sceneries are parsed once at startup (behind the splash).
sceneries = GameData.Instance.Sceneries;
Sceneries = GameData.Instance.Sceneries;
var groupNodes = new Dictionary<string, TreeViewItem>();
for (int i = 0; i < sceneries.Count; i++)
for (int i = 0; i < Sceneries.Count; i++)
{
var scenery = sceneries[i];
var scenery = Sceneries[i];
// case 1: no group - put without parent
if (string.IsNullOrEmpty(scenery.Group))
@@ -47,7 +47,7 @@ public partial class Scenarios : UserControl
groupNode = new TreeViewItem
{
Header = scenery.Group,
IsExpanded = true
IsExpanded = false
};
groupNodes[scenery.Group] = groupNode;
@@ -71,7 +71,7 @@ public partial class Scenarios : UserControl
var item = sceneryList.SelectedItem as TreeViewItem;
if (item?.Tag is not int tag)
return;
Scenery selectedScn = sceneries[tag];
Scenery selectedScn = Sceneries[tag];
vehicleList.Items.Clear();
// add all trainsets to list
@@ -97,7 +97,7 @@ public partial class Scenarios : UserControl
var tItem = sceneryList.SelectedItem as TreeViewItem;
if (tItem?.Tag is not int tTag)
return;
Scenery selectedScn = sceneries[tTag];
Scenery selectedScn = Sceneries[tTag];
// get selected trainset
ListBoxItem? vItem = vehicleList.SelectedItem as ListBoxItem;