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 } },