From ec345f94cbf606228a0c3246c3c0e2fdfd422d79 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 - 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 | 28 ++ McZapkie/Mover.cpp | 179 ++++++++---- tests/unit/README.md | 6 +- tests/unit/test_compressor.cpp | 80 ++++++ tests/unit/test_couplers.cpp | 422 ++++++++++++++++++++++++++++ tests/unit/test_gauge_positions.cpp | 46 +++ tests/unit/test_horn.cpp | 99 +++++++ tests/unit/test_radio.cpp | 28 ++ tests/unit/test_wheelslip.cpp | 62 ++++ tests/unit/vehicle.cpp | 104 ++++++- tests/unit/vehicle.h | 50 ++++ vehicle/Driver.cpp | 27 +- vehicle/DynObj.cpp | 38 ++- vehicle/DynObj.h | 4 +- vehicle/Gauge.cpp | 42 ++- vehicle/Train.cpp | 104 ++++--- vehicle/Train.h | 3 + vehicle/gaugepositions.h | 35 +++ vehicle/horn.cpp | 24 ++ vehicle/horn.h | 24 ++ 21 files changed, 1235 insertions(+), 171 deletions(-) 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_radio.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 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..ea5f1bf2 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 +{ + pneumatic, + 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::pneumatic}; // of the low and high tone horns (WarningSignal 1 and 2) + horn_supply Horn3Supply{horn_supply::pneumatic}; // 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}; @@ -2428,6 +2441,21 @@ 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 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..ed397edb 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; - } - */ - } + // TODO: a compressor not driven by the engine should draw power (some 5 A) from the high- or low voltage circuit } // ************************************************************************************************* @@ -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,20 @@ double TMoverParameters::CouplerForce(int const End, double dt) couplingtype &= ~coupling::control; } - if (Attach(End, otherend, othervehicle, couplingtype)) + if (false == AutomaticCouplingSafe(End, *othervehicle, otherend)) + { + // too fast for the couplers to catch, the vehicles collide instead (and may derail, see CollisionDetect()); + // the lock is reset once they part, so they don't couple as soon as the collision slows them down + thiscoupler.AutomaticCouplingAllowed = false; + othercoupler.AutomaticCouplingAllowed = false; + } + else 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 ); - */ } } } @@ -10468,6 +10516,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 +10733,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 +10829,15 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) // maksymalna predkosc dostepna na tarczce predkosciomierza extract_value(maxTachoSpeed, "MaxTachoSpeed", line, ""); + // horns + { + std::map> const hornsupplies{{"Pneumatic", horn_supply::pneumatic}, {"Electric", horn_supply::electric}}; + auto lookup = hornsupplies.find(extract_value("HornType", line)); + HornSupply = lookup != hornsupplies.end() ? lookup->second : horn_supply::pneumatic; + 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 +10997,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; } } 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_compressor.cpp b/tests/unit/test_compressor.cpp new file mode 100644 index 00000000..7f8577f9 --- /dev/null +++ b/tests/unit/test_compressor.cpp @@ -0,0 +1,80 @@ +#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 fed from the main circuit, which starts on its own, and an empty main reservoir for it to fill +vehicle compressor_vehicle(double const Delay) { + auto definition{definition::load("wagon")}; + definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 0.65).set("Brake", "MaxCP", 0.8); + definition.set("Brake", "CompressorSpeed", 0.01).set("Brake", "CompressorPower", "Main"); + 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); +} diff --git a/tests/unit/test_couplers.cpp b/tests/unit/test_couplers.cpp new file mode 100644 index 00000000..f6b3b721 --- /dev/null +++ b/tests/unit/test_couplers.cpp @@ -0,0 +1,422 @@ +#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) : m_previous{Global.crash_damage} { 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; +}; + +} // 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}; + auto parted{false}; + for (int i = 0; i < 300 && false == parted; ++i) { + pair.step(); + parted = pair.gap() > 0.05; + coupled = coupled || (false == parted && 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 && false == (touched && pair.gap() > 0.3); ++i) { + pair.step(); + touched = touched || pair.gap() < 0.0; + } + 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..89fc4e06 --- /dev/null +++ b/tests/unit/test_gauge_positions.cpp @@ -0,0 +1,46 @@ +#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) == ""); +} diff --git a/tests/unit/test_horn.cpp b/tests/unit/test_horn.cpp new file mode 100644 index 00000000..5df1f247 --- /dev/null +++ b/tests/unit/test_horn.cpp @@ -0,0 +1,99 @@ +#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::pneumatic); // 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::pneumatic); + 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)); +} 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_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..6b02ed68 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::unique_ptr make_standin(TMoverParameters &Mover) { + auto standin{std::make_unique(sizeof(TDynamicObject))}; // zeroed + reinterpret_cast(standin.get())->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 const &other{m_vehicles[1 - i]}; + auto &mover{self.owner->mover()}; + auto const &othermover{other.owner->mover()}; + auto &neighbour{mover.Neighbours[self.end]}; + neighbour.vehicle = as_neighbour(other.standin.get()); + 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..146275af 100644 --- a/tests/unit/vehicle.h +++ b/tests/unit/vehicle.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -64,8 +65,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 /* m */}; +// pair.run(5.0 /* 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::unique_ptr standin; // what the other vehicle sees as this one, see vehicle.cpp + }; + + void find_neighbours(); + + std::array m_vehicles; +}; + } // namespace test 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..af74d465 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -4825,12 +4825,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 ); @@ -7794,7 +7795,14 @@ 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::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 +7831,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 +7851,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..ef42eb40 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -815,7 +815,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..5f08d132 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" @@ -7841,16 +7842,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 +7854,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 +7880,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 +7892,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 +7942,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 +7987,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 +8009,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 +8040,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 +8058,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 +8081,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 +8099,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 +8118,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 +8137,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 +8158,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) @@ -10106,7 +10104,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 +10891,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(); } diff --git a/vehicle/Train.h b/vehicle/Train.h index 4fc11dc2..94ada767 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 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