From db220a1a98f6c388aea49cc6016b7444a0612b43 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sat, 17 Oct 2026 14:41:59 +0200 Subject: [PATCH] fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds fix(cab): the line contactors lamp goes out with the pressure switch (eu07/maszyna#470) The lamp of the open line contactors, which tells the start can be made, went out at 1 bar in the brake cylinders, while the pressure switch cuts the start off only at 2 bar (or below 3.6 bar in the main pipe), so with the brakes partly applied the start could be made with the lamp out. With a pressure switch fitted the lamp now follows it; without one it goes out at 1 bar as before. The same holds for the lamp of the second unit, which used to keep its last state between 1 bar and the cut-off. The condition is TMoverParameters::LineContactorsLampLit(). Tests: tests/unit/test_line_contactors.cpp. refactor: Sonar findings of PR 217 - horn_supply::pneumatic shadowed the pneumatic enumerator of the sound flags (cpp:S1117), it's horn_supply::air; the supplies read with using enum (S6177), - CouplerForce(): the automatic coupling refused or caught without another level of nested ifs (S134), the same outcome: too fast, the coupling is locked; caught, coupled and locked; otherwise left to try again, - CompressorCheck(): the power the compressor doesn't draw yet noted without a TODO (S1135), - tests: the previous crash damage setting in an in-class initializer (S3230), the loops which stop early with break rather than in their conditions (S886), the stand-ins of test::pair in a std::vector rather than an array (S5945), units in the comments of the example without /* */ (S1103). test: the cases the changes of the branch left without a test - Cntrl.: BDelay1 to BDelay4 reach the brake delays (now through std::format), the default start types of the ground and converter overload relays of a multiple unit and of other vehicles (out of the nested conditionals), - the compressor: fed from the low voltage it waits from the low voltage coming on; the pressure switch stops it above the cut-out pressure, without one the excess air is vented (the vented fraction rewritten), - the horns: a vehicle without any air sounds as before, the threshold pressure, an electric horn on 110 V, - the positions of a cab control below zero, and a control without position sounds. refactor(cab): the kind of the train heating switch as a flag Read once with the vehicle definition, rather than compared as text in each of the switch commands. feat(cab): train heating switch springing back (MaSzyna-EU07/maszyna#85) TrainHeating=[toggle]|impulse in the Switches: section of the vehicle definition makes the train heating switch an unstable one: it goes up to switch the heating on, down to switch it off, and back to the middle once let go, instead of staying where it was put. The position of such a switch isn't set from the heating state when the cab is entered. Tests: tests/unit/test_switches.cpp. feat(sound): mirror sounds (MaSzyna-EU07/maszyna#84, eu07/maszyna#322) The sound definitions of a vehicle (.mmd) take mirroropen: and mirrorclose:, played as a mirror sets off unfolding or folding, and the locations section mirrors: ... end, the offsets along the vehicle of the mirror pairs, as for the doors (negative ahead of the centre), each pair at the sides of the vehicle. As the animation, only the mirrors at the occupied cab sound. The start of a move is mirror_moves::motion in vehicle/mirrors.h. Tests: tests/unit/test_mirrors.cpp. feat(cab): joint oil and fuel pump switch (MaSzyna-EU07/maszyna#81) A cab control oilfuelpump_sw: switches both the oil and the fuel pump at once, with the new command oilfuelpumptoggle (no key by default). As a two-state switch it turns both pumps off when both are on and both on otherwise; as an impulse switch it's held to start them, like the separate pump switches. Its position follows the pumps when the cab is entered. refactor(editor): text fields filled without strncpy (MaSzyna-EU07/maszyna#112) The last three strncpy calls Sonar flags (cpp:S5816), in the road and track editors, copy the text into the ImGui field buffers with std::string::copy and terminate it right after; the text gets cut to the buffer as before. fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds - automatic couplers don't catch above the safe collision speed (#86): with crash damage on they couple at a closing speed of 15 km/h at most, a faster hit locks the coupling until the vehicles part and the collision runs its course. On the way: CollisionVelocity() read a head-on 10 + 10 km/h as 14.1 km/h (quaternions turn by half the angle), it's now taken from the touching ends; CollisionDetect() took the other vehicle's speed the wrong way round for vehicles facing each other. Head-on hits now derail sooner, at the intended limit, - a single horn lever can't sound both tones at once (eu07/maszyna#392), - a skid ends once the vehicle stops on locked wheels (eu07/maszyna#479), - electronic destination signs are blanked at the end of the timetable (eu07/maszyna#446), - horns need the air or the voltage to sound (eu07/maszyna#290, #76): HornType=[Pneumatic]|Electric and Horn3Type in Cntrl.; a pneumatic horn needs 0.2 MPa in the main reservoir (or the main pipe without one), an electric one the low voltage; the lever stays where it was put, - compressor start delay (#67): CompressorStartDelay [s] in Brake., counted from the compressor getting its power, after the engine starts say; none by default, - the channel and volume buttons of a radio without power do nothing (eu07/maszyna#429); the power check of the radio is TMoverParameters::RadioPowered(), - a lever moved smoothly sounds the positions it passes (#83). Sonar findings in the functions changed: nesting, nested conditionals, commented-out code, std::format, references to const. Unit tests: test::pair for two vehicles running into each other, test_couplers, test_horn, test_wheelslip, test_compressor, test_radio, test_gauge_positions. --- CMakeLists.txt | 1 + McZapkie/MOVER.h | 35 +++ McZapkie/Mover.cpp | 187 +++++++----- application/editormoderoad.cpp | 4 +- application/editormodetrack.cpp | 7 +- input/command.cpp | 2 + input/command.h | 1 + input/drivermouseinput.cpp | 3 + tests/unit/README.md | 6 +- tests/unit/test_cntrl.cpp | 39 +++ tests/unit/test_compressor.cpp | 115 ++++++++ tests/unit/test_couplers.cpp | 426 ++++++++++++++++++++++++++++ tests/unit/test_gauge_positions.cpp | 58 ++++ tests/unit/test_horn.cpp | 121 ++++++++ tests/unit/test_line_contactors.cpp | 48 ++++ tests/unit/test_mirrors.cpp | 32 +++ tests/unit/test_radio.cpp | 28 ++ tests/unit/test_switches.cpp | 16 ++ tests/unit/test_wheelslip.cpp | 62 ++++ tests/unit/vehicle.cpp | 104 ++++++- tests/unit/vehicle.h | 51 ++++ utilities/translation.cpp | 1 + vehicle/Driver.cpp | 27 +- vehicle/DynObj.cpp | 91 +++++- vehicle/DynObj.h | 16 +- vehicle/Gauge.cpp | 42 ++- vehicle/Train.cpp | 181 +++++++----- vehicle/Train.h | 7 +- vehicle/gaugepositions.h | 35 +++ vehicle/horn.cpp | 24 ++ vehicle/horn.h | 24 ++ vehicle/mirrors.h | 44 +++ vr/openvr_imp.cpp | 3 + 33 files changed, 1644 insertions(+), 197 deletions(-) create mode 100644 tests/unit/test_cntrl.cpp create mode 100644 tests/unit/test_compressor.cpp create mode 100644 tests/unit/test_couplers.cpp create mode 100644 tests/unit/test_gauge_positions.cpp create mode 100644 tests/unit/test_horn.cpp create mode 100644 tests/unit/test_line_contactors.cpp create mode 100644 tests/unit/test_mirrors.cpp create mode 100644 tests/unit/test_radio.cpp create mode 100644 tests/unit/test_switches.cpp create mode 100644 tests/unit/test_wheelslip.cpp create mode 100644 vehicle/gaugepositions.h create mode 100644 vehicle/horn.cpp create mode 100644 vehicle/horn.h create mode 100644 vehicle/mirrors.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 477f584e..dfe58b66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,7 @@ set(CORE_SOURCES "utilities/utilities.cpp" "scripting/ladderlogic.cpp" "vehicle/handover.cpp" +"vehicle/horn.cpp" ) add_library(eu07_core STATIC ${CORE_SOURCES}) target_link_libraries(eu07_core PUBLIC eu07_settings) diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index 29b3e75c..b0c5d8e9 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -676,6 +676,13 @@ struct TTractionParam }; /*powyzsze parametry zwiazane sa z torem po ktorym aktualnie pojazd jedzie*/ +// what a horn needs to sound: air from the main reservoir, or the low voltage +enum class horn_supply +{ + air, + electric +}; + /// High-level brake-system family. enum class TBrakeSystem { @@ -1710,6 +1717,10 @@ class TMoverParameters float DistanceCounterDoublePressPeriod = {1.f}; // czas w jakim nalezy podwojnie wcisnac przycisk, aby rozpoczac pomiar odleglosci TSecuritySystem SecuritySystem; int EmergencyBrakeWarningSignal{0}; // combined with basic WarningSignal when manual emergency brake is active + horn_supply HornSupply{horn_supply::air}; // of the low and high tone horns (WarningSignal 1 and 2) + horn_supply Horn3Supply{horn_supply::air}; // of the third horn (WarningSignal 4) + // main reservoir pressure [MPa] below which a pneumatic horn stays silent + static constexpr double HornMinPressure{0.2}; TUniversalCtrlTable UniCtrlList; /*lista pozycji uniwersalnego nastawnika*/ int UniCtrlListSize = 0; /*wielkosc listy pozycji uniwersalnego nastawnika*/ bool UniCtrlIntegratedBrakePNCtrl = false; /*zintegrowany nastawnik JH obsluguje hamulec PN*/ @@ -2001,6 +2012,8 @@ class TMoverParameters bool CompressorAllow = false; /*! zezwolenie na uruchomienie sprezarki NBMX*/ bool CompressorAllowLocal{true}; // local device state override (most units don't have this fitted so it's set to true not to intefere) bool CompressorGovernorLock{false}; // indicates whether compressor pressure switch was activated due to reaching cut-out pressure + double CompressorStartDelay{0.0}; // time [s] the compressor waits after getting its power before it can start, after the engine starts say + double CompressorPowerTime{0.0}; // time [s] since the compressor got its power bool CompressorTankValve{false}; // indicates excessive pressure is vented from compressor tank directly and instantly start_t CompressorStart{start_t::manual}; // whether the compressor is started manually, or another way start_t PantographCompressorStart{start_t::manual}; @@ -2228,6 +2241,7 @@ class TMoverParameters // TODO: move these switch types where they belong, cabin definition std::string PantSwitchType; std::string ConvSwitchType; + bool HeatingSwitchImpulse{false}; // the train heating switch springs back to its middle position std::string StLinSwitchType; bool Heating = false; // ogrzewanie 'Winger 020304 @@ -2428,6 +2442,27 @@ class TMoverParameters double BrakeForce(const TTrackParam &Track); double CouplerForce(int const End, double dt); void CollisionDetect(int const End, double const dt); + // closing speed of a collision [km/h] above which the vehicles involved may derail, adjusted for their mass ratio + static constexpr double SafeCollisionVelocity{15.0}; + // closing speed [km/h] of the End of this vehicle and the OtherEnd of the other vehicle it touches, along the coupler + // axis: the sum of the speeds in a head-on collision, the difference in a rear-end one, negative when they part + double CollisionVelocity(int const End, TMoverParameters const &Other, int const OtherEnd) const; + // whether the automatic couplers at the End of this vehicle and the OtherEnd of the other one can catch each other on + // contact, rather than the vehicles colliding (with crash damage enabled, only at a closing speed which doesn't derail them) + bool AutomaticCouplingSafe(int const End, TMoverParameters const &Other, int const OtherEnd) const; + // the horns of the Signal (WarningSignal bits) which have the air or the voltage they need to sound + int SoundingHorns(int const Signal) const; + // whether the lamp of the open line contactors is lit, telling the start can be made: the pressure switch, when + // fitted, cuts the start off and so puts out the lamp, otherwise the lamp goes out once the brakes are applied + bool LineContactorsLampLit() const + { + return false == StLinFlag && false == ControlPressureSwitch && (HasControlPressureSwitch || BrakePress < 1.0); + } + // whether the radio is switched on and has the low voltage to work + bool RadioPowered() const + { + return Radio && (Power24vIsAvailable || Power110vIsAvailable); + } /*obrot kol uwzgledniajacy poslizg*/ double ComputeRotatingWheel(double WForce, double dt, double n) const; diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 3b5b6215..02762301 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -16,6 +16,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/parser.h" #include "simulation/simulation.h" + +#include //--------------------------------------------------------------------------- // Ra: tu należy przenosić funcje z mover.pas, które nie są z niego wywoływane. @@ -1176,6 +1178,51 @@ double TMoverParameters::EngineMaxRPM() const std::numeric_limits::max(); // shouldn't ever get here but, eh } +// ************************************************************************************************* +// prędkość zbliżania się stykających się końców pojazdów przy zderzeniu, w km/h +// ************************************************************************************************* +double TMoverParameters::CollisionVelocity(int const End, TMoverParameters const &Other, int const OtherEnd) const +{ + // V runs along the vehicle's own axis, towards its front; the other vehicle's velocity is turned around when the + // vehicles face each other (touch with the same ends), as in CouplerForce() + auto const velocitydifference{V - Other.V * DirPatch(End, OtherEnd)}; + // the front end closes in on the other vehicle when this one moves forward faster, the rear end when it's slower + return (End == end::front ? velocitydifference : -velocitydifference) * 3.6; // m/s -> km/h +} + +bool TMoverParameters::AutomaticCouplingSafe(int const End, TMoverParameters const &Other, int const OtherEnd) const +{ + return false == Global.crash_damage || CollisionVelocity(End, Other, OtherEnd) <= SafeCollisionVelocity; +} + +int TMoverParameters::SoundingHorns(int const Signal) const +{ + auto const supplied{[this](horn_supply const Supply) { + if (Supply == horn_supply::electric) + { + return Power24vIsAvailable || Power110vIsAvailable; + } + if (VeselVolume > 0.0) + { + return Compressor >= HornMinPressure; + } + // a vehicle without a main reservoir of its own takes the air from the main pipe, a vehicle without either + // (a road vehicle, say) has no air to check + auto const mainpipe{((Couplers[end::front].AllowedFlag | Couplers[end::rear].AllowedFlag) & coupling::mainhose) != 0}; + return false == mainpipe || ScndPipePress >= HornMinPressure; + }}; + auto horns{0}; + if (supplied(HornSupply)) + { + horns |= Signal & (1 | 2); + } + if (supplied(Horn3Supply)) + { + horns |= Signal & 4; + } + return horns; +} + // ************************************************************************************************* // Q: 20160716 // Wykrywanie kolizji @@ -1192,8 +1239,11 @@ void TMoverParameters::CollisionDetect(int const End, double const dt) auto const otherend{Neighbours[End].vehicle_end}; auto const &othercoupler{othervehicle->Couplers[otherend]}; + // the velocities along this vehicle's axis: the other vehicle's one is turned around when the vehicles face each + // other (touch with the same ends), as in CouplerForce() + auto const otherdirection{DirPatch(End, otherend)}; auto velocity{V}; - auto othervehiclevelocity{othervehicle->V}; + auto othervehiclevelocity{othervehicle->V * otherdirection}; // calculate collision force and new velocities for involved vehicles auto const VirtualCoupling{(thiscoupler.CouplingFlag == coupling::faux)}; auto CCF{0.0}; @@ -1224,28 +1274,25 @@ void TMoverParameters::CollisionDetect(int const End, double const dt) damage_coupler(End); } - if (thiscoupler.CouplingFlag == coupling::faux || true == TestFlag(othervehicle->DamageFlag, dtrain_out)) - { // HACK: limit excessive speed derailment checks to vehicles which aren't part of the same consist - auto const safevelocitylimit{15.0}; - auto const velocitydifference{glm::length(glm::angleAxis(Rot.Rz, glm::dvec3{0, 1, 0}) * V - glm::angleAxis(othervehicle->Rot.Rz, glm::dvec3{0, 1, 0}) * othervehicle->V) * - 3.6}; // m/s -> km/h + // HACK: limit excessive speed derailment checks to vehicles which aren't part of the same consist + auto const outsideconsist{thiscoupler.CouplingFlag == coupling::faux || true == TestFlag(othervehicle->DamageFlag, dtrain_out)}; + auto const velocitydifference{outsideconsist ? CollisionVelocity(End, *othervehicle, otherend) : 0.0}; - if (velocitydifference > safevelocitylimit) + if (velocitydifference > SafeCollisionVelocity) + { + // HACK: crude estimation for potential derail, will take place with velocity difference > 15 km/h adjusted for vehicle mass ratio + if (false == TestFlag(DamageFlag, dtrain_out) || false == TestFlag(othervehicle->DamageFlag, dtrain_out)) { - // HACK: crude estimation for potential derail, will take place with velocity difference > 15 km/h adjusted for vehicle mass ratio - if (false == TestFlag(DamageFlag, dtrain_out) || false == TestFlag(othervehicle->DamageFlag, dtrain_out)) - { - WriteLog("Bad driving: " + Name + " and " + othervehicle->Name + " collided with velocity " + to_string(velocitydifference, 0) + " km/h"); - } + WriteLog("Bad driving: " + Name + " and " + othervehicle->Name + " collided with velocity " + to_string(velocitydifference, 0) + " km/h"); + } - if (velocitydifference > safevelocitylimit * (TotalMass / othervehicle->TotalMass)) - { - Derail(COLLISION); - } - if (velocitydifference > safevelocitylimit * (othervehicle->TotalMass / TotalMass)) - { - othervehicle->Derail(COLLISION); - } + if (velocitydifference > SafeCollisionVelocity * (TotalMass / othervehicle->TotalMass)) + { + Derail(COLLISION); + } + if (velocitydifference > SafeCollisionVelocity * (othervehicle->TotalMass / TotalMass)) + { + othervehicle->Derail(COLLISION); } } } @@ -1254,21 +1301,15 @@ void TMoverParameters::CollisionDetect(int const End, double const dt) if (false == TestFlag(DamageFlag, dtrain_out)) { auto const accelerationchange{(velocity - V) / dt}; - // if( accelerationchange / AccS < 1.0 ) { - // HACK: prevent excessive vehicle pinball cases AccS += accelerationchange; - // AccS = std::clamp( AccS, -2.0, 2.0 ); V = velocity; - // } } if (false == TestFlag(othervehicle->DamageFlag, dtrain_out)) { - auto const othervehicleaccelerationchange{(othervehiclevelocity - othervehicle->V) / dt}; - // if( othervehicleaccelerationchange / othervehicle->AccS < 1.0 ) { - // HACK: prevent excessive vehicle pinball cases + auto const othervehiclenewvelocity{othervehiclevelocity * otherdirection}; // back along the other vehicle's axis + auto const othervehicleaccelerationchange{(othervehiclenewvelocity - othervehicle->V) / dt}; othervehicle->AccS += othervehicleaccelerationchange; - othervehicle->V = othervehiclevelocity; - // } + othervehicle->V = othervehiclenewvelocity; } } @@ -4332,7 +4373,10 @@ void TMoverParameters::CompressorCheck(double dt) } } - auto const compressorpower{(CompressorPower == 0 ? Mains : CompressorPower == 3 ? Mains : Power110vIsAvailable)}; + // the main circuit, also for a compressor driven by the engine, otherwise the low voltage + auto const compressorpower{CompressorPower == 0 || CompressorPower == 3 ? Mains : Power110vIsAvailable}; + CompressorPowerTime = compressorpower ? CompressorPowerTime + dt : 0.0; + auto const compressorready{compressorpower && CompressorPowerTime >= CompressorStartDelay}; // TBD: split CompressorAllow into separate enable/disable flags, inherit compressor from basic_device auto const compressorenable{CompressorAllowLocal && (CompressorStart == start_t::automatic || CompressorAllow)}; auto const compressordisable{false == compressorenable}; @@ -4354,7 +4398,7 @@ void TMoverParameters::CompressorCheck(double dt) auto const compressorflag{CompressorFlag}; CompressorFlag = - compressorpower && false == compressordisable && (false == governorlock || CompressorPower == 3) && (CompressorFlag || (compressorenable && LastSwitchingTime > CtrlDelay)); + compressorready && false == compressordisable && (false == governorlock || CompressorPower == 3) && (CompressorFlag || (compressorenable && LastSwitchingTime > CtrlDelay)); if (CompressorFlag && CompressorFlag != compressorflag) { @@ -4384,30 +4428,23 @@ void TMoverParameters::CompressorCheck(double dt) { // vent some air out if there's no governor lock to stop the compressor from exceeding acceptable pressure level SetFlag(SoundFlag, sound::relay | sound::loud); - CompressedVolume *= false == governorlockispresent ? 0.80 : // arbitrary amount - CompressorTankValve ? MinCompressorF / MaxCompressorF : // drop to mincompressor level - 0.999; // HACK: drop a tiny bit so the sound doesn't trigger repeatedly + auto ventedfraction{0.999}; // HACK: drop a tiny bit so the sound doesn't trigger repeatedly + if (false == governorlockispresent) + { + ventedfraction = 0.80; // arbitrary amount + } + else if (CompressorTankValve) + { + ventedfraction = MinCompressorF / MaxCompressorF; // drop to mincompressor level + } + CompressedVolume *= ventedfraction; if (false == governorlockispresent || CompressorTankValve) { CompressorGovernorLock = false; } } - // tymczasowo tylko obciążenie sprężarki, tak z 5A na sprężarkę - // TODO: draw power from proper high- or low voltage circuit - if (CompressorPower == 3) - { - // diesel-powered compressor doesn't draw power - } - else - { - // TODO: drain power from 110v circuit - /* - if( compressorowner != nullptr ) { - compressorowner->TotalCurrent += 0.0015 * compressorowner->PantographVoltage; - } - */ - } + // a compressor not driven by the engine doesn't draw power (some 5 A) from the high- or low voltage circuit yet } // ************************************************************************************************* @@ -5008,7 +5045,7 @@ void TMoverParameters::ComputeTotalForce(double dt) } else { - EngineVoltage *= DirAbsolute; // DirActive*CabActive; + EngineVoltage *= DirAbsolute; // the reverser of the active cab, relative to the couplers } } // bo nie dzialalo else @@ -5032,6 +5069,14 @@ void TMoverParameters::ComputeTotalForce(double dt) { SlippingWheels = true; } + if (true == SlippingWheels && Vel < 0.1 // the same crude trap: (nearly) stopped vehicle... + && std::abs(nrot * M_PI * WheelDiameter - V) < 0.1 / 3.6) // ...with wheels turning at its speed, i.e. locked wheels at the end of a skid + { + // locked wheels never pass the rotation speed test below once the vehicle stops, and a skidding vehicle + // keeps its braking force capped by the sliding adhesion, so the rest of the consist could push it around + SlippingWheels = false; + nrot = V / M_PI / WheelDiameter; + } double temp_nrot = nrot; if (true == SlippingWheels) { @@ -5399,17 +5444,14 @@ double TMoverParameters::CouplerForce(int const End, double dt) couplingtype &= ~coupling::control; } - if (Attach(End, otherend, othervehicle, couplingtype)) - { - // HACK: we're reusing sound enum to mark whether vehicle was connected to another - SetFlag(AIFlag, sound::attachcoupler); - thiscoupler.AutomaticCouplingAllowed = false; - othercoupler.AutomaticCouplingAllowed = false; - } - /* - coupler.CouplingFlag = ( coupler.AutomaticCouplingFlag & othercoupler.AutomaticCouplingFlag ); - SetFlag( coupler.sounds, sound::attachcoupler ); - */ + // too fast for the couplers to catch, the vehicles collide instead (and may derail, see CollisionDetect()) + auto const safe{AutomaticCouplingSafe(End, *othervehicle, otherend)}; + auto const caught{safe && Attach(End, otherend, othervehicle, couplingtype)}; + // HACK: we're reusing sound enum to mark whether vehicle was connected to another + SetFlag(AIFlag, caught ? sound::attachcoupler : 0); + // coupled, or locked until the vehicles part, so they don't couple as soon as the collision slows them down + thiscoupler.AutomaticCouplingAllowed = safe && false == caught; + othercoupler.AutomaticCouplingAllowed = safe && false == caught; } } } @@ -10468,6 +10510,7 @@ void TMoverParameters::LoadFIZ_Brake(std::string const &line) auto lookup = compressorpowers.find(extract_value("CompressorPower", line)); CompressorPower = lookup != compressorpowers.end() ? lookup->second : 1; } + extract_value(CompressorStartDelay, "CompressorStartDelay", line, ""); if (true == extract_value(AirLeakRate, "AirLeakRate", line, "")) { @@ -10684,7 +10727,7 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) for (int idx = 0; idx < 4; ++idx) { - extract_value(BrakeDelay[idx], "BDelay" + std::to_string(idx + 1), line, ""); + extract_value(BrakeDelay[idx], std::format("BDelay{}", idx + 1), line, ""); } // brakedelays, brakedelayflag { @@ -10780,6 +10823,16 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) // maksymalna predkosc dostepna na tarczce predkosciomierza extract_value(maxTachoSpeed, "MaxTachoSpeed", line, ""); + // horns + { + using enum horn_supply; + std::map> const hornsupplies{{"Pneumatic", air}, {"Electric", electric}}; + auto lookup = hornsupplies.find(extract_value("HornType", line)); + HornSupply = lookup != hornsupplies.end() ? lookup->second : air; + lookup = hornsupplies.find(extract_value("Horn3Type", line)); + Horn3Supply = lookup != hornsupplies.end() ? lookup->second : HornSupply; + } + // dynamicbrake { std::map> dynamicbrakes{{"Passive", dbrake_passive}, {"Switch", dbrake_switch}, {"Reversal", dbrake_reversal}, {"Automatic", dbrake_automatic}}; @@ -10939,14 +10992,15 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) // ground relay { auto lookup = starts.find(extract_value("GroundRelayStart", line)); - GroundRelayStart = lookup != starts.end() ? lookup->second : TrainType == dt_EZT ? start_t::automatic : start_t::manual; + auto const defaultstart{TrainType == dt_EZT ? start_t::automatic : start_t::manual}; + GroundRelayStart = lookup != starts.end() ? lookup->second : defaultstart; } // converter overload relay { auto lookup = starts.find(extract_value("ConverterOverloadRelayStart", line)); - ConverterOverloadRelayStart = lookup != starts.end() ? lookup->second : - TrainType == dt_EZT ? start_t::converter : // relay activates when converter is switched off - start_t::manual; + // the relay of a multiple unit activates when the converter is switched off + auto const defaultstart{TrainType == dt_EZT ? start_t::converter : start_t::manual}; + ConverterOverloadRelayStart = lookup != starts.end() ? lookup->second : defaultstart; } } @@ -11339,6 +11393,7 @@ void TMoverParameters::LoadFIZ_Switches(std::string const &Input) extract_value(PantSwitchType, "Pantograph", Input, ""); extract_value(ConvSwitchType, "Converter", Input, ""); extract_value(StLinSwitchType, "MotorConnectors", Input, ""); + HeatingSwitchImpulse = ToLower(extract_value("TrainHeating", Input)) == "impulse"; // because people can't make up their minds whether it's "impulse" or "Impulse"... PantSwitchType = ToLower(PantSwitchType); ConvSwitchType = ToLower(ConvSwitchType); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index a07da795..9b5dc64a 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -120,8 +120,8 @@ bool is_curved(segment_data const &Path) void copy_text(char *Buffer, std::size_t const Size, std::string const &Text) { - std::strncpy(Buffer, Text.c_str(), Size - 1); - Buffer[Size - 1] = 0; + // as much of the text as fits, always terminated + Buffer[Text.copy(Buffer, Size - 1)] = 0; } std::string lower_case(std::string Text) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..8dfd831f 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1188,8 +1188,8 @@ void editor_mode::render_path_parameters(TTrack &Track) if (m_track_material_edited[i] != nullptr) continue; auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{}; - std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1); - m_track_materials[i].back() = '\0'; + auto &buffer{m_track_materials[i]}; + buffer[name.copy(buffer.data(), buffer.size() - 1)] = '\0'; } } for (int i = 0; i < 3; ++i) @@ -1238,8 +1238,7 @@ void editor_mode::render_path_circuits(TTrack &Track) { auto *track{&Track}; auto const copy = [](std::array &Buffer, std::string const &Text) { - std::strncpy(Buffer.data(), Text.c_str(), Buffer.size() - 1); - Buffer.back() = '\0'; + Buffer[Text.copy(Buffer.data(), Buffer.size() - 1)] = '\0'; }; auto const isolated_text = [&]() { std::string text; diff --git a/input/command.cpp b/input/command.cpp index d7966f37..0ae67cfa 100644 --- a/input/command.cpp +++ b/input/command.cpp @@ -405,6 +405,7 @@ commanddescription_sequence Commands_descriptions = { {"wiperswitchincrease", command_target::vehicle, command_mode::oneoff}, {"wiperswitchdecrease", command_target::vehicle, command_mode::oneoff}, {"lightsset", command_target::vehicle, command_mode::oneoff}, + {"oilfuelpumptoggle", command_target::vehicle, command_mode::oneoff}, }; // Maps of command and coresponding strings @@ -789,6 +790,7 @@ std::unordered_map const commandMap = { {"wiperswitchincrease", user_command::wiperswitchincrease}, {"wiperswitchdecrease", user_command::wiperswitchdecrease}, {"lightsset", user_command::lightsset}, + {"oilfuelpumptoggle", user_command::oilfuelpumptoggle}, {"none", user_command::none}}; } // namespace simulation diff --git a/input/command.h b/input/command.h index e03622a9..ddacef4a 100644 --- a/input/command.h +++ b/input/command.h @@ -403,6 +403,7 @@ enum class user_command wiperswitchincrease, wiperswitchdecrease, lightsset, + oilfuelpumptoggle, none = -1 }; diff --git a/input/drivermouseinput.cpp b/input/drivermouseinput.cpp index ab3d0aaf..22562b85 100644 --- a/input/drivermouseinput.cpp +++ b/input/drivermouseinput.cpp @@ -576,6 +576,9 @@ drivermouse_input::default_bindings() { { "oilpump_sw:", { oilpumptoggle, none } }, + { "oilfuelpump_sw:", { + oilfuelpumptoggle, + none } }, { "motorblowersfront_sw:", { motorblowerstogglefront, none } }, diff --git a/tests/unit/README.md b/tests/unit/README.md index bca46e83..97cc9491 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -56,8 +56,10 @@ TEST_CASE("brakes: service braking stops a heavier wagon too") { functions on the vehicle state the class calls, and the test calls them the same way: see `vehicle/handover.h`, which `TController::TakeControl()` uses, and `test_handover.cpp`. - `vehicle` creates the vehicle the way `TDynamicObject::Init()` does and steps it the way the - simulation loop does, on a straight track of the given friction and gradient. Coupled vehicles - aren't supported yet. + simulation loop does, on a straight track of the given friction and gradient. `pair` puts two of + them on that track facing each other with the given ends and runs them together, as the + simulation runs the vehicles of a scene: the coupler and buffer forces, collisions and automatic + coupling come from the physics (`test_couplers.cpp`). Longer consists aren't supported yet. - Check behaviour (speeds, pressures, states the player sees) rather than internal flags, and make the test check its own preconditions (`REQUIRE` that the braking skidded, before checking what a skid leads to). diff --git a/tests/unit/test_cntrl.cpp b/tests/unit/test_cntrl.cpp new file mode 100644 index 00000000..0dd3ddc2 --- /dev/null +++ b/tests/unit/test_cntrl.cpp @@ -0,0 +1,39 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::vehicle; + +// the Cntrl. section of the vehicle definition, as read by TMoverParameters::LoadFIZ_Cntrl() + +TEST_CASE("cntrl: the brake delays of the settings come from BDelay1 to BDelay4") { + vehicle wagon{definition::load("wagon").set("Cntrl", "BDelay1", 10.0).set("Cntrl", "BDelay2", 20.0).set("Cntrl", "BDelay3", 30.0).set("Cntrl", "BDelay4", 40.0)}; + CHECK(wagon->BrakeDelay[0] == doctest::Approx(10.0)); + // the vehicle varies the others a little, by 0.83 to 0.9 (CheckLocomotiveParameters()) + for (auto setting{1}; setting < 4; ++setting) { + CAPTURE(setting); + auto const given{10.0 * (setting + 1)}; + CHECK(wagon->BrakeDelay[setting] >= 0.83 * given); + CHECK(wagon->BrakeDelay[setting] <= 0.9 * given); + } +} + +TEST_CASE("cntrl: the relays of a multiple unit start on their own, of other vehicles by hand") { + SUBCASE("multiple unit") { + vehicle unit{definition::load("wagon").set("Param", "Type", "ezt")}; + REQUIRE(unit->TrainType == dt_EZT); + CHECK(unit->GroundRelayStart == start_t::automatic); + CHECK(unit->ConverterOverloadRelayStart == start_t::converter); // activates when the converter is switched off + } + SUBCASE("other vehicle") { + vehicle wagon{definition::load("wagon")}; + CHECK(wagon->GroundRelayStart == start_t::manual); + CHECK(wagon->ConverterOverloadRelayStart == start_t::manual); + } + SUBCASE("given in the definition") { + vehicle unit{definition::load("wagon").set("Param", "Type", "ezt").set("Cntrl", "GroundRelayStart", "Manual").set("Cntrl", "ConverterOverloadRelayStart", "Automatic")}; + CHECK(unit->GroundRelayStart == start_t::manual); + CHECK(unit->ConverterOverloadRelayStart == start_t::automatic); + } +} diff --git a/tests/unit/test_compressor.cpp b/tests/unit/test_compressor.cpp new file mode 100644 index 00000000..4a69c24a --- /dev/null +++ b/tests/unit/test_compressor.cpp @@ -0,0 +1,115 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::vehicle; + +// the compressor waiting a while after getting its power before it starts (CompressorStartDelay, +// MaSzyna-EU07/maszyna#67): in a diesel it starts some time after the engine, rather than with it + +namespace { + +double const step{0.01}; + +// a compressor which starts on its own, fed from the main circuit unless given another Power, its pressure +// switch between MinCP and MaxCP, and an empty main reservoir for it to fill +vehicle compressor_vehicle(double const Delay, std::string const &Power = "Main", double const MinCP = 0.65) { + auto definition{definition::load("wagon")}; + definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", MinCP).set("Brake", "MaxCP", 0.8); + definition.set("Brake", "CompressorSpeed", 0.01); + if (false == Power.empty()) { + definition.set("Brake", "CompressorPower", Power); + } + definition.set("Cntrl", "CompressorStart", "Automatic"); + if (Delay > 0.0) { + definition.set("Brake", "CompressorStartDelay", Delay); + } + vehicle vehicle{definition}; + vehicle->CompressedVolume = 0.0; + vehicle->LastSwitchingTime = 10.0; // nothing was switched lately + return vehicle; +} + +// runs the compressor for Seconds, the main circuit as it is, the time since the last switching going on as in +// ComputeTotalForce() +void run_compressor(vehicle &Vehicle, double const Seconds) { + for (auto i{std::lround(Seconds / step)}; i > 0; --i) { + Vehicle->LastSwitchingTime += step; + Vehicle->CompressorCheck(step); + } +} + +} // namespace + +TEST_CASE("compressor: without a start delay the compressor starts with its power") { + auto vehicle{compressor_vehicle(0.0)}; + run_compressor(vehicle, 1.0); + REQUIRE_FALSE(vehicle->CompressorFlag); // no power, no compressor + + vehicle->Mains = true; + run_compressor(vehicle, step); + CHECK(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: the compressor starts the delay after getting its power") { + auto vehicle{compressor_vehicle(5.0)}; + vehicle->Mains = true; + + run_compressor(vehicle, 4.9); + CHECK_FALSE(vehicle->CompressorFlag); + CHECK(vehicle->Compressor == doctest::Approx(0.0)); + + run_compressor(vehicle, 0.2); + CHECK(vehicle->CompressorFlag); + run_compressor(vehicle, 10.0); + CHECK(vehicle->Compressor > 0.0); // and fills the reservoir +} + +TEST_CASE("compressor: the delay starts over once the power is gone") { + auto vehicle{compressor_vehicle(5.0)}; + vehicle->Mains = true; + run_compressor(vehicle, 10.0); + REQUIRE(vehicle->CompressorFlag); + + vehicle->Mains = false; // the engine stopped + run_compressor(vehicle, 1.0); + CHECK_FALSE(vehicle->CompressorFlag); + + vehicle->Mains = true; // and started again + run_compressor(vehicle, 4.9); + CHECK_FALSE(vehicle->CompressorFlag); + run_compressor(vehicle, 0.2); + CHECK(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: a compressor fed from the low voltage waits from the low voltage coming on") { + auto vehicle{compressor_vehicle(5.0, "")}; // the default supply: the converter, the 110 V + vehicle->Mains = true; // which alone doesn't feed it + run_compressor(vehicle, 10.0); + REQUIRE_FALSE(vehicle->CompressorFlag); + + vehicle->Power110vIsAvailable = true; + run_compressor(vehicle, 4.9); + CHECK_FALSE(vehicle->CompressorFlag); + run_compressor(vehicle, 0.2); + CHECK(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: the pressure switch stops the compressor above the cut-out pressure") { + auto vehicle{compressor_vehicle(0.0)}; + vehicle->Mains = true; + vehicle->CompressedVolume = 0.85; // over MaxCP, in the reservoir of 1 m3 + run_compressor(vehicle, step); + CHECK(vehicle->CompressorGovernorLock); + CHECK_FALSE(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: without a pressure switch the air over the cut-out pressure is vented") { + auto vehicle{compressor_vehicle(0.0, "Main", 0.8)}; // MinCP the same as MaxCP: no pressure switch + vehicle->Mains = true; + vehicle->CompressedVolume = 0.85; + run_compressor(vehicle, step); + REQUIRE(vehicle->CompressorFlag); // nothing stops it + CHECK(vehicle->CompressedVolume < 0.85 * 0.81); // a fifth of the air let out +} diff --git a/tests/unit/test_couplers.cpp b/tests/unit/test_couplers.cpp new file mode 100644 index 00000000..46d2ff07 --- /dev/null +++ b/tests/unit/test_couplers.cpp @@ -0,0 +1,426 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "utilities/Globals.h" + +using test::definition; +using test::vehicle; + +namespace { + +// the wagon from data/wagon.json with automatic couplers at both ends, which couple the given connections +definition automatic_definition(int Flag = coupling::coupler | coupling::brakehose) { + return definition::load("wagon").set("BuffCoupl", "CType", "Automatic").set("BuffCoupl", "AutomaticFlag", Flag); +} + +// the wagon from data/wagon.json with automatic couplers at both ends, running at the given speed [km/h] +vehicle automatic_wagon(double Velocity, std::string const &Name) { + return vehicle{automatic_definition(), Velocity, Name}; +} + +// the same wagon, twice as heavy +vehicle heavy_automatic_wagon(double Velocity, std::string const &Name) { + return vehicle{automatic_definition().set("Param", "M", 2 * 42200), Velocity, Name}; +} + +bool derailed(vehicle &Vehicle) { + return TestFlag(Vehicle->DamageFlag, dtrain_out); +} + +// runs the pair for Seconds, returns whether the vehicles were coupled at any point +bool couple_during(test::pair &Pair, double Seconds) { + auto coupled{false}; + for (auto i{std::lround(Seconds / 0.01)}; i > 0; --i) { + Pair.step(); + coupled = coupled || Pair.coupled(); + } + return coupled; +} + +// sets the crash damage setting for the test, restores it after +class crash_damage_setting { +public: + explicit crash_damage_setting(bool Enabled) { Global.crash_damage = Enabled; } + ~crash_damage_setting() { Global.crash_damage = m_previous; } + crash_damage_setting(crash_damage_setting const &) = delete; + crash_damage_setting &operator=(crash_damage_setting const &) = delete; + +private: + bool m_previous{Global.crash_damage}; +}; + +} // namespace + +// issue #86: automatic couplers used to catch at any closing speed, so the vehicles never collided nor derailed + +TEST_CASE("couplers: automatic couplers catch a standing wagon hit at a walking pace") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(10.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + REQUIRE(moving->Couplers[end::front].type() == TCouplerType::Automatic); + REQUIRE(moving.speed() == doctest::Approx(10.0)); + + // the front of the moving wagon runs into the rear of the standing one + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(10.0)); + CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + CHECK(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front)); +} + +TEST_CASE("couplers: automatic couplers don't catch a wagon hit too fast, the wagons collide instead") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + REQUIRE(moving.speed() == doctest::Approx(30.0)); + + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) > TMoverParameters::SafeCollisionVelocity); + CHECK_FALSE(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + CHECK_FALSE(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front)); +} + +TEST_CASE("couplers: automatic couplers catch a wagon running ahead only a little slower") { + crash_damage_setting const crashdamage{true}; + auto faster{automatic_wagon(60.0, "faster")}; + auto slower{automatic_wagon(50.0, "slower")}; + + CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(10.0)); + CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(10.0)); + CHECK(faster->AutomaticCouplingSafe(end::front, slower.mover(), end::rear)); +} + +TEST_CASE("couplers: without crash damage automatic couplers catch at any speed") { + crash_damage_setting const crashdamage{false}; + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + + CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); +} + +// the closing speed used to treat the speed of each vehicle as a vector turned by half its heading, so for vehicles +// facing each other it came out as the root of the sum of the squares of their speeds, rather than the sum + +TEST_CASE("couplers: the closing speed of a rear-end collision is the difference of the speeds") { + auto faster{automatic_wagon(40.0, "faster")}; + auto slower{automatic_wagon(25.0, "slower")}; + + // running forwards, the front of the faster wagon hits the rear of the slower one + CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(15.0)); + CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(15.0)); + // the wagons running ahead of each other are parting + CHECK(slower->CollisionVelocity(end::front, faster.mover(), end::rear) == doctest::Approx(-15.0)); + + // backing up, the rear of the faster wagon hits the front of the slower one + auto backingfaster{automatic_wagon(-40.0, "backing faster")}; + auto backingslower{automatic_wagon(-25.0, "backing slower")}; + CHECK(backingfaster->CollisionVelocity(end::rear, backingslower.mover(), end::front) == doctest::Approx(15.0)); + CHECK(backingslower->CollisionVelocity(end::front, backingfaster.mover(), end::rear) == doctest::Approx(15.0)); +} + +TEST_CASE("couplers: the closing speed of a head-on collision is the sum of the speeds") { + crash_damage_setting const crashdamage{true}; + auto first{automatic_wagon(10.0, "first")}; + auto second{automatic_wagon(10.0, "second")}; + + // both wagons run forwards, their fronts meet + CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) == doctest::Approx(20.0)); + CHECK(second->CollisionVelocity(end::front, first.mover(), end::front) == doctest::Approx(20.0)); + // 20 km/h is above the limit, the couplers don't catch (the old formula gave 14.1 km/h and coupled them) + CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) > TMoverParameters::SafeCollisionVelocity); + CHECK_FALSE(first->AutomaticCouplingSafe(end::front, second.mover(), end::front)); + CHECK_FALSE(second->AutomaticCouplingSafe(end::front, first.mover(), end::front)); + + // both wagons back up, their rears meet + auto firstbacking{automatic_wagon(-10.0, "first backing")}; + auto secondbacking{automatic_wagon(-10.0, "second backing")}; + CHECK(firstbacking->CollisionVelocity(end::rear, secondbacking.mover(), end::rear) == doctest::Approx(20.0)); + + // running back to back, the wagons part + CHECK(first->CollisionVelocity(end::rear, second.mover(), end::rear) == doctest::Approx(-20.0)); + CHECK(first->AutomaticCouplingSafe(end::rear, second.mover(), end::rear)); + + // slow enough, the couplers catch + auto slowfirst{automatic_wagon(5.0, "slow first")}; + auto slowsecond{automatic_wagon(5.0, "slow second")}; + CHECK(slowfirst->CollisionVelocity(end::front, slowsecond.mover(), end::front) == doctest::Approx(10.0)); + CHECK(slowfirst->AutomaticCouplingSafe(end::front, slowsecond.mover(), end::front)); +} + +TEST_CASE("couplers: the closing speed of a hit on a standing vehicle is the speed of the moving one, whichever way it faces") { + auto moving{automatic_wagon(20.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + + // the standing wagon faces away from the moving one + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(20.0)); + CHECK(standing->CollisionVelocity(end::rear, moving.mover(), end::front) == doctest::Approx(20.0)); + // the standing wagon faces the moving one + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::front) == doctest::Approx(20.0)); + CHECK(standing->CollisionVelocity(end::front, moving.mover(), end::front) == doctest::Approx(20.0)); + // the moving wagon backs up into the standing one + auto backing{automatic_wagon(-20.0, "backing")}; + CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(20.0)); + CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::rear) == doctest::Approx(20.0)); + // the moving wagon runs away from the standing one + CHECK(moving->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(-20.0)); +} + +// the coupling as the simulation runs it: two vehicles on a track, closing in on each other until the couplers (or +// buffers) meet, the coupler forces and the collision computed by the physics, step by step + +TEST_CASE("couplers: automatic couplers catch right up to the safe closing speed") { + crash_damage_setting const crashdamage{true}; + auto standing{automatic_wagon(0.0, "standing")}; + + auto below{automatic_wagon(14.9, "below")}; + CHECK(below->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(14.9)); + CHECK(below->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + + auto above{automatic_wagon(15.1, "above")}; + CHECK(above->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(15.1)); + CHECK_FALSE(above->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + auto faster{automatic_wagon(15.5, "faster")}; + CHECK_FALSE(faster->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); +} + +TEST_CASE("couplers: a wagon rolling slowly into a standing one couples with it") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(5.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + pair.run(3.0); + + REQUIRE(pair.coupled()); + CHECK(moving->Couplers[end::front].CouplingFlag == (coupling::coupler | coupling::brakehose)); + CHECK(standing->Couplers[end::rear].CouplingFlag == (coupling::coupler | coupling::brakehose)); + CHECK(moving->Couplers[end::front].Connected == &standing.mover()); + CHECK_FALSE(derailed(moving)); + CHECK_FALSE(derailed(standing)); + // coupled, they run on together + pair.run(2.0); + CHECK(moving.speed() == doctest::Approx(standing.speed()).epsilon(0.05)); + CHECK(pair.gap() < 0.2); +} + +TEST_CASE("couplers: a wagon hitting a standing one too fast doesn't couple, nor once the hit has slowed it down") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + auto touched{false}; + auto parted{false}; + auto coupled{false}; + auto locked{true}; + for (int i = 0; i < 1000; ++i) { + pair.step(); + // as far as the couplers know: the distance between them at the start of the step + auto const distance{moving->Neighbours[end::front].distance}; + touched = touched || distance < 0.0; + parted = parted || (touched && distance > 0.05); + coupled = coupled || pair.coupled(); + if (touched && false == parted) { + locked = locked && false == moving->Couplers[end::front].AutomaticCouplingAllowed && false == standing->Couplers[end::rear].AutomaticCouplingAllowed; + } + } + + REQUIRE(touched); + CHECK_FALSE(coupled); + // the hit derailed and stopped them, the couplers next to each other, the automatic coupling locked all along + CHECK_FALSE(parted); + CHECK(locked); + CHECK(derailed(moving)); + CHECK(derailed(standing)); + CHECK(std::abs(moving.speed()) < 3.0); + CHECK(std::abs(standing.speed()) < 3.0); + CHECK_FALSE(couple_during(pair, 5.0)); +} + +TEST_CASE("couplers: the automatic coupling locked by a hit is unlocked once the vehicles part") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(3.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + // as a hit too fast or manual uncoupling leaves them + moving->Couplers[end::front].AutomaticCouplingAllowed = false; + standing->Couplers[end::rear].AutomaticCouplingAllowed = false; + + SUBCASE("parted by more than 5 cm, they couple on the next approach") { + test::pair pair{moving, end::front, standing, end::rear, 0.5}; + pair.step(); + CHECK(moving->Couplers[end::front].AutomaticCouplingAllowed); + CHECK(couple_during(pair, 3.0)); + } + SUBCASE("kept together, they don't") { + test::pair pair{moving, end::front, standing, end::rear, 0.0}; + auto coupled{false}; + for (int i = 0; i < 300; ++i) { + pair.step(); + if (pair.gap() > 0.05) { + break; // parted + } + coupled = coupled || pair.coupled(); + } + CHECK_FALSE(coupled); + } +} + +// the couplers refuse a hit by the closing speed when they meet, the derail check (CollisionDetect()) takes the one +// left when the buffers are pressed in all the way, after their springs have slowed the hit down: for the wagons here +// by about 7 km/h. so a hit a little over the safe closing speed only bounces the vehicles apart + +TEST_CASE("couplers: a hit a little too fast for the couplers bounces the vehicles apart, uncoupled and on the rails") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(20.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + CHECK_FALSE(couple_during(pair, 3.0)); + CHECK_FALSE(derailed(moving)); + CHECK_FALSE(derailed(standing)); + CHECK(pair.gap() > 0.5); + CHECK(standing.speed() > moving.speed()); +} + +TEST_CASE("couplers: a hit too fast for the couplers derails the vehicles over the limit adjusted for their mass ratio") { + crash_damage_setting const crashdamage{true}; + + SUBCASE("equal masses: both derail") { + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(moving)); + CHECK(derailed(standing)); + } + SUBCASE("half the mass: between the safe closing speed and twice that, only the lighter one derails") { + auto light{automatic_wagon(27.0, "light")}; + auto heavy{heavy_automatic_wagon(0.0, "heavy")}; + test::pair pair{light, end::front, heavy, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(light)); + CHECK_FALSE(derailed(heavy)); + } + SUBCASE("half the mass: above twice the safe closing speed both derail") { + auto light{automatic_wagon(45.0, "light")}; + auto heavy{heavy_automatic_wagon(0.0, "heavy")}; + test::pair pair{light, end::front, heavy, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(light)); + CHECK(derailed(heavy)); + } + SUBCASE("half the mass: below the safe closing speed the couplers catch, nothing derails") { + auto light{automatic_wagon(12.0, "light")}; + auto heavy{heavy_automatic_wagon(0.0, "heavy")}; + test::pair pair{light, end::front, heavy, end::rear, 1.0}; + CHECK(couple_during(pair, 5.0)); + CHECK_FALSE(derailed(light)); + CHECK_FALSE(derailed(heavy)); + } +} + +TEST_CASE("couplers: wagons running head-on into each other couple only up to the safe closing speed") { + crash_damage_setting const crashdamage{true}; + + SUBCASE("7 + 7 km/h, the couplers catch") { + auto first{automatic_wagon(7.0, "first")}; + auto second{automatic_wagon(7.0, "second")}; + test::pair pair{first, end::front, second, end::front, 1.0}; + CHECK(couple_during(pair, 3.0)); + CHECK_FALSE(derailed(first)); + CHECK_FALSE(derailed(second)); + } + SUBCASE("8 + 8 km/h, the wagons bounce apart") { + auto first{automatic_wagon(8.0, "first")}; + auto second{automatic_wagon(8.0, "second")}; + test::pair pair{first, end::front, second, end::front, 1.0}; + CHECK_FALSE(couple_during(pair, 3.0)); + CHECK_FALSE(derailed(first)); + CHECK_FALSE(derailed(second)); + // both backwards, away from each other + CHECK(first.speed() < 0.0); + CHECK(second.speed() < 0.0); + } + SUBCASE("backing up, 8 + 8 km/h, the wagons bounce apart") { + auto first{automatic_wagon(-8.0, "first")}; + auto second{automatic_wagon(-8.0, "second")}; + test::pair pair{first, end::rear, second, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 3.0)); + CHECK(first.speed() > 0.0); + CHECK(second.speed() > 0.0); + } + SUBCASE("15 + 15 km/h, both derail") { + auto first{automatic_wagon(15.0, "first")}; + auto second{automatic_wagon(15.0, "second")}; + test::pair pair{first, end::front, second, end::front, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(first)); + CHECK(derailed(second)); + } +} + +// the collision used to take the speed of a vehicle facing the other way as if it faced the same way, so a wagon +// pushed away by the buffers of the vehicle hitting it head-on counted as running into it: the hit slowed it down, +// and the hitter bounced back as from a wall + +TEST_CASE("couplers: a wagon hit head-on runs off the same as one hit from behind") { + // without automatic couplers or crash damage, the collision alone + crash_damage_setting const crashdamage{false}; + auto speeds_after_hit = [](int StruckEnd) { + auto moving{vehicle{definition::load("wagon"), 20.0, "moving"}}; + auto struck{vehicle{definition::load("wagon"), 0.0, "struck"}}; + test::pair pair{moving, end::front, struck, StruckEnd, 0.5}; + auto touched{false}; + for (int i = 0; i < 300; ++i) { + pair.step(); + touched = touched || pair.gap() < 0.0; + if (touched && pair.gap() > 0.3) { + break; // bounced off + } + } + REQUIRE(touched); + REQUIRE(pair.gap() > 0.3); + REQUIRE_FALSE(pair.coupled()); + return std::make_pair(moving.speed(), struck.speed()); + }; + + auto const [hitterfrombehind, struckfrombehind]{speeds_after_hit(end::rear)}; + auto const [hitterheadon, struckheadon]{speeds_after_hit(end::front)}; + + // hit from behind, the struck wagon runs off forwards, faster than the hitter + CHECK(struckfrombehind > 10.0); + CHECK(hitterfrombehind < struckfrombehind); + // hit head-on, it runs off backwards, as fast + CHECK(struckheadon == doctest::Approx(-struckfrombehind).epsilon(0.02)); + CHECK(hitterheadon == doctest::Approx(hitterfrombehind).epsilon(0.05)); +} + +// the other conditions of automatic coupling + +TEST_CASE("couplers: an automatic coupler doesn't catch a coupler of another type") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(5.0, "moving")}; + auto standing{vehicle{definition::load("wagon"), 0.0, "standing"}}; + REQUIRE(standing->Couplers[end::rear].type() == TCouplerType::Screw); + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + CHECK_FALSE(couple_during(pair, 3.0)); +} + +TEST_CASE("couplers: automatic couplers of different control systems couple without the control connection") { + crash_damage_setting const crashdamage{true}; + auto const flag{coupling::coupler | coupling::brakehose | coupling::control}; + auto moving{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 5.0, "moving"}}; + + SUBCASE("the same control system") { + auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 0.0, "standing"}}; + REQUIRE(standing->Couplers[end::rear].control_type == "first"); + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + REQUIRE(couple_during(pair, 3.0)); + CHECK(pair.coupling() == flag); + } + SUBCASE("a different one") { + auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "second"), 0.0, "standing"}}; + REQUIRE(standing->Couplers[end::rear].control_type == "second"); + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + REQUIRE(couple_during(pair, 3.0)); + CHECK(pair.coupling() == (coupling::coupler | coupling::brakehose)); + } +} diff --git a/tests/unit/test_gauge_positions.cpp b/tests/unit/test_gauge_positions.cpp new file mode 100644 index 00000000..791234e9 --- /dev/null +++ b/tests/unit/test_gauge_positions.cpp @@ -0,0 +1,58 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle/gaugepositions.h" + +#include + +// the sounds of the positions of a cab control (MaSzyna-EU07/maszyna#83): a brake lever moved smoothly (Num3/Num9) +// passes its positions instead of stopping at them, and sounds each one it reaches, as it does when put there + +namespace { + +// a brake lever with sounds at positions 0, 2 and 5; values in hundredths of a position +std::map lever_sounds() { + return {{0, "release"}, {2, "running"}, {5, "service"}}; +} + +std::string reached(int const Previous, int const Desired) { + auto positions{lever_sounds()}; + auto const position{gauge_positions::last_reached(positions, Previous, Desired)}; + return position != positions.end() ? position->second : ""; +} + +} // namespace + +TEST_CASE("gauge positions: a lever put at a position sounds it") { + CHECK(reached(100, 200) == "running"); + CHECK(reached(600, 500) == "service"); +} + +TEST_CASE("gauge positions: a lever moved smoothly past a position sounds it (MaSzyna-EU07/maszyna#83)") { + CHECK(reached(190, 205) == "running"); // moving up + CHECK(reached(510, 495) == "service"); // moving down +} + +TEST_CASE("gauge positions: of the positions passed in one go the last one reached sounds") { + CHECK(reached(50, 520) == "service"); // up past 2 and 5 + CHECK(reached(520, 10) == "running"); // down past 5 and 2, short of 0 + CHECK(reached(520, 0) == "release"); +} + +TEST_CASE("gauge positions: moving between the positions or off one sounds nothing") { + CHECK(reached(210, 290) == ""); + CHECK(reached(200, 230) == ""); // leaving a position isn't reaching it + CHECK(reached(200, 170) == ""); +} + +TEST_CASE("gauge positions: positions below zero count the same way") { + // a brake lever with the release position at -1 + std::map positions{{-1, "release"}, {0, "running"}}; + CHECK(gauge_positions::last_reached(positions, 50, -120)->second == "release"); // down past 0 and -1 + CHECK(gauge_positions::last_reached(positions, -150, -90)->second == "release"); // up past -1 +} + +TEST_CASE("gauge positions: a control without position sounds reaches none") { + std::map positions; + CHECK(gauge_positions::last_reached(positions, 0, 500) == positions.end()); +} diff --git a/tests/unit/test_horn.cpp b/tests/unit/test_horn.cpp new file mode 100644 index 00000000..b90853ef --- /dev/null +++ b/tests/unit/test_horn.cpp @@ -0,0 +1,121 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle.h" + +#include "vehicle/horn.h" + +// the horn keys of the cab: low tone (A) and high tone (shift+A), held to sound + +TEST_CASE("horn: a single lever for both tones sounds one tone at a time (eu07/maszyna#392)") { + auto signal{0}; + signal = horn::pressed(signal, horn::low, true); + REQUIRE(signal == horn::low); + + // the lever moved over to the high tone while the low tone key is still held + signal = horn::pressed(signal, horn::high, true); + CHECK(signal == horn::high); + + // letting go of the low tone key leaves the high tone sounding + signal = horn::released(signal, horn::low); + CHECK(signal == horn::high); + + signal = horn::released(signal, horn::high); + CHECK(signal == 0); +} + +TEST_CASE("horn: separate buttons for the tones can sound both at once") { + auto signal{0}; + signal = horn::pressed(signal, horn::low, false); + signal = horn::pressed(signal, horn::high, false); + CHECK(signal == (horn::low | horn::high)); + + signal = horn::released(signal, horn::low); + CHECK(signal == horn::high); +} + +TEST_CASE("horn: releasing a tone leaves the other bits of the signal alone") { + auto const third{4}; // the third horn, operated separately + CHECK(horn::released(horn::low | third, horn::low) == third); + CHECK(horn::pressed(third, horn::high, true) == (horn::high | third)); +} + +// what the horns need to sound (eu07/maszyna#290): a pneumatic horn the air of the main reservoir, an electric one +// the low voltage. the horn lever stays where the driver put it, only the horn is silent +namespace { + +test::vehicle horn_vehicle(test::definition Definition) { + // a main reservoir of its own + return test::vehicle{Definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 0.65).set("Brake", "MaxCP", 0.8)}; +} + +int const allhorns{horn::low | horn::high | 4}; + +} // namespace + +TEST_CASE("horn: a pneumatic horn sounds on the air of the main reservoir only") { + auto vehicle{horn_vehicle(test::definition::load("wagon"))}; + REQUIRE(vehicle->HornSupply == horn_supply::air); // the default + + vehicle->Compressor = 0.65; + CHECK(vehicle->SoundingHorns(allhorns) == allhorns); + vehicle->Compressor = 0.0; // the reservoir is empty + CHECK(vehicle->SoundingHorns(allhorns) == 0); +} + +TEST_CASE("horn: a vehicle without a main reservoir takes the horn air from the main pipe") { + test::vehicle vehicle{test::definition::load("wagon")}; + REQUIRE(vehicle->VeselVolume == 0.0); + + vehicle->ScndPipePress = 0.5; + CHECK(vehicle->SoundingHorns(horn::low) == horn::low); + vehicle->ScndPipePress = 0.0; + CHECK(vehicle->SoundingHorns(horn::low) == 0); +} + +TEST_CASE("horn: an electric horn sounds on the low voltage, whatever the air") { + auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "HornType", "Electric"))}; + REQUIRE(vehicle->HornSupply == horn_supply::electric); + REQUIRE(vehicle->Horn3Supply == horn_supply::electric); // the third horn follows the others unless told otherwise + + vehicle->Compressor = 0.0; + vehicle->Power24vIsAvailable = true; + CHECK(vehicle->SoundingHorns(allhorns) == allhorns); + vehicle->Power24vIsAvailable = false; + CHECK(vehicle->SoundingHorns(allhorns) == 0); +} + +TEST_CASE("horn: the third horn can have a supply of its own, an electric horn of a diesel say") { + auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "Horn3Type", "Electric"))}; + REQUIRE(vehicle->HornSupply == horn_supply::air); + REQUIRE(vehicle->Horn3Supply == horn_supply::electric); + + vehicle->Compressor = 0.0; + vehicle->Power24vIsAvailable = true; + CHECK(vehicle->SoundingHorns(allhorns) == 4); + vehicle->Compressor = 0.65; + vehicle->Power24vIsAvailable = false; + CHECK(vehicle->SoundingHorns(allhorns) == (horn::low | horn::high)); +} + +TEST_CASE("horn: a vehicle without any air, a road vehicle say, sounds its pneumatic horn as before") { + // no main reservoir and no main pipe connection at either end + test::vehicle vehicle{test::definition::load("wagon").set("BuffCoupl", "AllowedFlag", 3)}; + REQUIRE(vehicle->VeselVolume == 0.0); + vehicle->ScndPipePress = 0.0; + CHECK(vehicle->SoundingHorns(allhorns) == allhorns); +} + +TEST_CASE("horn: a pneumatic horn sounds from the threshold pressure up") { + auto vehicle{horn_vehicle(test::definition::load("wagon"))}; + vehicle->Compressor = TMoverParameters::HornMinPressure; + CHECK(vehicle->SoundingHorns(horn::low) == horn::low); + vehicle->Compressor = TMoverParameters::HornMinPressure - 0.01; + CHECK(vehicle->SoundingHorns(horn::low) == 0); +} + +TEST_CASE("horn: an electric horn sounds on 110 V too") { + auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "HornType", "Electric"))}; + vehicle->Power110vIsAvailable = true; + CHECK(vehicle->SoundingHorns(horn::high) == horn::high); +} diff --git a/tests/unit/test_line_contactors.cpp b/tests/unit/test_line_contactors.cpp new file mode 100644 index 00000000..783750ec --- /dev/null +++ b/tests/unit/test_line_contactors.cpp @@ -0,0 +1,48 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the lamp of the open line contactors (eu07/maszyna#470): lit, it tells the start can be made, so it should go out +// where the start is cut off, by the pressure switch at 2 bar in the brake cylinders, not at 1 bar already + +namespace { + +test::vehicle locomotive(bool const Pressureswitch) { + test::vehicle vehicle{test::definition::load("wagon").set("Engine", "PressureSwitch", Pressureswitch)}; + vehicle->StLinFlag = false; // the line contactors open + vehicle->PipePress = 5.0; + return vehicle; +} + +} // namespace + +TEST_CASE("line contactors: with a pressure switch the lamp goes out with the start cut off") { + auto vehicle{locomotive(true)}; + REQUIRE(vehicle->HasControlPressureSwitch); + + vehicle->BrakePress = 1.5; // braked, the start can still be made + vehicle->MotorConnectorsCheck(); // updates the pressure switch + REQUIRE_FALSE(vehicle->ControlPressureSwitch); + CHECK(vehicle->LineContactorsLampLit()); + + vehicle->BrakePress = 2.5; // the pressure switch cuts the start off + vehicle->MotorConnectorsCheck(); + REQUIRE(vehicle->ControlPressureSwitch); + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} + +TEST_CASE("line contactors: without a pressure switch the lamp goes out with the brakes applied, as before") { + auto vehicle{locomotive(false)}; + vehicle->BrakePress = 0.5; + vehicle->MotorConnectorsCheck(); + CHECK(vehicle->LineContactorsLampLit()); + vehicle->BrakePress = 1.5; + vehicle->MotorConnectorsCheck(); + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} + +TEST_CASE("line contactors: closed contactors put the lamp out") { + auto vehicle{locomotive(true)}; + vehicle->StLinFlag = true; + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} diff --git a/tests/unit/test_mirrors.cpp b/tests/unit/test_mirrors.cpp new file mode 100644 index 00000000..7408eeaa --- /dev/null +++ b/tests/unit/test_mirrors.cpp @@ -0,0 +1,32 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle/mirrors.h" + +// the sounds of the mirrors (MaSzyna-EU07/maszyna#84, eu07/maszyna#322): a mirror sounds as it sets off, unfolding +// or folding, and not again while it keeps moving the same way + +using mirror_moves::event; + +TEST_CASE("mirrors: a mirror sounds once as it sets off") { + mirror_moves::motion mirror; + CHECK(mirror.update(0.0) == event::none); // standing folded + CHECK(mirror.update(0.01) == event::unfolding); + CHECK(mirror.update(0.01) == event::none); // still unfolding + CHECK(mirror.update(0.0) == event::none); // unfolded + CHECK(mirror.update(-0.01) == event::folding); + CHECK(mirror.update(-0.01) == event::none); +} + +TEST_CASE("mirrors: a mirror turned back halfway sounds the other way") { + mirror_moves::motion mirror; + REQUIRE(mirror.update(0.01) == event::unfolding); + CHECK(mirror.update(-0.01) == event::folding); // the vehicle got too fast while it was unfolding +} + +TEST_CASE("mirrors: a mirror setting off again the same way sounds again") { + mirror_moves::motion mirror; + REQUIRE(mirror.update(0.01) == event::unfolding); + REQUIRE(mirror.update(0.0) == event::none); // stopped, the doors lost their permit say + CHECK(mirror.update(0.01) == event::unfolding); +} diff --git a/tests/unit/test_radio.cpp b/tests/unit/test_radio.cpp new file mode 100644 index 00000000..33a12f72 --- /dev/null +++ b/tests/unit/test_radio.cpp @@ -0,0 +1,28 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the radio works only switched on and with the low voltage (eu07/maszyna#429): its channel and volume buttons, +// the calls and the radio-stop all ask RadioPowered() + +TEST_CASE("radio: a radio switched on with the low voltage works") { + test::vehicle vehicle{test::definition::load("wagon")}; + vehicle->Radio = true; + SUBCASE("24 V") { vehicle->Power24vIsAvailable = true; } + SUBCASE("110 V") { vehicle->Power110vIsAvailable = true; } + CHECK(vehicle->RadioPowered()); +} + +TEST_CASE("radio: a radio switched off or without the low voltage doesn't") { + test::vehicle vehicle{test::definition::load("wagon")}; + SUBCASE("switched off") { + vehicle->Radio = false; + vehicle->Power24vIsAvailable = true; + } + SUBCASE("batteries off") { + vehicle->Radio = true; + vehicle->Power24vIsAvailable = false; + vehicle->Power110vIsAvailable = false; + } + CHECK_FALSE(vehicle->RadioPowered()); +} diff --git a/tests/unit/test_switches.cpp b/tests/unit/test_switches.cpp new file mode 100644 index 00000000..72b1e47c --- /dev/null +++ b/tests/unit/test_switches.cpp @@ -0,0 +1,16 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the kinds of the cab switches, given in the Switches: section of the vehicle definition + +TEST_CASE("switches: the train heating switch can spring back (MaSzyna-EU07/maszyna#85)") { + SUBCASE("impulse") { + test::vehicle vehicle{test::definition::load("cab_vehicle").set("Switches", "TrainHeating", "Impulse")}; + CHECK(vehicle->HeatingSwitchImpulse); // whichever the case it's written in + } + SUBCASE("a plain switch by default") { + test::vehicle vehicle{test::definition::load("cab_vehicle")}; + CHECK_FALSE(vehicle->HeatingSwitchImpulse); + } +} diff --git a/tests/unit/test_wheelslip.cpp b/tests/unit/test_wheelslip.cpp new file mode 100644 index 00000000..9b39e82c --- /dev/null +++ b/tests/unit/test_wheelslip.cpp @@ -0,0 +1,62 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::track; +using test::vehicle; + +namespace { + +struct braking_result { + bool skidded{false}; // the wheels locked while braking + bool stopped{false}; // came (almost) to a stop on the braking rail +}; + +// brakes the wagon from 40 km/h on the Braking rail until it's (nearly) stopped, then has it stand on the +// Standing one for 30 seconds +braking_result brake_to_stop_then_stand(vehicle &Coach, track const &Braking, track const &Standing) { + braking_result result; + Coach.hold_brake_pipe(5.0); + Coach.run(5.0, Braking); + Coach.hold_brake_pipe(3.5); // service braking + // a skidding wagon brakes on the sliding adhesion only, give it time + for (auto step = 0; step < 60000; ++step) { // 600 s in 0.01 s steps + result.skidded |= Coach->SlippingWheels; + if (auto const speed{std::abs(Coach.speed())}; speed > 0.0 && speed < 0.1) { + // almost stopped; a skidding wagon is still skidding at this point + result.stopped = true; + break; + } + Coach.step(Braking); + } + Coach.run(30.0, Standing); + return result; +} + +} // namespace + +// the engine doesn't let a stopped, braked vehicle slip (the Vel > 0.1 trap in TMoverParameters::ComputeTotalForce), +// so a wagon which stopped in a skid should be no different from one which stopped without it. it used to stay +// skidding instead, with its brakes capped by the sliding adhesion, and slid or crept away (eu07/maszyna#479) +TEST_CASE("wheelslip: a wagon which stopped in a skid holds like one which stopped without it") { + track const dry{0.15, 0.0}; + track const slippery{0.02, 0.0}; + // held only by the brakes: the sliding adhesion on this rail is too low for the slope + track const slipperyslope{0.02, 0.08}; + + vehicle normal{definition::load("wagon"), 40.0}; + auto const normalbraking{brake_to_stop_then_stand(normal, dry, slipperyslope)}; + vehicle skidding{definition::load("wagon"), 40.0}; + auto const skiddingbraking{brake_to_stop_then_stand(skidding, slippery, slipperyslope)}; + + // the test means something only when the braking went as planned + REQUIRE(normalbraking.stopped); + REQUIRE_FALSE(normalbraking.skidded); + REQUIRE(skiddingbraking.stopped); + REQUIRE(skiddingbraking.skidded); + + CHECK(std::abs(normal.speed()) < 0.1); + CHECK(std::abs(skidding.speed()) < 0.1); + CHECK_FALSE(skidding->SlippingWheels); +} diff --git a/tests/unit/vehicle.cpp b/tests/unit/vehicle.cpp index 135c5c2f..c0930d13 100644 --- a/tests/unit/vehicle.cpp +++ b/tests/unit/vehicle.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "vehicle.h" +#include "vehicle/DynObj.h" #include #include @@ -159,6 +160,18 @@ vehicle::vehicle(definition const &Definition, double Velocity, std::string cons } void vehicle::step(track const &Track, double Dt) { + compute_forces(Dt); + compute_movement(Track, Dt, TLocation{0.0, 0.0, 0.0}); +} + +void vehicle::compute_forces(double Dt) { + if (m_pipepressure >= 0.0) { + m_mover->Pipe->CreatePress(m_pipepressure); + } + m_mover->ComputeTotalForce(Dt); +} + +double vehicle::compute_movement(track const &Track, double Dt, TLocation const &Location) { TTrackShape shape; shape.Len = 1000.0; shape.dHtrack = Track.gradient; @@ -167,13 +180,8 @@ void vehicle::step(track const &Track, double Dt) { param.friction = Track.friction; param.CategoryFlag = 1; TTractionParam const traction; - TLocation const location{0.0, 0.0, 0.0}; TRotation const rotation{0.0, 0.0, 0.0}; - if (m_pipepressure >= 0.0) { - m_mover->Pipe->CreatePress(m_pipepressure); - } - m_mover->ComputeTotalForce(Dt); - m_mover->ComputeMovement(Dt, Dt, shape, param, traction, location, rotation); + return m_mover->ComputeMovement(Dt, Dt, shape, param, traction, Location, rotation); } void vehicle::run(double Seconds, track const &Track, double Dt) { @@ -184,4 +192,88 @@ void vehicle::run(double Seconds, track const &Track, double Dt) { } } +// the physics reaches the vehicle next to it through TDynamicObject::MoverParameters only (neighbour_data). the +// tests can't make a TDynamicObject, whose constructor and members come with the rest of the engine, so the +// neighbour is a zeroed block of its size with just that pointer set: enough for the physics, which reads nothing +// else of it, and never used as a whole object +static_assert(alignof(TDynamicObject) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__); + +namespace { + +std::vector make_standin(TMoverParameters &Mover) { + std::vector standin(sizeof(TDynamicObject)); // zeroed + reinterpret_cast(standin.data())->MoverParameters = &Mover; // NOSONAR see above + return standin; +} + +TDynamicObject *as_neighbour(std::byte *Standin) { + return reinterpret_cast(Standin); // NOSONAR see above +} + +} // namespace + +pair::pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap) { + auto const &first{First.mover()}; + auto const &second{Second.mover()}; + // the first vehicle in the middle, its FirstEnd towards the second one + m_vehicles[0] = {&First, FirstEnd, 0.0, FirstEnd == end::front ? 1.0 : -1.0, make_standin(First.mover())}; + m_vehicles[1] = {&Second, SecondEnd, 0.5 * first.Dim.L + Gap + 0.5 * second.Dim.L, SecondEnd == end::front ? -1.0 : 1.0, make_standin(Second.mover())}; +} + +pair::~pair() { + for (auto const &placed : m_vehicles) { + // the vehicles outlive the pair, they mustn't point at its stand-ins + for (auto &neighbour : placed.owner->mover().Neighbours) { + neighbour = neighbour_data(); + } + } +} + +void pair::find_neighbours() { + // as TDynamicObject::update_neighbours() + for (auto const &placed : m_vehicles) { + placed.owner->mover().Loc = TLocation{placed.position, 0.0, 0.0}; + } + for (int i = 0; i < 2; ++i) { + auto const &self{m_vehicles[i]}; + auto &other{m_vehicles[1 - i]}; // its stand-in is what this one points at + auto &mover{self.owner->mover()}; + auto const &othermover{other.owner->mover()}; + auto &neighbour{mover.Neighbours[self.end]}; + neighbour.vehicle = as_neighbour(other.standin.data()); + neighbour.vehicle_end = other.end; + neighbour.distance = static_cast(TMoverParameters::CouplerDist(&mover, &othermover) - mover.Couplers[self.end].adapter_length - + othermover.Couplers[other.end].adapter_length); + mover.Neighbours[1 - self.end] = neighbour_data(); + } +} + +void pair::step(track const &Track, double Dt) { + find_neighbours(); + for (auto const &placed : m_vehicles) { + placed.owner->compute_forces(Dt); + } + for (auto &placed : m_vehicles) { + placed.position += placed.heading * placed.owner->compute_movement(Track, Dt, TLocation{placed.position, 0.0, 0.0}); + } +} + +void pair::run(double Seconds, track const &Track, double Dt) { + // whole steps, as in vehicle::run() + auto const steps{std::lround(Seconds / Dt)}; + for (long i = 0; i < steps; ++i) { + step(Track, Dt); + } +} + +double pair::gap() const { + auto const &first{m_vehicles[0].owner->mover()}; + auto const &second{m_vehicles[1].owner->mover()}; + return std::abs(m_vehicles[1].position - m_vehicles[0].position) - 0.5 * (first.Dim.L + second.Dim.L); +} + +int pair::coupling() const { + return m_vehicles[0].owner->mover().Couplers[m_vehicles[0].end].CouplingFlag; +} + } // namespace test diff --git a/tests/unit/vehicle.h b/tests/unit/vehicle.h index ad036839..616e75d1 100644 --- a/tests/unit/vehicle.h +++ b/tests/unit/vehicle.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include @@ -64,8 +66,57 @@ public: double speed() const { return m_mover->V * 3.6; } private: + friend class pair; + + // the two halves of a step: the forces, taken by all the vehicles in the scene first, then the movement, + // the vehicle placed at Location. returns the distance travelled [m], positive forwards + void compute_forces(double Dt); + double compute_movement(track const &Track, double Dt, TLocation const &Location); + std::unique_ptr m_mover; double m_pipepressure{-1.0}; }; +// two vehicles on a straight track, the FirstEnd of the first one facing the SecondEnd of the second one Gap metres +// away (from coupler to coupler), with nothing else around. a step runs them the way the simulation runs the +// vehicles of a scene (vehicle_table::update()): each one finds its neighbour, then both take the forces, the +// coupler and buffer ones among them (CouplerForce()), then both move and handle the collisions (CollisionDetect()). +// they couple and part on their own, as the couplers let them; the speed of each runs along its own axis +// test::pair pair{moving, end::front, standing, end::rear, 1.0}; // 1 m apart +// pair.run(5.0); // for 5 s +// CHECK(pair.coupled()); +class pair { +public: + pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap); + ~pair(); + pair(pair const &) = delete; + pair &operator=(pair const &) = delete; + // the vehicles point at the stand-ins of the pair, which stay where they are + pair(pair &&) = delete; + pair &operator=(pair &&) = delete; + + // advances both vehicles by one step of Dt + void step(track const &Track = {}, double Dt = 0.01); + // advances both vehicles by Seconds in steps of Dt + void run(double Seconds, track const &Track = {}, double Dt = 0.01); + // distance between the facing couplers [m], negative while the buffers are pressed in + double gap() const; + // type of the coupling between the vehicles, coupling::faux when they aren't coupled + int coupling() const; + bool coupled() const { return coupling() != coupling::faux; } + +private: + struct placement { + vehicle *owner{nullptr}; + int end{0}; // the end facing the other vehicle + double position{0.0}; // of the middle of the vehicle along the track [m] + double heading{1.0}; // +1 when the vehicle runs forwards towards the growing position, -1 otherwise + std::vector standin; // what the other vehicle sees as this one, see vehicle.cpp + }; + + void find_neighbours(); + + std::array m_vehicles; +}; + } // namespace test diff --git a/utilities/translation.cpp b/utilities/translation.cpp index b1db1227..c0dbeb1a 100644 --- a/utilities/translation.cpp +++ b/utilities/translation.cpp @@ -218,6 +218,7 @@ std::string locale::label_cab_control(std::string const &Label) { "watercircuitslink_sw:", STRN("water circuits link") }, { "fuelpump_sw:", STRN("fuel pump") }, { "oilpump_sw:", STRN("oil pump") }, + { "oilfuelpump_sw:", STRN("oil and fuel pumps") }, { "motorblowersfront_sw:", STRN("motor blowers A") }, { "motorblowersrear_sw:", STRN("motor blowers B") }, { "motorblowersalloff_sw:", STRN("all motor blowers") }, diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 5bbaa80e..a6a9d181 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -30,6 +30,8 @@ http://mozilla.org/MPL/2.0/. #include "world/station.h" #include "input/keyboardinput.h" +#include + #define LOGVELOCITY 0 #define LOGORDERS 1 #define LOGSTOPS 1 @@ -1093,10 +1095,9 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub if (Point.fDist < passengerstopmaxdistance * 0.5 ) { // zaliczamy posterunek w pewnej odległości przed (choć W4 nie zasłania już semafora) #if LOGSTOPS - WriteLog( - pVehicle->asName + " as " + TrainParams.TrainName - + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute) - + " passed " + asNextStop); // informacja + WriteLog(std::format( + "{} as {}: at {}:{} passed {}", + pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute, asNextStop)); // informacja #endif // przy jakim dystansie (stanie licznika) ma przesunąć na następny postój fLastStopExpDist = mvOccupied->DistCounter + 0.250 + 0.001 * fLength; @@ -1253,10 +1254,9 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub TrainParams.StationIndexInc(); // przejście do następnej asNextStop = TrainParams.NextStop(); // pobranie kolejnego miejsca zatrzymania #if LOGSTOPS - WriteLog( - pVehicle->asName + " as " + TrainParams.TrainName - + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute) - + " next " + asNextStop); // informacja + WriteLog(std::format( + "{} as {}: at {}:{} next {}", + pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute, asNextStop)); // informacja #endif // update consist weight, brake settings and ai braking tables // NOTE: this calculation is expected to run after completing loading/unloading @@ -1297,13 +1297,16 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub else { // jeśli dojechaliśmy do końca rozkładu #if LOGSTOPS - WriteLog( - pVehicle->asName + " as " + TrainParams.TrainName - + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute) - + " end of route."); // informacja + WriteLog(std::format( + "{} as {}: at {}:{} end of route.", + pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute)); // informacja #endif asNextStop = TrainParams.NextStop(); // informacja o końcu trasy TrainParams.NewName("none"); // czyszczenie nieaktualnego rozkładu + // koniec kursu, gasimy wyświetlacze relacji (tablice blaszane zostają) + for( auto *vehicle { pVehicles[ end::front ] }; vehicle != nullptr; vehicle = vehicle->Next() ) { + vehicle->DestinationClear(); + } // ma nie podjeżdżać pod W4 i ma je pomijać iDrivigFlags &= ~moveStopCloser; if( false == TestFlag( iDrivigFlags, movePushPull ) ) { diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 32ba307f..c6793e45 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -4150,6 +4150,8 @@ bool TDynamicObject::Update(double dt, double dt1) // mirrors + auto const mirrorleft { dMirrorMoveL }; + auto const mirrorright { dMirrorMoveR }; if( MoverParameters->Vel > MoverParameters->MirrorVelClose || MoverParameters->CabActive == 0 && activation::mirrors || MoverParameters->MirrorForbidden ) { @@ -4180,6 +4182,8 @@ bool TDynamicObject::Update(double dt, double dt1) dMirrorMoveR + 1.0 * dt1 ); } } + play_mirror_sounds( side::left, m_mirrormotion[ side::left ].update( dMirrorMoveL - mirrorleft ) ); + play_mirror_sounds( side::right, m_mirrormotion[ side::right ].update( dMirrorMoveR - mirrorright ) ); // compartment lights // if the vehicle has a controller, we base the light state on state of the controller otherwise we check the vehicle itself @@ -4825,12 +4829,13 @@ void TDynamicObject::RenderSounds() { // horns { - // for moving vehicle combine regular horn activation flag with emergency brake horn activation flag, if the brake is active - auto const warningsignal { ( - MoverParameters->Vel > 0.5 && MoverParameters->AlarmChainFlag ? + // for moving vehicle combine regular horn activation flag with emergency brake horn activation flag, if the brake is active; + // the horns without the air or the voltage they need stay silent + auto const warningsignal { MoverParameters->SoundingHorns( + ( MoverParameters->Vel > 0.5 && MoverParameters->AlarmChainFlag ? MoverParameters->EmergencyBrakeWarningSignal : 0 ) - | MoverParameters->WarningSignal }; + | MoverParameters->WarningSignal ) }; if( TestFlag( warningsignal, 1 ) ) { sHorn1.play( sound_flags::exclusive | sound_flags::looping ); @@ -6363,6 +6368,19 @@ void TDynamicObject::LoadMMediaFile( std::string const &TypeName, std::string co } } + else if( token == "mirroropen:" || token == "mirrorclose:" ) { + sound_source soundtemplate { sound_placement::general }; + soundtemplate.deserialize( parser, sound_type::single ); + soundtemplate.owner( this ); + for( auto &mirror : m_mirrorsounds ) { + // apply configuration to all defined mirrors, but preserve their individual offsets + auto &sound { token == "mirroropen:" ? mirror.unfold : mirror.fold }; + auto const mirroroffset { sound.offset() }; + sound = soundtemplate; + sound.offset( mirroroffset ); + } + } + else if( token == "doorstepclose:" ) { sound_source soundtemplate { sound_placement::general }; soundtemplate.deserialize( parser, sound_type::single ); @@ -6576,6 +6594,27 @@ void TDynamicObject::LoadMMediaFile( std::string const &TypeName, std::string co } } + else if( token == "mirrors:" ) { + // a list of offsets along vehicle's z-axis of the mirror pairs, one at each cab; followed with "end" + while( (token = parser.getToken()) != "" + && token != "end" ) { + // vehicle faces +Z in 'its' space, as for the doors negative value means ahead of centre + auto const offset { std::atof( token.c_str() ) * -1.f }; + mirror_sounds mirror; + mirror.end = offset >= 0.f ? end::front : end::rear; + for( auto const mirrorside : { side::left, side::right } ) { + auto const location { glm::vec3 { + MoverParameters->Dim.W * ( mirrorside == side::left ? 0.5f : -0.5f ), + MoverParameters->Dim.H * 0.5f, + offset } }; + mirror.placement = mirrorside; + mirror.unfold.offset( location ); + mirror.fold.offset( location ); + m_mirrorsounds.emplace_back( mirror ); + } + } + } + else if( token == "tractionmotors:" ) { // a list of offsets along vehicle's z-axis; followed with "end" while( (token = parser.getToken()) != "" @@ -7794,7 +7833,29 @@ int TDynamicObject::RouteWish(TTrack *tr) return Mechanik ? Mechanik->CrossRoute(tr) : 0; // wg AI albo prosto }; -void TDynamicObject::DestinationSet(std::string to, std::string numer) { +void TDynamicObject::play_mirror_sounds( side const Side, mirror_moves::event const Event ) { + + if( Event == mirror_moves::event::none ) { return; } + // as UpdateMirror(), only the mirrors of the occupied cab move + if( MoverParameters->CabOccupied == 0 ) { return; } + auto const cabend { MoverParameters->CabOccupied > 0 ? end::front : end::rear }; + for( auto &mirror : m_mirrorsounds ) { + if( mirror.placement != Side || mirror.end != cabend ) { continue; } + auto &sound { Event == mirror_moves::event::unfolding ? mirror.unfold : mirror.fold }; + auto &other { Event == mirror_moves::event::unfolding ? mirror.fold : mirror.unfold }; + other.stop(); + sound.play( sound_flags::exclusive ); + } +} + +void TDynamicObject::DestinationClear() { + + if( false == DestinationSign.has_light ) { return; } // physical destination signs remain up until manually changed + // update_destinations() shows the blank sign in place of the missing destination + DestinationSign.destination = null_handle; +} + +void TDynamicObject::DestinationSet(std::string const &to, std::string const &numer) { // ustawienie stacji docelowej oraz wymiennej tekstury 4, jeśli istnieje plik // w zasadzie, to każdy wagon mógłby mieć inną stację docelową // zwłaszcza w towarowych, pod kątem zautomatyzowania maewrów albo pracy górki @@ -7823,6 +7884,15 @@ void TDynamicObject::DestinationSet(std::string to, std::string numer) { if( DestinationSign.script.empty() ) { return; } // no script so no way to make the texture if( numer == "none" ) { return; } // blank or incomplete/malformed timetable, don't bother + auto const trainname { [this]() -> std::string { + if( ctOwner != nullptr ) { return ctOwner->TrainName(); } + if( Mechanik != nullptr ) { return Mechanik->TrainName(); } + return "none"; } }; // shouldn't get here but, eh + auto const instance { [this]() -> std::string { + if( DestinationSign.instancing == "name" ) { return MoverParameters->Name; } + if( DestinationSign.instancing == "type" ) { return MoverParameters->TypeName; } + return "none"; } }; + std::string signrequest { "make:" + DestinationSign.script @@ -7834,17 +7904,10 @@ void TDynamicObject::DestinationSet(std::string to, std::string numer) { ctOwner->Vehicle()->MoverParameters->Name ) // owned vehicle, safer to point to owner as carriages can have identical names // basic instancing string // NOTE: underscore doesn't have any magic meaning for the time being, it's just less likely to conflict with regular dictionary keys - + "&_id1=" + ( - ctOwner != nullptr ? ctOwner->TrainName() : - Mechanik != nullptr ? Mechanik->TrainName() : - "none" ) }; // shouldn't get here but, eh + + "&_id1=" + trainname() }; // TBD, TODO: replace instancing with support for variables in extra parameters string? if( false == DestinationSign.instancing.empty() ) { - signrequest += - "&_id2=" + ( - DestinationSign.instancing == "name" ? MoverParameters->Name : - DestinationSign.instancing == "type" ? MoverParameters->TypeName : - "none" ); + signrequest += "&_id2=" + instance(); } // optionl extra parameters if( false == DestinationSign.parameters.empty() ) { diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 36335a32..5d52ba16 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "interfaces/ITexture.h" #include "audio/sound.h" #include "world/Spring.h" +#include "vehicle/mirrors.h" #include @@ -393,6 +394,13 @@ private: side placement {}; }; + struct mirror_sounds { + sound_source unfold { sound_placement::general }; + sound_source fold { sound_placement::general }; + side placement {}; + int end {}; // of the vehicle, the mirror moves with the cab at this end occupied + }; + struct exchange_sounds { sound_source loading { sound_placement::general }; sound_source unloading { sound_placement::general }; @@ -567,6 +575,8 @@ private: std::vector m_pantographsounds; // typically 2 but can be less (or more?) std::vector m_doorsounds; // can expect symmetrical arrangement, but don't count on it bool m_doorlocks { false }; // sound helper, current state of door locks + std::vector m_mirrorsounds; + std::array m_mirrormotion; // left and right sound_source sHorn1 { sound_placement::external, 5 * EU07_SOUND_RUNNINGNOISECUTOFFRANGE }; sound_source sHorn2 { sound_placement::external, 5 * EU07_SOUND_RUNNINGNOISECUTOFFRANGE }; sound_source sHorn3 { sound_placement::external, 5 * EU07_SOUND_RUNNINGNOISECUTOFFRANGE }; @@ -703,6 +713,8 @@ private: void Move(double fDistance); void FastMove(double fDistance); void RenderSounds(); + // plays the sounds of the mirrors on the Side, of the occupied cab, starting the Event + void play_mirror_sounds( side const Side, mirror_moves::event const Event ); inline glm::dvec3 GetPosition() const { return vPosition; }; // converts location from vehicle coordinates frame to world frame @@ -815,7 +827,9 @@ private: void ParamSet(int what, int into); // zapytanie do AI, po którym segmencie skrzyżowania jechać int RouteWish(TTrack *tr); - void DestinationSet(std::string to, std::string numer); + void DestinationSet(std::string const &to, std::string const &numer); + // blanks electronic destination sign at the end of the service; physical signs stay until changed + void DestinationClear(); material_handle DestinationFind( std::string Destination ); void OverheadTrack(float o); glm::dvec3 get_future_movement() const; diff --git a/vehicle/Gauge.cpp b/vehicle/Gauge.cpp index d7a41062..af8e9a06 100644 --- a/vehicle/Gauge.cpp +++ b/vehicle/Gauge.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "vehicle/Gauge.h" +#include "vehicle/gaugepositions.h" #include "utilities/parser.h" #include "model/Model3d.h" #include "vehicle/DynObj.h" @@ -302,42 +303,37 @@ bool TGauge::UpdateValue( float fNewDesired ) { auto const desiredtimes100 = static_cast( std::round( 100.0 * fNewDesired ) ); - if( desiredtimes100 == static_cast( std::round( 100.0 * m_targetvalue ) ) ) { + auto const previoustimes100 = static_cast( std::round( 100.0 * m_targetvalue ) ); + if( desiredtimes100 == previoustimes100 ) { return true; } m_targetvalue = fNewDesired; - // if there's any sound associated with new requested value, play it - // check value-specific table first... - if( auto const fullinteger { desiredtimes100 % 100 == 0 }; fullinteger ) { - // filter out values other than full integers - auto const lookup = m_soundfxvalues.find( desiredtimes100 / 100 ); - if( lookup != m_soundfxvalues.end() ) { - lookup->second.play(); - return true; - } - } - else { + if( desiredtimes100 % 100 != 0 ) { // toggle the control to continous range/exclusive sound mode from now on m_soundtype = sound_flags::exclusive; } + // if there's any sound associated with a position reached on the way to the new requested value, play it + // check value-specific table first... + if( auto const reached = gauge_positions::last_reached( m_soundfxvalues, previoustimes100, desiredtimes100 ); + reached != m_soundfxvalues.end() ) { + reached->second.play(); + return true; + } // ...and if there isn't any, fall back on the basic set... // HACK: crude way to discern controls with continuous and quantized value range - if( auto const currentvalue = GetValue(); currentvalue < fNewDesired ) { + auto const currentvalue = GetValue(); + if( currentvalue < fNewDesired && false == m_soundfxincrease.empty() ) { // shift up - if( false == m_soundfxincrease.empty() ) { - m_soundfxincrease.play( m_soundtype ); - return true; - } + m_soundfxincrease.play( m_soundtype ); + return true; } - else if( currentvalue > fNewDesired ) { + if( currentvalue > fNewDesired && false == m_soundfxdecrease.empty() ) { // shift down - if( false == m_soundfxdecrease.empty() ) { - m_soundfxdecrease.play( m_soundtype ); - return true; - } + m_soundfxdecrease.play( m_soundtype ); + return true; } return false; // no suitable sound was found -}; +} void TGauge::PutValue(float fNewDesired) { // McZapkie-281102: natychmiastowe wpisanie wartosci diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index f0b2367f..1272a137 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "vehicle/Train.h" +#include "vehicle/horn.h" #include "utilities/Globals.h" #include "simulation/simulation.h" @@ -340,6 +341,7 @@ TTrain::commandhandler_map const TTrain::m_commandhandlers = { {user_command::fuelpumpenable, &TTrain::OnCommand_fuelpumpenable}, {user_command::fuelpumpdisable, &TTrain::OnCommand_fuelpumpdisable}, {user_command::oilpumptoggle, &TTrain::OnCommand_oilpumptoggle}, + {user_command::oilfuelpumptoggle, &TTrain::OnCommand_oilfuelpumptoggle}, {user_command::oilpumpenable, &TTrain::OnCommand_oilpumpenable}, {user_command::oilpumpdisable, &TTrain::OnCommand_oilpumpdisable}, {user_command::waterheaterbreakertoggle, &TTrain::OnCommand_waterheaterbreakertoggle}, @@ -3995,6 +3997,37 @@ void TTrain::OnCommand_oilpumpdisable(TTrain *Train, command_data const &Command } } +void TTrain::OnCommand_oilfuelpumptoggle(TTrain *Train, command_data const &Command) +{ + if (Command.action == GLFW_REPEAT) + { + return; + } + + auto *vehicle{Train->mvControlled}; + if (Train->ggOilFuelPumpButton.type() == TGaugeType::push) + { + // impulse switch, as the separate ones: held to start both pumps + auto const pressed{Command.action == GLFW_PRESS}; + Train->ggOilFuelPumpButton.UpdateValue(pressed ? 1.0 : 0.0, Train->dsbSwitch); + vehicle->FuelPumpSwitch(pressed); + vehicle->OilPumpSwitch(pressed); + return; + } + // two-state switch, only cares about press events + if (Command.action != GLFW_PRESS) + { + return; + } + // both pumps go off if both were on, otherwise both go on + auto const enable{false == (vehicle->FuelPump.is_enabled && vehicle->OilPump.is_enabled)}; + Train->ggOilFuelPumpButton.UpdateValue(enable ? 1.0 : 0.0, Train->dsbSwitch); + vehicle->FuelPumpSwitch(enable); + vehicle->FuelPumpSwitchOff(false == enable); + vehicle->OilPumpSwitch(enable); + vehicle->OilPumpSwitchOff(false == enable); +} + void TTrain::OnCommand_waterheaterbreakertoggle(TTrain *Train, command_data const &Command) { @@ -6605,7 +6638,7 @@ void TTrain::OnCommand_heatingtoggle(TTrain *Train, command_data const &Command) OnCommand_heatingdisable(Train, Command); } } - else if (Command.action == GLFW_RELEASE && Train->ggTrainHeatingButton.type() == TGaugeType::push) + else if (Command.action == GLFW_RELEASE && (Train->ggTrainHeatingButton.type() == TGaugeType::push || Train->mvOccupied->HeatingSwitchImpulse)) { // impulse switch // visual feedback @@ -6623,6 +6656,11 @@ void TTrain::OnCommand_heatingenable(TTrain *Train, command_data const &Command) // visual feedback Train->ggTrainHeatingButton.UpdateValue(1.0, Train->dsbSwitch); } + else if (Command.action == GLFW_RELEASE && Train->mvOccupied->HeatingSwitchImpulse) + { + // the switch springs back to the middle + Train->ggTrainHeatingButton.UpdateValue(0.0, Train->dsbSwitch); + } } void TTrain::OnCommand_heatingdisable(TTrain *Train, command_data const &Command) @@ -6632,8 +6670,21 @@ void TTrain::OnCommand_heatingdisable(TTrain *Train, command_data const &Command { Train->mvOccupied->HeatingSwitch(false); - // visual feedback - Train->ggTrainHeatingButton.UpdateValue(Train->ggTrainHeatingButton.type() == TGaugeType::push ? 1.0 : 0.0, Train->dsbSwitch); + // visual feedback: an impulse switch is pushed the other way, down + auto position{0.0}; + if (Train->mvOccupied->HeatingSwitchImpulse) + { + position = -1.0; + } + else if (Train->ggTrainHeatingButton.type() == TGaugeType::push) + { + position = 1.0; + } + Train->ggTrainHeatingButton.UpdateValue(position, Train->dsbSwitch); + } + else if (Command.action == GLFW_RELEASE && Train->mvOccupied->HeatingSwitchImpulse) + { + Train->ggTrainHeatingButton.UpdateValue(0.0, Train->dsbSwitch); } } @@ -7841,16 +7892,10 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman if (Command.action == GLFW_PRESS) { // only need to react to press, sound will continue until stopped - if (false == TestFlag(Train->mvOccupied->WarningSignal, 1)) + if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::low)) { // turn on - Train->mvOccupied->WarningSignal |= 1; - /* - if( true == TestFlag( Train->mvOccupied->WarningSignal, 2 ) ) { - // low and high horn are treated as mutually exclusive - Train->mvControlled->WarningSignal &= ~2; - } - */ + Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::low, Train->has_single_horn_lever()); // visual feedback Train->ggHornButton.UpdateValue(-1.0); Train->ggHornLowButton.UpdateValue(1.0); @@ -7859,13 +7904,13 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman else if (Command.action == GLFW_RELEASE) { // turn off - /* - // NOTE: we turn off both low and high horn, due to unreliability of release event when shift key is involved - Train->mvOccupied->WarningSignal &= ~( 1 | 2 ); - */ - Train->mvOccupied->WarningSignal &= ~1; + Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::low); // visual feedback - Train->ggHornButton.UpdateValue(0.0); + if (false == (Train->has_single_horn_lever() && TestFlag(Train->mvOccupied->WarningSignal, horn::high))) + { + // leave the single lever alone if it was moved to the other tone in the meantime + Train->ggHornButton.UpdateValue(0.0); + } Train->ggHornLowButton.UpdateValue(0.0); } } @@ -7885,16 +7930,10 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma if (Command.action == GLFW_PRESS) { // only need to react to press, sound will continue until stopped - if (false == TestFlag(Train->mvOccupied->WarningSignal, 2)) + if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::high)) { // turn on - Train->mvOccupied->WarningSignal |= 2; - /* - if( true == TestFlag( Train->mvOccupied->WarningSignal, 1 ) ) { - // low and high horn are treated as mutually exclusive - Train->mvControlled->WarningSignal &= ~1; - } - */ + Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::high, Train->has_single_horn_lever()); // visual feedback Train->ggHornButton.UpdateValue(1.0); Train->ggHornHighButton.UpdateValue(1.0); @@ -7903,13 +7942,13 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma else if (Command.action == GLFW_RELEASE) { // turn off - /* - // NOTE: we turn off both low and high horn, due to unreliability of release event when shift key is involved - Train->mvOccupied->WarningSignal &= ~( 1 | 2 ); - */ - Train->mvOccupied->WarningSignal &= ~2; + Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::high); // visual feedback - Train->ggHornButton.UpdateValue(0.0); + if (false == (Train->has_single_horn_lever() && TestFlag(Train->mvOccupied->WarningSignal, horn::low))) + { + // leave the single lever alone if it was moved to the other tone in the meantime + Train->ggHornButton.UpdateValue(0.0); + } Train->ggHornHighButton.UpdateValue(0.0); } } @@ -7953,12 +7992,7 @@ void TTrain::OnCommand_radiotoggle(TTrain *Train, command_data const &Command) return; } - // NOTE: we ignore the lack of 3d model to allow system reset after receiving radio-stop signal - /* - if( false == Train->m_controlmapper.contains( "radio_sw:" ) ) { - return; - } - */ + // NOTE: works without the switch in the 3d model too, to allow system reset after receiving radio-stop signal // only reacting to press, so the sound can loop uninterrupted if (false == Train->mvOccupied->Radio) { @@ -8003,9 +8037,13 @@ void TTrain::OnCommand_radiochannelincrease(TTrain *Train, command_data const &C if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->RadioChannel() + 1; - OnCommand_radiochannelset(Train, newCommand); + // the channel buttons of a radio switched off or without power do nothing + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->RadioChannel() + 1; + OnCommand_radiochannelset(Train, newCommand); + } Train->ggRadioChannelNext.UpdateValue(1.0); } @@ -8021,9 +8059,12 @@ void TTrain::OnCommand_radiochanneldecrease(TTrain *Train, command_data const &C if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->RadioChannel() - 1; - OnCommand_radiochannelset(Train, newCommand); + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->RadioChannel() - 1; + OnCommand_radiochannelset(Train, newCommand); + } Train->ggRadioChannelPrevious.UpdateValue(1.0); } @@ -8049,7 +8090,7 @@ void TTrain::OnCommand_radiostopsend(TTrain *Train, command_data const &Command) if (Command.action == GLFW_PRESS) { - if (true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->mvOccupied->RadioPowered()) { simulation::Region->RadioStop(Train->Dynamic()->GetPosition()); } @@ -8067,7 +8108,7 @@ void TTrain::OnCommand_radiostopenable(TTrain *Train, command_data const &Comman { if (Command.action == GLFW_PRESS && Train->ggRadioStop.GetValue() == 0) { - if (true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->mvOccupied->RadioPowered()) { simulation::Region->RadioStop(Train->Dynamic()->GetPosition()); } @@ -8090,7 +8131,7 @@ void TTrain::OnCommand_radiostoptest(TTrain *Train, command_data const &Command) if (Command.action == GLFW_PRESS) { - if (Train->RadioChannel() == 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->RadioChannel() == 10 && Train->mvOccupied->RadioPowered()) { Train->Dynamic()->RadioStop(); } @@ -8108,7 +8149,7 @@ void TTrain::OnCommand_radiocall1send(TTrain *Train, command_data const &Command { if (Command.action == GLFW_PRESS) { - if (Train->RadioChannel() != 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->RadioChannel() != 10 && Train->mvOccupied->RadioPowered()) { simulation::Events.queue_receivers(radio_message::call1, Train->Dynamic()->GetPosition()); } @@ -8127,7 +8168,7 @@ void TTrain::OnCommand_radiocall3send(TTrain *Train, command_data const &Command if (Command.action == GLFW_PRESS) { - if (Train->RadioChannel() != 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->RadioChannel() != 10 && Train->mvOccupied->RadioPowered()) { simulation::Events.queue_receivers(radio_message::call3, Train->Dynamic()->GetPosition()); } @@ -8146,9 +8187,13 @@ void TTrain::OnCommand_radiovolumeincrease(TTrain *Train, command_data const &Co if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->m_radiovolume + 0.125; - OnCommand_radiovolumeset(Train, newCommand); + // so do the volume buttons + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->m_radiovolume + 0.125; + OnCommand_radiovolumeset(Train, newCommand); + } Train->ggRadioVolumeNext.UpdateValue(1.0); } else if (Command.action == GLFW_RELEASE) @@ -8163,9 +8208,12 @@ void TTrain::OnCommand_radiovolumedecrease(TTrain *Train, command_data const &Co if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->m_radiovolume - 0.125; - OnCommand_radiovolumeset(Train, newCommand); + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->m_radiovolume - 0.125; + OnCommand_radiovolumeset(Train, newCommand); + } Train->ggRadioVolumePrevious.UpdateValue(1.0); } else if (Command.action == GLFW_RELEASE) @@ -8931,7 +8979,7 @@ bool TTrain::Update(double const Deltatime) btLampkaOpory.Turn(mvControlled->StLinFlag && mvControlled->ResistorsFlagCheck()); btLampkaBezoporowa.Turn(mvControlled->ResistorsFlagCheck() || mvControlled->MainCtrlActualPos == 0); // do EU04 - btLampkaStyczn.Turn(!mvControlled->StLinFlag && !mvControlled->ControlPressureSwitch && mvControlled->BrakePress < 1.0); // mozna prowadzic rozruch + btLampkaStyczn.Turn(mvControlled->LineContactorsLampLit()); // mozna prowadzic rozruch btLampkaPrzekRozn.Turn(!mvControlled->GroundRelay && !mvControlled->ControlPressureSwitch && mvControlled->BrakePress < 1.0); // relay is off and needs a reset btLampkaNadmSil.Turn(mvControlled->FuseFlagCheck() && !mvControlled->ControlPressureSwitch && mvControlled->BrakePress < 1.0); // relay is off and needs a reset btLampkaUkrotnienie.Turn(TestFlag(mvControlled->Couplers[mvControlled->CabOccupied == 1 ? rear : front].CouplingFlag, control)); @@ -9180,14 +9228,7 @@ bool TTrain::Update(double const Deltatime) btLampkaOporyB.Turn(mover->ResistorsFlagCheck()); btLampkaBezoporowaB.Turn(true == mover->ResistorsFlagCheck() || mover->MainCtrlActualPos == 0); // do EU04 - if (mover->StLinFlag || mover->ControlPressureSwitch) - { - btLampkaStycznB.Turn(false); - } - else if (mover->BrakePress < 1.0) - { - btLampkaStycznB.Turn(true); // mozna prowadzic rozruch - } + btLampkaStycznB.Turn(mover->LineContactorsLampLit()); // mozna prowadzic rozruch // hunter-271211: sygnalizacja poslizgu w pierwszym pojezdzie, gdy wystapi w drugim if (mover->SlippingWheels) { @@ -9583,6 +9624,7 @@ bool TTrain::Update(double const Deltatime) ggWaterCircuitsLinkButton.Update(); ggFuelPumpButton.Update(); ggOilPumpButton.Update(); + ggOilFuelPumpButton.Update(); ggMotorBlowersFrontButton.Update(); ggMotorBlowersRearButton.Update(); ggMotorBlowersAllOffButton.Update(); @@ -10106,7 +10148,7 @@ void TTrain::update_sounds_radio() std::erase_if(m_radiomessages, [](auto const &source) { return false == source.second->is_playing(); }); } // adjust audibility of remaining messages based on current radio conditions - auto const radioenabled{true == mvOccupied->Radio && (mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable)}; + auto const radioenabled{mvOccupied->RadioPowered()}; for (auto const &[messagechannel, messagesound] : m_radiomessages) { auto const volume{true == radioenabled && Dynamic()->Mechanik != nullptr && messagechannel == RadioChannel() ? m_radiovolume : 0.0}; @@ -10893,7 +10935,7 @@ void TTrain::radio_message(sound_source *Message, int const Channel) m_radiomessages.emplace_back(Channel, std::make_shared(m_radiosound)); // assign sound to the template and play it auto &message = *m_radiomessages.back().second.get(); - auto const radioenabled{true == mvOccupied->Radio && (mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable)}; + auto const radioenabled{mvOccupied->RadioPowered()}; auto const volume{true == radioenabled && Dynamic()->Mechanik != nullptr && Channel == RadioChannel() ? 1.0 : 0.0}; message.copy_sounds(*Message).gain(static_cast(volume)).play(); } @@ -11081,6 +11123,7 @@ void TTrain::clear_cab_controls() ggWaterCircuitsLinkButton.Clear(); ggFuelPumpButton.Clear(); ggOilPumpButton.Clear(); + ggOilFuelPumpButton.Clear(); ggMotorBlowersFrontButton.Clear(); ggMotorBlowersRearButton.Clear(); ggMotorBlowersAllOffButton.Clear(); @@ -11362,7 +11405,7 @@ void TTrain::set_cab_controls(int const Cab) ggDoorStepButton.PutValue(mvOccupied->Doors.step_enabled ? 1.f : 0.f); } // heating - if (false == ggTrainHeatingButton.is_push()) + if (false == ggTrainHeatingButton.is_push() && false == mvControlled->HeatingSwitchImpulse) { ggTrainHeatingButton.PutValue(mvControlled->Heating ? 1.f : 0.f); } @@ -11415,6 +11458,11 @@ void TTrain::set_cab_controls(int const Cab) { ggOilPumpButton.PutValue(mvControlled->OilPump.is_enabled ? 1.f : 0.f); } + // joint oil and fuel pump + if (ggOilFuelPumpButton.type() != TGaugeType::push) + { + ggOilFuelPumpButton.PutValue(mvControlled->FuelPump.is_enabled && mvControlled->OilPump.is_enabled ? 1.f : 0.f); + } // traction motor fans if (ggMotorBlowersFrontButton.type() != TGaugeType::push) { @@ -11742,6 +11790,7 @@ bool TTrain::initialize_gauge(cParser &Parser, std::string const &Label, int con {"watercircuitslink_sw:", ggWaterCircuitsLinkButton}, {"fuelpump_sw:", ggFuelPumpButton}, {"oilpump_sw:", ggOilPumpButton}, + {"oilfuelpump_sw:", ggOilFuelPumpButton}, {"oilpressb:", ggOilPressB}, {"motorblowersfront_sw:", ggMotorBlowersFrontButton}, {"motorblowersrear_sw:", ggMotorBlowersRearButton}, diff --git a/vehicle/Train.h b/vehicle/Train.h index 4fc11dc2..009b349b 100644 --- a/vehicle/Train.h +++ b/vehicle/Train.h @@ -195,6 +195,9 @@ class TTrain { bool initialize_button(cParser &Parser, std::string const &Label, int const Cabindex); // helper, returns true for EMU with oerlikon brake bool is_eztoer() const; + // helper, returns true if both horn tones are operated with the same lever, without separate low and high tone buttons + bool has_single_horn_lever() const { + return ggHornButton.SubModel != nullptr && ggHornLowButton.SubModel == nullptr && ggHornHighButton.SubModel == nullptr; } // locates nearest vehicle belonging to the consist TDynamicObject *find_nearest_consist_vehicle(bool freefly, glm::vec3 pos) const; // mover master controller to specified position @@ -339,6 +342,7 @@ class TTrain { static void OnCommand_fuelpumpenable( TTrain *Train, command_data const &Command ); static void OnCommand_fuelpumpdisable( TTrain *Train, command_data const &Command ); static void OnCommand_oilpumptoggle( TTrain *Train, command_data const &Command ); + static void OnCommand_oilfuelpumptoggle( TTrain *Train, command_data const &Command ); static void OnCommand_oilpumpenable( TTrain *Train, command_data const &Command ); static void OnCommand_oilpumpdisable( TTrain *Train, command_data const &Command ); static void OnCommand_waterheaterbreakertoggle( TTrain *Train, command_data const &Command ); @@ -692,7 +696,8 @@ public: // reszta może by?publiczna TGauge ggWaterHeaterButton; // water heater switch TGauge ggWaterCircuitsLinkButton; TGauge ggFuelPumpButton; // fuel pump switch - TGauge ggOilPumpButton; // fuel pump switch + TGauge ggOilPumpButton; // oil pump switch + TGauge ggOilFuelPumpButton; // joint oil and fuel pump switch TGauge ggMotorBlowersFrontButton; // front traction motor fan switch TGauge ggMotorBlowersRearButton; // rear traction motor fan switch TGauge ggMotorBlowersAllOffButton; // motor fans shutdown switch diff --git a/vehicle/gaugepositions.h b/vehicle/gaugepositions.h new file mode 100644 index 00000000..fc82ed44 --- /dev/null +++ b/vehicle/gaugepositions.h @@ -0,0 +1,35 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +// positions of a cab control which have sounds of their own +namespace gauge_positions { + +// the entry of the last position of Positions the control reached on its way from the Previous to the Desired value, +// both in hundredths of a position, or Positions.end() if it reached none: a lever moved smoothly passes the +// positions rather than stops at them, and should sound each one it gets to +template +typename std::map::iterator last_reached(std::map &Positions, int const Previous, int const Desired) { + auto reached{Positions.end()}; + for (auto position{Positions.begin()}; position != Positions.end(); ++position) { + auto const at{position->first * 100}; + if (Desired > Previous && at > Previous && at <= Desired) { + reached = position; // moving up, the last one in the ascending order is the one reached last + } + else if (Desired < Previous && at < Previous && at >= Desired) { + return position; // moving down, the first one is + } + } + return reached; +} + +} // namespace gauge_positions diff --git a/vehicle/horn.cpp b/vehicle/horn.cpp new file mode 100644 index 00000000..006274bb --- /dev/null +++ b/vehicle/horn.cpp @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "vehicle/horn.h" + +namespace horn { + +int pressed(int const Signal, int const Tone, bool const Singlelever) { + auto const other{Tone == low ? high : low}; + return (Signal | Tone) & (Singlelever ? ~other : ~0); +} + +int released(int const Signal, int const Tone) { + return Signal & ~Tone; +} + +} // namespace horn diff --git a/vehicle/horn.h b/vehicle/horn.h new file mode 100644 index 00000000..615db2f1 --- /dev/null +++ b/vehicle/horn.h @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +// the horn controls of the cab, on the WarningSignal bits of the vehicle (1: low tone, 2: high tone) +namespace horn { + +int const low{1}; +int const high{2}; + +// the signal after the key or button of the Tone is pressed. a single lever for both tones, without +// separate buttons for them, can't sound both tones at once +int pressed(int Signal, int Tone, bool Singlelever); +// the signal after the key or button of the Tone is released +int released(int Signal, int Tone); + +} // namespace horn diff --git a/vehicle/mirrors.h b/vehicle/mirrors.h new file mode 100644 index 00000000..cf373554 --- /dev/null +++ b/vehicle/mirrors.h @@ -0,0 +1,44 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +// the mirrors of a vehicle, unfolded on the side of the open doors and folded at speed +namespace mirror_moves { + +enum class event { + none, + unfolding, + folding +}; + +// the move of a mirror from one update to the next. a mirror sounds as it sets off, unfolding or folding, not on +// every update it keeps moving the same way +struct motion { + int direction{0}; // of the last update: +1 unfolding, -1 folding, 0 standing + + // the event the Change of the mirror position (0: folded, 1: unfolded) since the last update starts, if any + event update(double const Change) { + auto now{0}; + if (Change > 0.0) { + now = 1; + } + else if (Change < 0.0) { + now = -1; + } + auto const setsoff{now != 0 && now != direction}; + direction = now; + if (false == setsoff) { + return event::none; + } + return now > 0 ? event::unfolding : event::folding; + } +}; + +} // namespace mirror_moves diff --git a/vr/openvr_imp.cpp b/vr/openvr_imp.cpp index 088fd5b5..15467d00 100644 --- a/vr/openvr_imp.cpp +++ b/vr/openvr_imp.cpp @@ -474,6 +474,9 @@ std::unordered_map vr_openvr::m_buttonb { "oilpump_sw:", { user_command::oilpumptoggle, user_command::none } }, + { "oilfuelpump_sw:", { + user_command::oilfuelpumptoggle, + user_command::none } }, { "motorblowersfront_sw:", { user_command::motorblowerstogglefront, user_command::none } },