diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index cde87bd8f..95ce25349 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -2479,11 +2479,24 @@ class TMoverParameters { return false == ControlPressureSwitch && (HasControlPressureSwitch || BrakePress < 1.0); } - // whether the lamp of the open line contactors is lit, telling the start can be made + // whether the lamp of the open line contactors is lit. on the electric locomotives it tells the start can be made, so + // it goes out with the start cut off; on the diesel-electric ones (the SU45 and SU46 family) it just tells the + // contactors are open, whatever opened them: the master controller at 0, or the pressure switch (eu07/maszyna#263) bool LineContactorsLampLit() const { + if (EngineType == TEngineType::DieselElectric) + { + return false == StLinFlag; + } return false == StLinFlag && MainCircuitLampsEnabled(); } + // whether the cab command opening the main circuit opens it where the only cab switch of the circuit is the button + // closing it: on a diesel that's the starter button, and the command is the one way to stop an engine whose fuel pump + // starts on its own (eu07/maszyna#427). the other vehicles with such a button keep ignoring the command + bool MainCircuitOpensWithStarterOnly() const + { + return EngineType == TEngineType::DieselEngine || EngineType == TEngineType::DieselElectric; + } // whether the pumps a joint oil and fuel pump switch works, those not started automatically, are all switched on bool OilFuelPumpsEnabled() const { diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 82bcabc66..730a74667 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -5172,6 +5172,7 @@ void TMoverParameters::ComputeTotalForce(double dt) double TMoverParameters::BrakeForceR(double ratio, double velocity) const { double press = 0; + ratio = Hamulec->GetBrakeRatio(ratio); if (MBPM > 2) { press = MaxBrakePress[1] + (MaxBrakePress[3] - MaxBrakePress[1]) * std::min(1.0, (TotalMass - Mass) / (MBPM - Mass)); @@ -5187,8 +5188,8 @@ double TMoverParameters::BrakeForceR(double ratio, double velocity) const press = MaxBrakePress[3]; if (DynamicBrakeType == dbrake_automatic) ratio = ratio + (1.5 - ratio) * std::min(1.0, Vel * 0.02); - if (BrakeDelayFlag & bdelay_R && BrakeMethod % 128 != bp_Cosid && BrakeMethod % 128 != bp_D1 && BrakeMethod % 128 != bp_D2 && Power < 1 && velocity < 40) - ratio = ratio / 2; + if (BrakeDelayFlag & bdelay_R && BrakeMethod % 128 != bp_Cosid && BrakeMethod % 128 != bp_D1 && BrakeMethod % 128 != bp_D2 && Power < 1) + ratio *= Hamulec->GetRapidRatio(velocity); if (TrainType == dt_DMU && velocity < 30.0) { ratio -= 0.3; diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index 17fa79ee7..1e7a5be66 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -16,6 +16,11 @@ Copyright (C) 2007-2014 Maciej Cierniak #include "Oerlikon_ESt.h" #include "utilities/utilities.h" +// wstepny nacisk sprezyn na glowny przyrzad rozrzadczy, wyrazony jako roznica cisnien ZS - PG [bar] +static double const dPSprezyny = 0.21; +// najmniejszy stopien hamowania w cylindrze [bar] (tablica 3-9, proby 12 i 22: 0,25-0,30) +static double const pStopnia = 0.28; + double d2A( double const d ) { return d * d * 0.7854 / 1000.0; @@ -218,10 +223,11 @@ double TNESt3::GetPF( double const PP, double const dt, double const Vel ) // pr CheckReleaser(dt); // luzowanie + double const cel{ CelBCP( CVP, VVP ) }; if ((BrakeStatus & b_hld) == b_off) dV = PF(0.0, BCP, Nozzles[dTO] * nastG + (1.0 - nastG) * Nozzles[dOO]) - * dt * (0.1 + 4.9 * std::min(0.2, BCP - ((CVP - 0.05 - VVP) * BVM + 0.1))); + * dt * (0.1 + 4.9 * std::min(0.2, BCP - (cel > 0.0 ? cel + 0.1 : 0.0))); // bez stopnia luzuje do zera else dV = 0.0; // BrakeCyl.Flow(-dV); @@ -229,7 +235,7 @@ double TNESt3::GetPF( double const PP, double const dt, double const Vel ) // pr if ( (BrakeStatus & b_on) == b_on && Przekladniki[1]->P() * HBG300 < MaxBP ) dV = PF( BVP, BCP, Nozzles[dTN] * (nastG + 2.0 * (BCP < Podskok ? 1.0 : 0.0)) + Nozzles[dON] * (1.0 - nastG) ) - * dt * (0.1 + 4.9 * std::min( 0.2, (CVP - 0.05 - VVP) * BVM - BCP ) ); + * dt * (0.1 + 4.9 * std::min( 0.2, cel - BCP ) ); else dV = 0; // BrakeCyl.Flow(-dV); @@ -313,7 +319,7 @@ void TNESt3::Init( double const PP, double const HPP, double const LPP, double c Miedzypoj->CreateCap(5.0); Miedzypoj->CreatePress(PP); - BVM = 1.0 / (HPP - 0.05 - LPP) * MaxBP; + BVM = 1.0 / (HPP - dPSprezyny - LPP) * MaxBP; BrakeDelayFlag = BDF; @@ -327,6 +333,17 @@ void TNESt3::Init( double const PP, double const HPP, double const LPP, double c RapidStaly = false; } +double TNESt3::GetRapidRatio(double const Vel) const +{ + // ESt3 has no rapid relay, and with disc brakes the rapid step stays engaged at all speeds + if( ( PrzekRapid == nullptr ) || RapidStaly ) { + return 1.0; + } + // a braking vehicle comes from above, so the step is still engaged down to the speed + // at which GetPF() drops it, not the one at which it engages + return ( Vel < 50.0 ? 1.0 / PrzekRapid->GetRapidMult() : 1.0 ); +} + double TNESt3::GetCRP() { return CntrlRes->P(); @@ -334,6 +351,24 @@ double TNESt3::GetCRP() // return Miedzypoj.P; } +double TNESt3::GetBrakeRatio( double const Ratio ) const // udzial cisnienia w cylindrze dla udzialu spadku w PG +{ + if( MaxBP <= 0.0 || BVM <= 0.0 ) { + return Ratio; + } + // pelny spadek sluzbowy w PG (HPP - LPP), odtworzony z przelozenia + double const spadek{ MaxBP / BVM + dPSprezyny }; + double const cel{ ( Ratio * spadek - dPSprezyny ) * BVM }; + return ( cel > 0.0 ? std::max( cel, pStopnia ) / MaxBP : 0.0 ); +} + +double TNESt3::CelBCP( double const CVP, double const VVP ) const // docelowe cisnienie w cylindrze +{ + double const cel{ ( CVP - dPSprezyny - VVP ) * BVM }; + // gdy zawor hamuje, nie daje mniej niz najmniejszy stopien + return ( cel > 0.0 ? std::max( cel, pStopnia ) : cel ); +} + void TNESt3::CheckState(double const BCP, double & /*dV1*/) // glowny przyrzad rozrzadczy { double const VVP{ ValveRes->P() }; @@ -344,14 +379,14 @@ void TNESt3::CheckState(double const BCP, double & /*dV1*/) // glowny przyrzad r Przys_blok = false; // sprawdzanie stanu - // if ((BrakeStatus and 1)=1)and(BCP>0.25)then - if (VVP + 0.01 + BCP / BVM < CVP - 0.05 && Przys_blok) + double const cel{ CelBCP( CVP, VVP ) }; + if (BCP < cel - 0.01 * BVM && Przys_blok) BrakeStatus |= b_on | b_hld; // hamowanie stopniowe; - else if (VVP - 0.01 + (BCP - 0.1) / BVM > CVP - 0.05) + else if (BCP - 0.1 > cel + 0.01 * BVM) BrakeStatus &= ~( b_on | b_hld ); // luzowanie; - else if (VVP + BCP / BVM > CVP - 0.05) + else if (BCP > cel) BrakeStatus &= ~b_on; // zatrzymanie napelaniania; - else if (VVP + (BCP - 0.1) / BVM < CVP - 0.05 && BCP > 0.25) // zatrzymanie luzowania + else if (BCP - 0.1 < cel && BCP > 0.25) // zatrzymanie luzowania BrakeStatus |= b_hld; if( ( BrakeStatus & b_hld ) == 0 ) @@ -380,7 +415,7 @@ void TNESt3::CheckReleaser(double const dt) // odluzniacz // odluzniacz automatyczny if ((BrakeStatus & b_rls) == b_rls) { - CntrlRes->Flow(PF(CVP, 0, 0.02) * dt); + CntrlRes->Flow(PF(CVP, 0, 0.0049) * dt); // ZS 15 l z 5,0 do 0,4 bar w 23 s (proba odbioru nr 27) if (CVP < VVP + 0.3 || false == autom) BrakeStatus &= ~b_rls; } @@ -549,7 +584,7 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie { // dON,dOO,dTN,dTO,dP,dS Nozzles[dON] = 3.7; Nozzles[dOO] = 2.50; - Nozzles[dTN] = 1.65; + Nozzles[dTN] = 1.62; Nozzles[dTO] = 1.39; Nozzles[dP] = 2.65; Nozzles[dPd] = 1.80; @@ -638,6 +673,14 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie Nozzles[dSd] = 1.1; Nozzles[dSm] = 0.9; + if( PrzekCiagly != nullptr ) { + // dysze cylindra skalibrowane na komore impulsowa przekladnika AL2 (1 l) wg czasow z tablicy 3-9 + Nozzles[dON] *= 0.914; + Nozzles[dTN] *= 0.885; + Nozzles[dOO] *= 0.82; + Nozzles[dTO] *= 0.8; + } + // przeliczanie z mm^2 na l/m for (auto &nozzle : Nozzles) { @@ -646,7 +689,8 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie for (int i = 1; i < 4; ++i) { Przekladniki[i]->BrakeRes = BrakeRes; - Przekladniki[i]->CreateCap(i); + // komora impulsowa przekladnika AL2 ma 1 l + Przekladniki[i]->CreateCap( ( Przekladniki[i] == PrzekCiagly ) ? 1.0 : i ); Przekladniki[i]->CreatePress(BrakeCyl->P()); if (i < 3) Przekladniki[i]->Next = Przekladniki[i + 1]; diff --git a/McZapkie/Oerlikon_ESt.h b/McZapkie/Oerlikon_ESt.h index dca531568..02d65a285 100644 --- a/McZapkie/Oerlikon_ESt.h +++ b/McZapkie/Oerlikon_ESt.h @@ -111,6 +111,7 @@ class TRapid : public TPrzekladnik { public: void SetRapidParams(double mult, double size); void SetRapidStatus(bool rs); + double GetRapidMult() const { return RapidMult; } void Update(double dt) override; using TPrzekladnik::TPrzekladnik; }; @@ -194,6 +195,9 @@ class TNESt3 : public TBrake { double GetPF( double const PP, double const dt, double const Vel ) override; // przeplyw miedzy komora wstepna i PG void EStParams(double i_crc) const; // parametry charakterystyczne dla ESt double GetCRP() override; + double GetRapidRatio(double const Vel) const override; + double CelBCP(double const CVP, double const VVP) const; // docelowe cisnienie w cylindrze + double GetBrakeRatio(double const Ratio) const override; // udzial cisnienia w cylindrze dla udzialu spadku w PG void CheckState(double const BCP, double &dV1); // glowny przyrzad rozrzadczy void CheckReleaser(double const dt); // odluzniacz double CVs(double const BP) const; // napelniacz sterujacego diff --git a/McZapkie/hamulce.h b/McZapkie/hamulce.h index aa5877acd..30b7bba73 100644 --- a/McZapkie/hamulce.h +++ b/McZapkie/hamulce.h @@ -429,6 +429,10 @@ class TBrake double GetVRP() const; // cisnienie komory wstepnej rozdzielacza /// Returns control reservoir (ZS) pressure [bar]; defaults to the auxiliary reservoir for valves without a dedicated ZS. virtual double GetCRP(); // cisnienie zbiornika sterujacego + /// Share of the full cylinder pressure the distributor settles at for the given share + /// of the full service pipe drop. Used only to predict the brake force; the default is proportional. + /// Share of the full service brake pipe drop. + virtual double GetBrakeRatio(double const Ratio) const { return Ratio; } /// /// Sets the brake delay (G/P/R/M) selector if requested mode is supported. /// @@ -451,6 +455,11 @@ class TBrake /// Sets the rapid step ratio. Default no-op; overridden where supported. /// Rapid ratio. virtual void SetRM(double const RMR) { /* no effect for this device type */ } // ustalenie przelozenia rapida + /// Share of the full cylinder pressure the distributor delivers at the given speed + /// while the vehicle is slowing down through it, for a vehicle set to R. Used only to predict + /// the brake force; the default is the approximation BrakeForceR() has always used. + /// Vehicle velocity [km/h]. + virtual double GetRapidRatio(double const Vel) const { return Vel < 40.0 ? 0.5 : 1.0; } /// Sets the velocity threshold for the rapid step. /// Velocity threshold (same unit as Vel passed to GetPF). virtual void SetRV(double const RVR) diff --git a/simulation/simulationenvironment.cpp b/simulation/simulationenvironment.cpp index 9bc11c205..480c80280 100644 --- a/simulation/simulationenvironment.cpp +++ b/simulation/simulationenvironment.cpp @@ -74,15 +74,15 @@ world_environment::compute_weather() const { } } - Global.fTurbidity = Global.Overcast <= 0.10 ? 3 : + Global.fTurbidity = Global.Overcast <= 0.10 ? 4 : Global.Overcast <= 0.20 ? 4 : Global.Overcast <= 0.30 ? 5 : Global.Overcast <= 0.40 ? 5 : - Global.Overcast <= 0.50 ? 5 : - Global.Overcast <= 0.60 ? 5 : - Global.Overcast <= 0.70 ? 6 : - Global.Overcast <= 0.80 ? 7 : - Global.Overcast <= 0.90 ? 8 : + Global.Overcast <= 0.50 ? 6 : + Global.Overcast <= 0.60 ? 6 : + Global.Overcast <= 0.70 ? 7 : + Global.Overcast <= 0.80 ? 8 : + Global.Overcast <= 0.90 ? 9 : 9; } diff --git a/tests/unit/data/diesel_electric.json b/tests/unit/data/diesel_electric.json new file mode 100644 index 000000000..bef605880 --- /dev/null +++ b/tests/unit/data/diesel_electric.json @@ -0,0 +1,46 @@ +{ + "_comment": "Diesel-electric locomotive made after the SU46 (303D): the line contactors close on the first power position of the master controller, the pressure switch (on by default) opens them with too low a pressure in the main pipe or the brakes applied.", + "Param": { "Category": "train", "M": 105000, "Mred": 15000, "Vmax": 120, "PWR": 1400, "SandCap": 400 }, + "Dimensions": { "L": 18.99, "H": 4.292, "W": 2.904, "Cx": 0.3 }, + "Wheels": { "D": 1.1, "AIM": 4000, "Tw": 1.435, "Axle": "Co'Co'", "Ad": 1.75, "Bd": 10.3, "Rmin": 100 }, + "Brake": { + "BrakeValve": "LSt", "NBpA": 4, "MBF": 85.0, "MaxBP": 4.4, "LocMaxBP": 4.4, "MaxASBP": 1.3, "RM": 0.71, "BCN": 12, "BCR": 0.114, + "BCD": 0.06, "BCM": 5.84, "HiPP": 5, "LoPP": 3.5, "Vv": 0.9, "MinCP": 7, "MaxCP": 8.5, "CompressorSpeed": 0.05, "BCS": 1, "BSA": 2, + "BM": "P10-Bgu", "CompressorPower": "Converter", "BVV": 200, "BRE": 0.8 + }, + "BuffCoupl": { "CType": "Screw", "kB": 9200, "DmaxB": 0.15, "FmaxB": 740, "kC": 4800, "DmaxC": 0.15, "FmaxC": 780, "beta": 0.58, "AllowedFlag": 103 }, + "Cntrl": { + "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BrakeSystem": "Pneumatic", "BDelay1": 15, "BDelay2": 6, "BDelay3": 36, "BDelay4": 32, + "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": 15, "SCPN": 4, "AutoRelay": "Yes", "IniCDelay": 0.5, "SCDelay": 0.1, + "BrakeDelays": "GPR", "ConverterStart": "Automatic", "ConverterStartDelay": 10 + }, + "Light": { "Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": 110, "LMaxCurrent": 100 }, + "Engine": { + "AIM": 1.6, "EngineType": "DumbDE", "Trans": "18:62", "Ftmax": 317000, "Flat": 0, "Vhyp": 15.0, "Vadd": 3.8, "Cr": 1.0, "RelayType": 46, + "OilMinPressure": 0.055, "OilMaxPressure": 0.6, "WaterMinTemperature": 40, "WaterMaxTemperature": 92, "WaterCoolingTemperature": 76, + "HeatKW": 0.068599, "HeatKV": 0.3, "HeatKFE": 1, "HeatKFS": 87.76269, "HeatKFO": 26.72716, "HeatKFO2": 57.72 + }, + "Circuit": { "CircuitRes": 0.01, "ImaxLo": 3300, "IminLo": 2500 }, + "_power": "rows: engine speed [rpm], power [kW], generator voltage limit, current limit, one per master controller position", + "WWList": { + "Size": 15, + "rows": [ + [696, 0, 0, 0], + [696, 0, 0, 0], + [696, 80, 350, 1000], + [696, 80, 350, 1000], + [763, 214, 455, 1400], + [830, 321, 500, 1600], + [897, 428, 545, 1800], + [964, 535, 590, 2000], + [1031, 642, 635, 2200], + [1098, 748, 680, 2400], + [1165, 855, 725, 2600], + [1232, 962, 770, 2800], + [1299, 1069, 815, 3000], + [1366, 1176, 865, 3200], + [1433, 1283, 910, 3400], + [1500, 1390, 960, 3600] + ] + } +} diff --git a/tests/unit/data/diesel_shunter.json b/tests/unit/data/diesel_shunter.json new file mode 100644 index 000000000..3cbcdb992 --- /dev/null +++ b/tests/unit/data/diesel_shunter.json @@ -0,0 +1,53 @@ +{ + "_comment": "Diesel shunter with a mechanical gearbox, made after the SM03 (Ls150): the fuel pump starts on its own with the engine, a converter run by the engine feeds the low voltage circuit, the only cab control of the engine is the starter button.", + "Param": { "Category": "train", "M": 23200, "Vmax": 25.68, "PWR": 111.855, "SandCap": 200 }, + "Dimensions": { "L": 6.94, "H": 3.21, "W": 2.97, "Cx": 0.3 }, + "Wheels": { "D": 0.94, "Tw": 1.435, "AIM": 250, "Axle": "B", "Ad": 2.5, "Bd": 2.5, "Rmin": 50, "BearingType": "Roll" }, + "Brake": { + "BrakeValve": "K", "NBpA": 2, "MBF": 85.0, "MaxBP": 3.5, "LocMaxBP": 5.0, "BCN": 1, "BCR": 0.114, "BCD": 0.12, "BCM": 10.0, + "HiPP": 5, "LoPP": 3.5, "Vv": 0.3, "MinCP": 7.0, "MaxCP": 8.0, "CompressorSpeed": 0.0125, "CompressorPower": "Main", + "BCS": 0.8, "BSA": 0, "BM": "P10-Bg", "BVV": 40, "BRE": 0.9 + }, + "BuffCoupl": { "CType": "Screw", "kB": 9200, "DmaxB": 0.15, "FmaxB": 740, "kC": 4800, "DmaxC": 0.15, "FmaxC": 780, "beta": 0.58, "AllowedFlag": 3 }, + "Cntrl": { + "BrakeSystem": "Pneumatic", "BDelay1": 15, "BDelay2": 4, "ASB": "Manual", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", + "DynamicBrake": "Passive", "MCPN": 9, "SCPN": 7, "AutoRelay": "No", "IniCDelay": 0.5, "SCDelay": 0.1, "BrakeDelays": "P", + "BrakeHandle": "Knorr", "LocBrakeHandle": "Knorr", "FuelStart": "Automatic", "ConverterStart": "Automatic" + }, + "Light": { "Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": 24, "LMaxCurrent": 100 }, + "Engine": { + "EngineType": "DieselEngine", "Trans": "1:1", "nmin": 900, "nmax": 1500, "nmax_cutoff": 1550, "AIM": 100, + "OilMinPressure": 0.343, "OilMaxPressure": 0.686, "WaterCoolingTemperature": 80, "WaterShutters": "yes", "WaterAuxCircuit": "no", + "HeatKFO": 160, "HeatKFO2": 180, "HeatKFS": 160, "HeatKFE": 7, "HeatKW": 2, "HeatKV": 0.3, "IsTC": "No", "EUS": 0.35, "EDS": 1.0 + }, + "_gears": "rows: gear, ratio, engagement speed, top speed; the even ones neutral", + "MotorParamTable": { + "minVelfullengage": 3.8, "engageDia": 1, "engageMaxForce": 800, "engagefriction": 1, + "rows": [ + [0, 0, 0, -1], + [1, 46.88, -1, 5.73], + [2, 0, 0, -1], + [3, 28.65, 2.9, 9.42], + [4, 0, 0, -1], + [5, 17.12, 4.7, 15.69], + [6, 0, 0, -1], + [7, 10.46, 10.6, 100] + ] + }, + "_throttle": "rows: position, fill, speed governor mode; position 0 idles", + "DList": { + "Size": 9, "Mmax": 784.5, "nMmax": 15, "Mnmax": 686.4, "nmax": 25, "nominalfill": 1, "Mstand": 200.0, + "rows": [ + [0, 0.1, 0], + [1, 0.2, 2], + [2, 0.3, 2], + [3, 0.4, 2], + [4, 0.5, 2], + [5, 0.6, 2], + [6, 0.7, 2], + [7, 0.8, 2], + [8, 0.9, 2], + [9, 1.0, 2] + ] + } +} diff --git a/tests/unit/scenario.cpp b/tests/unit/scenario.cpp index b933a0ab5..4af8be18a 100644 --- a/tests/unit/scenario.cpp +++ b/tests/unit/scenario.cpp @@ -5,6 +5,21 @@ namespace test { +namespace { + +// whether the pressure switch of a vehicle in the train still cuts the start off: a half of a locomotive whose +// switch is on when the controller leaves zero doesn't close its line contactors until the controller is back at zero +bool start_cut_off(consist &Train) { + for (std::size_t i = 0; i < Train.size(); ++i) { + if (Train[i].mover().ControlPressureSwitch) { + return true; + } + } + return false; +} + +} // namespace + scenario::scenario(std::string Name, std::vector Channels, track const &Track) : m_name(std::move(Name)), m_channels(std::move(Channels)), m_track(Track) { reseed_random(); } @@ -53,6 +68,11 @@ void scenario::get_going() { void scenario::full_power(double Seconds) { auto &controlling{driver().controlling}; + // as a driver does, wait for the brakes to let go far enough for every pressure switch of the train to allow the + // start; otherwise whether all the halves of a locomotive start hangs on thousandths of a bar of their cylinders + for (int i = 0; i < 120 && start_cut_off(m_train); ++i) { + wait(1.0); + } controlling.IncMainCtrl(1); for (int i = 0; i < 90 && false == controlling.StLinFlag; ++i) { wait(1.0); diff --git a/tests/unit/test_engine_shutdown.cpp b/tests/unit/test_engine_shutdown.cpp new file mode 100644 index 000000000..9230330f4 --- /dev/null +++ b/tests/unit/test_engine_shutdown.cpp @@ -0,0 +1,74 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// stopping the engine of a diesel shunter like the SM03 (eu07/maszyna#427): its fuel pump starts on its own with the +// engine and the engine runs the converter which feeds the low voltage circuit, so neither the fuel pump switch nor the +// battery switch stop it. the cab has only the starter button, and the command opening the main circuit has to stop it + +namespace { + +// the shunter with its engine started from the cab and idling +test::vehicle running_shunter() { + test::vehicle shunter{test::definition::load("diesel_shunter")}; + shunter->CabOccupied = 1; + shunter->CabActivisation(); + shunter->BatterySwitch(true); + shunter->OilPumpSwitch(true); + shunter.run(20.0); // the oil pressure builds up + shunter->MainSwitch(true); + shunter.run(15.0); + return shunter; +} + +} // namespace + +TEST_CASE("engine shutdown: the shunter starts with the starter button") { + auto shunter{running_shunter()}; + REQUIRE(shunter->EngineType == TEngineType::DieselEngine); + REQUIRE(shunter->FuelPump.start_type == start_t::automatic); + CHECK(shunter->Mains); + CHECK(shunter->FuelPump.is_active); + CHECK(shunter->enrot > 0.9 * shunter->dizel_nmin); +} + +TEST_CASE("engine shutdown: neither the fuel pump switch nor the battery switch stop the engine") { + auto shunter{running_shunter()}; + shunter->FuelPumpSwitchOff(true, range_t::local); // the pump started automatically ignores its switch + shunter.run(10.0); + CHECK(shunter->Mains); + shunter->BatterySwitch(false); + shunter.run(10.0); + CHECK(shunter->Power110vIsAvailable); // the converter run by the engine keeps feeding the fuel pump + CHECK(shunter->Mains); + CHECK(shunter->enrot > 0.9 * shunter->dizel_nmin); +} + +TEST_CASE("engine shutdown: the command opening the main circuit stops the engine and it stays stopped") { + auto shunter{running_shunter()}; + REQUIRE(shunter->MainCircuitOpensWithStarterOnly()); // the cab passes the command on with only the starter button + CHECK(shunter->MainSwitch(false)); + shunter.run(20.0); + CHECK_FALSE(shunter->Mains); + CHECK_FALSE(shunter->FuelPump.is_active); + CHECK(shunter->enrot == 0.0); +} + +TEST_CASE("engine shutdown: the engine starts again after it was stopped") { + auto shunter{running_shunter()}; + shunter->MainSwitch(false); + shunter.run(20.0); + REQUIRE(shunter->enrot == 0.0); + shunter->MainSwitch(true); + shunter.run(15.0); + CHECK(shunter->Mains); + CHECK(shunter->enrot > 0.9 * shunter->dizel_nmin); +} + +TEST_CASE("engine shutdown: vehicles without a diesel keep ignoring the command with only the button closing the circuit") { + test::vehicle wagon{test::definition::load("wagon")}; + CHECK_FALSE(wagon->MainCircuitOpensWithStarterOnly()); + test::vehicle electric{test::definition::load("wagon").set("Engine", "EngineType", "ElectricSeriesMotor")}; + REQUIRE(electric->EngineType == TEngineType::ElectricSeriesMotor); + CHECK_FALSE(electric->MainCircuitOpensWithStarterOnly()); +} diff --git a/tests/unit/test_line_contactors.cpp b/tests/unit/test_line_contactors.cpp index 79c419edf..fd7e24f08 100644 --- a/tests/unit/test_line_contactors.cpp +++ b/tests/unit/test_line_contactors.cpp @@ -57,3 +57,74 @@ TEST_CASE("line contactors: the ground relay and overload lamps follow the same vehicle->MotorConnectorsCheck(); CHECK_FALSE(vehicle->MainCircuitLampsEnabled()); } + +// on the diesel-electric SU45 and SU46 (eu07/maszyna#263) the lamp tells just the line contactors are open: lit with the +// master controller at 0 and with the start cut off by too low a pressure in the main pipe, out on the power positions + +namespace { + +// the diesel-electric locomotive with its engine running and the brakes released, the master controller at 0 +test::vehicle diesel_electric() { + test::vehicle vehicle{test::definition::load("diesel_electric")}; + vehicle->CabOccupied = 1; + vehicle->CabActivisation(); + vehicle->BatterySwitch(true); + vehicle->OilPumpSwitch(true); + vehicle->FuelPumpSwitch(true); + vehicle.run(20.0); // the oil pressure builds up + vehicle.hold_brake_pipe(5.0); // bar + vehicle->MainSwitch(true); + vehicle.run(30.0); + vehicle->DirectionForward(); + vehicle.run(60.0); // the brakes release + return vehicle; +} + +} // namespace + +TEST_CASE("line contactors: on a diesel-electric the lamp is lit at 0 and goes out on a power position") { + auto vehicle{diesel_electric()}; + REQUIRE(vehicle->EngineType == TEngineType::DieselElectric); + REQUIRE(vehicle->Mains); + REQUIRE_FALSE(vehicle->ControlPressureSwitch); + CHECK_FALSE(vehicle->StLinFlag); + CHECK(vehicle->LineContactorsLampLit()); + + vehicle->IncMainCtrl(1); + vehicle.run(3.0); + CHECK(vehicle->StLinFlag); + CHECK_FALSE(vehicle->LineContactorsLampLit()); + + vehicle->DecMainCtrl(1); + vehicle.run(3.0); + CHECK_FALSE(vehicle->StLinFlag); + CHECK(vehicle->LineContactorsLampLit()); +} + +TEST_CASE("line contactors: on a diesel-electric the lamp lights with the contactors opened by too low a main pipe pressure") { + auto vehicle{diesel_electric()}; + vehicle->IncMainCtrl(1); + vehicle.run(1.0); + vehicle->IncMainCtrl(1); + vehicle.run(2.0); + REQUIRE(vehicle->StLinFlag); + + vehicle.hold_brake_pipe(2.5); // bar, below the 3.6 bar of the pressure switch + vehicle.run(10.0); + REQUIRE(vehicle->ControlPressureSwitch); + CHECK_FALSE(vehicle->StLinFlag); + CHECK(vehicle->LineContactorsLampLit()); // it went out here + CHECK_FALSE(vehicle->MainCircuitLampsEnabled()); // the ground relay and overload lamps still go out with the start cut off + + // the pressure back, the contactors stay open on the second position and the lamp stays lit, as it should; + // they close again only on the first one + vehicle.hold_brake_pipe(5.0); + vehicle.run(60.0); + REQUIRE_FALSE(vehicle->ControlPressureSwitch); + CHECK_FALSE(vehicle->StLinFlag); + CHECK(vehicle->LineContactorsLampLit()); + vehicle->DecMainCtrl(1); + vehicle.run(3.0); + CHECK(vehicle->StLinFlag); + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} diff --git a/utilities/color.h b/utilities/color.h index be853a3d4..bbf17e096 100644 --- a/utilities/color.h +++ b/utilities/color.h @@ -15,9 +15,9 @@ glm::vec3 XYZtoRGB( glm::vec3 const &XYZ ) { // M^-1 for Adobe RGB from http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html - float const mi[ 3 ][ 3 ] = { 2.041369f, -0.969266f, 0.0134474f, -0.5649464f, 1.8760108f, -0.1183897f, -0.3446944f, 0.041556f, 1.0154096f }; +// float const mi[ 3 ][ 3 ] = { 2.041369f, -0.969266f, 0.0134474f, -0.5649464f, 1.8760108f, -0.1183897f, -0.3446944f, 0.041556f, 1.0154096f }; // m^-1 for sRGB: -// float const mi[ 3 ][ 3 ] = { 3.240479f, -0.969256f, 0.055648f, -1.53715f, 1.875991f, -0.204043f, -0.49853f, 0.041556f, 1.057311f }; + float const mi[ 3 ][ 3 ] = { 3.240479f, -0.969256f, 0.055648f, -1.53715f, 1.875991f, -0.204043f, -0.49853f, 0.041556f, 1.057311f }; return glm::vec3{ XYZ.x*mi[ 0 ][ 0 ] + XYZ.y*mi[ 1 ][ 0 ] + XYZ.z*mi[ 2 ][ 0 ], diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 013bb5505..0fc337325 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -2944,6 +2944,9 @@ bool TController::ReleaseEngine() { return OK; } +// defined further down, with the braking demand it is used for +static double brake_response_horizon( TMoverParameters const *Vehicle, TBrakeSystem const Brakesystem, double const Acceleration, double const Brakea0 ); + bool TController::IncBrake() { // zwiększenie hamowania auto OK { false }; @@ -3056,7 +3059,7 @@ bool TController::IncBrake() } double deltaAcc = -AccDesired*BrakeAccFactor() - (fBrake_a0[0] + 4.0 * (/*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition - 1 - pos_corr)*fBrake_a1[0]); - if( deltaAcc > fBrake_a1[0]) + if( deltaAcc > brake_increase_margin() ) { if( /*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition < 0.1 ) { OK = /*mvOccupied->*/BrakeLevelAdd( BrakingInitialLevel ); //GBH @@ -3083,6 +3086,20 @@ bool TController::IncBrake() } } } + else if( BrakeCtrlPosition > 0.0 + && AccDesired < 0.0 // only while braking is asked for, not when called for a train slipping back while starting + && ( fBrakeHeldTime > ( + mvOccupied->BrakeDelayFlag > bdelay_G ? + mvOccupied->BrakeDelay[ 1 ] : + mvOccupied->BrakeDelay[ 3 ] ) + // a train falling further behind needs no full fill time to be seen not to get enough + || ( ( fShortfall > 0.02 ) + && ( fBrakeHeldTime > brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) ) ) ) ) { + // the table says the present position is enough, but the brake has had its full fill + // time since the handle last moved and the train is still not slowing down as asked: + // believe the train rather than the table + OK = BrakeLevelAdd( BrakingLevelIncrease ); + } else OK = false; } @@ -3199,7 +3216,9 @@ bool TController::DecBrake() { if( fBrake_a0[0] != 0.0 || fBrake_a1[0] != 0.0 ) { auto const pos_diff { ( is_dmu() ? 0.25 : 1.0 ) }; - deltaAcc = -AccDesired * BrakeAccFactor() - ( fBrake_a0[ 0 ] + 4 * (/*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition - pos_diff )*fBrake_a1[ 0 ] ); + // a distributor applied at all gives at least its minimum stage, about what the first position does, so + // a position short of the first one brakes no weaker than that + deltaAcc = -AccDesired * BrakeAccFactor() - ( fBrake_a0[ 0 ] + 4 * std::max( 0.0, BrakeCtrlPosition - pos_diff ) * fBrake_a1[ 0 ] ); } if (deltaAcc < 0 && /*GBH mvOccupied->BrakeCtrlPosR*/ BrakeCtrlPosition > 0) { OK = /*mvOccupied->*/ BrakeLevelAdd(-0.25); @@ -3729,6 +3748,7 @@ void TController::BrakeLevelSet(double b) if (BrakeCtrlPosition == b) return; // nie przeliczać, jak nie ma zmiany BrakeCtrlPosition = std::clamp(b, (double)gbh_MIN, (double)gbh_MAX); + fBrakeHeldTime = 0.0; } bool TController::BrakeLevelAdd(double b) @@ -5866,6 +5886,7 @@ TController::update_timers( double dt ) { ElapsedTime += dt; WaitingTime += dt; fBrakeTime -= dt; // wpisana wartość jest zmniejszana do 0, gdy ujemna należy zmienić nastawę hamulca + fBrakeHeldTime += dt; if( mvOccupied->fBrakeCtrlPos != mvOccupied->Handle->GetPos( bh_FS ) ) { // brake charging timeout starts after charging ends BrakeChargingCooldown += dt; @@ -6669,12 +6690,27 @@ TController::determine_proximity_ranges() { // na jaka odleglosc i z jaka predkoscia ma podjechac do przeszkody // jeśli pociąg if( is_train() ) { - fMinProximityDist = std::clamp( 5 + iVehicles, 10, 15 ); - fMaxProximityDist = std::clamp( 10 + iVehicles, 15, 40 ); + if( IsAtPassengerStop ) { + // a stop point keeps the margins its W4 parameters (head or middle of the consist, + // platform length) were set up against + fMinProximityDist = std::clamp( 5 + iVehicles, 10, 15 ); + fMaxProximityDist = std::clamp( 10 + iVehicles, 15, 40 ); + if( IsCargoTrain ) { + fMinProximityDist += 10.0; + } + } + else { + // a train stops 8-15 m short of the signal whatever its length; closer than the lower + // edge it stops at once, the braking aims at the middle of the window, 3.5 m further out + fMinProximityDist = 8.0; + // the final approach begins far enough out to slow from the coasting speed down to the window, + // and at the same distance whether the train is moving or standing, so it does not flip + // between creeping closer and holding still when it comes to a stand short of the window + fMaxProximityDist = 50.0; + } if( IsCargoTrain ) { - // increase distances for cargo trains to take into account slower reaction to brakes - fMinProximityDist += 10.0; + // start the final approach earlier for cargo trains, their brakes react more slowly fMaxProximityDist += 10.0; /* if( IsHeavyCargoTrain ) { @@ -6693,7 +6729,7 @@ TController::determine_proximity_ranges() { if( mvOccupied->Vel < 0.1 ) { // jak stanie za daleko, to niech nie dociąga paru metrów - fMaxProximityDist = 50.0; + fMaxProximityDist = std::max( fMaxProximityDist, 50.0 ); } if( iDrivigFlags & moveLate ) { @@ -7306,6 +7342,26 @@ TController::pick_optimal_speed( double const Range ) { is_car() ? -2.0 : -0.9, is_car() ? 2.0 : 0.9 ); + if( is_train() + && ( ( OrderCurrentGet() & Obey_train ) != 0 ) + && ( VelNext == 0.0 ) + && ( ActualProximityDist <= fMaxProximityDist ) + && ( fAccGravity < -0.025 ) ) { + // uphill, the gradient alone brings a slow train to a stand within metres, far sooner than a released + // brake applies; so the train brake goes on while the train is coming to a stand, stays on while it + // stands, and is applied for real if the train has started to roll back. done last, so that none of + // the adjustments above, made for a train moving forward, can undo it + auto const vel { DirectionalVel() }; + if( std::abs( vel ) < EU07_AI_NOMOVEMENT ) { + VelDesired = 0.0; + AccDesired = std::min( AccDesired, EU07_AI_NOACCELERATION ); + } + else if( ( vel < 0.0 ) + || ( ( vel < 5.0 ) && ( AccDesired < 0.0 ) ) ) { + AccDesired = std::min( AccDesired, fAccGravity - 0.15 ); + } + } + // if the route ahead is blocked we might need to head the other way check_route_behind( 1000 ); // NOTE: legacy scan range value } @@ -7471,6 +7527,74 @@ TController::adjust_desired_speed_for_limits() { } } +// how long the present acceleration of a consist lasts whatever is done with the handle now: half the time +// its pneumatic brake takes to apply (while speeding up) or to release (while slowing down), which in G +// is tens of seconds; never shorter than the look-ahead the AI has always used +static double +brake_response_horizon( TMoverParameters const *Vehicle, TBrakeSystem const Brakesystem, double const Acceleration, double const Brakea0 ) { + + auto horizon { ( 1.0 - Brakea0 ) * 30.0 / 3.6 }; + if( Brakesystem == TBrakeSystem::Pneumatic ) { + auto const isrelease { Acceleration < 0.0 }; + horizon = std::max( + horizon, + 0.5 * ( + Vehicle->BrakeDelayFlag > bdelay_G ? + Vehicle->BrakeDelay[ isrelease ? 0 : 1 ] : + Vehicle->BrakeDelay[ isrelease ? 2 : 3 ] ) ); + } + return horizon; +} + +// distance [m] in which the gradient alone, with no brake and no power, brings a train rolling uphill at Velocity [km/h] to a stand +static double +uphill_stopping_distance( double const Velocity, double const Gravity ) { + return ( Velocity / 3.6 ) * ( Velocity / 3.6 ) / ( 2.0 * std::max( 0.01, -Gravity ) ); +} + +// speed [km/h] from which the gradient alone brings a train rolling uphill to a stand in Distance [m] +static double +uphill_coasting_speed( double const Distance, double const Gravity ) { + return 3.6 * std::sqrt( 2.0 * std::max( 0.01, -Gravity ) * std::max( 0.0, Distance ) ); +} + +// resultant acceleration [m/s2] to ask for now so that a steady application of the pneumatic brake takes the train +// from Velocity down to Target [km/h] over Distance [m], with the brake force following the speed the way the +// consist brake table says it does (cast iron blocks grip harder as the train slows) +static double +shaped_braking_demand( + double const Velocity, double const Target, double const Distance, double const Gravity, + double const Vmax, double const *Brakea0, double const *Brakea1, int const Tablesize ) { + + auto const fullbraking = [&]( double const Kmh ) { + auto const index { std::clamp( static_cast( Tablesize * Kmh / std::max( 1.0, Vmax ) ), 1, Tablesize ) }; + return std::max( 0.01, Brakea0[ index ] + 12.0 * Brakea1[ index ] ); }; + auto const travel = [&]( double const Ratio ) { + auto const steps { 24 }; + auto const step { ( Velocity - Target ) / steps }; + auto distance { 0.0 }; + for( auto i { 0 }; i < steps; ++i ) { + auto const kmh { Target + ( i + 0.5 ) * step }; + auto const deceleration { Ratio * fullbraking( kmh ) - Gravity }; + if( deceleration <= 0.001 ) { + return std::numeric_limits::max(); + } + distance += ( kmh / 3.6 ) * ( step / 3.6 ) / deceleration; + } + return distance; }; + + auto low { 0.0 }; + auto high { 3.0 }; + if( travel( high ) > Distance ) { + return Gravity - high * fullbraking( Velocity ); + } + for( auto i { 0 }; i < 30; ++i ) { + auto const middle { 0.5 * ( low + high ) }; + ( travel( middle ) > Distance ? low : high ) = middle; + } + return Gravity - high * fullbraking( Velocity ); +} + void TController::adjust_desired_speed_for_target_speed( double const Range ) { // ustalanie zadanego przyspieszenia @@ -7484,6 +7608,7 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { // gdy jedzie wolniej niż potrzeba, albo nie ma przeszkód na drodze // normalna jazda AccDesired = VelDesired != 0.0 ? AccPreferred : -0.01; + fAccDesiredGravity = 0.0; auto const vel { DirectionalVel() }; @@ -7503,7 +7628,10 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { } else if (ActualProximityDist > fMinProximityDist) { // jedzie szybciej, niż trzeba na końcu ActualProximityDist, ale jeszcze jest daleko - if (ActualProximityDist < fMaxProximityDist) { + // only a stop has a final approach of its own inside the proximity range; a lower speed ahead + // keeps the braking curve below right up to where it begins, or the train would stop braking + // (and even take power) that far out + if( ( VelNext == 0.0 ) && ( ActualProximityDist < fMaxProximityDist ) ) { // jak minął już maksymalny dystans po prostu hamuj (niski stopień) // ma stanąć, a jest w drodze hamowania albo ma jechać /* @@ -7520,9 +7648,33 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { } else { // hamowanie tak, aby stanąć - VelDesired = VelNext; - AccDesired = ( VelNext * VelNext - vel * vel ) / ( 25.92 * ( ActualProximityDist + 0.1 - 0.5*fMinProximityDist ) ); - AccDesired = std::min( AccDesired, fAccThreshold ); + if( ( OrderCurrentGet() & Obey_train ) == 0 ) { + VelDesired = VelNext; + AccDesired = ( VelNext * VelNext - vel * vel ) / ( 25.92 * ( ActualProximityDist + 0.1 - 0.5*fMinProximityDist ) ); + AccDesired = std::min( AccDesired, fAccThreshold ); + } + else { + // a train keeps its desired speed until the lower edge of the stopping window (a desired + // speed of 0 here asks for hard braking whatever the distance left), aims at the middle + // of the window, and asks only for what that takes + auto const stoppingroom { std::max( 0.5, ActualProximityDist - stopping_margin() ) }; + AccDesired = ( + BrakeSystem == TBrakeSystem::Pneumatic ? + // cast iron blocks grip harder as the train comes to a stand + shaped_braking_demand( + vel, VelNext, stoppingroom, fAccGravity, + mvOccupied->Vmax, fBrake_a0, fBrake_a1, BrakeAccTableSize ) : + ( VelNext * VelNext - vel * vel ) / ( 25.92 * stoppingroom ) ); + if( ( fAccGravity < -0.025 ) + && ( AccDesired >= fAccGravity - 0.001 ) // no brake needed at all + && ( ActualProximityDist - uphill_stopping_distance( vel, fAccGravity ) > stopping_margin() ) ) { + // uphill the gradient alone would bring the train to a stand short of the + // middle of the stopping window; keep it rolling at the present speed until that changes, + // but no faster than the gradient alone can take off by the middle of the window + AccDesired = std::min( 0.0, AccPreferred ); + VelDesired = min_speed( VelDesired, uphill_coasting_speed( ActualProximityDist - stopping_margin(), fAccGravity ) ); + } + } } } else { @@ -7546,17 +7698,57 @@ TController::adjust_desired_speed_for_target_speed( double const Range ) { if( brakingdistance + std::max(slowdowndistance, fMaxProximityDist) >= ActualProximityDist - fMaxProximityDist ) { // don't slow down prematurely; as long as we have room to come to a full stop at a safe distance, we're good // ensure some minimal coasting speed, otherwise a vehicle entering this zone at very low speed will be crawling forever - auto const brakingpointoffset = VelNext * braking_distance_multiplier( VelNext ); - AccDesired = std::min( - AccDesired, - ( VelNext * VelNext - vel * vel ) - / ( 25.92 - * std::max( - ActualProximityDist - brakingpointoffset, - std::min( - ActualProximityDist, - brakingpointoffset ) ) - + 0.1 ) ); // najpierw hamuje mocniej, potem zluzuje + auto const brakingpointoffset = ( + ( VelNext == 0.0 ) && is_train() && ( OrderCurrentGet() & Obey_train ) ? + stopping_margin() : // the same stopping point as the final approach + VelNext * braking_distance_multiplier( VelNext ) ); + // a pneumatic brake acts on the state the train will be in once it has responded; while the + // train is still speeding up, plan the deceleration from there, so more is asked for at once + auto velocity { vel }; + auto distance { ActualProximityDist }; + if( ( BrakeSystem == TBrakeSystem::Pneumatic ) + && ( AbsAccS > 0.0 ) ) { + auto const horizon { brake_response_horizon( mvOccupied, BrakeSystem, AbsAccS, fBrake_a0[ 0 ] ) }; + velocity = vel + 3.6 * horizon * AbsAccS; + distance = std::max( 0.5 * ActualProximityDist, ActualProximityDist - ( vel + velocity ) * 0.5 / 3.6 * horizon ); + } + auto const brakingroom { + std::max( + distance - brakingpointoffset, + std::min( + distance, + brakingpointoffset ) ) }; + auto deceleration { ( VelNext * VelNext - velocity * velocity ) / ( 25.92 * brakingroom + 0.1 ) }; + if( ( BrakeSystem == TBrakeSystem::Pneumatic ) && ( velocity > VelNext ) ) { + // follow the shape of the consist brake force over speed rather than assume a constant + // deceleration, over the distance left once the brake has had time to fill (while the + // train is still speeding up, the prediction above has taken that off already) + auto room { brakingroom }; + if( ( AbsAccS <= 0.0 ) + && ( BrakeCtrlPosition <= 0.0 ) ) { // only while the brake is still to be applied: once it acts, the room it needs to respond is behind + room = std::max( + 0.5 * brakingroom, + brakingroom - vel / 3.6 * brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) ); + } + deceleration = shaped_braking_demand( + velocity, VelNext, room + 0.1 / 25.92, fAccGravity, + mvOccupied->Vmax, fBrake_a0, fBrake_a1, BrakeAccTableSize ); + } + AccDesired = std::min( AccDesired, deceleration ); // najpierw hamuje mocniej, potem zluzuje + if( vel > VelNext ) { + if( deceleration < fAccGravity - 0.001 ) { + // no power while still above the speed being braked for + AccDesired = std::min( AccDesired, EU07_AI_NOACCELERATION ); + } + else if( ( fAccGravity < -0.025 ) + && ( ( OrderCurrentGet() & Obey_train ) != 0 ) + && ( ActualProximityDist - uphill_stopping_distance( vel, fAccGravity ) > brakingpointoffset ) ) { + // uphill the gradient alone slows the train down more than it is asked to; + // keep it rolling at the present speed until that changes + AccDesired = std::min( 0.0, AccPreferred ); + VelDesired = min_speed( VelDesired, uphill_coasting_speed( ActualProximityDist - brakingpointoffset, fAccGravity ) ); + } + } } } } @@ -7642,35 +7834,61 @@ TController::adjust_desired_speed_for_current_speed() { if( fAccGravity > 0.025 ) { // going sharply downhill we may need to start braking sooner than usual // try to estimate increase of current velocity before engaged brakes start working - auto const speedestimate = vel + ( 1.0 - fBrake_a0[ 0 ] ) * 30.0 * AbsAccS; - if( speedestimate > VelDesired ) { - // jesli jedzie za szybko do AKTUALNEGO - if( VelDesired == 0.0 ) { - // jesli stoj, to hamuj, ale i tak juz za pozno :) - AccDesired = std::min( AccDesired, -0.85 ); // hamuj solidnie - } - else { - // if it looks like we'll exceed maximum speed start thinking about slight slowing down - AccDesired = std::min( AccDesired, -0.05 + fAccThreshold ); - // HACK: for cargo trains with high braking threshold ensure we cross that threshold - if( true == IsCargoTrain - && fBrake_a0[0] > 0.2 ) { - AccDesired -= std::clamp( fBrake_a0[ 0 ] - 0.2, 0.0, 0.15 ); - } - } + // look ahead for as long as the brake takes to respond, since that is how long the + // present acceleration will last whatever is done with the handle now; a pneumatic + // brake applies and releases on its own time scale, which in G is tens of seconds + auto const horizon { brake_response_horizon( mvOccupied, BrakeSystem, AbsAccS, fBrake_a0[ 0 ] ) }; + auto const speedestimate = vel + 3.6 * horizon * AbsAccS; + // already below a lower limit ahead, the train has no business speeding up towards it + auto holdlimit { VelDesired }; + if( ( VelNext > 0.0 ) + && ( VelNext < holdlimit ) + && ( vel <= VelNext + fVelPlus ) + && ( ActualProximityDist < std::max( 2.0 * fBrakeDist, 500.0 ) ) ) { + holdlimit = VelNext; + } + // hold a band just below the limit; the steeper the descent the harder the train pulls + // away once the brake eases, so the band widens with the gradient (about 3 km/h at 20 per mille) + // an electro-pneumatic or electric brake responds at once and needs far less room + auto const holdspeed { holdlimit - ( + BrakeSystem == TBrakeSystem::Pneumatic ? + std::clamp( fAccGravity * 15.0, 1.0, 3.0 ) : + std::clamp( fAccGravity * 7.5, 1.0, 1.5 ) ) }; + // the predicted difference is to be made up over the time the brake takes to respond, not at once: with a + // slow brake, a demand sized to remove it at once comes on as a deep application, which then takes as + // long to let go, and the train ends up well below the speed it was to hold + auto const holdgain { std::min( 0.04, 1.0 / ( 3.6 * std::max( 1.0, horizon ) ) ) }; + if( ( speedestimate > VelDesired ) + && ( VelDesired == 0.0 ) ) { + // jesli stoj, to hamuj, ale i tak juz za pozno :) + AccDesired = std::min( AccDesired, -0.85 ); // hamuj solidnie } else { - // stop accelerating when close enough to target speed + // one continuous law on both sides of the target, proportional to the predicted + // difference: a demand that jumps to the braking threshold above the target and to + // full acceleration below it alternates, with a brake that responds in seconds, + // between a hard application and a full release AccDesired = std::min( AccDesired, // but don't override decceleration for VelNext - std::lerp( // ease off as you close to the target velocity - EU07_AI_NOACCELERATION, AccPreferred, - std::clamp( VelDesired - speedestimate, 0.0, fVelMinus ) / fVelMinus ) ); + std::clamp( + EU07_AI_NOACCELERATION - holdgain * ( speedestimate - holdspeed ), + -0.05 + fAccThreshold, + std::max( EU07_AI_NOACCELERATION, AccPreferred ) ) ); + // HACK: for cargo trains with high braking threshold ensure we cross that threshold + if( IsCargoTrain + && fBrake_a0[0] > 0.2 + && speedestimate > holdlimit ) { + AccDesired -= std::clamp( fBrake_a0[ 0 ] - 0.2, 0.0, 0.15 ); + } } // final tweaks if( vel > EU07_AI_NOMOVEMENT ) { // going downhill also take into account impact of gravity AccDesired -= fAccGravity; + // from here on AccDesired is what the brake alone has to deliver, which is what + // IncBrake() sizes the handle position for; AbsAccS already contains the gravity, + // so the comparisons with it have to add this part back + fAccDesiredGravity = fAccGravity; // HACK: if the max allowed speed was exceeded something went wrong; brake harder AccDesired -= 0.15 * std::clamp( vel - VelDesired, 0.0, 5.0 ); } @@ -7737,6 +7955,10 @@ TController::adjust_desired_speed_for_braking_test() { if( false == AIControllFlag ) { return; } // TBD: add notification about braking test and enable it for human driver as well? auto const vel { DirectionalVel() }; + // what the route ahead asks for; the test may brake harder than that, never softer + auto const routeaccdesired { AccDesired }; + auto const routeaccdesiredgravity { fAccDesiredGravity }; + auto const routeveldesired { VelDesired }; switch( DynamicBrakeTest ) { case 0: { @@ -7754,6 +7976,7 @@ TController::adjust_desired_speed_for_braking_test() { } case 1: { AccDesired = EU07_AI_BRAKINGTESTACCELERATION; + fAccDesiredGravity = 0.0; VelDesired = DBT_VelocityBrake; if( ElapsedTime - DBT_BrakingTime > 1 ) { ForcePNBrake = true; @@ -7764,6 +7987,7 @@ TController::adjust_desired_speed_for_braking_test() { } case 2: { AccDesired = EU07_AI_BRAKINGTESTACCELERATION; + fAccDesiredGravity = 0.0; VelDesired = DBT_VelocityBrake; if( ElapsedTime - DBT_BrakingTime > 2 ) { DBT_BrakingTime = ElapsedTime; @@ -7775,6 +7999,7 @@ TController::adjust_desired_speed_for_braking_test() { } case 3: { AccDesired = safe_clamp( -AbsAccS, fAccThreshold * 1.01, fAccThreshold * 1.21 ); + fAccDesiredGravity = 0.0; VelDesired = DBT_VelocityBrake; if( vel <= DBT_VelocityRelease ) { DynamicBrakeTest = 4; @@ -7800,6 +8025,14 @@ TController::adjust_desired_speed_for_braking_test() { break; } } + // a stop or a restriction coming up while the test runs takes precedence + if( ( DynamicBrakeTest >= 1 ) + && ( DynamicBrakeTest <= 3 ) + && ( routeaccdesired < AccDesired ) ) { + AccDesired = routeaccdesired; + fAccDesiredGravity = routeaccdesiredgravity; + VelDesired = std::min( VelDesired, routeveldesired ); + } } void @@ -7888,7 +8121,9 @@ void TController::control_tractive_force() { (ActualProximityDist > (is_car() ? fMinProximityDist : // cars are allowed to move within min proximity distance fMaxProximityDist) ? // other vehicle types keep wider margin true : - velocity + 1.0 < VelNext) ) { + velocity + 1.0 < VelNext + // uphill, a train told to hold its speed on the way to a stop needs the power to do it + || VelNext == 0.0 && (OrderCurrentGet() & Obey_train) != 0 && AccDesired >= 0.0 && fAccGravity < -0.025 && ActualProximityDist > fMinProximityDist) ) { // ...jeśli prędkość w kierunku czoła jest mniejsza od dozwolonej o margines... // within proximity margin don't exceed next scheduled speed, outside of the margin accelerate as you like // ...to można przyspieszyć @@ -7903,9 +8138,7 @@ void TController::control_tractive_force() { cue_action( driver_hint::mastercontrollersetzerospeed ); } else if( velocity > VelDesired + SpeedCtrlMargin - || ( fAccGravity < -0.01 ? - AccDesired < 0.0 : - AbsAccS > AccDesired + 10.05 ) + || ( fAccGravity < -0.01 ? is_too_fast_uphill() : AbsAccS > AccDesired + 10.05 ) || IsAnyCouplerStretched ) { // jak za bardzo przyspiesza albo prędkość przekroczona // dodany wyjatek na "pelna w przod" @@ -7917,6 +8150,13 @@ void TController::control_tractive_force() { SpeedSet(); // ciągla regulacja prędkości } +bool TController::is_too_fast_uphill() const { + + if( AccDesired < 0.0 ) { return true; } + // holding the speed uphill on the way to a stop asks for no speeding up + return ( VelNext == 0.0 && ActualProximityDist <= fMaxProximityDist && AbsAccS > AccDesired + 0.05 ); +} + void TController::increase_tractive_force() { if( fActionTime < 0.0 ) { return; } // gdy jest nakaz poczekać z jazdą, to nie ruszać @@ -7939,6 +8179,20 @@ void TController::increase_tractive_force() { cue_action( driver_hint::tractiveforceincrease ); } +// how far the deceleration may fall short of the demand before the brake is applied harder. approaching a lower +// speed, a pneumatic brake given a full step only once that much is missing acts too late: the demand +// creeps up meanwhile, and in a long G train the next step takes many seconds to act, so half a step is enough +double +TController::brake_increase_margin() const { + + if( ( BrakeSystem == TBrakeSystem::Pneumatic ) + && ( VelNext > 0.0 ) + && ( VelNext < DirectionalVel() ) ) { + return 0.5 * fBrake_a1[ 0 ]; + } + return fBrake_a1[ 0 ]; +} + void TController::control_braking_force() { // don't touch the brakes when compressing buffers during coupling/uncoupling @@ -7952,6 +8206,23 @@ void TController::control_braking_force() { cue_action(driver_hint::brakingforcesetzero); } + // AbsAccS is the resultant acceleration, gravity included, so it is compared with the + // resultant the AI asks for, not with the brake-only demand AccDesired holds on a descent + auto const accdesiredresultant { AccDesired + fAccDesiredGravity }; + + // every few seconds, note how far the train falls short of the demand and whether that is getting worse: a brake + // still filling, or blocks gripping harder as the train slows down, make up a shortfall on their own, but a + // shortfall that keeps growing at a constant handle means the train is falling further behind its braking curve + if( ElapsedTime - fShortfallTime >= 3.0 ) { + auto const shortfall { AbsAccS - accdesiredresultant }; + iShortfallGrowth = ( + ( ( AccDesired < 0.0 ) && ( shortfall > fShortfall + 0.005 ) ) ? + iShortfallGrowth + 1 : + 0 ); + fShortfall = shortfall; + fShortfallTime = ElapsedTime; + } + if( is_emu() && !ForcePNBrake ) { // właściwie, to warunek powinien być na działający EP // Ra: to dobrze hamuje EP w EZT @@ -7962,14 +8233,15 @@ void TController::control_braking_force() { auto const AccMax{ std::min(fBrake_a0[0] + 12 * fBrake_a1[0], mvOccupied->MED_amax) }; auto const accmargin = AccMax > 1.1 * AccDesired && fAccGravity < 0.025 ? 0.05 : 0.0; - if( AccDesired < accthreshold // jeśli hamować - u góry ustawia się hamowanie na fAccThreshold - && ( AbsAccS > AccDesired + accmargin - || BrakeCtrlPosition < 0 ) ) { + if( ( AccDesired < accthreshold // jeśli hamować - u góry ustawia się hamowanie na fAccThreshold + && ( AbsAccS > accdesiredresultant + accmargin + || BrakeCtrlPosition < 0 ) ) + || ( fAccGravity < -0.05 && velocity < -0.1 && AccDesired < 0.0 ) ) { // also brake if uphill and slipping back // hamować bardziej, gdy aktualne opóźnienie hamowania mniejsze niż (AccDesired) cue_action( driver_hint::brakingforceincrease ); } else if( OrderCurrentGet() != Disconnect ) { // przy odłączaniu nie zwalniamy tu hamulca - if( AbsAccS < AccDesired - 0.05 ) { + if( AbsAccS < accdesiredresultant - 0.05 ) { // jeśli opóźnienie większe od wymaganego (z histerezą) luzowanie, gdy za dużo // TBD: check if the condition isn't redundant with the DecBrake() code if( BrakeCtrlPosition >= 0 && VelDesired > 0.0 ) { @@ -7984,9 +8256,12 @@ void TController::control_braking_force() { } // type & dt_ezt else { // a stara wersja w miarę dobrze działa na składy wagonowe - if( (AccDesired < fAccGravity - 0.1 && AbsAccS > AccDesired + fBrake_a1[0] // regular braking - || fAccGravity < -0.05 && velocity < -0.1) && - (fBrakeTime < 0.0 || AccDesired < fAccGravity - 0.5 || BrakeCtrlPosition <= 0) ) { // also brake if uphill and slipping back + if( (accdesiredresultant < fAccGravity - 0.1 && AbsAccS > accdesiredresultant + brake_increase_margin() // regular braking + || fAccGravity < -0.05 && velocity < -0.1) && // also brake if uphill and slipping back + (fBrakeTime < 0.0 + // the size of the brake demand decides whether to skip the reaction time, so the brake-only part + // (on a descent a heavy train needs its brake built up at once, the gravity does not help it slow) + || AccDesired < fAccGravity - 0.5 || BrakeCtrlPosition <= 0) ) { // u góry ustawia się hamowanie na fAccThreshold // jeśli upłynął czas reakcji hamulca, chyba że nagłe albo luzował // TODO: check whether brake delay variable still has any purpose @@ -7994,7 +8269,21 @@ void TController::control_braking_force() { // Ra: ten czas należy zmniejszyć, jeśli czas dojazdu do zatrzymania jest mniejszy (3.0 + 0.5 * ((mvOccupied->BrakeDelayFlag > bdelay_G ? mvOccupied->BrakeDelay[1] : mvOccupied->BrakeDelay[3]) - 3.0)) * 0.5); // Ra: tymczasowo, bo przeżyna S1 } - if ( AccDesired < fAccGravity - 0.05 && AccDesired - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 && OrderCurrentGet() != Disconnect && VelDesired > 0.0 ) { + else if( ( BrakeSystem == TBrakeSystem::Pneumatic ) + && ( AccDesired < 0.0 ) + && ( BrakeCtrlPosition > 0.0 ) + && ( fShortfall > 0.02 ) + && ( iShortfallGrowth >= 2 ) + && ( fBrakeHeldTime > brake_response_horizon( mvOccupied, BrakeSystem, 1.0, fBrake_a0[ 0 ] ) ) ) { + // the train has been falling further behind the demand for a while at a handle left alone for longer than + // the brake takes to respond: a small step now, while it still is small, rather than the deep one the + // margin above would allow only once the train is far behind + cue_action( driver_hint::brakingforceincrease ); + iShortfallGrowth = 0; + } + if ( (accdesiredresultant < fAccGravity - 0.05 + || AccDesired >= EU07_AI_NOACCELERATION) // no braking asked for, yet the train is slowing down more than that + && accdesiredresultant - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 && OrderCurrentGet() != Disconnect && VelDesired > 0.0 ) { // jak hamuje, to nie tykaj kranu za często // yB: luzuje hamulec dopiero przy różnicy opóźnień rzędu 0.2 // przy odłączaniu nie zwalniamy tu hamulca @@ -8070,8 +8359,11 @@ void TController::control_releaser() { } } // wyluzuj lokomotywę, to szybciej ruszymy + // this rule is for starting off, so only below 1 km/h: at speed, just after an application, it would vent + // the control reservoir of a locomotive that may have to brake again before the pipe has refilled it if( mvOccupied->BrakePress > 0.4 - && mvOccupied->Hamulec->GetCRP() > 4.9 ) { + && mvOccupied->Hamulec->GetCRP() > 4.9 + && mvOccupied->Vel < 1.0 ) { actuate = true; } // keep engine brakes released during coupling/uncoupling diff --git a/vehicle/Driver.h b/vehicle/Driver.h index f88e115ac..f1975fca6 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -311,6 +311,8 @@ private: double DirectionalVel() const { return mvOccupied->Vel * sign( iDirection * mvOccupied->V ); } + double brake_increase_margin() const; + void update_timers( double const dt ); void update_logs( double const dt ); void determine_consist_state(); @@ -358,7 +360,14 @@ private: void control_motor_connectors(); void control_tractive_force(); void increase_tractive_force(); + bool is_too_fast_uphill() const; void control_braking_force(); + // how far short of a stop the AI aims: in train running the middle of the 8-15 m window before a signal, + // otherwise (a W4 stop point, shunting, coupling) half the minimum proximity distance, as before + double stopping_margin() const { + return ( ( OrderCurrentGet() & Obey_train ) != 0 ) && !IsAtPassengerStop ? + fMinProximityDist + 3.5 : + 0.5 * fMinProximityDist; } void apply_independent_brake_only(); void check_route_ahead( double const Range ); void check_route_behind( double const Range ); @@ -393,6 +402,11 @@ private: double AccDesired = 0.0; // przyspieszenie, jakie ma utrzymywać (<0:nie przyspieszaj,<-0.1:hamuj) double VelDesired = 0.0; // predkość, z jaką ma jechać, wynikająca z analizy tableki; <=VelSignal double fAccDesiredAv = 0.0; // uśrednione przyspieszenie z kolejnych przebłysków świadomości, żeby ograniczyć migotanie + double fBrakeHeldTime = 0.0; // czas od ostatniego przestawienia kranu hamulca zasadniczego + double fShortfall = 0.0; // how far the train fell short of the braking demand at the last check [m/s2] + double fShortfallTime = 0.0; // when that was checked + int iShortfallGrowth = 0; // how many checks in a row the shortfall has grown + double fAccDesiredGravity = 0.0; // część AccDesired odjęta na spadku jako kompensacja pochylenia; AbsAccS zawiera ją już sam double VelforDriver = -1.0; // prędkość, używana przy zmianie kierunku (ograniczenie przy nieznajmości szlaku?) double VelSignal = 0.0; // ograniczenie prędkości z kompilacji znaków i sygnałów // normalnie na początku ma stać, no chyba że jedzie double VelLimit = -1.0; // predkość zadawana przez event jednokierunkowego ograniczenia prędkości // -1: brak ograniczenia prędkości diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index 893251f7b..2dd2103de 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -3709,13 +3709,14 @@ void TTrain::OnCommand_linebreakeropen(TTrain *Train, command_data const &Comman { Train->ggMainButton.UpdateValue(0.0, Train->dsbSwitch); } - else if (Train->ggMainOnButton.SubModel != nullptr) + else if (Train->ggMainOnButton.SubModel != nullptr && false == Train->mvControlled->MainCircuitOpensWithStarterOnly()) { // NOTE: legacy behaviour, for vehicles equipped only with impulse close switch // it doesn't make any real sense to animate this one, but some people can't get over how there's no visual reaction to their keypress Train->ggMainOnButton.UpdateValue(1.0, Train->dsbSwitch); return; } + // a diesel with only the starter button gets its engine stopped. the button stays put: held down, it would start the engine again // play sound immediately when the switch is hit, not after release Train->fMainRelayTimer = 0.0f; @@ -5241,15 +5242,37 @@ void TTrain::OnCommand_headlighttoggleleft(TTrain *Train, command_data const &Co if (Command.action == GLFW_PRESS) { // only reacting to press, so the switch doesn't flip back and forth if key is held down - if ((Train->mvOccupied->iLights[vehicleend] & light::headlight_left) == 0) + + if (Train->ggLeftEndLightButton.SubModel != nullptr || Train->ggLeftLightButton.SubModel == nullptr) { - // turn on - OnCommand_headlightenableleft(Train, Command); - } - else - { - // turn off - OnCommand_headlightdisableleft(Train, Command); + if ((Train->mvOccupied->iLights[vehicleend] & light::headlight_left) == 0) + { + // turn on + OnCommand_headlightenableleft(Train, Command); + } + else + { + // turn off + OnCommand_headlightdisableleft(Train, Command); + } + } else { + // if the light is controlled by 3-way switch, handle both headlight and redmarker + if ((Train->mvOccupied->iLights[vehicleend] & light::headlight_left) == 0 && + (Train->mvOccupied->iLights[vehicleend] & light::redmarker_left) == 0) + { + // turn on + OnCommand_headlightenableleft(Train, Command); + } + else if (Train->mvOccupied->iLights[vehicleend] & light::headlight_left) + { + // switch to red + OnCommand_redmarkerenableleft(Train, Command); + } + else + { + // disable + OnCommand_redmarkerdisableleft(Train, Command); + } } } } @@ -5324,15 +5347,37 @@ void TTrain::OnCommand_headlighttoggleright(TTrain *Train, command_data const &C if (Command.action == GLFW_PRESS) { // only reacting to press, so the switch doesn't flip back and forth if key is held down - if ((Train->mvOccupied->iLights[vehicleend] & light::headlight_right) == 0) + + if (Train->ggRightEndLightButton.SubModel != nullptr || Train->ggRightLightButton.SubModel == nullptr) { - // turn on - OnCommand_headlightenableright(Train, Command); - } - else - { - // turn off - OnCommand_headlightdisableright(Train, Command); + if ((Train->mvOccupied->iLights[vehicleend] & light::headlight_right) == 0) + { + // turn on + OnCommand_headlightenableright(Train, Command); + } + else + { + // turn off + OnCommand_headlightdisableright(Train, Command); + } + } else { + // if the light is controlled by 3-way switch, handle both headlight and redmarker + if ((Train->mvOccupied->iLights[vehicleend] & light::headlight_right) == 0 && + (Train->mvOccupied->iLights[vehicleend] & light::redmarker_right) == 0) + { + // turn on + OnCommand_headlightenableright(Train, Command); + } + else if (Train->mvOccupied->iLights[vehicleend] & light::headlight_right) + { + // switch to red + OnCommand_redmarkerenableright(Train, Command); + } + else + { + // disable + OnCommand_redmarkerdisableright(Train, Command); + } } } } diff --git a/world/Event.cpp b/world/Event.cpp index fa752078b..624cdd714 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1025,13 +1025,42 @@ whois_event::run_() { auto *targetcell { static_cast( std::get( target ) ) }; if( targetcell == nullptr ) { continue; } // event effect code + // +48: station after the next one, unused, stop there or passthrough // +40: next station name, unused, stop at next station (duplicate of +0 2nd numeric value) // +32: vehicle name // +24: vehicle type, consist brake level, obstacle distance // +16: load type, load amount, max load amount // +8: destination, direction, engine power // +0: train name, station count, stop on next station - if( m_input.flags & flags::whois_name ) { + if( ( m_input.flags & ( flags::whois_name | flags::whois_load | flags::mode_alt ) ) + == ( flags::whois_name | flags::whois_load ) ) { + // +48 (the whois_name | whois_load combination was unused; +56, same with mode_alt + // set, stays free for a future mode) + // the timetable entry one past the next stop, and whether the train stops there + auto const *owner { ( + m_activator->Mechanik != nullptr && m_activator->Mechanik->primary() ? + m_activator->Mechanik : + m_activator->ctOwner ) }; + auto const *entry { ( + owner != nullptr ? + &owner->TrainTimetable().TimeTableEntryAhead( 1 ) : + nullptr ) }; + auto const aheadname { ( entry != nullptr ? entry->StationName : "none" ) }; + auto const aheadstop { ( entry != nullptr && entry->Ah >= 0 ? 1 : 0 ) }; + + targetcell->UpdateValues( + aheadname, // station after the next one + 0, // unused + aheadstop, // stop there or passthrough + m_input.flags & ( flags::text | flags::value1 | flags::value2 ) ); + + WriteLog( + "Type: WhoIs (" + std::to_string( m_input.flags ) + ") - " + + "[station after next: " + aheadname + "], " + + "[X], " + + "[stop there: " + ( aheadstop != 0 ? "yes" : "no" ) + "]" ); + } + else if( m_input.flags & flags::whois_name ) { // +32 or +40 // next station name if( m_input.flags & flags::mode_alt ) { diff --git a/world/mtable.cpp b/world/mtable.cpp index b69cacfb9..123ae00f6 100644 --- a/world/mtable.cpp +++ b/world/mtable.cpp @@ -77,6 +77,15 @@ bool TTrainParameters::IsLastStop() const { return StationIndex >= StationCount; } +TMTableLine const &TTrainParameters::TimeTableEntryAhead( int Offset ) const +{ // wpis rozkladu Offset pozycji za aktualnym (bez pomijania przelotow); + // poza zakresem 1..StationCount zwraca techniczny wpis zerowy + int const idx { StationIndex + Offset }; + if( ( idx < 1 ) || ( idx > StationCount ) ) + return TimeTable[0]; + return TimeTable[idx]; +} + bool TTrainParameters::IsMaintenance() const { if( StationIndex <= StationCount ) diff --git a/world/mtable.h b/world/mtable.h index 44cda7311..841fea815 100644 --- a/world/mtable.h +++ b/world/mtable.h @@ -70,6 +70,9 @@ class TTrainParameters std::string ShowRelation() const; double WatchMTable(double DistCounter) const; std::string NextStop() const; + // wpis rozkladu Offset pozycji za StationIndex (Offset>=1); poza zakresem 1..StationCount + // zwraca techniczny wpis zerowy (TimeTable[0], StationName=="nowhere", Ah==-1) + TMTableLine const &TimeTableEntryAhead( int Offset ) const; sound_source next_stop_sound() const; sound_source last_stop_sound() const; bool IsStop() const;