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 @@ +