diff --git a/source/OpenBVE/Game/AI/AI.SimpleHuman.cs b/source/OpenBVE/Game/AI/AI.SimpleHuman.cs
index 9a4e4e472c..684e587b41 100644
--- a/source/OpenBVE/Game/AI/AI.SimpleHuman.cs
+++ b/source/OpenBVE/Game/AI/AI.SimpleHuman.cs
@@ -40,7 +40,7 @@ internal class SimpleHumanDriverAI : GeneralAI
/// The index to the first motor car, if the driver car is not a motor car
private readonly int MotorCar;
// functions
- internal SimpleHumanDriverAI(TrainBase train, double Limit)
+ internal SimpleHumanDriverAI(TrainBase train, double speedLimit)
{
Train = train;
TimeLastProcessed = 0.0;
@@ -57,7 +57,7 @@ internal SimpleHumanDriverAI(TrainBase train, double Limit)
{
LastStation = -1;
}
- SpeedLimit = Limit;
+ SpeedLimit = speedLimit;
MotorCar = train.DriverCar;
if (!train.Cars[train.DriverCar].TractionModel.ProvidesPower)
{
@@ -921,7 +921,7 @@ private void LookAheadForwards()
if (dist > 25)
{
- var edec = Train.CurrentSpeed * Train.CurrentSpeed / (2.0 * dist);
+ double edec = Train.CurrentSpeed * Train.CurrentSpeed / (2.0 * dist);
if (edec > dec) dec = edec;
}
@@ -952,7 +952,7 @@ private void LookAheadForwards()
dist -= 5.0;
}
- var edec = Train.CurrentSpeed * Train.CurrentSpeed / (2.0 * dist);
+ double edec = Train.CurrentSpeed * Train.CurrentSpeed / (2.0 * dist);
if (edec > dec) dec = edec;
}
}
diff --git a/source/OpenBVE/UserInterface/formMain.Start.cs b/source/OpenBVE/UserInterface/formMain.Start.cs
index f4f3d23fca..3b0154b62a 100644
--- a/source/OpenBVE/UserInterface/formMain.Start.cs
+++ b/source/OpenBVE/UserInterface/formMain.Start.cs
@@ -742,7 +742,11 @@ private void PopulateTrainList(string selectedFolder, ListView listView, bool pa
{
File = Path.CombineFile(Folders[i], "train.xml");
}
- Item.ImageKey = System.IO.File.Exists(File) ? "train" : "folder";
+ if (!System.IO.File.Exists(File))
+ {
+ File = Path.CombineFile(Folders[i], "vehicle.txt");
+ }
+ Item.ImageKey = System.IO.File.Exists(File) ? "train" : "folder";
Item.Tag = Folders[i];
}
}
@@ -818,8 +822,12 @@ private void listviewTrainFolders_SelectedIndexChanged(object sender, EventArgs
{
File = Path.CombineFile(t, "train.xml");
}
-
- if (System.IO.File.Exists(File))
+ if (!System.IO.File.Exists(File))
+ {
+ File = Path.CombineFile(t, "vehicle.txt");
+ }
+
+ if (System.IO.File.Exists(File))
{
Result.TrainFolder = t;
ShowTrain(false);
diff --git a/source/OpenBveApi/Math/Extensions.cs b/source/OpenBveApi/Math/Extensions.cs
index 292b612274..4cf5fc6444 100644
--- a/source/OpenBveApi/Math/Extensions.cs
+++ b/source/OpenBveApi/Math/Extensions.cs
@@ -25,7 +25,7 @@ public static double SqrtC(double X)
return System.Math.Sqrt(X);
}
- /// Returns the tamgent of the specified angle
+ /// Returns the tangent of the specified angle
/// This function will return 0 if the result would be ComplexInfinity
public static double TanC(double X)
{
@@ -38,5 +38,18 @@ public static double TanC(double X)
}
return System.Math.Tan(X);
}
+
+ /// Linearly interpolates a value in a range, using keys
+ /// The first number
+ /// The first key
+ /// The second number
+ /// The second key
+ /// The interpolation value between keys
+ /// The interpolated number
+ public static double LinearInterpolation(double x0, double y0, double x1, double y1, double x)
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ return x0 == x1 ? y0 : y0 + (y1 - y0) * (x - x0) / (x1 - x0);
+ }
}
}
diff --git a/source/Plugins/Formats.OpenBve/CFG/ConfigFile.cs b/source/Plugins/Formats.OpenBve/CFG/ConfigFile.cs
index bcfbeceabc..4fa36b1337 100644
--- a/source/Plugins/Formats.OpenBve/CFG/ConfigFile.cs
+++ b/source/Plugins/Formats.OpenBve/CFG/ConfigFile.cs
@@ -44,12 +44,12 @@ public class ConfigFile : Block where T1 : struct, Enum where T2
{
/// The version of the file (if set)
public readonly double Version;
- public ConfigFile(string fileName, HostInterface currentHost, string expectedHeader = null, double minVersion = 0, double maxVersion = 0, bool defaultFirstBlock = false)
- : this(File.ReadAllLines(fileName, TextEncoding.GetSystemEncodingFromFile(fileName)), currentHost, expectedHeader, minVersion, maxVersion, defaultFirstBlock)
+ public ConfigFile(string fileName, HostInterface currentHost, string expectedHeader = null, double minVersion = 0, double maxVersion = 0, char commentSeparator = ';', bool defaultFirstBlock = false)
+ : this(File.ReadAllLines(fileName, TextEncoding.GetSystemEncodingFromFile(fileName)), currentHost, expectedHeader, minVersion, maxVersion, commentSeparator, defaultFirstBlock)
{
}
- public ConfigFile(string[] lines, HostInterface currentHost, string expectedHeader = null, double minVersion = 0, double maxVersion = 0, bool defaultFirstBlock = false) : base(-1, default, currentHost)
+ public ConfigFile(string[] lines, HostInterface currentHost, string expectedHeader = null, double minVersion = 0, double maxVersion = 0, char commentSeparator = ';', bool defaultFirstBlock = false) : base(-1, default, currentHost)
{
List blockLines = new List();
bool addToBlock = defaultFirstBlock;
@@ -66,14 +66,14 @@ public ConfigFile(string[] lines, HostInterface currentHost, string expectedHead
for (int i = 0; i < lines.Length; i++)
{
- int j = lines[i].IndexOf(';');
+ int j = lines[i].IndexOf(commentSeparator);
if (j >= 0)
{
- lines[i] = lines[i].Substring(0, j).Trim();
+ lines[i] = lines[i].Substring(0, j).Trim().Trim(',');
}
else
{
- lines[i] = lines[i].Trim();
+ lines[i] = lines[i].Trim().Trim(',');
}
if (headerOK == false)
{
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/MotorNoiseTxtKey.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/MotorNoiseTxtKey.cs
new file mode 100644
index 0000000000..ac72a04028
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/MotorNoiseTxtKey.cs
@@ -0,0 +1,8 @@
+namespace Formats.OpenBve
+{
+ public enum MotorNoiseTxtKey
+ {
+ Volume,
+ Frequency
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/MotorNoiseTxtSection.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/MotorNoiseTxtSection.cs
new file mode 100644
index 0000000000..6c3b531c89
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/MotorNoiseTxtSection.cs
@@ -0,0 +1,8 @@
+namespace Formats.OpenBve
+{
+ public enum MotorNoiseTxtSection
+ {
+ Power,
+ Brake
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/ParametersTxtKey.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/ParametersTxtKey.cs
new file mode 100644
index 0000000000..b5f8b54295
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/ParametersTxtKey.cs
@@ -0,0 +1,66 @@
+namespace Formats.OpenBve
+{
+ public enum ParametersTxtKey
+ {
+ FirstCar,
+ LoadCompensating,
+ BrakeNotchCount,
+ PowerNotchCount,
+ HoldingSpeedNotchCount,
+ AtsCancelNotch,
+ MotorBrakeNotch,
+ ReverserText,
+ PowerText,
+ BrakeText,
+ HoldingSpeedText,
+ Power,
+ Neutral,
+ Brake,
+ RegenerationLimit,
+ RegenerationStartLimit,
+ LeverDelay,
+ BrakePriority,
+ SlipVelocityCoefficient,
+ SlipVelocity,
+ SlipAcceleration,
+ BalanceAcceleration,
+ HoldingTime,
+ ReferenceAcceleration,
+ ReferenceDeceleration,
+ CurrentDecrease,
+ CurrentIncrease,
+ MaximumPressure,
+ PressureRates,
+ SapBcRatio,
+ SapBcOffset,
+ BpInitialPressure,
+ ApplySpeed,
+ ReleaseSpeed,
+ RapidReleaseSpeed,
+ VolumeRatio,
+ InitialPressure,
+ ApplyStart,
+ ApplyStop,
+ ReleaseStart,
+ ReleaseStop,
+ PistonArea,
+ UpperPressure,
+ LowerPressure,
+ CompressionSpeed,
+ MotorCarWeight,
+ TrailerWeight,
+ MotorCarCount,
+ TrailerCount,
+ MotorcarInertiaFactor,
+ TrailerInertiaFactor,
+ CarLength,
+ Capacity,
+ BodyWeight,
+ BoardingSpeed,
+ AlightingSpeed,
+ CloseTime,
+ X,
+ Y,
+ Z
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/ParametersTxtSection.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/ParametersTxtSection.cs
new file mode 100644
index 0000000000..0145981e92
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/ParametersTxtSection.cs
@@ -0,0 +1,25 @@
+namespace Formats.OpenBve
+{
+ public enum ParametersTxtSection
+ {
+ Default,
+ Cab,
+ OneLeverCab,
+ ConstantSpeedControl,
+ MainCircuit,
+ PowerReAdhesion,
+ ECB,
+ SMEE,
+ CI,
+ SAP,
+ ER,
+ BP,
+ BC,
+ Brake,
+ Compressor,
+ Dynamics,
+ Passengers,
+ Door,
+ ViewPoint
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/PerformanceCurveTxtKey.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/PerformanceCurveTxtKey.cs
new file mode 100644
index 0000000000..ad1143f8e9
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/PerformanceCurveTxtKey.cs
@@ -0,0 +1,12 @@
+namespace Formats.OpenBve
+{
+ public enum PerformanceCurveTxtKey
+ {
+ Params,
+ Force,
+ MaxForce,
+ Current,
+ MaxCurrent,
+ NoLoadCurrent
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/PerformanceCurveTxtSection.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/PerformanceCurveTxtSection.cs
new file mode 100644
index 0000000000..66de720d80
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/PerformanceCurveTxtSection.cs
@@ -0,0 +1,8 @@
+namespace Formats.OpenBve
+{
+ public enum PerformanceCurveTxtSection
+ {
+ Power,
+ Brake
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/VehicleTxtKey.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/VehicleTxtKey.cs
new file mode 100644
index 0000000000..feca431b25
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/VehicleTxtKey.cs
@@ -0,0 +1,16 @@
+namespace Formats.OpenBve
+{
+ public enum VehicleTxtKey
+ {
+ Title,
+ Author,
+ Image,
+ Comment,
+ PerformanceCurve,
+ Parameters,
+ Panel,
+ Sound,
+ ATS,
+ MotorNoise
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/BVE5/VehicleTxtSection.cs b/source/Plugins/Formats.OpenBve/Enums/BVE5/VehicleTxtSection.cs
new file mode 100644
index 0000000000..b4db22a03a
--- /dev/null
+++ b/source/Plugins/Formats.OpenBve/Enums/BVE5/VehicleTxtSection.cs
@@ -0,0 +1,7 @@
+namespace Formats.OpenBve
+{
+ public enum VehicleTxtSection
+ {
+ Summary
+ }
+}
diff --git a/source/Plugins/Formats.OpenBve/Enums/SoundCfg/SoundCfgSection.cs b/source/Plugins/Formats.OpenBve/Enums/SoundCfg/SoundCfgSection.cs
index ed96dfad58..d744300def 100644
--- a/source/Plugins/Formats.OpenBve/Enums/SoundCfg/SoundCfgSection.cs
+++ b/source/Plugins/Formats.OpenBve/Enums/SoundCfg/SoundCfgSection.cs
@@ -12,7 +12,8 @@ public enum SoundCfgSection
/// Motor sounds
Motor,
/// Switch sounds
- Switch,
+ Switch = 4,
+ Joint = 4,
/// Brake sounds
Brake,
/// Compressor sounds
@@ -20,6 +21,7 @@ public enum SoundCfgSection
/// Air suspension sounds
Suspension = 7,
Spring = 7,
+ AirSpring = 7,
/// Horn sounds
Horn,
/// Door open / close sounds
@@ -36,7 +38,7 @@ public enum SoundCfgSection
/// Sounds played when the power handle is moved
MasterController = 14,
PowerHandle = 14,
- /// Sounds played when the reverseer is moved
+ /// Sounds played when the reverser is moved
Reverser = 15,
ReverserHandle = 15,
/// Breaker sounds
diff --git a/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj b/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj
index 5193bf3cc2..6dd78e51fa 100644
--- a/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj
+++ b/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj
@@ -54,6 +54,12 @@
+
+
+
+
+
+
@@ -64,6 +70,8 @@
+
+
diff --git a/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs b/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs
index f2ad3aae3a..0a93b2186f 100644
--- a/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs
+++ b/source/Plugins/Route.Bve5/MapParser/ConfirmComponents.cs
@@ -197,7 +197,7 @@ private static void ConfirmGradient(IList Blocks)
for (int k = StartBlock; k < i; k++)
{
double CurrentDistance = Blocks[k].StartingDistance;
- double CurrentPitch = LinearInterpolation(StartDistance, StartPitch, EndDistance, EndPitch, CurrentDistance);
+ double CurrentPitch = Extensions.LinearInterpolation(StartDistance, StartPitch, EndDistance, EndPitch, CurrentDistance);
Blocks[k].Pitch = CurrentPitch;
}
@@ -238,7 +238,7 @@ private static void ConfirmGradient(IList Blocks)
for (int k = StartBlock + 1; k < i; k++)
{
double CurrentDistance = Blocks[k].StartingDistance;
- double CurrentPitch = LinearInterpolation(StartDistance, StartPitch, EndDistance, EndPitch, CurrentDistance);
+ double CurrentPitch = Extensions.LinearInterpolation(StartDistance, StartPitch, EndDistance, EndPitch, CurrentDistance);
Blocks[k].Pitch = CurrentPitch;
}
diff --git a/source/Plugins/Route.Bve5/MapParser/Functions.cs b/source/Plugins/Route.Bve5/MapParser/Functions.cs
index 76a5b8014d..88a206950e 100644
--- a/source/Plugins/Route.Bve5/MapParser/Functions.cs
+++ b/source/Plugins/Route.Bve5/MapParser/Functions.cs
@@ -76,11 +76,6 @@ private static double Multiple(double x, int y)
return x % y == 0 ? x : x + (y - x % y);
}
- private static double LinearInterpolation(double x0, double y0, double x1, double y1, double x)
- {
- return x0 == x1 ? y0 : y0 + (y1 - y0) * (x - x0) / (x1 - x0);
- }
-
private static void SineHalfWavelengthDiminishingTangentCurve(double Length, double TargetRadius, double TargetCant, double CurrentPosition, out double CurrentRadius, out double CurrentCant)
{
// Sine Half-Wavelength Diminishing Tangent Curve
@@ -191,7 +186,7 @@ private static double GetTrackCoordinate(double distance0, double xy0, double di
return center.Y + Math.Sqrt(squaring);
}
- return LinearInterpolation(distance0, xy0, distance1, xy1, distance);
+ return Extensions.LinearInterpolation(distance0, xy0, distance1, xy1, distance);
}
private static double RZtoRoll(double RY, double RZ)
diff --git a/source/Plugins/Train.OpenBve/Enums/TrainFormat.cs b/source/Plugins/Train.OpenBve/Enums/TrainFormat.cs
new file mode 100644
index 0000000000..7cc9df10ed
--- /dev/null
+++ b/source/Plugins/Train.OpenBve/Enums/TrainFormat.cs
@@ -0,0 +1,10 @@
+namespace Train.OpenBve
+{
+ internal enum TrainFormat
+ {
+ OpenBVE,
+ TrainAI,
+ XML,
+ BVE5
+ }
+}
diff --git a/source/Plugins/Train.OpenBve/Plugin.cs b/source/Plugins/Train.OpenBve/Plugin.cs
index 7f18c60ba7..d38180e825 100644
--- a/source/Plugins/Train.OpenBve/Plugin.cs
+++ b/source/Plugins/Train.OpenBve/Plugin.cs
@@ -36,6 +36,8 @@ public class Plugin : TrainInterface
internal TrainDatParser TrainDatParser;
+ internal VehicleTxtParser VehicleTxtParser;
+
internal ExtensionsCfgParser ExtensionsCfgParser;
internal SoundCfgParser SoundCfgParser;
@@ -69,6 +71,7 @@ public class Plugin : TrainInterface
public Plugin()
{
TrainDatParser = new TrainDatParser(this);
+ VehicleTxtParser = new VehicleTxtParser(this);
if (ExtensionsCfgParser == null)
{
@@ -128,15 +131,9 @@ public override bool CanLoadTrain(string path)
string[] lines = File.ReadAllLines(vehicleTxt);
for (int i = 10; i < lines.Length; i++)
{
- if (lines[i].StartsWith(@"bvets vehicle ", StringComparison.InvariantCultureIgnoreCase))
+ if (VehicleTxtParser.CanLoad(vehicleTxt))
{
- /*
- * BVE5 format train
- * When the BVE5 plugin is implemented, this should return false, as BVE5 trains
- * often seem to keep the train.dat lying around and we need to use the right plugin
- *
- * For the moment however, this is ignored....
- */
+ return true;
}
}
}
@@ -174,6 +171,10 @@ public override bool CanLoadTrain(string path)
*
* For the moment however, this is ignored....
*/
+ if (VehicleTxtParser.CanLoad(path))
+ {
+ return true;
+ }
}
if (path.EndsWith("train.dat", StringComparison.InvariantCultureIgnoreCase) || path.EndsWith("train.ai", StringComparison.InvariantCultureIgnoreCase))
@@ -203,6 +204,8 @@ public override bool LoadTrain(Encoding encoding, string trainPath, ref Abstract
IsLoading = true;
CurrentControls = currentControls;
TrainBase currentTrain = train as TrainBase;
+ TrainFormat trainFormat = TrainFormat.OpenBVE;
+
if (currentTrain == null)
{
CurrentHost.ReportProblem(ProblemType.InvalidData, "Train was not valid");
@@ -210,7 +213,17 @@ public override bool LoadTrain(Encoding encoding, string trainPath, ref Abstract
return false;
}
- if (currentTrain.State == TrainState.Bogus)
+ // determine format
+ if (!currentTrain.IsPlayerTrain && File.Exists(Path.CombineFile(trainPath, "train.ai")))
+ {
+ trainFormat = TrainFormat.TrainAI;
+ }
+ else if (File.Exists(Path.CombineFile(trainPath, "vehicle.txt")))
+ {
+ trainFormat = TrainFormat.BVE5;
+ }
+
+ if (currentTrain.State == TrainState.Bogus)
{
// bogus train
string trainData = Path.CombineFile(FileSystem.GetDataFolder("Compatibility", "PreTrain"), "train.dat");
@@ -245,7 +258,21 @@ public override bool LoadTrain(Encoding encoding, string trainPath, ref Abstract
{
trainData = Path.CombineFile(currentTrain.TrainFolder, "train.dat");
}
- TrainDatParser.Parse(trainData, encoding, currentTrain);
+ if (!File.Exists(trainData))
+ {
+ trainData = Path.CombineFile(currentTrain.TrainFolder, "vehicle.txt");
+ }
+
+
+ if (trainFormat == TrainFormat.BVE5)
+ {
+ VehicleTxtParser.Parse(trainData, currentTrain);
+ }
+ else
+ {
+ TrainDatParser.Parse(trainData, encoding, currentTrain);
+ }
+
LastProgress = 0.1;
Thread.Sleep(1);
if (Cancel)
@@ -255,7 +282,7 @@ public override bool LoadTrain(Encoding encoding, string trainPath, ref Abstract
}
}
// add panel section
- if (currentTrain.IsPlayerTrain) {
+ if (currentTrain.IsPlayerTrain && trainFormat != TrainFormat.BVE5) {
ParsePanelConfig(currentTrain, encoding);
Thread.Sleep(1);
if (Cancel)
@@ -263,7 +290,7 @@ public override bool LoadTrain(Encoding encoding, string trainPath, ref Abstract
IsLoading = false;
return false;
}
- FileSystem.AppendToLogFile("Train panel loaded sucessfully.");
+ FileSystem.AppendToLogFile("Train panel loaded successfully.");
}
CurrentProgress = 0.5;
@@ -360,7 +387,7 @@ public override bool LoadTrain(Encoding encoding, string trainPath, ref Abstract
}
}
- if (currentTrain.State != TrainState.Bogus)
+ if (currentTrain.State != TrainState.Bogus && trainFormat != TrainFormat.BVE5)
{
if (Cancel) return false;
SoundCfgParser.ParseSoundConfig(currentTrain);
@@ -435,6 +462,13 @@ public override string GetDescription(string trainPath, Encoding encoding = null
}
}
}
+
+ string vehicleTxt = Path.CombineFile(trainPath, "vehicle.txt");
+ if (File.Exists(vehicleTxt))
+ {
+ return VehicleTxtParser.GetDescription(vehicleTxt);
+ }
+
string descriptionFile = Path.CombineFile(trainPath, "train.txt");
if (!File.Exists(descriptionFile))
{
@@ -458,7 +492,7 @@ public override string GetDescription(string trainPath, Encoding encoding = null
}
catch(Exception ex)
{
- CurrentHost.ReportProblem(ProblemType.UnexpectedException, "Unable to get the description for train " + trainPath + " due to the exeception: " + ex.Message);
+ CurrentHost.ReportProblem(ProblemType.UnexpectedException, "Unable to get the description for train " + trainPath + " due to the exception: " + ex.Message);
}
return string.Empty;
}
diff --git a/source/Plugins/Train.OpenBve/Train.OpenBve.csproj b/source/Plugins/Train.OpenBve/Train.OpenBve.csproj
index 2e17a7051f..438f8efa5d 100644
--- a/source/Plugins/Train.OpenBve/Train.OpenBve.csproj
+++ b/source/Plugins/Train.OpenBve/Train.OpenBve.csproj
@@ -51,6 +51,7 @@
+
@@ -67,6 +68,9 @@
+
+
+
diff --git a/source/Plugins/Train.OpenBve/Train/BVE/TrainDatParser.cs b/source/Plugins/Train.OpenBve/Train/BVE/TrainDatParser.cs
index e6f19e65fb..1c886ee292 100644
--- a/source/Plugins/Train.OpenBve/Train/BVE/TrainDatParser.cs
+++ b/source/Plugins/Train.OpenBve/Train/BVE/TrainDatParser.cs
@@ -1159,57 +1159,8 @@ internal void Parse(string FileName, Encoding Encoding, TrainBase Train) {
}
}
- bool[] motorCars = new bool[Train.Cars.Length];
+ bool[] motorCars = AssignMotorCars(MotorCars, TrailerCars, FrontCarIsMotorCar);
- // assign motor cars
- if (MotorCars == 1) {
- if (FrontCarIsMotorCar | TrailerCars == 0) {
- motorCars[0] = true;
- } else {
- motorCars[Cars - 1] = true;
- }
- } else if (MotorCars == 2) {
- if (FrontCarIsMotorCar | TrailerCars == 0) {
- motorCars[0] = true;
- motorCars[Cars - 1] = true;
- } else if (TrailerCars == 1) {
- motorCars[1] = true;
- motorCars[2] = true;
- } else {
- int i = (int)Math.Ceiling(0.25 * (Cars - 1));
- int j = (int)Math.Floor(0.75 * (Cars - 1));
- motorCars[i] = true;
- motorCars[j] = true;
- }
- } else if (MotorCars > 0) {
- if (FrontCarIsMotorCar) {
- motorCars[0] = true;
- double t = 1.0 + TrailerCars / (double)(MotorCars - 1);
- double r = 0.0;
- double x = 0.0;
- while (true) {
- double y = x + t - r;
- x = Math.Ceiling(y);
- r = x - y;
- int i = (int)x;
- if (i >= Cars) break;
- motorCars[i] = true;
- }
- } else {
- motorCars[1] = true;
- double t = 1.0 + (TrailerCars - 1) / (double)(MotorCars - 1);
- double r = 0.0;
- double x = 1.0;
- while (true) {
- double y = x + t - r;
- x = Math.Ceiling(y);
- r = x - y;
- int i = (int)x;
- if (i >= Cars) break;
- motorCars[i] = true;
- }
- }
- }
double MotorDeceleration = Math.Sqrt(MaximumAcceleration * BrakeDeceleration);
// apply brake-specific attributes for all cars
for (int i = 0; i < Cars; i++) {
@@ -1392,28 +1343,13 @@ internal void Parse(string FileName, Encoding Encoding, TrainBase Train) {
Train.Cars[i].Height = CarHeight;
Train.Cars[i].Length = CarLength;
Train.Cars[i].Specs.CriticalTopplingAngle = 0.5 * Math.PI - Math.Atan(2 * Train.Cars[i].Specs.CenterOfGravityHeight / Train.Cars[i].Width);
- }
-
- if (Cars == 1)
- {
- Train.Cars[Train.Cars.Length - 1].BeaconReceiver.TriggerType = EventTriggerType.SingleCarTrain;
- }
- else
- {
- Train.Cars[Train.Cars.Length - 1].BeaconReceiver.TriggerType = EventTriggerType.TrainRear;
- }
-
-
- Plugin.MotorSoundTables = Tables;
- Plugin.AccelerationCurves = AccelerationCurves;
- Plugin.MaximumAcceleration = MaximumAcceleration;
- // assign motor/trailer-specific settings
- for (int i = 0; i < Cars; i++) {
+ // assign motor/trailer-specific settings
Train.Cars[i].ConstSpeed = new CarConstSpeed(Train.Cars[i]);
Train.Cars[i].HoldBrake = new CarHoldBrake(Train.Cars[i]);
Train.Cars[i].ReAdhesionDevice = new BveReAdhesionDevice(Train.Cars[i], ReAdhesionDevice);
- if (Train.Cars[i].TractionModel.ProvidesPower) {
+ if (Train.Cars[i].TractionModel.ProvidesPower)
+ {
// motor car
Train.Cars[i].EmptyMass = MotorCarMass;
Train.Cars[i].CargoMass = 0;
@@ -1423,12 +1359,29 @@ internal void Parse(string FileName, Encoding Encoding, TrainBase Train) {
Train.Cars[i].TractionModel.AccelerationCurves[j] = AccelerationCurves[j].Clone(1.0 + TrailerCars * TrailerCarMass / (MotorCars * MotorCarMass));
}
Train.Cars[i].TractionModel.MaximumPossibleAcceleration = MaximumAcceleration;
- } else {
+ }
+ else
+ {
// trailer car
Train.Cars[i].EmptyMass = TrailerCarMass;
Train.Cars[i].CargoMass = 0;
}
}
+
+ if (Cars == 1)
+ {
+ Train.Cars[Train.Cars.Length - 1].BeaconReceiver.TriggerType = EventTriggerType.SingleCarTrain;
+ }
+ else
+ {
+ Train.Cars[Train.Cars.Length - 1].BeaconReceiver.TriggerType = EventTriggerType.TrainRear;
+ }
+
+
+ Plugin.MotorSoundTables = Tables;
+ Plugin.AccelerationCurves = AccelerationCurves;
+ Plugin.MaximumAcceleration = MaximumAcceleration;
+
// driver
Train.Cars[Train.DriverCar].Driver.X = Driver.X;
@@ -1438,10 +1391,85 @@ internal void Parse(string FileName, Encoding Encoding, TrainBase Train) {
{
Train.Cars[DriverCar].HasInteriorView = true;
}
-
- // finish
-
}
+
+ internal static bool[] AssignMotorCars(int MotorCars, int TrailerCars, bool FrontCarIsMotorCar)
+ {
+ int Cars = MotorCars + TrailerCars;
+ bool[] motorCars = new bool[Cars];
+
+ // assign motor cars
+ if (MotorCars == 1)
+ {
+ if (FrontCarIsMotorCar | TrailerCars == 0)
+ {
+ motorCars[0] = true;
+ }
+ else
+ {
+ motorCars[Cars - 1] = true;
+ }
+ }
+ else if (MotorCars == 2)
+ {
+ if (FrontCarIsMotorCar | TrailerCars == 0)
+ {
+ motorCars[0] = true;
+ motorCars[Cars - 1] = true;
+ }
+ else if (TrailerCars == 1)
+ {
+ motorCars[1] = true;
+ motorCars[2] = true;
+ }
+ else
+ {
+ int i = (int)Math.Ceiling(0.25 * (Cars - 1));
+ int j = (int)Math.Floor(0.75 * (Cars - 1));
+ motorCars[i] = true;
+ motorCars[j] = true;
+ }
+ }
+ else if (MotorCars > 0)
+ {
+ if (FrontCarIsMotorCar)
+ {
+ motorCars[0] = true;
+ double t = 1.0 + TrailerCars / (double)(MotorCars - 1);
+ double r = 0.0;
+ double x = 0.0;
+ while (true)
+ {
+ double y = x + t - r;
+ x = Math.Ceiling(y);
+ r = x - y;
+ int i = (int)x;
+ if (i >= Cars) break;
+ motorCars[i] = true;
+ }
+ }
+ else
+ {
+ motorCars[1] = true;
+ double t = 1.0 + (TrailerCars - 1) / (double)(MotorCars - 1);
+ double r = 0.0;
+ double x = 1.0;
+ while (true)
+ {
+ double y = x + t - r;
+ x = Math.Ceiling(y);
+ r = x - y;
+ int i = (int)x;
+ if (i >= Cars) break;
+ motorCars[i] = true;
+ }
+ }
+ }
+
+ return motorCars;
+ }
}
+
+
}
diff --git a/source/Plugins/Train.OpenBve/Train/BVE5/MotorSoundTable.cs b/source/Plugins/Train.OpenBve/Train/BVE5/MotorSoundTable.cs
index cfe013b3b4..9fffbefdc1 100644
--- a/source/Plugins/Train.OpenBve/Train/BVE5/MotorSoundTable.cs
+++ b/source/Plugins/Train.OpenBve/Train/BVE5/MotorSoundTable.cs
@@ -1,3 +1,27 @@
+//Simplified BSD License (BSD-2-Clause)
+//
+//Copyright (c) 2026, Christopher Lees, The OpenBVE Project
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
using System;
using System.Collections.Generic;
using System.IO;
@@ -31,7 +55,7 @@ internal static BVE5MotorSound Parse(CarBase car, string motorSoundPitch, string
}
else
{
- Plugin.CurrentHost.AddMessage("Missing BVE5 MotorSound table file.");
+ Plugin.CurrentHost.AddMessage("BVE5: Missing MotorSound table file.");
}
if (File.Exists(brakeSoundPitch) && File.Exists(brakeSoundGain))
@@ -41,7 +65,7 @@ internal static BVE5MotorSound Parse(CarBase car, string motorSoundPitch, string
}
else
{
- Plugin.CurrentHost.AddMessage("Missing BVE5 BrakeSound table file.");
+ Plugin.CurrentHost.AddMessage("BVE5: Missing BrakeSound table file.");
}
@@ -65,7 +89,7 @@ private static void ParsePitchTable(string pitchFile, ref SortedDictionary setPoints = new SortedDictionary();
@@ -145,7 +169,7 @@ private static void ParseVolumeTable(string volumeFile, ref SortedDictionary setPoints = new SortedDictionary();
diff --git a/source/Plugins/Train.OpenBve/Train/BVE5/PerformanceData.cs b/source/Plugins/Train.OpenBve/Train/BVE5/PerformanceData.cs
new file mode 100644
index 0000000000..c3dad178b1
--- /dev/null
+++ b/source/Plugins/Train.OpenBve/Train/BVE5/PerformanceData.cs
@@ -0,0 +1,75 @@
+//Simplified BSD License (BSD-2-Clause)
+//
+//Copyright (c) 2026, Christopher Lees, The OpenBVE Project
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using Formats.OpenBve;
+using System;
+using TrainManager.Motor;
+
+namespace Train.OpenBve
+{
+ internal partial class VehicleTxtParser
+ {
+
+ internal Bve5PerformanceData ParsePerformanceData(Block curveBlock, string directoryName)
+ {
+ if (!curveBlock.GetPath(PerformanceCurveTxtKey.Params, directoryName, out string powerParamsFile))
+ {
+ throw new Exception("BVE5: Performance curves parameters file missing.");
+ }
+ Bve5PerformanceTable ParamsTable = ParsePerformanceTable(powerParamsFile, 0.01, 2.01);
+
+ if (!curveBlock.GetPath(PerformanceCurveTxtKey.Force, directoryName, out string powerForceFile))
+ {
+ throw new Exception("BVE5: Force curves parameters file missing.");
+ }
+ Bve5PerformanceTable ForceTable = ParsePerformanceTable(powerForceFile, 0.01, 2.01);
+
+ if (!curveBlock.GetPath(PerformanceCurveTxtKey.MaxForce, directoryName, out string powerMaxForceFile))
+ {
+ throw new Exception("BVE5: Force curves parameters file missing.");
+ }
+ Bve5PerformanceTable MaxForceTable = ParsePerformanceTable(powerMaxForceFile, 0.01, 2.01);
+
+ if (!curveBlock.GetPath(PerformanceCurveTxtKey.Current, directoryName, out string powerCurrentFile))
+ {
+ throw new Exception("BVE5: Current curves parameters file missing.");
+ }
+ Bve5PerformanceTable CurrentTable = ParsePerformanceTable(powerCurrentFile, 0.01, 2.01);
+
+ if (!curveBlock.GetPath(PerformanceCurveTxtKey.MaxCurrent, directoryName, out string powerMaxCurrentFile))
+ {
+ throw new Exception("BVE5: Current curves parameters file missing.");
+ }
+ Bve5PerformanceTable MaxCurrentTable = ParsePerformanceTable(powerMaxCurrentFile, 0.01, 2.01);
+
+ if (!curveBlock.GetPath(PerformanceCurveTxtKey.NoLoadCurrent, directoryName, out string noLoadCurrentFile))
+ {
+ throw new Exception("BVE5: Current curves parameters file missing.");
+ }
+ Bve5PerformanceTable NoLoadCurrentTable = ParsePerformanceTable(powerMaxCurrentFile, 0.01, 2.01);
+
+ return new Bve5PerformanceData(ParamsTable, ForceTable, MaxForceTable, CurrentTable, MaxCurrentTable, NoLoadCurrentTable);
+ }
+ }
+}
diff --git a/source/Plugins/Train.OpenBve/Train/BVE5/PerformanceTable.cs b/source/Plugins/Train.OpenBve/Train/BVE5/PerformanceTable.cs
new file mode 100644
index 0000000000..2fcfde49b4
--- /dev/null
+++ b/source/Plugins/Train.OpenBve/Train/BVE5/PerformanceTable.cs
@@ -0,0 +1,136 @@
+//Simplified BSD License (BSD-2-Clause)
+//
+//Copyright (c) 2026, Christopher Lees, The OpenBVE Project
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using OpenBveApi.Interface;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using OpenBveApi.Math;
+using TrainManager.Motor;
+
+namespace Train.OpenBve
+{
+ internal partial class VehicleTxtParser
+ {
+ public Bve5PerformanceTable ParsePerformanceTable(string fileName, double minVersion, double maxVersion)
+ {
+ // bvets vehicle performance table 0.01
+ // SPEED KEY ==> P1 --Pn
+ // https://note.com/like_a_lake_com/n/nf04df0645f54
+
+ // bvets vehicle performance table 1.00 (K_SEI3500R)
+ // SPEED KEY ==> S1a,S1,S2,S3,S4,S5,S6,S7,P1 -- Pn,WF1,WF2,WF3,WF4
+
+ // BveTs Vehicle Performance Table 2.00
+ // SPEED KEY ==> P1 -- Pn
+
+ // For power / brake, figure is given in newtons of acceleration (per car?) for each notch
+ // For power / brake current tables, figure is given in amps for each notch
+
+ string[] lines = File.ReadAllLines(fileName); // whilst encoding may be specified for these, it shouldn't matter with all numbers
+ bool headerOK = false;
+
+ double Version = 0;
+ Tuple[] CurveEntries = null;
+ List> tempEntries = new List>();
+ for (int i = 0; i < lines.Length; i++)
+ {
+ int j = lines[i].IndexOf('#');
+ if (j >= 0)
+ {
+ lines[i] = lines[i].Substring(0, j).Trim().Trim(',');
+ }
+ else
+ {
+ lines[i] = lines[i].Trim().Trim(',');
+ }
+
+ if (headerOK == false)
+ {
+ int vi = lines[i].Length - 1;
+ if (minVersion > 0 || maxVersion > 0)
+ {
+ while (char.IsDigit(lines[i][vi]) || lines[i][vi] == '.')
+ {
+ vi--;
+ }
+
+ Version = double.Parse(lines[i].Substring(vi));
+ lines[i] = lines[i].Substring(0, vi);
+ if (Version < minVersion || Version > maxVersion)
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Error, false,
+ "Expected a version between " + minVersion + " and " + maxVersion + " , found " +
+ Version);
+ }
+ }
+
+ if (!string.IsNullOrEmpty(lines[i]) && string.Compare(lines[i], "bvets vehicle performance table", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ headerOK = true;
+ }
+ }
+
+ if (lines[i].Length > 0 && char.IsDigit(lines[i][0]))
+ {
+ if (!headerOK)
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Error, false, "BVE5: The expected Vehicle Performance Table header was not found.");
+ headerOK = true;
+ }
+
+ string[] splitLine = lines[i].Split(',');
+ if (NumberFormats.TryParseDoubleVb6(splitLine[0], out double speedValue))
+ {
+ if (splitLine.Length >= 2)
+ {
+ double[] notchValues = new double[splitLine.Length - 1];
+ for (int k = 1; k < splitLine.Length; k++)
+ {
+ if (!NumberFormats.TryParseDoubleVb6(splitLine[k], out notchValues[k - 1]))
+ {
+ notchValues[k - 1] = 0;
+ Plugin.CurrentHost.AddMessage(MessageType.Error, false, "BVE5: Entry for notch " + (k - 1) + " is not valid at Line " + i + " of Vehicle Performance Table " + fileName);
+ }
+ }
+ tempEntries.Add(new Tuple(speedValue, notchValues));
+ }
+ else
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Error, false, "BVE5: Entry is not valid at Line " + i + " of Vehicle Performance Table " + fileName);
+ }
+ }
+ else
+ {
+ Plugin.CurrentHost.AddMessage(MessageType.Error, false, "BVE5: Speed value " + splitLine[0] + " is not valid at Line " + i + " of Vehicle Performance Table " + fileName);
+ }
+ }
+ // sort entries
+ CurveEntries = tempEntries.OrderByDescending(x => x.Item1).ToArray();
+ }
+ return new Bve5PerformanceTable(CurveEntries, Version);
+ }
+ }
+}
diff --git a/source/Plugins/Train.OpenBve/Train/BVE5/VehicleTxt.cs b/source/Plugins/Train.OpenBve/Train/BVE5/VehicleTxt.cs
new file mode 100644
index 0000000000..3f0da3833b
--- /dev/null
+++ b/source/Plugins/Train.OpenBve/Train/BVE5/VehicleTxt.cs
@@ -0,0 +1,532 @@
+//Simplified BSD License (BSD-2-Clause)
+//
+//Copyright (c) 2026, Christopher Lees, The OpenBVE Project
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using Formats.OpenBve;
+using LibRender2.Trains;
+using OpenBveApi.Graphics;
+using OpenBveApi.Interface;
+using OpenBveApi.Math;
+using OpenBveApi.Objects;
+using OpenBveApi.Routes;
+using System;
+using System.IO;
+using TrainManager.BrakeSystems;
+using TrainManager.Car;
+using TrainManager.Handles;
+using TrainManager.Motor;
+using TrainManager.Power;
+using TrainManager.SafetySystems;
+using TrainManager.Trains;
+
+namespace Train.OpenBve
+{
+ internal partial class VehicleTxtParser
+ {
+ internal Plugin Plugin;
+
+ internal VehicleTxtParser(Plugin plugin)
+ {
+ Plugin = plugin;
+ }
+
+ /// Checks whether the parser can load the specified file.
+ /// The file path of the specified vehicle.txt
+ /// Whether the parser can load the specified file.
+ internal bool CanLoad(string fileName)
+ {
+ try
+ {
+ using (StreamReader reader = new StreamReader(fileName))
+ {
+ var firstLine = reader.ReadLine() ?? "";
+ string b = string.Empty;
+ if (!firstLine.ToLowerInvariant().Trim().StartsWith("bvets vehicle"))
+ {
+ return false;
+ }
+ for (int i = 15; i < firstLine.Length; i++)
+ {
+ if (char.IsDigit(firstLine[i]) || firstLine[i] == '.')
+ {
+ b += firstLine[i];
+ }
+ else
+ {
+ break;
+ }
+ }
+ if (b.Length > 0)
+ {
+ NumberFormats.TryParseDoubleVb6(b, out double version);
+ if (version > 2.0)
+ {
+ throw new Exception(version + " is not a supported BVE5 vehicle version");
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private int MotorCars = 5;
+ private int TrailerCars = 5;
+ private bool FirstCarIsMotorCar = false;
+ private bool OneLeverCab = false;
+ private int PowerNotches = 8;
+ private int BrakeNotches = 5;
+ private int HoldBrakeNotch = 0;
+ private int AtsCancelNotch = 1;
+ private int MotorBrakeNotch = 1;
+ private string[] PowerNotchDescriptions = { };
+ private string[] BrakeNotchDescriptions = { };
+ private string[] ReverserNotchDescriptions = { };
+ private string[] HoldNotchDescriptions = { };
+ private bool constantSpeedPower = false;
+ private bool constantSpeedNeutral = false;
+ private bool constantSpeedBrake = false;
+ private bool LoadCompensatingDevice = true;
+ private double RegenerationLimit = 5;
+ private double RegenerationStartLimit;
+ private double PowerLeverDelay = 0.5;
+ private bool BrakePriority = false;
+ private double SlipVelocityCoefficient = 0;
+ private double SlipVelocity = 2;
+ private double SlipAcceleration = 10;
+ private double BalanceAcceleration = -2;
+ private double HoldingTime = 2;
+ private double ReferenceAcceleration = 4;
+ private double ReferenceDeceleration = 4;
+ private double CurrentDecrease = 0;
+ private double CurrentIncrease = 0;
+ private ParametersTxtSection BrakeType;
+ private double MaximumPressure = 440000;
+ private double[] BrakeNotchPressures;
+ private double SapBcRatio = 0.94;
+ private double SapBcOffset = 30000;
+ private double BpInitialPressure = 490000;
+ private double BrakeLeverDelay = 0.2;
+ private double SapApplySpeed = 500;
+ private double SapReleaseSpeed = 500;
+ private double SapVolumeRatio = 20;
+ private double ErApplySpeed = 500;
+ private double ErReleaseSpeed = 500;
+ private double ErVolumeRatio = 20;
+ private double ErRapidReleaseSpeed = 1000;
+ private double BpApplySpeed = 500;
+ private double BpReleaseSpeed = 500;
+ private double BpVolumeRatio = 20;
+ private double BpRapidReleaseSpeed = 1000;
+ private double CompressorLowerPressure = 700000;
+ private double CompressorUpperPressure = 800000;
+ private double CompressionSpeed = 5000;
+ private double MotorCarWeight = 31500;
+ private double TrailerCarWeight = 31500;
+ private double MotorCarInertiaFactor = 0.1;
+ private double TrailerCarInertiaFactor = 0.05;
+ private double CarLength = 20;
+ private double PassengerCapacity = 150; // default value at crush loading presumably
+ private double PassengerWeight = 65;
+ private double BoardingSpeed = 3.15; // time taken for 1 passenger to board in s
+ private double AlightingSpeed = 6.3; // time taken for 1 passenger to alight in s
+ private double DoorCloseSpeed = 5;
+ private Vector3 Driver = new Vector3(-1, 2.5, -1);
+ private Bve5PerformanceData PowerPerformanceData;
+ private Bve5PerformanceData BrakePerformanceData;
+ internal string GetDescription(string fileName)
+ {
+ ConfigFile cfg = new ConfigFile(fileName, Plugin.CurrentHost, "bvets vehicle", 0.03, 2.01, '#', true);
+ // n.b. very early versions of this used a [Summary] block, but later don't use blocks at all
+ Block vehicleBlock = cfg.ReadNextBlock();
+ vehicleBlock.GetValue(VehicleTxtKey.Comment, out string comment);
+ return comment;
+ }
+
+ internal void Parse(string fileName, TrainBase train)
+ {
+ ConfigFile cfg = new ConfigFile(fileName, Plugin.CurrentHost, "bvets vehicle", 0.03, 2.01, '#', true);
+ // n.b. very early versions of this used a [Summary] block, but later don't use blocks at all
+ Block vehicleBlock = cfg.ReadNextBlock();
+ if (!vehicleBlock.GetPath(VehicleTxtKey.Parameters, Path.GetDirectoryName(fileName), out string parametersFile))
+ {
+ throw new Exception("BVE5: Unable to find the vehicle parameters file.");
+ }
+ ConfigFile parameters = new ConfigFile(parametersFile, Plugin.CurrentHost, "Bvets Vehicle Parameters", 2, 3, '#', true);
+ // performance curves
+ if (!vehicleBlock.GetPath(VehicleTxtKey.PerformanceCurve, Path.GetDirectoryName(fileName), out string performanceCurveTxtFile))
+ {
+ throw new Exception("BVE5: Unable to find the PerformanceCurve file.");
+ }
+ ConfigFile performanceCurves = new ConfigFile(performanceCurveTxtFile, Plugin.CurrentHost);
+ parameters.ReadBlock(ParametersTxtSection.Default, out Block defaultBlock);
+ // stuff not actually in a block...
+ defaultBlock.GetValue(ParametersTxtKey.FirstCar, out string carType);
+ FirstCarIsMotorCar = carType.Equals("M", StringComparison.InvariantCultureIgnoreCase);
+ defaultBlock.TryGetValue(ParametersTxtKey.LoadCompensating, ref LoadCompensatingDevice);
+ // cab
+ if(parameters.ReadBlock(new[] { ParametersTxtSection.Cab, ParametersTxtSection.OneLeverCab}, out Block cabBlock))
+ {
+ if (cabBlock.Key == ParametersTxtSection.OneLeverCab)
+ {
+ OneLeverCab = true;
+ }
+
+ cabBlock.TryGetValue(ParametersTxtKey.PowerNotchCount, ref PowerNotches);
+ cabBlock.TryGetValue(ParametersTxtKey.BrakeNotchCount, ref BrakeNotches);
+ cabBlock.TryGetValue(ParametersTxtKey.HoldingSpeedNotchCount, ref HoldBrakeNotch);
+ cabBlock.TryGetValue(ParametersTxtKey.AtsCancelNotch, ref AtsCancelNotch);
+ cabBlock.TryGetValue(ParametersTxtKey.MotorBrakeNotch, ref MotorBrakeNotch);
+ cabBlock.TryGetStringArray(ParametersTxtKey.ReverserText, ',', ref ReverserNotchDescriptions);
+ cabBlock.TryGetStringArray(ParametersTxtKey.PowerText, ',', ref PowerNotchDescriptions);
+ cabBlock.TryGetStringArray(ParametersTxtKey.BrakeText, ',', ref BrakeNotchDescriptions);
+ cabBlock.TryGetStringArray(ParametersTxtKey.HoldingSpeedText, ',', ref HoldNotchDescriptions);
+ }
+ else
+ {
+ throw new Exception("BVE5: The vehicle parameters must contain either a Cab or OneLeverCab section.");
+ }
+ // constant speed device
+ if (parameters.ReadBlock(ParametersTxtSection.ConstantSpeedControl, out Block constantSpeedBlock))
+ {
+ constantSpeedBlock.TryGetValue(ParametersTxtKey.Power, ref constantSpeedPower);
+ constantSpeedBlock.TryGetValue(ParametersTxtKey.Neutral, ref constantSpeedNeutral);
+ constantSpeedBlock.TryGetValue(ParametersTxtKey.Brake, ref constantSpeedBrake);
+ }
+
+ // main circuit
+ if (parameters.ReadBlock(ParametersTxtSection.MainCircuit, out Block mainCircuitBlock))
+ {
+ mainCircuitBlock.TryGetValue(ParametersTxtKey.RegenerationLimit, ref RegenerationLimit);
+ if (!mainCircuitBlock.TryGetValue(ParametersTxtKey.RegenerationStartLimit, ref RegenerationStartLimit))
+ {
+ RegenerationStartLimit = RegenerationLimit + 5;
+ }
+ mainCircuitBlock.TryGetValue(ParametersTxtKey.LeverDelay, ref PowerLeverDelay);
+ mainCircuitBlock.TryGetValue(ParametersTxtKey.SlipVelocityCoefficient, ref SlipVelocityCoefficient);
+ }
+
+ // power re-adhesion
+ if (parameters.ReadBlock(ParametersTxtSection.PowerReAdhesion, out Block powerReAdhesionBlock))
+ {
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.SlipVelocity, ref SlipVelocity);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.SlipAcceleration, ref SlipAcceleration);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.BalanceAcceleration, ref BalanceAcceleration);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.HoldingTime, ref HoldingTime);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.ReferenceAcceleration, ref ReferenceAcceleration);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.ReferenceDeceleration, ref ReferenceDeceleration);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.CurrentDecrease, ref CurrentDecrease);
+ powerReAdhesionBlock.TryGetValue(ParametersTxtKey.CurrentIncrease, ref CurrentIncrease);
+ }
+
+ // brakes
+ if (parameters.ReadBlock(new[] { ParametersTxtSection.ECB, ParametersTxtSection.SMEE, ParametersTxtSection.CI }, out Block brakeBlock))
+ {
+ BrakeType = brakeBlock.Key;
+ brakeBlock.TryGetValue(ParametersTxtKey.MaximumPressure, ref MaximumPressure);
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (MaximumPressure == 1)
+ {
+ if ((BrakeType == ParametersTxtSection.ECB || BrakeType == ParametersTxtSection.SMEE) && !brakeBlock.TryGetDoubleArray(ParametersTxtKey.PressureRates, ',', ref BrakeNotchPressures) || BrakeNotchPressures.Length != BrakeNotches)
+ {
+ // CHECK: If no pressure is supplied for higher notches, does BVE5 actually just use the last one??
+ BrakeNotchPressures = new double[] { };
+ MaximumPressure = 440000;
+ Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "BVE5: Invalid list of brake notch pressures supplied.");
+ }
+ }
+ brakeBlock.TryGetValue(ParametersTxtKey.SapBcRatio, ref SapBcRatio);
+ brakeBlock.TryGetValue(ParametersTxtKey.SapBcOffset, ref SapBcOffset);
+ brakeBlock.TryGetValue(ParametersTxtKey.BpInitialPressure, ref BpInitialPressure);
+ brakeBlock.TryGetValue(ParametersTxtKey.LeverDelay, ref BrakeLeverDelay);
+ }
+ else
+ {
+ throw new Exception("BVE5: The vehicle parameters must contain either a ECB, SMEE, or CI block.");
+ }
+
+ if (BrakeType == ParametersTxtSection.ECB)
+ {
+ // straight air pipe
+ parameters.ReadBlock(ParametersTxtSection.SAP, out Block sapBlock);
+ sapBlock.TryGetValue(ParametersTxtKey.ApplySpeed, ref SapApplySpeed);
+ sapBlock.TryGetValue(ParametersTxtKey.ReleaseSpeed, ref SapReleaseSpeed);
+ sapBlock.TryGetValue(ParametersTxtKey.VolumeRatio, ref SapVolumeRatio);
+ }
+
+ if (BrakeType == ParametersTxtSection.CI)
+ {
+ // equalizing reservoir
+ parameters.ReadBlock(ParametersTxtSection.ER, out Block erBlock);
+ erBlock.TryGetValue(ParametersTxtKey.ApplySpeed, ref ErApplySpeed);
+ erBlock.TryGetValue(ParametersTxtKey.ReleaseSpeed, ref ErReleaseSpeed);
+ erBlock.TryGetValue(ParametersTxtKey.VolumeRatio, ref ErVolumeRatio);
+ erBlock.TryGetValue(ParametersTxtKey.RapidReleaseSpeed, ref ErRapidReleaseSpeed);
+ }
+
+ if (BrakeType == ParametersTxtSection.SMEE || BrakeType == ParametersTxtSection.CI)
+ {
+ // brake pipe
+ parameters.ReadBlock(ParametersTxtSection.BP, out Block bpBlock);
+ bpBlock.TryGetValue(ParametersTxtKey.ApplySpeed, ref BpApplySpeed);
+ bpBlock.TryGetValue(ParametersTxtKey.ReleaseSpeed, ref BpReleaseSpeed);
+ bpBlock.TryGetValue(ParametersTxtKey.VolumeRatio, ref BpVolumeRatio);
+ bpBlock.TryGetValue(ParametersTxtKey.RapidReleaseSpeed, ref BpRapidReleaseSpeed);
+ }
+
+ // compressor
+ if (parameters.ReadBlock(ParametersTxtSection.Compressor, out Block compressorBlock))
+ {
+ compressorBlock.TryGetValue(ParametersTxtKey.LowerPressure, ref CompressorLowerPressure);
+ compressorBlock.TryGetValue(ParametersTxtKey.UpperPressure, ref CompressorUpperPressure);
+ compressorBlock.TryGetValue(ParametersTxtKey.CompressionSpeed, ref CompressionSpeed);
+ }
+
+ // dynamics
+ if (parameters.ReadBlock(ParametersTxtSection.Dynamics, out Block dynamicsBlock))
+ {
+ dynamicsBlock.TryGetValue(ParametersTxtKey.MotorCarWeight, ref MotorCarWeight);
+ dynamicsBlock.TryGetValue(ParametersTxtKey.TrailerWeight, ref TrailerCarWeight);
+ dynamicsBlock.TryGetValue(ParametersTxtKey.MotorCarCount, ref MotorCars);
+ dynamicsBlock.TryGetValue(ParametersTxtKey.TrailerCount, ref TrailerCars);
+ dynamicsBlock.TryGetValue(ParametersTxtKey.MotorcarInertiaFactor, ref MotorCarInertiaFactor);
+ dynamicsBlock.TryGetValue(ParametersTxtKey.TrailerInertiaFactor, ref TrailerCarInertiaFactor);
+ dynamicsBlock.TryGetValue(ParametersTxtKey.CarLength, ref CarLength);
+ }
+
+ // passengers
+ if (parameters.ReadBlock(ParametersTxtSection.Passengers, out Block passengersBlock))
+ {
+ passengersBlock.TryGetValue(ParametersTxtKey.Capacity, ref PassengerCapacity);
+ passengersBlock.TryGetValue(ParametersTxtKey.BodyWeight, ref PassengerWeight);
+ passengersBlock.TryGetValue(ParametersTxtKey.BoardingSpeed, ref BoardingSpeed);
+ passengersBlock.TryGetValue(ParametersTxtKey.AlightingSpeed, ref AlightingSpeed);
+ }
+
+ // door
+ if (parameters.ReadBlock(ParametersTxtSection.Door, out Block doorsBlock))
+ {
+ doorsBlock.TryGetValue(ParametersTxtKey.CloseTime, ref DoorCloseSpeed);
+ }
+
+ // viewpoint
+ if (parameters.ReadBlock(ParametersTxtSection.ViewPoint, out Block viewPointBlock))
+ {
+ viewPointBlock.TryGetValue(ParametersTxtKey.X, ref Driver.X);
+ viewPointBlock.TryGetValue(ParametersTxtKey.Y, ref Driver.Y);
+ viewPointBlock.TryGetValue(ParametersTxtKey.Z, ref Driver.Z);
+ }
+
+ if (MotorCars == 0)
+ {
+ throw new Exception("BVE5: A train must have at least one motor car.");
+ }
+
+ if (!performanceCurves.ReadBlock(PerformanceCurveTxtSection.Power, out Block powerPerformanceBlock))
+ {
+ throw new Exception("BVE5: Power performance data section missing.");
+ }
+ PowerPerformanceData = ParsePerformanceData(powerPerformanceBlock, Path.GetDirectoryName(performanceCurveTxtFile));
+
+ if (!performanceCurves.ReadBlock(PerformanceCurveTxtSection.Brake, out Block brakePerformanceBlock))
+ {
+ throw new Exception("BVE5: Brake performance data section missing.");
+ }
+ BrakePerformanceData = ParsePerformanceData(brakePerformanceBlock, Path.GetDirectoryName(performanceCurveTxtFile));
+
+ bool[] motorCars = TrainDatParser.AssignMotorCars(MotorCars, TrailerCars, FirstCarIsMotorCar);
+
+ /*
+ double OperatingPressure;
+ if (BrakeType == ParametersTxtSection.CI)
+ {
+ OperatingPressure = 440000.0 + 0.75 * (690000.0 - 440000.0);
+ if (OperatingPressure > 690000.0)
+ {
+ OperatingPressure = 690000.0;
+ }
+ }
+ else
+ {
+ if (440000.0 < 480000.0 & 690000.0 > 500000.0)
+ {
+ OperatingPressure = 490000.0;
+ }
+ else
+ {
+ OperatingPressure = 440000.0 + 0.75 * (690000.0 - 440000.0);
+ }
+ }
+ */
+
+ train.Cars = new CarBase[MotorCars + TrailerCars];
+ for (int i = 0; i < train.Cars.Length; i++)
+ {
+ train.Cars[i] = new CarBase(train, i);
+ train.Cars[i].Coupler = new Coupler(0.9 * 0.3, 1.1 * 0.3, train.Cars[i / 2], train.Cars.Length > 1 ? train.Cars[i / 2 + 1] : null);
+ switch (BrakeType)
+ {
+ case ParametersTxtSection.ECB:
+ // electric command brake
+ train.Cars[i].CarBrake = new ElectricCommandBrake(EletropneumaticBrakeType.None, train.Cars[i], 0,0, 0, BrakePerformanceData);
+ break;
+ case ParametersTxtSection.SMEE:
+ // electromagnetic straight air brake
+ train.Cars[i].CarBrake = new ElectromagneticStraightAirBrake(EletropneumaticBrakeType.None, train.Cars[i], BrakePerformanceData);
+ break;
+ case ParametersTxtSection.CI:
+ // automatic air brake
+ //train.Cars[i].CarBrake = new ElectricCommandBrake();
+ break;
+ default:
+ throw new Exception("BVE5: Invalid brake type.");
+ }
+
+ train.Cars[i].CarBrake.MainReservoir = new MainReservoir(690000.0, 780000.0, 0.01, (BrakeType == ParametersTxtSection.CI ? 0.25 : 0.075) / train.Cars.Length);
+ train.Cars[i].CarBrake.MainReservoir.Volume = 0.5;
+
+ train.Cars[i].CarBrake.EqualizingReservoir = new EqualizingReservoir(50000.0, 250000.0, 200000.0);
+ train.Cars[i].CarBrake.EqualizingReservoir.NormalPressure = 1.005 * BpInitialPressure;
+ train.Cars[i].CarBrake.EqualizingReservoir.Volume = 0.015; // very small reservoir for observation, so guess at 15L
+
+ train.Cars[i].CarBrake.BrakePipe = new BrakePipe(BpInitialPressure, 10000000.0, 1500000.0, 5000000.0, BrakeType == ParametersTxtSection.ECB);
+ train.Cars[i].CarBrake.BrakePipe.Volume = Math.Pow(0.0175 * Math.PI, 2) * (train.Cars.Length * 1.05); // Assuming Railway Group Standards 3.5cm diameter brake pipe, 5% extra length for bends etc.
+
+ AirBrake airBrake = train.Cars[i].CarBrake as AirBrake;
+ airBrake.StraightAirPipe = new StraightAirPipe(300000.0, 400000.0, 200000.0);
+ double r = 200000.0 / 440000.0 - 1.0;
+ if (r < 0.1) r = 0.1;
+ if (r > 1.0) r = 1.0;
+ train.Cars[i].CarBrake.AuxiliaryReservoir = new AuxiliaryReservoir(0.975 * BpInitialPressure, 200000.0, 0.5, r);
+ train.Cars[i].CarBrake.AuxiliaryReservoir.Volume = 0.16; // guessed 1/3 of main reservoir volume
+ if (motorCars[i])
+ {
+ train.Cars[i].TractionModel = new Bve5MotorCar(train.Cars[i], PowerPerformanceData, BrakePerformanceData);
+ airBrake.BrakeType = TrainManager.BrakeSystems.BrakeType.Main;
+ airBrake.Compressor = new Compressor(5000.0, train.Cars[i].CarBrake.MainReservoir, train.Cars[i]);
+ train.Cars[i].EmptyMass = MotorCarWeight;
+ }
+ else
+ {
+ train.Cars[i].TractionModel = new Bve5TrailerCar(train.Cars[i]);
+ airBrake.BrakeType = TrainManager.BrakeSystems.BrakeType.Auxiliary;
+ train.Cars[i].EmptyMass = TrailerCarWeight;
+ }
+
+ train.Cars[i].CarBrake.BrakeCylinder = new BrakeCylinder(440000.0, 440000.0, BrakeType == ParametersTxtSection.CI ? 300000.0 : 0.3 * 300000.0, 300000.0, 300000.0);
+ train.Cars[i].CarBrake.BrakeCylinder.Volume = 0.14; // 35cm diameter, 15cm stroke
+ train.Cars[i].CarBrake.JerkUp = 10;
+ train.Cars[i].CarBrake.JerkDown = 10;
+ train.Cars[i].CarBrake.Initialize(Plugin.CurrentOptions.TrainStart);
+
+ train.Cars[i].Doors[0] = new Door(-1, 1000, 0);
+ train.Cars[i].Doors[1] = new Door(1, 1000, 0);
+ train.Cars[i].ConstSpeed = new CarConstSpeed(train.Cars[i]);
+ train.Cars[i].HoldBrake = new CarHoldBrake(train.Cars[i]);
+
+ train.Cars[i].Specs.JerkPowerUp = 10;
+ train.Cars[i].Specs.JerkPowerDown = 10;
+
+ train.Cars[i].Width = 2.6;
+ train.Cars[i].Height = 3.6;
+ train.Cars[i].Length = CarLength;
+ train.Cars[i].Specs.ExposedFrontalArea = 0.6 * train.Cars[i].Width * train.Cars[i].Height;
+ train.Cars[i].Specs.UnexposedFrontalArea = 0.2 * train.Cars[i].Width * train.Cars[i].Height;
+ train.Cars[i].Specs.CenterOfGravityHeight = 1.6;
+ train.Cars[i].Specs.CriticalTopplingAngle = 0.5 * Math.PI - Math.Atan(2 * train.Cars[i].Specs.CenterOfGravityHeight / train.Cars[i].Width);
+ train.Cars[i].FrontAxle.Follower.TriggerType = i == 0 ? EventTriggerType.FrontCarFrontAxle : EventTriggerType.OtherCarFrontAxle;
+ train.Cars[i].RearAxle.Follower.TriggerType = i == train.Cars.Length - 1 ? EventTriggerType.RearCarRearAxle : EventTriggerType.OtherCarRearAxle;
+ train.Cars[i].BeaconReceiver.TriggerType = i == 0 ? EventTriggerType.TrainFront : EventTriggerType.None;
+ train.Cars[i].BeaconReceiverPosition = 0.5 * CarLength;
+ train.Cars[i].FrontAxle.Position = 0.4 * CarLength;
+ train.Cars[i].RearAxle.Position = -0.4 * CarLength;
+ train.Cars[i].ChangeCarSection(CarSectionType.NotVisible);
+ }
+
+ train.Handles.EmergencyBrake = new EmergencyHandle(train);
+ train.Handles.Power = new PowerHandle(PowerNotches, train);
+ train.Handles.Brake = new BrakeHandle(BrakeNotches, train.Handles.EmergencyBrake, train);
+ train.Handles.HasLocoBrake = false;
+ train.Handles.LocoBrake = new LocoBrakeHandle(0, train.Handles.EmergencyBrake, train);
+ train.Handles.LocoBrakeType = LocoBrakeType.Independant;
+ train.Handles.HoldBrake = new HoldBrakeHandle(train);
+ train.Handles.HasHoldBrake = false;
+
+
+ train.DriverCar = 0; // CHECK: BVE5 doesn't seem to allow setting of non-zero driver car
+
+ train.SafetySystems.PassAlarm = new PassAlarm(PassAlarmType.Loop, train.Cars[0]); // CHECK: No pass alarm setting in the BVE5 documentation, but is in the K_SEI3500R sound.txt
+ train.Cars[0].Breaker = new Breaker(train.Cars[0]);
+ train.SafetySystems.PilotLamp = new PilotLamp(train.Cars[0]);
+ train.SafetySystems.Headlights = new LightSource(train, 1);
+
+ if (vehicleBlock.GetPath(VehicleTxtKey.Panel, Path.GetDirectoryName(fileName), out string panelFile))
+ {
+ train.Cars[0].CarSections.Add(CarSectionType.Interior, new CarSection(Plugin.CurrentHost, ObjectType.Overlay, true));
+ train.Cars[0].Driver = Driver;
+ Plugin.Panel2CfgParser.ParsePanel2Config(Path.GetFileName(panelFile), Path.GetDirectoryName(panelFile), train.Cars[0]);
+ train.Cars[train.DriverCar].CameraRestrictionMode = CameraRestrictionMode.On;
+ Plugin.Renderer.Camera.CurrentRestriction = CameraRestrictionMode.On;
+ }
+
+ if (vehicleBlock.GetPath(VehicleTxtKey.MotorNoise, Path.GetDirectoryName(fileName), out string motorNoiseFile))
+ {
+ string noisePath = Path.GetDirectoryName(motorNoiseFile);
+ Block motorNoiseBlock = new ConfigFile(motorNoiseFile, Plugin.CurrentHost);
+ if (motorNoiseBlock.ReadBlock(MotorNoiseTxtSection.Power, out Block powerNoiseBlock) &&
+ motorNoiseBlock.ReadBlock(MotorNoiseTxtSection.Brake, out Block brakeNoiseBlock))
+ {
+ powerNoiseBlock.GetPath(MotorNoiseTxtKey.Frequency, noisePath, out string powerFreq);
+ powerNoiseBlock.GetPath(MotorNoiseTxtKey.Volume, noisePath, out string powerVol);
+ brakeNoiseBlock.GetPath(MotorNoiseTxtKey.Frequency, noisePath, out string brakeFreq);
+ brakeNoiseBlock.GetPath(MotorNoiseTxtKey.Volume, noisePath, out string brakeVol);
+
+ for (int i = 0; i < train.Cars.Length; i++)
+ {
+ train.Cars[i].TractionModel.MotorSounds = Bve5MotorSoundTableParser.Parse(train.Cars[i], powerFreq, powerVol, brakeFreq, brakeVol);
+ }
+ }
+ }
+
+ if (vehicleBlock.GetPath(VehicleTxtKey.Sound, Path.GetDirectoryName(fileName), out string soundFile))
+ {
+ Plugin.BVE4SoundParser.Parse(soundFile, Path.GetDirectoryName(soundFile), train);
+ }
+
+
+
+ train.CameraCar = 0;
+ train.Specs.AveragesPressureDistribution = true;
+ train.PlaceCars(0.0);
+
+ }
+
+ }
+}
diff --git a/source/TrainManager/Brake/AirBrake/AirBrake.cs b/source/TrainManager/Brake/AirBrake/AirBrake.cs
index 683bc38d08..baff78c36b 100644
--- a/source/TrainManager/Brake/AirBrake/AirBrake.cs
+++ b/source/TrainManager/Brake/AirBrake/AirBrake.cs
@@ -1,5 +1,6 @@
using OpenBveApi.Trains;
using TrainManager.Car;
+using TrainManager.Motor;
using TrainManager.Power;
namespace TrainManager.BrakeSystems
@@ -18,6 +19,10 @@ protected AirBrake(CarBase car, AccelerationCurve[] decelerationCurves) : base(c
{
}
+ protected AirBrake(CarBase car, Bve5PerformanceData performanceData) : base(car, performanceData)
+ {
+ }
+
public override void Initialize(TrainStartMode startMode)
{
switch (startMode)
diff --git a/source/TrainManager/Brake/AirBrake/ElectricCommandBrake.cs b/source/TrainManager/Brake/AirBrake/ElectricCommandBrake.cs
index 21c95264c1..1541fe5489 100644
--- a/source/TrainManager/Brake/AirBrake/ElectricCommandBrake.cs
+++ b/source/TrainManager/Brake/AirBrake/ElectricCommandBrake.cs
@@ -1,6 +1,7 @@
using System;
using TrainManager.Car;
using TrainManager.Handles;
+using TrainManager.Motor;
using TrainManager.Power;
namespace TrainManager.BrakeSystems
@@ -17,6 +18,16 @@ public ElectricCommandBrake(EletropneumaticBrakeType type, CarBase car, double B
this.DecelerationCurves = DecelerationCurves;
}
+ public ElectricCommandBrake(EletropneumaticBrakeType type, CarBase car, double MotorDeceleration, double MotorDecelerationDelayUp, double MotorDecelerationDelayDown, Bve5PerformanceData PerformanceData) : base(car, PerformanceData)
+ {
+ electropneumaticBrakeType = type;
+ this.BrakeControlSpeed = 0;
+ motorDeceleration = MotorDeceleration;
+ motorDecelerationDelayUp = MotorDecelerationDelayUp;
+ motorDecelerationDelayDown = MotorDecelerationDelayDown;
+ this.DecelerationCurves = null;
+ }
+
public override void Update(double timeElapsed, double currentSpeed, AbstractHandle brakeHandle, out double deceleration)
{
airSound = null;
diff --git a/source/TrainManager/Brake/AirBrake/ElectromagneticStraightAirBrake.cs b/source/TrainManager/Brake/AirBrake/ElectromagneticStraightAirBrake.cs
index 26a9f999d4..7024b98f3b 100644
--- a/source/TrainManager/Brake/AirBrake/ElectromagneticStraightAirBrake.cs
+++ b/source/TrainManager/Brake/AirBrake/ElectromagneticStraightAirBrake.cs
@@ -1,6 +1,7 @@
using System;
using TrainManager.Car;
using TrainManager.Handles;
+using TrainManager.Motor;
using TrainManager.Power;
namespace TrainManager.BrakeSystems
@@ -15,6 +16,16 @@ public class ElectromagneticStraightAirBrake : AirBrake
motorDecelerationDelayUp = 0;
motorDecelerationDelayDown = 0;
}
+
+ public ElectromagneticStraightAirBrake(EletropneumaticBrakeType type, CarBase car, Bve5PerformanceData performanceData) : base(car, performanceData)
+ {
+ electropneumaticBrakeType = type;
+ BrakeControlSpeed = 0;
+ motorDeceleration = 0;
+ motorDecelerationDelayUp = 0;
+ motorDecelerationDelayDown = 0;
+ }
+
public ElectromagneticStraightAirBrake(EletropneumaticBrakeType type, CarBase car, double brakeControlSpeed, double MotorDeceleration, double MotorDecelerationDelayUp, double MotorDecelerationDelayDown, AccelerationCurve[] decelerationCurves) : base(car, decelerationCurves)
{
electropneumaticBrakeType = type;
diff --git a/source/TrainManager/Brake/CarBrake.cs b/source/TrainManager/Brake/CarBrake.cs
index 78409ebbd5..8cb372e330 100644
--- a/source/TrainManager/Brake/CarBrake.cs
+++ b/source/TrainManager/Brake/CarBrake.cs
@@ -1,7 +1,9 @@
+using OpenBveApi.Math;
using OpenBveApi.Trains;
using SoundManager;
using TrainManager.Car;
using TrainManager.Handles;
+using TrainManager.Motor;
using TrainManager.Power;
namespace TrainManager.BrakeSystems
@@ -62,6 +64,8 @@ public abstract class CarBrake
public CarSound Release = new CarSound();
internal AccelerationCurve[] DecelerationCurves;
+
+ internal Bve5PerformanceData BrakePerformanceData;
/// A non-negative floating point number representing the jerk in m/s when the deceleration produced by the electric brake is increased.
public double JerkUp;
/// A non-negative floating point number representing the jerk in m/s when the deceleration produced by the electric brake is decreased.
@@ -72,7 +76,13 @@ protected CarBrake(CarBase car, AccelerationCurve[] decelerationCurves)
Car = car;
this.DecelerationCurves = decelerationCurves;
}
-
+
+ protected CarBrake(CarBase car, Bve5PerformanceData performanceData)
+ {
+ Car = car;
+ this.BrakePerformanceData = performanceData;
+ }
+
/// Updates the brake system
/// The frame time elapsed
/// The current speed of the train
@@ -97,6 +107,25 @@ internal double GetRate(double Ratio, double Factor)
/// The deceleration in m/s
public double DecelerationAtServiceMaximumPressure(int Notch, double currentSpeed)
{
+ if (BrakePerformanceData != null)
+ {
+ // first find the load proportion
+ // this is used to interpolate between Load and MaxLoad
+ double loadingRatio = Car.Cargo.Ratio / 250;
+ double noLoad = BrakePerformanceData.ForceTable.GetValue(currentSpeed, Notch);
+ double maxLoad = BrakePerformanceData.MaxForceTable.GetValue(currentSpeed, Notch);
+ double newtons = Extensions.LinearInterpolation(noLoad, 0, maxLoad, 1, loadingRatio);
+
+ /*
+ * According to Newton's second law, Acceleration = Force / Mass
+ * Load factor has already been taken into account above, so this *should* just be a simple
+ * calculation
+ *
+ * See also note in motor car properties.
+ */
+ return Car.CurrentMass / newtons;
+ }
+
if (DecelerationCurves == null || DecelerationCurves.Length == 0)
{
return 0;
diff --git a/source/TrainManager/Motor/BVE5/Bve5MotorCar.cs b/source/TrainManager/Motor/BVE5/Bve5MotorCar.cs
new file mode 100644
index 0000000000..b565ed884b
--- /dev/null
+++ b/source/TrainManager/Motor/BVE5/Bve5MotorCar.cs
@@ -0,0 +1,66 @@
+using OpenBveApi.Math;
+using TrainManager.Car;
+
+namespace TrainManager.Motor
+{
+ public class Bve5MotorCar : TractionModel
+ {
+ public Bve5PerformanceData PowerPerformanceData;
+
+ public Bve5PerformanceData BrakePerformanceData;
+
+ public Bve5MotorCar(CarBase car, Bve5PerformanceData powerPerformanceData,
+ Bve5PerformanceData brakePerformanceData) : base(car)
+ {
+ PowerPerformanceData = powerPerformanceData;
+ BrakePerformanceData = brakePerformanceData;
+ }
+
+ public override void Update(double timeElapsed)
+ {
+ }
+
+ public override double TargetAcceleration
+ {
+ get
+ {
+ // first find the load proportion
+ // this is used to interpolate between Load and MaxLoad
+ double loadingRatio = BaseCar.Cargo.Ratio / 250;
+ // BVE5 performance table returns result in newtons
+ double newtons = 0;
+ if (BaseCar.baseTrain.Handles.Brake.Actual > 0)
+ {
+ double noLoad = BrakePerformanceData.ForceTable.GetValue(BaseCar.CurrentSpeed,
+ BaseCar.baseTrain.Handles.Brake.Actual);
+ double maxLoad = BrakePerformanceData.MaxForceTable.GetValue(BaseCar.CurrentSpeed,
+ BaseCar.baseTrain.Handles.Brake.Actual);
+ newtons = Extensions.LinearInterpolation(0, noLoad, 1, maxLoad, loadingRatio);
+ }
+ else if (BaseCar.baseTrain.Handles.Power.Actual > 0)
+ {
+ double noLoad = PowerPerformanceData.ForceTable.GetValue(BaseCar.CurrentSpeed,
+ BaseCar.baseTrain.Handles.Brake.Actual);
+ double maxLoad = PowerPerformanceData.MaxForceTable.GetValue(BaseCar.CurrentSpeed,
+ BaseCar.baseTrain.Handles.Brake.Actual);
+ newtons = Extensions.LinearInterpolation(0, noLoad, 1, maxLoad, loadingRatio);
+ }
+
+ if (newtons == 0)
+ {
+ return 0;
+ }
+
+ /*
+ * According to Newton's second law, Acceleration = Force / Mass
+ * Load factor has already been taken into account above, so this *should* just be a simple
+ * calculation
+ *
+ * NOTE: 'correct' (or at least better) acceleration figures seem to be attained by applying the
+ * car's mass only, rather than that of the train
+ */
+ return newtons / BaseCar.CurrentMass;
+ }
+ }
+ }
+}
diff --git a/source/TrainManager/Motor/BVE5/Bve5PerformanceData.cs b/source/TrainManager/Motor/BVE5/Bve5PerformanceData.cs
new file mode 100644
index 0000000000..1b030b7a4f
--- /dev/null
+++ b/source/TrainManager/Motor/BVE5/Bve5PerformanceData.cs
@@ -0,0 +1,73 @@
+//Simplified BSD License (BSD-2-Clause)
+//
+//Copyright (c) 2026, Christopher Lees, The OpenBVE Project
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using OpenBveApi.Math;
+
+namespace TrainManager.Motor
+{
+ public class Bve5PerformanceData
+ {
+ /// The controlling parameters
+ public readonly Bve5PerformanceTable ParamsTable;
+ /// The acceleration force, when empty
+ public readonly Bve5PerformanceTable ForceTable;
+ /// The acceleration force, when fully loaded
+ public readonly Bve5PerformanceTable MaxForceTable;
+ /// The ammeter current, when empty
+ public readonly Bve5PerformanceTable CurrentTable;
+ /// The ammeter current when fully loaded
+ public readonly Bve5PerformanceTable MaxCurrentTable;
+ /// The ammeter current when the force is zero
+ public readonly Bve5PerformanceTable NoLoadCurrentTable;
+
+ public Bve5PerformanceData(Bve5PerformanceTable paramsTable, Bve5PerformanceTable forceTable, Bve5PerformanceTable maxForceTable, Bve5PerformanceTable currentTable, Bve5PerformanceTable maxCurrentTable, Bve5PerformanceTable noLoadCurrentTable)
+ {
+ ParamsTable = paramsTable;
+ ForceTable = forceTable;
+ MaxForceTable = maxForceTable;
+ CurrentTable = currentTable;
+ MaxCurrentTable = maxCurrentTable;
+ NoLoadCurrentTable = noLoadCurrentTable;
+ }
+
+ public double GetForce(double speed, int notch, double loadingRatio)
+ {
+ double noLoad = ForceTable.GetValue(speed, notch);
+ double maxLoad = MaxForceTable.GetValue(speed, notch);
+ return Extensions.LinearInterpolation(noLoad, 0, maxLoad, 1, loadingRatio);
+ }
+
+ public double GetCurrent(double speed, int notch, double loadingRatio)
+ {
+ double newtons = GetForce(speed, notch, loadingRatio);
+ if (newtons == 0)
+ {
+ return NoLoadCurrentTable.GetValue(speed, notch);
+ }
+ double noLoad = CurrentTable.GetValue(speed, notch);
+ double maxLoad = MaxCurrentTable.GetValue(speed, notch);
+ return Extensions.LinearInterpolation(noLoad, 0, maxLoad, 1, loadingRatio);
+ }
+ }
+}
diff --git a/source/TrainManager/Motor/BVE5/Bve5PerformanceTable.cs b/source/TrainManager/Motor/BVE5/Bve5PerformanceTable.cs
new file mode 100644
index 0000000000..2c203800f0
--- /dev/null
+++ b/source/TrainManager/Motor/BVE5/Bve5PerformanceTable.cs
@@ -0,0 +1,74 @@
+//Simplified BSD License (BSD-2-Clause)
+//
+//Copyright (c) 2026, Christopher Lees, The OpenBVE Project
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using System;
+using OpenBveApi.Math;
+
+namespace TrainManager.Motor
+{
+ /// Describes a BVE5 Performance Table
+ public class Bve5PerformanceTable
+ {
+ /// The performance curve entries
+ public readonly Tuple[] CurveEntries;
+ /// The table version
+ public readonly double Version;
+
+ public Bve5PerformanceTable(Tuple[] curveEntries, double version)
+ {
+ CurveEntries = curveEntries;
+ Version = version;
+ }
+
+ public double GetValue(double speed, double notch)
+ {
+ if (CurveEntries.Length == 0)
+ {
+ return 0;
+ }
+ int entry = 0;
+ // if between two entries, value is linearly interpolated
+ for (int i = 0; i < CurveEntries.Length - 1; i++)
+ {
+ if (CurveEntries[i].Item1 >= speed && CurveEntries[i + 1].Item1 <= speed)
+ {
+ entry = i;
+ break;
+ }
+ }
+
+ // check if we have enough notches, if not return the last
+ int notch1 = (int)Math.Min(notch, CurveEntries[entry].Item2.Length - 1);
+ if (CurveEntries.Length == 1)
+ {
+ return Extensions.LinearInterpolation(0, 0, CurveEntries[0].Item1, CurveEntries[0].Item2[notch1], speed);
+ }
+ // no guarantee that notch lengths will match per entry either
+ int notch2 = (int)Math.Min(notch, CurveEntries[entry + 1].Item2.Length - 1);
+
+ return Extensions.LinearInterpolation(CurveEntries[entry].Item1, CurveEntries[entry].Item2[notch1], CurveEntries[entry + 1].Item1, CurveEntries[entry + 1].Item2[notch2], speed);
+
+ }
+ }
+}
diff --git a/source/TrainManager/Motor/BVE5/Bve5TrailerCar.cs b/source/TrainManager/Motor/BVE5/Bve5TrailerCar.cs
new file mode 100644
index 0000000000..b366bc069c
--- /dev/null
+++ b/source/TrainManager/Motor/BVE5/Bve5TrailerCar.cs
@@ -0,0 +1,16 @@
+using TrainManager.Car;
+
+namespace TrainManager.Motor
+{
+ public class Bve5TrailerCar : TractionModel
+ {
+ public Bve5TrailerCar(CarBase car) : base(car)
+ {
+ }
+
+ public override void Update(double timeElapsed)
+ {
+ // Nothing to do
+ }
+ }
+}
diff --git a/source/TrainManager/TrainManager.csproj b/source/TrainManager/TrainManager.csproj
index 6858c04855..764b767daf 100644
--- a/source/TrainManager/TrainManager.csproj
+++ b/source/TrainManager/TrainManager.csproj
@@ -124,6 +124,9 @@
+
+
+
@@ -224,6 +227,7 @@
+