From 1cc0cd306a2311f164bf00328a9e05b5d9a5b58f Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 16 Jun 2026 22:19:42 +0200 Subject: [PATCH] fixes for trainset generation --- StarterNG/Classes/Trainset.cs | 291 ++++++++++++++++++++++++--- StarterNG/Classes/VehicleDatabase.cs | 96 ++++----- StarterNG/StarterNG.csproj | 1 + StarterNG/Views/Depot.axaml.cs | 56 +++++- StarterNG/Views/Scenarios.axaml.cs | 14 +- 5 files changed, 365 insertions(+), 93 deletions(-) diff --git a/StarterNG/Classes/Trainset.cs b/StarterNG/Classes/Trainset.cs index 4d44275..783bc3f 100644 --- a/StarterNG/Classes/Trainset.cs +++ b/StarterNG/Classes/Trainset.cs @@ -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(); 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; + + /// + /// 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) --- @@ -203,6 +234,67 @@ public class Dynamic /// 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)) + { + 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++]); + } + + /// + /// 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(); + 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(); + } + /// Deep copy, used when importing a trainset into the editable consist. 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(Destinations), MiniName = MiniName, Flipped = Flipped }; +} + +/// +/// 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 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; + set => Flags = value ? -AbsFlags : AbsFlags; + } + + 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); + 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; + } + + /// 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)); + if (brake != null) + Parameters.Insert(0, brake.ToParameter()); + } + + public Coupling Clone() => new() + { + Flags = Flags, + Parameters = new List(Parameters) + }; + + /// Serialises back to "<flags>[.param[.param…]]". + 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(); + } +} + +/// +/// 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) + { + 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 ?? ""); } \ No newline at end of file diff --git a/StarterNG/Classes/VehicleDatabase.cs b/StarterNG/Classes/VehicleDatabase.cs index 3ea04b2..be9845e 100644 --- a/StarterNG/Classes/VehicleDatabase.cs +++ b/StarterNG/Classes/VehicleDatabase.cs @@ -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; /// Root object of a single vehicle JSON file. public class VehicleEntry { - [JsonPropertyName("uuid")] public string? Uuid { get; set; } - [JsonPropertyName("schema_version")] public int SchemaVersion { get; set; } - [JsonPropertyName("groups")] public List Groups { get; set; } = new(); - [JsonPropertyName("textures")] public List Textures { get; set; } = new(); - [JsonPropertyName("sets")] public List Sets { get; set; } = new(); - [JsonPropertyName("unknown")] public List Unknown { get; set; } = new(); + [JsonProperty("uuid")] public string? Uuid { get; set; } + [JsonProperty("schema_version")] public int SchemaVersion { get; set; } + [JsonProperty("groups")] public List Groups { get; set; } = new(); + [JsonProperty("textures")] public List Textures { get; set; } = new(); + [JsonProperty("sets")] public List Sets { get; set; } = new(); + [JsonProperty("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(); + [JsonProperty("vehicles")] public List Vehicles { get; set; } = new(); } /// Legacy header / vehicle category group. 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; } } /// A single .mat skin entry. 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 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 Aliases { get; set; } = new(); + [JsonProperty("meta")] public VehicleMeta? Meta { get; set; } /// Full skin path = directory + skinfile. [JsonIgnore] public string FullPath => Directory + Skinfile; @@ -70,25 +69,25 @@ public class VehicleTexture /// Alternate mapping from a malformed multi-= legacy line. 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; } } /// Parsed metadata from the legacy // comment section. 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 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 Extra { get; set; } = new(); } /// @@ -110,11 +109,12 @@ public class VehicleDatabase /// skinfile (without extension, lower-case) -> texture. public Dictionary 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 }; /// @@ -177,7 +177,7 @@ public class VehicleDatabase { try { - var collection = JsonSerializer.Deserialize( + var collection = JsonConvert.DeserializeObject( 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( + var entry = JsonConvert.DeserializeObject( File.ReadAllText(file), JsonOpts); if (entry is not null) Ingest(entry); @@ -359,8 +359,8 @@ public class VehicleDatabase /// Automatic consist definition (legacy ^x grouping). 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 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 TextureRefs { get; set; } = new(); } diff --git a/StarterNG/StarterNG.csproj b/StarterNG/StarterNG.csproj index 7541b4c..fa5f740 100644 --- a/StarterNG/StarterNG.csproj +++ b/StarterNG/StarterNG.csproj @@ -27,6 +27,7 @@ + diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs index 41e2573..3b1b0a2 100644 --- a/StarterNG/Views/Depot.axaml.cs +++ b/StarterNG/Views/Depot.axaml.cs @@ -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< 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 }; } diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs index 8445082..194b547 100644 --- a/StarterNG/Views/Scenarios.axaml.cs +++ b/StarterNG/Views/Scenarios.axaml.cs @@ -14,20 +14,20 @@ namespace StarterNG.Views; public partial class Scenarios : UserControl { - public List sceneries; + public List 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(); - 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;