diff --git a/CMakeLists.txt b/CMakeLists.txt
index 477f584e..dfe58b66 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -462,6 +462,7 @@ set(CORE_SOURCES
"utilities/utilities.cpp"
"scripting/ladderlogic.cpp"
"vehicle/handover.cpp"
+"vehicle/horn.cpp"
)
add_library(eu07_core STATIC ${CORE_SOURCES})
target_link_libraries(eu07_core PUBLIC eu07_settings)
diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h
index 29b3e75c..ea5f1bf2 100644
--- a/McZapkie/MOVER.h
+++ b/McZapkie/MOVER.h
@@ -676,6 +676,13 @@ struct TTractionParam
};
/*powyzsze parametry zwiazane sa z torem po ktorym aktualnie pojazd jedzie*/
+// what a horn needs to sound: air from the main reservoir, or the low voltage
+enum class horn_supply
+{
+ pneumatic,
+ electric
+};
+
/// High-level brake-system family.
enum class TBrakeSystem
{
@@ -1710,6 +1717,10 @@ class TMoverParameters
float DistanceCounterDoublePressPeriod = {1.f}; // czas w jakim nalezy podwojnie wcisnac przycisk, aby rozpoczac pomiar odleglosci
TSecuritySystem SecuritySystem;
int EmergencyBrakeWarningSignal{0}; // combined with basic WarningSignal when manual emergency brake is active
+ horn_supply HornSupply{horn_supply::pneumatic}; // of the low and high tone horns (WarningSignal 1 and 2)
+ horn_supply Horn3Supply{horn_supply::pneumatic}; // of the third horn (WarningSignal 4)
+ // main reservoir pressure [MPa] below which a pneumatic horn stays silent
+ static constexpr double HornMinPressure{0.2};
TUniversalCtrlTable UniCtrlList; /*lista pozycji uniwersalnego nastawnika*/
int UniCtrlListSize = 0; /*wielkosc listy pozycji uniwersalnego nastawnika*/
bool UniCtrlIntegratedBrakePNCtrl = false; /*zintegrowany nastawnik JH obsluguje hamulec PN*/
@@ -2001,6 +2012,8 @@ class TMoverParameters
bool CompressorAllow = false; /*! zezwolenie na uruchomienie sprezarki NBMX*/
bool CompressorAllowLocal{true}; // local device state override (most units don't have this fitted so it's set to true not to intefere)
bool CompressorGovernorLock{false}; // indicates whether compressor pressure switch was activated due to reaching cut-out pressure
+ double CompressorStartDelay{0.0}; // time [s] the compressor waits after getting its power before it can start, after the engine starts say
+ double CompressorPowerTime{0.0}; // time [s] since the compressor got its power
bool CompressorTankValve{false}; // indicates excessive pressure is vented from compressor tank directly and instantly
start_t CompressorStart{start_t::manual}; // whether the compressor is started manually, or another way
start_t PantographCompressorStart{start_t::manual};
@@ -2428,6 +2441,21 @@ class TMoverParameters
double BrakeForce(const TTrackParam &Track);
double CouplerForce(int const End, double dt);
void CollisionDetect(int const End, double const dt);
+ // closing speed of a collision [km/h] above which the vehicles involved may derail, adjusted for their mass ratio
+ static constexpr double SafeCollisionVelocity{15.0};
+ // closing speed [km/h] of the End of this vehicle and the OtherEnd of the other vehicle it touches, along the coupler
+ // axis: the sum of the speeds in a head-on collision, the difference in a rear-end one, negative when they part
+ double CollisionVelocity(int const End, TMoverParameters const &Other, int const OtherEnd) const;
+ // whether the automatic couplers at the End of this vehicle and the OtherEnd of the other one can catch each other on
+ // contact, rather than the vehicles colliding (with crash damage enabled, only at a closing speed which doesn't derail them)
+ bool AutomaticCouplingSafe(int const End, TMoverParameters const &Other, int const OtherEnd) const;
+ // the horns of the Signal (WarningSignal bits) which have the air or the voltage they need to sound
+ int SoundingHorns(int const Signal) const;
+ // whether the radio is switched on and has the low voltage to work
+ bool RadioPowered() const
+ {
+ return Radio && (Power24vIsAvailable || Power110vIsAvailable);
+ }
/*obrot kol uwzgledniajacy poslizg*/
double ComputeRotatingWheel(double WForce, double dt, double n) const;
diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp
index 3b5b6215..ed397edb 100644
--- a/McZapkie/Mover.cpp
+++ b/McZapkie/Mover.cpp
@@ -16,6 +16,8 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/Logs.h"
#include "utilities/parser.h"
#include "simulation/simulation.h"
+
+#include
//---------------------------------------------------------------------------
// Ra: tu należy przenosić funcje z mover.pas, które nie są z niego wywoływane.
@@ -1176,6 +1178,51 @@ double TMoverParameters::EngineMaxRPM() const
std::numeric_limits::max(); // shouldn't ever get here but, eh
}
+// *************************************************************************************************
+// prędkość zbliżania się stykających się końców pojazdów przy zderzeniu, w km/h
+// *************************************************************************************************
+double TMoverParameters::CollisionVelocity(int const End, TMoverParameters const &Other, int const OtherEnd) const
+{
+ // V runs along the vehicle's own axis, towards its front; the other vehicle's velocity is turned around when the
+ // vehicles face each other (touch with the same ends), as in CouplerForce()
+ auto const velocitydifference{V - Other.V * DirPatch(End, OtherEnd)};
+ // the front end closes in on the other vehicle when this one moves forward faster, the rear end when it's slower
+ return (End == end::front ? velocitydifference : -velocitydifference) * 3.6; // m/s -> km/h
+}
+
+bool TMoverParameters::AutomaticCouplingSafe(int const End, TMoverParameters const &Other, int const OtherEnd) const
+{
+ return false == Global.crash_damage || CollisionVelocity(End, Other, OtherEnd) <= SafeCollisionVelocity;
+}
+
+int TMoverParameters::SoundingHorns(int const Signal) const
+{
+ auto const supplied{[this](horn_supply const Supply) {
+ if (Supply == horn_supply::electric)
+ {
+ return Power24vIsAvailable || Power110vIsAvailable;
+ }
+ if (VeselVolume > 0.0)
+ {
+ return Compressor >= HornMinPressure;
+ }
+ // a vehicle without a main reservoir of its own takes the air from the main pipe, a vehicle without either
+ // (a road vehicle, say) has no air to check
+ auto const mainpipe{((Couplers[end::front].AllowedFlag | Couplers[end::rear].AllowedFlag) & coupling::mainhose) != 0};
+ return false == mainpipe || ScndPipePress >= HornMinPressure;
+ }};
+ auto horns{0};
+ if (supplied(HornSupply))
+ {
+ horns |= Signal & (1 | 2);
+ }
+ if (supplied(Horn3Supply))
+ {
+ horns |= Signal & 4;
+ }
+ return horns;
+}
+
// *************************************************************************************************
// Q: 20160716
// Wykrywanie kolizji
@@ -1192,8 +1239,11 @@ void TMoverParameters::CollisionDetect(int const End, double const dt)
auto const otherend{Neighbours[End].vehicle_end};
auto const &othercoupler{othervehicle->Couplers[otherend]};
+ // the velocities along this vehicle's axis: the other vehicle's one is turned around when the vehicles face each
+ // other (touch with the same ends), as in CouplerForce()
+ auto const otherdirection{DirPatch(End, otherend)};
auto velocity{V};
- auto othervehiclevelocity{othervehicle->V};
+ auto othervehiclevelocity{othervehicle->V * otherdirection};
// calculate collision force and new velocities for involved vehicles
auto const VirtualCoupling{(thiscoupler.CouplingFlag == coupling::faux)};
auto CCF{0.0};
@@ -1224,28 +1274,25 @@ void TMoverParameters::CollisionDetect(int const End, double const dt)
damage_coupler(End);
}
- if (thiscoupler.CouplingFlag == coupling::faux || true == TestFlag(othervehicle->DamageFlag, dtrain_out))
- { // HACK: limit excessive speed derailment checks to vehicles which aren't part of the same consist
- auto const safevelocitylimit{15.0};
- auto const velocitydifference{glm::length(glm::angleAxis(Rot.Rz, glm::dvec3{0, 1, 0}) * V - glm::angleAxis(othervehicle->Rot.Rz, glm::dvec3{0, 1, 0}) * othervehicle->V) *
- 3.6}; // m/s -> km/h
+ // HACK: limit excessive speed derailment checks to vehicles which aren't part of the same consist
+ auto const outsideconsist{thiscoupler.CouplingFlag == coupling::faux || true == TestFlag(othervehicle->DamageFlag, dtrain_out)};
+ auto const velocitydifference{outsideconsist ? CollisionVelocity(End, *othervehicle, otherend) : 0.0};
- if (velocitydifference > safevelocitylimit)
+ if (velocitydifference > SafeCollisionVelocity)
+ {
+ // HACK: crude estimation for potential derail, will take place with velocity difference > 15 km/h adjusted for vehicle mass ratio
+ if (false == TestFlag(DamageFlag, dtrain_out) || false == TestFlag(othervehicle->DamageFlag, dtrain_out))
{
- // HACK: crude estimation for potential derail, will take place with velocity difference > 15 km/h adjusted for vehicle mass ratio
- if (false == TestFlag(DamageFlag, dtrain_out) || false == TestFlag(othervehicle->DamageFlag, dtrain_out))
- {
- WriteLog("Bad driving: " + Name + " and " + othervehicle->Name + " collided with velocity " + to_string(velocitydifference, 0) + " km/h");
- }
+ WriteLog("Bad driving: " + Name + " and " + othervehicle->Name + " collided with velocity " + to_string(velocitydifference, 0) + " km/h");
+ }
- if (velocitydifference > safevelocitylimit * (TotalMass / othervehicle->TotalMass))
- {
- Derail(COLLISION);
- }
- if (velocitydifference > safevelocitylimit * (othervehicle->TotalMass / TotalMass))
- {
- othervehicle->Derail(COLLISION);
- }
+ if (velocitydifference > SafeCollisionVelocity * (TotalMass / othervehicle->TotalMass))
+ {
+ Derail(COLLISION);
+ }
+ if (velocitydifference > SafeCollisionVelocity * (othervehicle->TotalMass / TotalMass))
+ {
+ othervehicle->Derail(COLLISION);
}
}
}
@@ -1254,21 +1301,15 @@ void TMoverParameters::CollisionDetect(int const End, double const dt)
if (false == TestFlag(DamageFlag, dtrain_out))
{
auto const accelerationchange{(velocity - V) / dt};
- // if( accelerationchange / AccS < 1.0 ) {
- // HACK: prevent excessive vehicle pinball cases
AccS += accelerationchange;
- // AccS = std::clamp( AccS, -2.0, 2.0 );
V = velocity;
- // }
}
if (false == TestFlag(othervehicle->DamageFlag, dtrain_out))
{
- auto const othervehicleaccelerationchange{(othervehiclevelocity - othervehicle->V) / dt};
- // if( othervehicleaccelerationchange / othervehicle->AccS < 1.0 ) {
- // HACK: prevent excessive vehicle pinball cases
+ auto const othervehiclenewvelocity{othervehiclevelocity * otherdirection}; // back along the other vehicle's axis
+ auto const othervehicleaccelerationchange{(othervehiclenewvelocity - othervehicle->V) / dt};
othervehicle->AccS += othervehicleaccelerationchange;
- othervehicle->V = othervehiclevelocity;
- // }
+ othervehicle->V = othervehiclenewvelocity;
}
}
@@ -4332,7 +4373,10 @@ void TMoverParameters::CompressorCheck(double dt)
}
}
- auto const compressorpower{(CompressorPower == 0 ? Mains : CompressorPower == 3 ? Mains : Power110vIsAvailable)};
+ // the main circuit, also for a compressor driven by the engine, otherwise the low voltage
+ auto const compressorpower{CompressorPower == 0 || CompressorPower == 3 ? Mains : Power110vIsAvailable};
+ CompressorPowerTime = compressorpower ? CompressorPowerTime + dt : 0.0;
+ auto const compressorready{compressorpower && CompressorPowerTime >= CompressorStartDelay};
// TBD: split CompressorAllow into separate enable/disable flags, inherit compressor from basic_device
auto const compressorenable{CompressorAllowLocal && (CompressorStart == start_t::automatic || CompressorAllow)};
auto const compressordisable{false == compressorenable};
@@ -4354,7 +4398,7 @@ void TMoverParameters::CompressorCheck(double dt)
auto const compressorflag{CompressorFlag};
CompressorFlag =
- compressorpower && false == compressordisable && (false == governorlock || CompressorPower == 3) && (CompressorFlag || (compressorenable && LastSwitchingTime > CtrlDelay));
+ compressorready && false == compressordisable && (false == governorlock || CompressorPower == 3) && (CompressorFlag || (compressorenable && LastSwitchingTime > CtrlDelay));
if (CompressorFlag && CompressorFlag != compressorflag)
{
@@ -4384,30 +4428,23 @@ void TMoverParameters::CompressorCheck(double dt)
{
// vent some air out if there's no governor lock to stop the compressor from exceeding acceptable pressure level
SetFlag(SoundFlag, sound::relay | sound::loud);
- CompressedVolume *= false == governorlockispresent ? 0.80 : // arbitrary amount
- CompressorTankValve ? MinCompressorF / MaxCompressorF : // drop to mincompressor level
- 0.999; // HACK: drop a tiny bit so the sound doesn't trigger repeatedly
+ auto ventedfraction{0.999}; // HACK: drop a tiny bit so the sound doesn't trigger repeatedly
+ if (false == governorlockispresent)
+ {
+ ventedfraction = 0.80; // arbitrary amount
+ }
+ else if (CompressorTankValve)
+ {
+ ventedfraction = MinCompressorF / MaxCompressorF; // drop to mincompressor level
+ }
+ CompressedVolume *= ventedfraction;
if (false == governorlockispresent || CompressorTankValve)
{
CompressorGovernorLock = false;
}
}
- // tymczasowo tylko obciążenie sprężarki, tak z 5A na sprężarkę
- // TODO: draw power from proper high- or low voltage circuit
- if (CompressorPower == 3)
- {
- // diesel-powered compressor doesn't draw power
- }
- else
- {
- // TODO: drain power from 110v circuit
- /*
- if( compressorowner != nullptr ) {
- compressorowner->TotalCurrent += 0.0015 * compressorowner->PantographVoltage;
- }
- */
- }
+ // TODO: a compressor not driven by the engine should draw power (some 5 A) from the high- or low voltage circuit
}
// *************************************************************************************************
@@ -5008,7 +5045,7 @@ void TMoverParameters::ComputeTotalForce(double dt)
}
else
{
- EngineVoltage *= DirAbsolute; // DirActive*CabActive;
+ EngineVoltage *= DirAbsolute; // the reverser of the active cab, relative to the couplers
}
} // bo nie dzialalo
else
@@ -5032,6 +5069,14 @@ void TMoverParameters::ComputeTotalForce(double dt)
{
SlippingWheels = true;
}
+ if (true == SlippingWheels && Vel < 0.1 // the same crude trap: (nearly) stopped vehicle...
+ && std::abs(nrot * M_PI * WheelDiameter - V) < 0.1 / 3.6) // ...with wheels turning at its speed, i.e. locked wheels at the end of a skid
+ {
+ // locked wheels never pass the rotation speed test below once the vehicle stops, and a skidding vehicle
+ // keeps its braking force capped by the sliding adhesion, so the rest of the consist could push it around
+ SlippingWheels = false;
+ nrot = V / M_PI / WheelDiameter;
+ }
double temp_nrot = nrot;
if (true == SlippingWheels)
{
@@ -5399,17 +5444,20 @@ double TMoverParameters::CouplerForce(int const End, double dt)
couplingtype &= ~coupling::control;
}
- if (Attach(End, otherend, othervehicle, couplingtype))
+ if (false == AutomaticCouplingSafe(End, *othervehicle, otherend))
+ {
+ // too fast for the couplers to catch, the vehicles collide instead (and may derail, see CollisionDetect());
+ // the lock is reset once they part, so they don't couple as soon as the collision slows them down
+ thiscoupler.AutomaticCouplingAllowed = false;
+ othercoupler.AutomaticCouplingAllowed = false;
+ }
+ else if (Attach(End, otherend, othervehicle, couplingtype))
{
// HACK: we're reusing sound enum to mark whether vehicle was connected to another
SetFlag(AIFlag, sound::attachcoupler);
thiscoupler.AutomaticCouplingAllowed = false;
othercoupler.AutomaticCouplingAllowed = false;
}
- /*
- coupler.CouplingFlag = ( coupler.AutomaticCouplingFlag & othercoupler.AutomaticCouplingFlag );
- SetFlag( coupler.sounds, sound::attachcoupler );
- */
}
}
}
@@ -10468,6 +10516,7 @@ void TMoverParameters::LoadFIZ_Brake(std::string const &line)
auto lookup = compressorpowers.find(extract_value("CompressorPower", line));
CompressorPower = lookup != compressorpowers.end() ? lookup->second : 1;
}
+ extract_value(CompressorStartDelay, "CompressorStartDelay", line, "");
if (true == extract_value(AirLeakRate, "AirLeakRate", line, ""))
{
@@ -10684,7 +10733,7 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line)
for (int idx = 0; idx < 4; ++idx)
{
- extract_value(BrakeDelay[idx], "BDelay" + std::to_string(idx + 1), line, "");
+ extract_value(BrakeDelay[idx], std::format("BDelay{}", idx + 1), line, "");
}
// brakedelays, brakedelayflag
{
@@ -10780,6 +10829,15 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line)
// maksymalna predkosc dostepna na tarczce predkosciomierza
extract_value(maxTachoSpeed, "MaxTachoSpeed", line, "");
+ // horns
+ {
+ std::map> const hornsupplies{{"Pneumatic", horn_supply::pneumatic}, {"Electric", horn_supply::electric}};
+ auto lookup = hornsupplies.find(extract_value("HornType", line));
+ HornSupply = lookup != hornsupplies.end() ? lookup->second : horn_supply::pneumatic;
+ lookup = hornsupplies.find(extract_value("Horn3Type", line));
+ Horn3Supply = lookup != hornsupplies.end() ? lookup->second : HornSupply;
+ }
+
// dynamicbrake
{
std::map> dynamicbrakes{{"Passive", dbrake_passive}, {"Switch", dbrake_switch}, {"Reversal", dbrake_reversal}, {"Automatic", dbrake_automatic}};
@@ -10939,14 +10997,15 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line)
// ground relay
{
auto lookup = starts.find(extract_value("GroundRelayStart", line));
- GroundRelayStart = lookup != starts.end() ? lookup->second : TrainType == dt_EZT ? start_t::automatic : start_t::manual;
+ auto const defaultstart{TrainType == dt_EZT ? start_t::automatic : start_t::manual};
+ GroundRelayStart = lookup != starts.end() ? lookup->second : defaultstart;
}
// converter overload relay
{
auto lookup = starts.find(extract_value("ConverterOverloadRelayStart", line));
- ConverterOverloadRelayStart = lookup != starts.end() ? lookup->second :
- TrainType == dt_EZT ? start_t::converter : // relay activates when converter is switched off
- start_t::manual;
+ // the relay of a multiple unit activates when the converter is switched off
+ auto const defaultstart{TrainType == dt_EZT ? start_t::converter : start_t::manual};
+ ConverterOverloadRelayStart = lookup != starts.end() ? lookup->second : defaultstart;
}
}
diff --git a/tests/unit/README.md b/tests/unit/README.md
index bca46e83..97cc9491 100644
--- a/tests/unit/README.md
+++ b/tests/unit/README.md
@@ -56,8 +56,10 @@ TEST_CASE("brakes: service braking stops a heavier wagon too") {
functions on the vehicle state the class calls, and the test calls them the same way: see
`vehicle/handover.h`, which `TController::TakeControl()` uses, and `test_handover.cpp`.
- `vehicle` creates the vehicle the way `TDynamicObject::Init()` does and steps it the way the
- simulation loop does, on a straight track of the given friction and gradient. Coupled vehicles
- aren't supported yet.
+ simulation loop does, on a straight track of the given friction and gradient. `pair` puts two of
+ them on that track facing each other with the given ends and runs them together, as the
+ simulation runs the vehicles of a scene: the coupler and buffer forces, collisions and automatic
+ coupling come from the physics (`test_couplers.cpp`). Longer consists aren't supported yet.
- Check behaviour (speeds, pressures, states the player sees) rather than internal flags, and make
the test check its own preconditions (`REQUIRE` that the braking skidded, before checking what a
skid leads to).
diff --git a/tests/unit/test_compressor.cpp b/tests/unit/test_compressor.cpp
new file mode 100644
index 00000000..7f8577f9
--- /dev/null
+++ b/tests/unit/test_compressor.cpp
@@ -0,0 +1,80 @@
+#include "stdafx.h"
+#include "doctest.h"
+#include "vehicle.h"
+
+using test::definition;
+using test::vehicle;
+
+// the compressor waiting a while after getting its power before it starts (CompressorStartDelay,
+// MaSzyna-EU07/maszyna#67): in a diesel it starts some time after the engine, rather than with it
+
+namespace {
+
+double const step{0.01};
+
+// a compressor fed from the main circuit, which starts on its own, and an empty main reservoir for it to fill
+vehicle compressor_vehicle(double const Delay) {
+ auto definition{definition::load("wagon")};
+ definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 0.65).set("Brake", "MaxCP", 0.8);
+ definition.set("Brake", "CompressorSpeed", 0.01).set("Brake", "CompressorPower", "Main");
+ definition.set("Cntrl", "CompressorStart", "Automatic");
+ if (Delay > 0.0) {
+ definition.set("Brake", "CompressorStartDelay", Delay);
+ }
+ vehicle vehicle{definition};
+ vehicle->CompressedVolume = 0.0;
+ vehicle->LastSwitchingTime = 10.0; // nothing was switched lately
+ return vehicle;
+}
+
+// runs the compressor for Seconds, the main circuit as it is, the time since the last switching going on as in
+// ComputeTotalForce()
+void run_compressor(vehicle &Vehicle, double const Seconds) {
+ for (auto i{std::lround(Seconds / step)}; i > 0; --i) {
+ Vehicle->LastSwitchingTime += step;
+ Vehicle->CompressorCheck(step);
+ }
+}
+
+} // namespace
+
+TEST_CASE("compressor: without a start delay the compressor starts with its power") {
+ auto vehicle{compressor_vehicle(0.0)};
+ run_compressor(vehicle, 1.0);
+ REQUIRE_FALSE(vehicle->CompressorFlag); // no power, no compressor
+
+ vehicle->Mains = true;
+ run_compressor(vehicle, step);
+ CHECK(vehicle->CompressorFlag);
+}
+
+TEST_CASE("compressor: the compressor starts the delay after getting its power") {
+ auto vehicle{compressor_vehicle(5.0)};
+ vehicle->Mains = true;
+
+ run_compressor(vehicle, 4.9);
+ CHECK_FALSE(vehicle->CompressorFlag);
+ CHECK(vehicle->Compressor == doctest::Approx(0.0));
+
+ run_compressor(vehicle, 0.2);
+ CHECK(vehicle->CompressorFlag);
+ run_compressor(vehicle, 10.0);
+ CHECK(vehicle->Compressor > 0.0); // and fills the reservoir
+}
+
+TEST_CASE("compressor: the delay starts over once the power is gone") {
+ auto vehicle{compressor_vehicle(5.0)};
+ vehicle->Mains = true;
+ run_compressor(vehicle, 10.0);
+ REQUIRE(vehicle->CompressorFlag);
+
+ vehicle->Mains = false; // the engine stopped
+ run_compressor(vehicle, 1.0);
+ CHECK_FALSE(vehicle->CompressorFlag);
+
+ vehicle->Mains = true; // and started again
+ run_compressor(vehicle, 4.9);
+ CHECK_FALSE(vehicle->CompressorFlag);
+ run_compressor(vehicle, 0.2);
+ CHECK(vehicle->CompressorFlag);
+}
diff --git a/tests/unit/test_couplers.cpp b/tests/unit/test_couplers.cpp
new file mode 100644
index 00000000..f6b3b721
--- /dev/null
+++ b/tests/unit/test_couplers.cpp
@@ -0,0 +1,422 @@
+#include "stdafx.h"
+#include "doctest.h"
+#include "vehicle.h"
+#include "utilities/Globals.h"
+
+using test::definition;
+using test::vehicle;
+
+namespace {
+
+// the wagon from data/wagon.json with automatic couplers at both ends, which couple the given connections
+definition automatic_definition(int Flag = coupling::coupler | coupling::brakehose) {
+ return definition::load("wagon").set("BuffCoupl", "CType", "Automatic").set("BuffCoupl", "AutomaticFlag", Flag);
+}
+
+// the wagon from data/wagon.json with automatic couplers at both ends, running at the given speed [km/h]
+vehicle automatic_wagon(double Velocity, std::string const &Name) {
+ return vehicle{automatic_definition(), Velocity, Name};
+}
+
+// the same wagon, twice as heavy
+vehicle heavy_automatic_wagon(double Velocity, std::string const &Name) {
+ return vehicle{automatic_definition().set("Param", "M", 2 * 42200), Velocity, Name};
+}
+
+bool derailed(vehicle &Vehicle) {
+ return TestFlag(Vehicle->DamageFlag, dtrain_out);
+}
+
+// runs the pair for Seconds, returns whether the vehicles were coupled at any point
+bool couple_during(test::pair &Pair, double Seconds) {
+ auto coupled{false};
+ for (auto i{std::lround(Seconds / 0.01)}; i > 0; --i) {
+ Pair.step();
+ coupled = coupled || Pair.coupled();
+ }
+ return coupled;
+}
+
+// sets the crash damage setting for the test, restores it after
+class crash_damage_setting {
+public:
+ explicit crash_damage_setting(bool Enabled) : m_previous{Global.crash_damage} { Global.crash_damage = Enabled; }
+ ~crash_damage_setting() { Global.crash_damage = m_previous; }
+ crash_damage_setting(crash_damage_setting const &) = delete;
+ crash_damage_setting &operator=(crash_damage_setting const &) = delete;
+
+private:
+ bool m_previous;
+};
+
+} // namespace
+
+// issue #86: automatic couplers used to catch at any closing speed, so the vehicles never collided nor derailed
+
+TEST_CASE("couplers: automatic couplers catch a standing wagon hit at a walking pace") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(10.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ REQUIRE(moving->Couplers[end::front].type() == TCouplerType::Automatic);
+ REQUIRE(moving.speed() == doctest::Approx(10.0));
+
+ // the front of the moving wagon runs into the rear of the standing one
+ CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(10.0));
+ CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
+ CHECK(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front));
+}
+
+TEST_CASE("couplers: automatic couplers don't catch a wagon hit too fast, the wagons collide instead") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(30.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ REQUIRE(moving.speed() == doctest::Approx(30.0));
+
+ CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) > TMoverParameters::SafeCollisionVelocity);
+ CHECK_FALSE(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
+ CHECK_FALSE(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front));
+}
+
+TEST_CASE("couplers: automatic couplers catch a wagon running ahead only a little slower") {
+ crash_damage_setting const crashdamage{true};
+ auto faster{automatic_wagon(60.0, "faster")};
+ auto slower{automatic_wagon(50.0, "slower")};
+
+ CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(10.0));
+ CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(10.0));
+ CHECK(faster->AutomaticCouplingSafe(end::front, slower.mover(), end::rear));
+}
+
+TEST_CASE("couplers: without crash damage automatic couplers catch at any speed") {
+ crash_damage_setting const crashdamage{false};
+ auto moving{automatic_wagon(30.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+
+ CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
+}
+
+// the closing speed used to treat the speed of each vehicle as a vector turned by half its heading, so for vehicles
+// facing each other it came out as the root of the sum of the squares of their speeds, rather than the sum
+
+TEST_CASE("couplers: the closing speed of a rear-end collision is the difference of the speeds") {
+ auto faster{automatic_wagon(40.0, "faster")};
+ auto slower{automatic_wagon(25.0, "slower")};
+
+ // running forwards, the front of the faster wagon hits the rear of the slower one
+ CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(15.0));
+ CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(15.0));
+ // the wagons running ahead of each other are parting
+ CHECK(slower->CollisionVelocity(end::front, faster.mover(), end::rear) == doctest::Approx(-15.0));
+
+ // backing up, the rear of the faster wagon hits the front of the slower one
+ auto backingfaster{automatic_wagon(-40.0, "backing faster")};
+ auto backingslower{automatic_wagon(-25.0, "backing slower")};
+ CHECK(backingfaster->CollisionVelocity(end::rear, backingslower.mover(), end::front) == doctest::Approx(15.0));
+ CHECK(backingslower->CollisionVelocity(end::front, backingfaster.mover(), end::rear) == doctest::Approx(15.0));
+}
+
+TEST_CASE("couplers: the closing speed of a head-on collision is the sum of the speeds") {
+ crash_damage_setting const crashdamage{true};
+ auto first{automatic_wagon(10.0, "first")};
+ auto second{automatic_wagon(10.0, "second")};
+
+ // both wagons run forwards, their fronts meet
+ CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) == doctest::Approx(20.0));
+ CHECK(second->CollisionVelocity(end::front, first.mover(), end::front) == doctest::Approx(20.0));
+ // 20 km/h is above the limit, the couplers don't catch (the old formula gave 14.1 km/h and coupled them)
+ CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) > TMoverParameters::SafeCollisionVelocity);
+ CHECK_FALSE(first->AutomaticCouplingSafe(end::front, second.mover(), end::front));
+ CHECK_FALSE(second->AutomaticCouplingSafe(end::front, first.mover(), end::front));
+
+ // both wagons back up, their rears meet
+ auto firstbacking{automatic_wagon(-10.0, "first backing")};
+ auto secondbacking{automatic_wagon(-10.0, "second backing")};
+ CHECK(firstbacking->CollisionVelocity(end::rear, secondbacking.mover(), end::rear) == doctest::Approx(20.0));
+
+ // running back to back, the wagons part
+ CHECK(first->CollisionVelocity(end::rear, second.mover(), end::rear) == doctest::Approx(-20.0));
+ CHECK(first->AutomaticCouplingSafe(end::rear, second.mover(), end::rear));
+
+ // slow enough, the couplers catch
+ auto slowfirst{automatic_wagon(5.0, "slow first")};
+ auto slowsecond{automatic_wagon(5.0, "slow second")};
+ CHECK(slowfirst->CollisionVelocity(end::front, slowsecond.mover(), end::front) == doctest::Approx(10.0));
+ CHECK(slowfirst->AutomaticCouplingSafe(end::front, slowsecond.mover(), end::front));
+}
+
+TEST_CASE("couplers: the closing speed of a hit on a standing vehicle is the speed of the moving one, whichever way it faces") {
+ auto moving{automatic_wagon(20.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+
+ // the standing wagon faces away from the moving one
+ CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(20.0));
+ CHECK(standing->CollisionVelocity(end::rear, moving.mover(), end::front) == doctest::Approx(20.0));
+ // the standing wagon faces the moving one
+ CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::front) == doctest::Approx(20.0));
+ CHECK(standing->CollisionVelocity(end::front, moving.mover(), end::front) == doctest::Approx(20.0));
+ // the moving wagon backs up into the standing one
+ auto backing{automatic_wagon(-20.0, "backing")};
+ CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(20.0));
+ CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::rear) == doctest::Approx(20.0));
+ // the moving wagon runs away from the standing one
+ CHECK(moving->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(-20.0));
+}
+
+// the coupling as the simulation runs it: two vehicles on a track, closing in on each other until the couplers (or
+// buffers) meet, the coupler forces and the collision computed by the physics, step by step
+
+TEST_CASE("couplers: automatic couplers catch right up to the safe closing speed") {
+ crash_damage_setting const crashdamage{true};
+ auto standing{automatic_wagon(0.0, "standing")};
+
+ auto below{automatic_wagon(14.9, "below")};
+ CHECK(below->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(14.9));
+ CHECK(below->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
+
+ auto above{automatic_wagon(15.1, "above")};
+ CHECK(above->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(15.1));
+ CHECK_FALSE(above->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
+ auto faster{automatic_wagon(15.5, "faster")};
+ CHECK_FALSE(faster->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
+}
+
+TEST_CASE("couplers: a wagon rolling slowly into a standing one couples with it") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(5.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+
+ pair.run(3.0);
+
+ REQUIRE(pair.coupled());
+ CHECK(moving->Couplers[end::front].CouplingFlag == (coupling::coupler | coupling::brakehose));
+ CHECK(standing->Couplers[end::rear].CouplingFlag == (coupling::coupler | coupling::brakehose));
+ CHECK(moving->Couplers[end::front].Connected == &standing.mover());
+ CHECK_FALSE(derailed(moving));
+ CHECK_FALSE(derailed(standing));
+ // coupled, they run on together
+ pair.run(2.0);
+ CHECK(moving.speed() == doctest::Approx(standing.speed()).epsilon(0.05));
+ CHECK(pair.gap() < 0.2);
+}
+
+TEST_CASE("couplers: a wagon hitting a standing one too fast doesn't couple, nor once the hit has slowed it down") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(30.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+
+ auto touched{false};
+ auto parted{false};
+ auto coupled{false};
+ auto locked{true};
+ for (int i = 0; i < 1000; ++i) {
+ pair.step();
+ // as far as the couplers know: the distance between them at the start of the step
+ auto const distance{moving->Neighbours[end::front].distance};
+ touched = touched || distance < 0.0;
+ parted = parted || (touched && distance > 0.05);
+ coupled = coupled || pair.coupled();
+ if (touched && false == parted) {
+ locked = locked && false == moving->Couplers[end::front].AutomaticCouplingAllowed && false == standing->Couplers[end::rear].AutomaticCouplingAllowed;
+ }
+ }
+
+ REQUIRE(touched);
+ CHECK_FALSE(coupled);
+ // the hit derailed and stopped them, the couplers next to each other, the automatic coupling locked all along
+ CHECK_FALSE(parted);
+ CHECK(locked);
+ CHECK(derailed(moving));
+ CHECK(derailed(standing));
+ CHECK(std::abs(moving.speed()) < 3.0);
+ CHECK(std::abs(standing.speed()) < 3.0);
+ CHECK_FALSE(couple_during(pair, 5.0));
+}
+
+TEST_CASE("couplers: the automatic coupling locked by a hit is unlocked once the vehicles part") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(3.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ // as a hit too fast or manual uncoupling leaves them
+ moving->Couplers[end::front].AutomaticCouplingAllowed = false;
+ standing->Couplers[end::rear].AutomaticCouplingAllowed = false;
+
+ SUBCASE("parted by more than 5 cm, they couple on the next approach") {
+ test::pair pair{moving, end::front, standing, end::rear, 0.5};
+ pair.step();
+ CHECK(moving->Couplers[end::front].AutomaticCouplingAllowed);
+ CHECK(couple_during(pair, 3.0));
+ }
+ SUBCASE("kept together, they don't") {
+ test::pair pair{moving, end::front, standing, end::rear, 0.0};
+ auto coupled{false};
+ auto parted{false};
+ for (int i = 0; i < 300 && false == parted; ++i) {
+ pair.step();
+ parted = pair.gap() > 0.05;
+ coupled = coupled || (false == parted && pair.coupled());
+ }
+ CHECK_FALSE(coupled);
+ }
+}
+
+// the couplers refuse a hit by the closing speed when they meet, the derail check (CollisionDetect()) takes the one
+// left when the buffers are pressed in all the way, after their springs have slowed the hit down: for the wagons here
+// by about 7 km/h. so a hit a little over the safe closing speed only bounces the vehicles apart
+
+TEST_CASE("couplers: a hit a little too fast for the couplers bounces the vehicles apart, uncoupled and on the rails") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(20.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+
+ CHECK_FALSE(couple_during(pair, 3.0));
+ CHECK_FALSE(derailed(moving));
+ CHECK_FALSE(derailed(standing));
+ CHECK(pair.gap() > 0.5);
+ CHECK(standing.speed() > moving.speed());
+}
+
+TEST_CASE("couplers: a hit too fast for the couplers derails the vehicles over the limit adjusted for their mass ratio") {
+ crash_damage_setting const crashdamage{true};
+
+ SUBCASE("equal masses: both derail") {
+ auto moving{automatic_wagon(30.0, "moving")};
+ auto standing{automatic_wagon(0.0, "standing")};
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+ CHECK_FALSE(couple_during(pair, 5.0));
+ CHECK(derailed(moving));
+ CHECK(derailed(standing));
+ }
+ SUBCASE("half the mass: between the safe closing speed and twice that, only the lighter one derails") {
+ auto light{automatic_wagon(27.0, "light")};
+ auto heavy{heavy_automatic_wagon(0.0, "heavy")};
+ test::pair pair{light, end::front, heavy, end::rear, 1.0};
+ CHECK_FALSE(couple_during(pair, 5.0));
+ CHECK(derailed(light));
+ CHECK_FALSE(derailed(heavy));
+ }
+ SUBCASE("half the mass: above twice the safe closing speed both derail") {
+ auto light{automatic_wagon(45.0, "light")};
+ auto heavy{heavy_automatic_wagon(0.0, "heavy")};
+ test::pair pair{light, end::front, heavy, end::rear, 1.0};
+ CHECK_FALSE(couple_during(pair, 5.0));
+ CHECK(derailed(light));
+ CHECK(derailed(heavy));
+ }
+ SUBCASE("half the mass: below the safe closing speed the couplers catch, nothing derails") {
+ auto light{automatic_wagon(12.0, "light")};
+ auto heavy{heavy_automatic_wagon(0.0, "heavy")};
+ test::pair pair{light, end::front, heavy, end::rear, 1.0};
+ CHECK(couple_during(pair, 5.0));
+ CHECK_FALSE(derailed(light));
+ CHECK_FALSE(derailed(heavy));
+ }
+}
+
+TEST_CASE("couplers: wagons running head-on into each other couple only up to the safe closing speed") {
+ crash_damage_setting const crashdamage{true};
+
+ SUBCASE("7 + 7 km/h, the couplers catch") {
+ auto first{automatic_wagon(7.0, "first")};
+ auto second{automatic_wagon(7.0, "second")};
+ test::pair pair{first, end::front, second, end::front, 1.0};
+ CHECK(couple_during(pair, 3.0));
+ CHECK_FALSE(derailed(first));
+ CHECK_FALSE(derailed(second));
+ }
+ SUBCASE("8 + 8 km/h, the wagons bounce apart") {
+ auto first{automatic_wagon(8.0, "first")};
+ auto second{automatic_wagon(8.0, "second")};
+ test::pair pair{first, end::front, second, end::front, 1.0};
+ CHECK_FALSE(couple_during(pair, 3.0));
+ CHECK_FALSE(derailed(first));
+ CHECK_FALSE(derailed(second));
+ // both backwards, away from each other
+ CHECK(first.speed() < 0.0);
+ CHECK(second.speed() < 0.0);
+ }
+ SUBCASE("backing up, 8 + 8 km/h, the wagons bounce apart") {
+ auto first{automatic_wagon(-8.0, "first")};
+ auto second{automatic_wagon(-8.0, "second")};
+ test::pair pair{first, end::rear, second, end::rear, 1.0};
+ CHECK_FALSE(couple_during(pair, 3.0));
+ CHECK(first.speed() > 0.0);
+ CHECK(second.speed() > 0.0);
+ }
+ SUBCASE("15 + 15 km/h, both derail") {
+ auto first{automatic_wagon(15.0, "first")};
+ auto second{automatic_wagon(15.0, "second")};
+ test::pair pair{first, end::front, second, end::front, 1.0};
+ CHECK_FALSE(couple_during(pair, 5.0));
+ CHECK(derailed(first));
+ CHECK(derailed(second));
+ }
+}
+
+// the collision used to take the speed of a vehicle facing the other way as if it faced the same way, so a wagon
+// pushed away by the buffers of the vehicle hitting it head-on counted as running into it: the hit slowed it down,
+// and the hitter bounced back as from a wall
+
+TEST_CASE("couplers: a wagon hit head-on runs off the same as one hit from behind") {
+ // without automatic couplers or crash damage, the collision alone
+ crash_damage_setting const crashdamage{false};
+ auto speeds_after_hit = [](int StruckEnd) {
+ auto moving{vehicle{definition::load("wagon"), 20.0, "moving"}};
+ auto struck{vehicle{definition::load("wagon"), 0.0, "struck"}};
+ test::pair pair{moving, end::front, struck, StruckEnd, 0.5};
+ auto touched{false};
+ for (int i = 0; i < 300 && false == (touched && pair.gap() > 0.3); ++i) {
+ pair.step();
+ touched = touched || pair.gap() < 0.0;
+ }
+ REQUIRE(touched);
+ REQUIRE(pair.gap() > 0.3);
+ REQUIRE_FALSE(pair.coupled());
+ return std::make_pair(moving.speed(), struck.speed());
+ };
+
+ auto const [hitterfrombehind, struckfrombehind]{speeds_after_hit(end::rear)};
+ auto const [hitterheadon, struckheadon]{speeds_after_hit(end::front)};
+
+ // hit from behind, the struck wagon runs off forwards, faster than the hitter
+ CHECK(struckfrombehind > 10.0);
+ CHECK(hitterfrombehind < struckfrombehind);
+ // hit head-on, it runs off backwards, as fast
+ CHECK(struckheadon == doctest::Approx(-struckfrombehind).epsilon(0.02));
+ CHECK(hitterheadon == doctest::Approx(hitterfrombehind).epsilon(0.05));
+}
+
+// the other conditions of automatic coupling
+
+TEST_CASE("couplers: an automatic coupler doesn't catch a coupler of another type") {
+ crash_damage_setting const crashdamage{true};
+ auto moving{automatic_wagon(5.0, "moving")};
+ auto standing{vehicle{definition::load("wagon"), 0.0, "standing"}};
+ REQUIRE(standing->Couplers[end::rear].type() == TCouplerType::Screw);
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+
+ CHECK_FALSE(couple_during(pair, 3.0));
+}
+
+TEST_CASE("couplers: automatic couplers of different control systems couple without the control connection") {
+ crash_damage_setting const crashdamage{true};
+ auto const flag{coupling::coupler | coupling::brakehose | coupling::control};
+ auto moving{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 5.0, "moving"}};
+
+ SUBCASE("the same control system") {
+ auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 0.0, "standing"}};
+ REQUIRE(standing->Couplers[end::rear].control_type == "first");
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+ REQUIRE(couple_during(pair, 3.0));
+ CHECK(pair.coupling() == flag);
+ }
+ SUBCASE("a different one") {
+ auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "second"), 0.0, "standing"}};
+ REQUIRE(standing->Couplers[end::rear].control_type == "second");
+ test::pair pair{moving, end::front, standing, end::rear, 1.0};
+ REQUIRE(couple_during(pair, 3.0));
+ CHECK(pair.coupling() == (coupling::coupler | coupling::brakehose));
+ }
+}
diff --git a/tests/unit/test_gauge_positions.cpp b/tests/unit/test_gauge_positions.cpp
new file mode 100644
index 00000000..89fc4e06
--- /dev/null
+++ b/tests/unit/test_gauge_positions.cpp
@@ -0,0 +1,46 @@
+#include "stdafx.h"
+#include "doctest.h"
+
+#include "vehicle/gaugepositions.h"
+
+#include
+
+// the sounds of the positions of a cab control (MaSzyna-EU07/maszyna#83): a brake lever moved smoothly (Num3/Num9)
+// passes its positions instead of stopping at them, and sounds each one it reaches, as it does when put there
+
+namespace {
+
+// a brake lever with sounds at positions 0, 2 and 5; values in hundredths of a position
+std::map lever_sounds() {
+ return {{0, "release"}, {2, "running"}, {5, "service"}};
+}
+
+std::string reached(int const Previous, int const Desired) {
+ auto positions{lever_sounds()};
+ auto const position{gauge_positions::last_reached(positions, Previous, Desired)};
+ return position != positions.end() ? position->second : "";
+}
+
+} // namespace
+
+TEST_CASE("gauge positions: a lever put at a position sounds it") {
+ CHECK(reached(100, 200) == "running");
+ CHECK(reached(600, 500) == "service");
+}
+
+TEST_CASE("gauge positions: a lever moved smoothly past a position sounds it (MaSzyna-EU07/maszyna#83)") {
+ CHECK(reached(190, 205) == "running"); // moving up
+ CHECK(reached(510, 495) == "service"); // moving down
+}
+
+TEST_CASE("gauge positions: of the positions passed in one go the last one reached sounds") {
+ CHECK(reached(50, 520) == "service"); // up past 2 and 5
+ CHECK(reached(520, 10) == "running"); // down past 5 and 2, short of 0
+ CHECK(reached(520, 0) == "release");
+}
+
+TEST_CASE("gauge positions: moving between the positions or off one sounds nothing") {
+ CHECK(reached(210, 290) == "");
+ CHECK(reached(200, 230) == ""); // leaving a position isn't reaching it
+ CHECK(reached(200, 170) == "");
+}
diff --git a/tests/unit/test_horn.cpp b/tests/unit/test_horn.cpp
new file mode 100644
index 00000000..5df1f247
--- /dev/null
+++ b/tests/unit/test_horn.cpp
@@ -0,0 +1,99 @@
+#include "stdafx.h"
+#include "doctest.h"
+
+#include "vehicle.h"
+
+#include "vehicle/horn.h"
+
+// the horn keys of the cab: low tone (A) and high tone (shift+A), held to sound
+
+TEST_CASE("horn: a single lever for both tones sounds one tone at a time (eu07/maszyna#392)") {
+ auto signal{0};
+ signal = horn::pressed(signal, horn::low, true);
+ REQUIRE(signal == horn::low);
+
+ // the lever moved over to the high tone while the low tone key is still held
+ signal = horn::pressed(signal, horn::high, true);
+ CHECK(signal == horn::high);
+
+ // letting go of the low tone key leaves the high tone sounding
+ signal = horn::released(signal, horn::low);
+ CHECK(signal == horn::high);
+
+ signal = horn::released(signal, horn::high);
+ CHECK(signal == 0);
+}
+
+TEST_CASE("horn: separate buttons for the tones can sound both at once") {
+ auto signal{0};
+ signal = horn::pressed(signal, horn::low, false);
+ signal = horn::pressed(signal, horn::high, false);
+ CHECK(signal == (horn::low | horn::high));
+
+ signal = horn::released(signal, horn::low);
+ CHECK(signal == horn::high);
+}
+
+TEST_CASE("horn: releasing a tone leaves the other bits of the signal alone") {
+ auto const third{4}; // the third horn, operated separately
+ CHECK(horn::released(horn::low | third, horn::low) == third);
+ CHECK(horn::pressed(third, horn::high, true) == (horn::high | third));
+}
+
+// what the horns need to sound (eu07/maszyna#290): a pneumatic horn the air of the main reservoir, an electric one
+// the low voltage. the horn lever stays where the driver put it, only the horn is silent
+namespace {
+
+test::vehicle horn_vehicle(test::definition Definition) {
+ // a main reservoir of its own
+ return test::vehicle{Definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 0.65).set("Brake", "MaxCP", 0.8)};
+}
+
+int const allhorns{horn::low | horn::high | 4};
+
+} // namespace
+
+TEST_CASE("horn: a pneumatic horn sounds on the air of the main reservoir only") {
+ auto vehicle{horn_vehicle(test::definition::load("wagon"))};
+ REQUIRE(vehicle->HornSupply == horn_supply::pneumatic); // the default
+
+ vehicle->Compressor = 0.65;
+ CHECK(vehicle->SoundingHorns(allhorns) == allhorns);
+ vehicle->Compressor = 0.0; // the reservoir is empty
+ CHECK(vehicle->SoundingHorns(allhorns) == 0);
+}
+
+TEST_CASE("horn: a vehicle without a main reservoir takes the horn air from the main pipe") {
+ test::vehicle vehicle{test::definition::load("wagon")};
+ REQUIRE(vehicle->VeselVolume == 0.0);
+
+ vehicle->ScndPipePress = 0.5;
+ CHECK(vehicle->SoundingHorns(horn::low) == horn::low);
+ vehicle->ScndPipePress = 0.0;
+ CHECK(vehicle->SoundingHorns(horn::low) == 0);
+}
+
+TEST_CASE("horn: an electric horn sounds on the low voltage, whatever the air") {
+ auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "HornType", "Electric"))};
+ REQUIRE(vehicle->HornSupply == horn_supply::electric);
+ REQUIRE(vehicle->Horn3Supply == horn_supply::electric); // the third horn follows the others unless told otherwise
+
+ vehicle->Compressor = 0.0;
+ vehicle->Power24vIsAvailable = true;
+ CHECK(vehicle->SoundingHorns(allhorns) == allhorns);
+ vehicle->Power24vIsAvailable = false;
+ CHECK(vehicle->SoundingHorns(allhorns) == 0);
+}
+
+TEST_CASE("horn: the third horn can have a supply of its own, an electric horn of a diesel say") {
+ auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "Horn3Type", "Electric"))};
+ REQUIRE(vehicle->HornSupply == horn_supply::pneumatic);
+ REQUIRE(vehicle->Horn3Supply == horn_supply::electric);
+
+ vehicle->Compressor = 0.0;
+ vehicle->Power24vIsAvailable = true;
+ CHECK(vehicle->SoundingHorns(allhorns) == 4);
+ vehicle->Compressor = 0.65;
+ vehicle->Power24vIsAvailable = false;
+ CHECK(vehicle->SoundingHorns(allhorns) == (horn::low | horn::high));
+}
diff --git a/tests/unit/test_radio.cpp b/tests/unit/test_radio.cpp
new file mode 100644
index 00000000..33a12f72
--- /dev/null
+++ b/tests/unit/test_radio.cpp
@@ -0,0 +1,28 @@
+#include "stdafx.h"
+#include "doctest.h"
+#include "vehicle.h"
+
+// the radio works only switched on and with the low voltage (eu07/maszyna#429): its channel and volume buttons,
+// the calls and the radio-stop all ask RadioPowered()
+
+TEST_CASE("radio: a radio switched on with the low voltage works") {
+ test::vehicle vehicle{test::definition::load("wagon")};
+ vehicle->Radio = true;
+ SUBCASE("24 V") { vehicle->Power24vIsAvailable = true; }
+ SUBCASE("110 V") { vehicle->Power110vIsAvailable = true; }
+ CHECK(vehicle->RadioPowered());
+}
+
+TEST_CASE("radio: a radio switched off or without the low voltage doesn't") {
+ test::vehicle vehicle{test::definition::load("wagon")};
+ SUBCASE("switched off") {
+ vehicle->Radio = false;
+ vehicle->Power24vIsAvailable = true;
+ }
+ SUBCASE("batteries off") {
+ vehicle->Radio = true;
+ vehicle->Power24vIsAvailable = false;
+ vehicle->Power110vIsAvailable = false;
+ }
+ CHECK_FALSE(vehicle->RadioPowered());
+}
diff --git a/tests/unit/test_wheelslip.cpp b/tests/unit/test_wheelslip.cpp
new file mode 100644
index 00000000..9b39e82c
--- /dev/null
+++ b/tests/unit/test_wheelslip.cpp
@@ -0,0 +1,62 @@
+#include "stdafx.h"
+#include "doctest.h"
+#include "vehicle.h"
+
+using test::definition;
+using test::track;
+using test::vehicle;
+
+namespace {
+
+struct braking_result {
+ bool skidded{false}; // the wheels locked while braking
+ bool stopped{false}; // came (almost) to a stop on the braking rail
+};
+
+// brakes the wagon from 40 km/h on the Braking rail until it's (nearly) stopped, then has it stand on the
+// Standing one for 30 seconds
+braking_result brake_to_stop_then_stand(vehicle &Coach, track const &Braking, track const &Standing) {
+ braking_result result;
+ Coach.hold_brake_pipe(5.0);
+ Coach.run(5.0, Braking);
+ Coach.hold_brake_pipe(3.5); // service braking
+ // a skidding wagon brakes on the sliding adhesion only, give it time
+ for (auto step = 0; step < 60000; ++step) { // 600 s in 0.01 s steps
+ result.skidded |= Coach->SlippingWheels;
+ if (auto const speed{std::abs(Coach.speed())}; speed > 0.0 && speed < 0.1) {
+ // almost stopped; a skidding wagon is still skidding at this point
+ result.stopped = true;
+ break;
+ }
+ Coach.step(Braking);
+ }
+ Coach.run(30.0, Standing);
+ return result;
+}
+
+} // namespace
+
+// the engine doesn't let a stopped, braked vehicle slip (the Vel > 0.1 trap in TMoverParameters::ComputeTotalForce),
+// so a wagon which stopped in a skid should be no different from one which stopped without it. it used to stay
+// skidding instead, with its brakes capped by the sliding adhesion, and slid or crept away (eu07/maszyna#479)
+TEST_CASE("wheelslip: a wagon which stopped in a skid holds like one which stopped without it") {
+ track const dry{0.15, 0.0};
+ track const slippery{0.02, 0.0};
+ // held only by the brakes: the sliding adhesion on this rail is too low for the slope
+ track const slipperyslope{0.02, 0.08};
+
+ vehicle normal{definition::load("wagon"), 40.0};
+ auto const normalbraking{brake_to_stop_then_stand(normal, dry, slipperyslope)};
+ vehicle skidding{definition::load("wagon"), 40.0};
+ auto const skiddingbraking{brake_to_stop_then_stand(skidding, slippery, slipperyslope)};
+
+ // the test means something only when the braking went as planned
+ REQUIRE(normalbraking.stopped);
+ REQUIRE_FALSE(normalbraking.skidded);
+ REQUIRE(skiddingbraking.stopped);
+ REQUIRE(skiddingbraking.skidded);
+
+ CHECK(std::abs(normal.speed()) < 0.1);
+ CHECK(std::abs(skidding.speed()) < 0.1);
+ CHECK_FALSE(skidding->SlippingWheels);
+}
diff --git a/tests/unit/vehicle.cpp b/tests/unit/vehicle.cpp
index 135c5c2f..6b02ed68 100644
--- a/tests/unit/vehicle.cpp
+++ b/tests/unit/vehicle.cpp
@@ -1,5 +1,6 @@
#include "stdafx.h"
#include "vehicle.h"
+#include "vehicle/DynObj.h"
#include
#include
@@ -159,6 +160,18 @@ vehicle::vehicle(definition const &Definition, double Velocity, std::string cons
}
void vehicle::step(track const &Track, double Dt) {
+ compute_forces(Dt);
+ compute_movement(Track, Dt, TLocation{0.0, 0.0, 0.0});
+}
+
+void vehicle::compute_forces(double Dt) {
+ if (m_pipepressure >= 0.0) {
+ m_mover->Pipe->CreatePress(m_pipepressure);
+ }
+ m_mover->ComputeTotalForce(Dt);
+}
+
+double vehicle::compute_movement(track const &Track, double Dt, TLocation const &Location) {
TTrackShape shape;
shape.Len = 1000.0;
shape.dHtrack = Track.gradient;
@@ -167,13 +180,8 @@ void vehicle::step(track const &Track, double Dt) {
param.friction = Track.friction;
param.CategoryFlag = 1;
TTractionParam const traction;
- TLocation const location{0.0, 0.0, 0.0};
TRotation const rotation{0.0, 0.0, 0.0};
- if (m_pipepressure >= 0.0) {
- m_mover->Pipe->CreatePress(m_pipepressure);
- }
- m_mover->ComputeTotalForce(Dt);
- m_mover->ComputeMovement(Dt, Dt, shape, param, traction, location, rotation);
+ return m_mover->ComputeMovement(Dt, Dt, shape, param, traction, Location, rotation);
}
void vehicle::run(double Seconds, track const &Track, double Dt) {
@@ -184,4 +192,88 @@ void vehicle::run(double Seconds, track const &Track, double Dt) {
}
}
+// the physics reaches the vehicle next to it through TDynamicObject::MoverParameters only (neighbour_data). the
+// tests can't make a TDynamicObject, whose constructor and members come with the rest of the engine, so the
+// neighbour is a zeroed block of its size with just that pointer set: enough for the physics, which reads nothing
+// else of it, and never used as a whole object
+static_assert(alignof(TDynamicObject) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__);
+
+namespace {
+
+std::unique_ptr make_standin(TMoverParameters &Mover) {
+ auto standin{std::make_unique(sizeof(TDynamicObject))}; // zeroed
+ reinterpret_cast(standin.get())->MoverParameters = &Mover; // NOSONAR see above
+ return standin;
+}
+
+TDynamicObject *as_neighbour(std::byte *Standin) {
+ return reinterpret_cast(Standin); // NOSONAR see above
+}
+
+} // namespace
+
+pair::pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap) {
+ auto const &first{First.mover()};
+ auto const &second{Second.mover()};
+ // the first vehicle in the middle, its FirstEnd towards the second one
+ m_vehicles[0] = {&First, FirstEnd, 0.0, FirstEnd == end::front ? 1.0 : -1.0, make_standin(First.mover())};
+ m_vehicles[1] = {&Second, SecondEnd, 0.5 * first.Dim.L + Gap + 0.5 * second.Dim.L, SecondEnd == end::front ? -1.0 : 1.0, make_standin(Second.mover())};
+}
+
+pair::~pair() {
+ for (auto const &placed : m_vehicles) {
+ // the vehicles outlive the pair, they mustn't point at its stand-ins
+ for (auto &neighbour : placed.owner->mover().Neighbours) {
+ neighbour = neighbour_data();
+ }
+ }
+}
+
+void pair::find_neighbours() {
+ // as TDynamicObject::update_neighbours()
+ for (auto const &placed : m_vehicles) {
+ placed.owner->mover().Loc = TLocation{placed.position, 0.0, 0.0};
+ }
+ for (int i = 0; i < 2; ++i) {
+ auto const &self{m_vehicles[i]};
+ auto const &other{m_vehicles[1 - i]};
+ auto &mover{self.owner->mover()};
+ auto const &othermover{other.owner->mover()};
+ auto &neighbour{mover.Neighbours[self.end]};
+ neighbour.vehicle = as_neighbour(other.standin.get());
+ neighbour.vehicle_end = other.end;
+ neighbour.distance = static_cast(TMoverParameters::CouplerDist(&mover, &othermover) - mover.Couplers[self.end].adapter_length -
+ othermover.Couplers[other.end].adapter_length);
+ mover.Neighbours[1 - self.end] = neighbour_data();
+ }
+}
+
+void pair::step(track const &Track, double Dt) {
+ find_neighbours();
+ for (auto const &placed : m_vehicles) {
+ placed.owner->compute_forces(Dt);
+ }
+ for (auto &placed : m_vehicles) {
+ placed.position += placed.heading * placed.owner->compute_movement(Track, Dt, TLocation{placed.position, 0.0, 0.0});
+ }
+}
+
+void pair::run(double Seconds, track const &Track, double Dt) {
+ // whole steps, as in vehicle::run()
+ auto const steps{std::lround(Seconds / Dt)};
+ for (long i = 0; i < steps; ++i) {
+ step(Track, Dt);
+ }
+}
+
+double pair::gap() const {
+ auto const &first{m_vehicles[0].owner->mover()};
+ auto const &second{m_vehicles[1].owner->mover()};
+ return std::abs(m_vehicles[1].position - m_vehicles[0].position) - 0.5 * (first.Dim.L + second.Dim.L);
+}
+
+int pair::coupling() const {
+ return m_vehicles[0].owner->mover().Couplers[m_vehicles[0].end].CouplingFlag;
+}
+
} // namespace test
diff --git a/tests/unit/vehicle.h b/tests/unit/vehicle.h
index ad036839..146275af 100644
--- a/tests/unit/vehicle.h
+++ b/tests/unit/vehicle.h
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include
#include
@@ -64,8 +65,57 @@ public:
double speed() const { return m_mover->V * 3.6; }
private:
+ friend class pair;
+
+ // the two halves of a step: the forces, taken by all the vehicles in the scene first, then the movement,
+ // the vehicle placed at Location. returns the distance travelled [m], positive forwards
+ void compute_forces(double Dt);
+ double compute_movement(track const &Track, double Dt, TLocation const &Location);
+
std::unique_ptr m_mover;
double m_pipepressure{-1.0};
};
+// two vehicles on a straight track, the FirstEnd of the first one facing the SecondEnd of the second one Gap metres
+// away (from coupler to coupler), with nothing else around. a step runs them the way the simulation runs the
+// vehicles of a scene (vehicle_table::update()): each one finds its neighbour, then both take the forces, the
+// coupler and buffer ones among them (CouplerForce()), then both move and handle the collisions (CollisionDetect()).
+// they couple and part on their own, as the couplers let them; the speed of each runs along its own axis
+// test::pair pair{moving, end::front, standing, end::rear, 1.0 /* m */};
+// pair.run(5.0 /* s */);
+// CHECK(pair.coupled());
+class pair {
+public:
+ pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap);
+ ~pair();
+ pair(pair const &) = delete;
+ pair &operator=(pair const &) = delete;
+ // the vehicles point at the stand-ins of the pair, which stay where they are
+ pair(pair &&) = delete;
+ pair &operator=(pair &&) = delete;
+
+ // advances both vehicles by one step of Dt
+ void step(track const &Track = {}, double Dt = 0.01);
+ // advances both vehicles by Seconds in steps of Dt
+ void run(double Seconds, track const &Track = {}, double Dt = 0.01);
+ // distance between the facing couplers [m], negative while the buffers are pressed in
+ double gap() const;
+ // type of the coupling between the vehicles, coupling::faux when they aren't coupled
+ int coupling() const;
+ bool coupled() const { return coupling() != coupling::faux; }
+
+private:
+ struct placement {
+ vehicle *owner{nullptr};
+ int end{0}; // the end facing the other vehicle
+ double position{0.0}; // of the middle of the vehicle along the track [m]
+ double heading{1.0}; // +1 when the vehicle runs forwards towards the growing position, -1 otherwise
+ std::unique_ptr standin; // what the other vehicle sees as this one, see vehicle.cpp
+ };
+
+ void find_neighbours();
+
+ std::array m_vehicles;
+};
+
} // namespace test
diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp
index 5bbaa80e..a6a9d181 100644
--- a/vehicle/Driver.cpp
+++ b/vehicle/Driver.cpp
@@ -30,6 +30,8 @@ http://mozilla.org/MPL/2.0/.
#include "world/station.h"
#include "input/keyboardinput.h"
+#include
+
#define LOGVELOCITY 0
#define LOGORDERS 1
#define LOGSTOPS 1
@@ -1093,10 +1095,9 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub
if (Point.fDist < passengerstopmaxdistance * 0.5 ) {
// zaliczamy posterunek w pewnej odległości przed (choć W4 nie zasłania już semafora)
#if LOGSTOPS
- WriteLog(
- pVehicle->asName + " as " + TrainParams.TrainName
- + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute)
- + " passed " + asNextStop); // informacja
+ WriteLog(std::format(
+ "{} as {}: at {}:{} passed {}",
+ pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute, asNextStop)); // informacja
#endif
// przy jakim dystansie (stanie licznika) ma przesunąć na następny postój
fLastStopExpDist = mvOccupied->DistCounter + 0.250 + 0.001 * fLength;
@@ -1253,10 +1254,9 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub
TrainParams.StationIndexInc(); // przejście do następnej
asNextStop = TrainParams.NextStop(); // pobranie kolejnego miejsca zatrzymania
#if LOGSTOPS
- WriteLog(
- pVehicle->asName + " as " + TrainParams.TrainName
- + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute)
- + " next " + asNextStop); // informacja
+ WriteLog(std::format(
+ "{} as {}: at {}:{} next {}",
+ pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute, asNextStop)); // informacja
#endif
// update consist weight, brake settings and ai braking tables
// NOTE: this calculation is expected to run after completing loading/unloading
@@ -1297,13 +1297,16 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub
else {
// jeśli dojechaliśmy do końca rozkładu
#if LOGSTOPS
- WriteLog(
- pVehicle->asName + " as " + TrainParams.TrainName
- + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute)
- + " end of route."); // informacja
+ WriteLog(std::format(
+ "{} as {}: at {}:{} end of route.",
+ pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute)); // informacja
#endif
asNextStop = TrainParams.NextStop(); // informacja o końcu trasy
TrainParams.NewName("none"); // czyszczenie nieaktualnego rozkładu
+ // koniec kursu, gasimy wyświetlacze relacji (tablice blaszane zostają)
+ for( auto *vehicle { pVehicles[ end::front ] }; vehicle != nullptr; vehicle = vehicle->Next() ) {
+ vehicle->DestinationClear();
+ }
// ma nie podjeżdżać pod W4 i ma je pomijać
iDrivigFlags &= ~moveStopCloser;
if( false == TestFlag( iDrivigFlags, movePushPull ) ) {
diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp
index 32ba307f..af74d465 100644
--- a/vehicle/DynObj.cpp
+++ b/vehicle/DynObj.cpp
@@ -4825,12 +4825,13 @@ void TDynamicObject::RenderSounds() {
// horns
{
- // for moving vehicle combine regular horn activation flag with emergency brake horn activation flag, if the brake is active
- auto const warningsignal { (
- MoverParameters->Vel > 0.5 && MoverParameters->AlarmChainFlag ?
+ // for moving vehicle combine regular horn activation flag with emergency brake horn activation flag, if the brake is active;
+ // the horns without the air or the voltage they need stay silent
+ auto const warningsignal { MoverParameters->SoundingHorns(
+ ( MoverParameters->Vel > 0.5 && MoverParameters->AlarmChainFlag ?
MoverParameters->EmergencyBrakeWarningSignal :
0 )
- | MoverParameters->WarningSignal };
+ | MoverParameters->WarningSignal ) };
if( TestFlag( warningsignal, 1 ) ) {
sHorn1.play( sound_flags::exclusive | sound_flags::looping );
@@ -7794,7 +7795,14 @@ int TDynamicObject::RouteWish(TTrack *tr)
return Mechanik ? Mechanik->CrossRoute(tr) : 0; // wg AI albo prosto
};
-void TDynamicObject::DestinationSet(std::string to, std::string numer) {
+void TDynamicObject::DestinationClear() {
+
+ if( false == DestinationSign.has_light ) { return; } // physical destination signs remain up until manually changed
+ // update_destinations() shows the blank sign in place of the missing destination
+ DestinationSign.destination = null_handle;
+}
+
+void TDynamicObject::DestinationSet(std::string const &to, std::string const &numer) {
// ustawienie stacji docelowej oraz wymiennej tekstury 4, jeśli istnieje plik
// w zasadzie, to każdy wagon mógłby mieć inną stację docelową
// zwłaszcza w towarowych, pod kątem zautomatyzowania maewrów albo pracy górki
@@ -7823,6 +7831,15 @@ void TDynamicObject::DestinationSet(std::string to, std::string numer) {
if( DestinationSign.script.empty() ) { return; } // no script so no way to make the texture
if( numer == "none" ) { return; } // blank or incomplete/malformed timetable, don't bother
+ auto const trainname { [this]() -> std::string {
+ if( ctOwner != nullptr ) { return ctOwner->TrainName(); }
+ if( Mechanik != nullptr ) { return Mechanik->TrainName(); }
+ return "none"; } }; // shouldn't get here but, eh
+ auto const instance { [this]() -> std::string {
+ if( DestinationSign.instancing == "name" ) { return MoverParameters->Name; }
+ if( DestinationSign.instancing == "type" ) { return MoverParameters->TypeName; }
+ return "none"; } };
+
std::string signrequest {
"make:"
+ DestinationSign.script
@@ -7834,17 +7851,10 @@ void TDynamicObject::DestinationSet(std::string to, std::string numer) {
ctOwner->Vehicle()->MoverParameters->Name ) // owned vehicle, safer to point to owner as carriages can have identical names
// basic instancing string
// NOTE: underscore doesn't have any magic meaning for the time being, it's just less likely to conflict with regular dictionary keys
- + "&_id1=" + (
- ctOwner != nullptr ? ctOwner->TrainName() :
- Mechanik != nullptr ? Mechanik->TrainName() :
- "none" ) }; // shouldn't get here but, eh
+ + "&_id1=" + trainname() };
// TBD, TODO: replace instancing with support for variables in extra parameters string?
if( false == DestinationSign.instancing.empty() ) {
- signrequest +=
- "&_id2=" + (
- DestinationSign.instancing == "name" ? MoverParameters->Name :
- DestinationSign.instancing == "type" ? MoverParameters->TypeName :
- "none" );
+ signrequest += "&_id2=" + instance();
}
// optionl extra parameters
if( false == DestinationSign.parameters.empty() ) {
diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h
index 36335a32..ef42eb40 100644
--- a/vehicle/DynObj.h
+++ b/vehicle/DynObj.h
@@ -815,7 +815,9 @@ private:
void ParamSet(int what, int into);
// zapytanie do AI, po którym segmencie skrzyżowania jechać
int RouteWish(TTrack *tr);
- void DestinationSet(std::string to, std::string numer);
+ void DestinationSet(std::string const &to, std::string const &numer);
+ // blanks electronic destination sign at the end of the service; physical signs stay until changed
+ void DestinationClear();
material_handle DestinationFind( std::string Destination );
void OverheadTrack(float o);
glm::dvec3 get_future_movement() const;
diff --git a/vehicle/Gauge.cpp b/vehicle/Gauge.cpp
index d7a41062..af8e9a06 100644
--- a/vehicle/Gauge.cpp
+++ b/vehicle/Gauge.cpp
@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "vehicle/Gauge.h"
+#include "vehicle/gaugepositions.h"
#include "utilities/parser.h"
#include "model/Model3d.h"
#include "vehicle/DynObj.h"
@@ -302,42 +303,37 @@ bool
TGauge::UpdateValue( float fNewDesired ) {
auto const desiredtimes100 = static_cast( std::round( 100.0 * fNewDesired ) );
- if( desiredtimes100 == static_cast( std::round( 100.0 * m_targetvalue ) ) ) {
+ auto const previoustimes100 = static_cast( std::round( 100.0 * m_targetvalue ) );
+ if( desiredtimes100 == previoustimes100 ) {
return true;
}
m_targetvalue = fNewDesired;
- // if there's any sound associated with new requested value, play it
- // check value-specific table first...
- if( auto const fullinteger { desiredtimes100 % 100 == 0 }; fullinteger ) {
- // filter out values other than full integers
- auto const lookup = m_soundfxvalues.find( desiredtimes100 / 100 );
- if( lookup != m_soundfxvalues.end() ) {
- lookup->second.play();
- return true;
- }
- }
- else {
+ if( desiredtimes100 % 100 != 0 ) {
// toggle the control to continous range/exclusive sound mode from now on
m_soundtype = sound_flags::exclusive;
}
+ // if there's any sound associated with a position reached on the way to the new requested value, play it
+ // check value-specific table first...
+ if( auto const reached = gauge_positions::last_reached( m_soundfxvalues, previoustimes100, desiredtimes100 );
+ reached != m_soundfxvalues.end() ) {
+ reached->second.play();
+ return true;
+ }
// ...and if there isn't any, fall back on the basic set...
// HACK: crude way to discern controls with continuous and quantized value range
- if( auto const currentvalue = GetValue(); currentvalue < fNewDesired ) {
+ auto const currentvalue = GetValue();
+ if( currentvalue < fNewDesired && false == m_soundfxincrease.empty() ) {
// shift up
- if( false == m_soundfxincrease.empty() ) {
- m_soundfxincrease.play( m_soundtype );
- return true;
- }
+ m_soundfxincrease.play( m_soundtype );
+ return true;
}
- else if( currentvalue > fNewDesired ) {
+ if( currentvalue > fNewDesired && false == m_soundfxdecrease.empty() ) {
// shift down
- if( false == m_soundfxdecrease.empty() ) {
- m_soundfxdecrease.play( m_soundtype );
- return true;
- }
+ m_soundfxdecrease.play( m_soundtype );
+ return true;
}
return false; // no suitable sound was found
-};
+}
void TGauge::PutValue(float fNewDesired)
{ // McZapkie-281102: natychmiastowe wpisanie wartosci
diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp
index f0b2367f..5f08d132 100644
--- a/vehicle/Train.cpp
+++ b/vehicle/Train.cpp
@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "vehicle/Train.h"
+#include "vehicle/horn.h"
#include "utilities/Globals.h"
#include "simulation/simulation.h"
@@ -7841,16 +7842,10 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman
if (Command.action == GLFW_PRESS)
{
// only need to react to press, sound will continue until stopped
- if (false == TestFlag(Train->mvOccupied->WarningSignal, 1))
+ if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::low))
{
// turn on
- Train->mvOccupied->WarningSignal |= 1;
- /*
- if( true == TestFlag( Train->mvOccupied->WarningSignal, 2 ) ) {
- // low and high horn are treated as mutually exclusive
- Train->mvControlled->WarningSignal &= ~2;
- }
- */
+ Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::low, Train->has_single_horn_lever());
// visual feedback
Train->ggHornButton.UpdateValue(-1.0);
Train->ggHornLowButton.UpdateValue(1.0);
@@ -7859,13 +7854,13 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman
else if (Command.action == GLFW_RELEASE)
{
// turn off
- /*
- // NOTE: we turn off both low and high horn, due to unreliability of release event when shift key is involved
- Train->mvOccupied->WarningSignal &= ~( 1 | 2 );
- */
- Train->mvOccupied->WarningSignal &= ~1;
+ Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::low);
// visual feedback
- Train->ggHornButton.UpdateValue(0.0);
+ if (false == (Train->has_single_horn_lever() && TestFlag(Train->mvOccupied->WarningSignal, horn::high)))
+ {
+ // leave the single lever alone if it was moved to the other tone in the meantime
+ Train->ggHornButton.UpdateValue(0.0);
+ }
Train->ggHornLowButton.UpdateValue(0.0);
}
}
@@ -7885,16 +7880,10 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma
if (Command.action == GLFW_PRESS)
{
// only need to react to press, sound will continue until stopped
- if (false == TestFlag(Train->mvOccupied->WarningSignal, 2))
+ if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::high))
{
// turn on
- Train->mvOccupied->WarningSignal |= 2;
- /*
- if( true == TestFlag( Train->mvOccupied->WarningSignal, 1 ) ) {
- // low and high horn are treated as mutually exclusive
- Train->mvControlled->WarningSignal &= ~1;
- }
- */
+ Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::high, Train->has_single_horn_lever());
// visual feedback
Train->ggHornButton.UpdateValue(1.0);
Train->ggHornHighButton.UpdateValue(1.0);
@@ -7903,13 +7892,13 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma
else if (Command.action == GLFW_RELEASE)
{
// turn off
- /*
- // NOTE: we turn off both low and high horn, due to unreliability of release event when shift key is involved
- Train->mvOccupied->WarningSignal &= ~( 1 | 2 );
- */
- Train->mvOccupied->WarningSignal &= ~2;
+ Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::high);
// visual feedback
- Train->ggHornButton.UpdateValue(0.0);
+ if (false == (Train->has_single_horn_lever() && TestFlag(Train->mvOccupied->WarningSignal, horn::low)))
+ {
+ // leave the single lever alone if it was moved to the other tone in the meantime
+ Train->ggHornButton.UpdateValue(0.0);
+ }
Train->ggHornHighButton.UpdateValue(0.0);
}
}
@@ -7953,12 +7942,7 @@ void TTrain::OnCommand_radiotoggle(TTrain *Train, command_data const &Command)
return;
}
- // NOTE: we ignore the lack of 3d model to allow system reset after receiving radio-stop signal
- /*
- if( false == Train->m_controlmapper.contains( "radio_sw:" ) ) {
- return;
- }
- */
+ // NOTE: works without the switch in the 3d model too, to allow system reset after receiving radio-stop signal
// only reacting to press, so the sound can loop uninterrupted
if (false == Train->mvOccupied->Radio)
{
@@ -8003,9 +7987,13 @@ void TTrain::OnCommand_radiochannelincrease(TTrain *Train, command_data const &C
if (Command.action == GLFW_PRESS)
{
- command_data newCommand = Command;
- newCommand.param1 = Train->RadioChannel() + 1;
- OnCommand_radiochannelset(Train, newCommand);
+ // the channel buttons of a radio switched off or without power do nothing
+ if (Train->mvOccupied->RadioPowered())
+ {
+ command_data newCommand = Command;
+ newCommand.param1 = Train->RadioChannel() + 1;
+ OnCommand_radiochannelset(Train, newCommand);
+ }
Train->ggRadioChannelNext.UpdateValue(1.0);
}
@@ -8021,9 +8009,12 @@ void TTrain::OnCommand_radiochanneldecrease(TTrain *Train, command_data const &C
if (Command.action == GLFW_PRESS)
{
- command_data newCommand = Command;
- newCommand.param1 = Train->RadioChannel() - 1;
- OnCommand_radiochannelset(Train, newCommand);
+ if (Train->mvOccupied->RadioPowered())
+ {
+ command_data newCommand = Command;
+ newCommand.param1 = Train->RadioChannel() - 1;
+ OnCommand_radiochannelset(Train, newCommand);
+ }
Train->ggRadioChannelPrevious.UpdateValue(1.0);
}
@@ -8049,7 +8040,7 @@ void TTrain::OnCommand_radiostopsend(TTrain *Train, command_data const &Command)
if (Command.action == GLFW_PRESS)
{
- if (true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable))
+ if (Train->mvOccupied->RadioPowered())
{
simulation::Region->RadioStop(Train->Dynamic()->GetPosition());
}
@@ -8067,7 +8058,7 @@ void TTrain::OnCommand_radiostopenable(TTrain *Train, command_data const &Comman
{
if (Command.action == GLFW_PRESS && Train->ggRadioStop.GetValue() == 0)
{
- if (true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable))
+ if (Train->mvOccupied->RadioPowered())
{
simulation::Region->RadioStop(Train->Dynamic()->GetPosition());
}
@@ -8090,7 +8081,7 @@ void TTrain::OnCommand_radiostoptest(TTrain *Train, command_data const &Command)
if (Command.action == GLFW_PRESS)
{
- if (Train->RadioChannel() == 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable))
+ if (Train->RadioChannel() == 10 && Train->mvOccupied->RadioPowered())
{
Train->Dynamic()->RadioStop();
}
@@ -8108,7 +8099,7 @@ void TTrain::OnCommand_radiocall1send(TTrain *Train, command_data const &Command
{
if (Command.action == GLFW_PRESS)
{
- if (Train->RadioChannel() != 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable))
+ if (Train->RadioChannel() != 10 && Train->mvOccupied->RadioPowered())
{
simulation::Events.queue_receivers(radio_message::call1, Train->Dynamic()->GetPosition());
}
@@ -8127,7 +8118,7 @@ void TTrain::OnCommand_radiocall3send(TTrain *Train, command_data const &Command
if (Command.action == GLFW_PRESS)
{
- if (Train->RadioChannel() != 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable))
+ if (Train->RadioChannel() != 10 && Train->mvOccupied->RadioPowered())
{
simulation::Events.queue_receivers(radio_message::call3, Train->Dynamic()->GetPosition());
}
@@ -8146,9 +8137,13 @@ void TTrain::OnCommand_radiovolumeincrease(TTrain *Train, command_data const &Co
if (Command.action == GLFW_PRESS)
{
- command_data newCommand = Command;
- newCommand.param1 = Train->m_radiovolume + 0.125;
- OnCommand_radiovolumeset(Train, newCommand);
+ // so do the volume buttons
+ if (Train->mvOccupied->RadioPowered())
+ {
+ command_data newCommand = Command;
+ newCommand.param1 = Train->m_radiovolume + 0.125;
+ OnCommand_radiovolumeset(Train, newCommand);
+ }
Train->ggRadioVolumeNext.UpdateValue(1.0);
}
else if (Command.action == GLFW_RELEASE)
@@ -8163,9 +8158,12 @@ void TTrain::OnCommand_radiovolumedecrease(TTrain *Train, command_data const &Co
if (Command.action == GLFW_PRESS)
{
- command_data newCommand = Command;
- newCommand.param1 = Train->m_radiovolume - 0.125;
- OnCommand_radiovolumeset(Train, newCommand);
+ if (Train->mvOccupied->RadioPowered())
+ {
+ command_data newCommand = Command;
+ newCommand.param1 = Train->m_radiovolume - 0.125;
+ OnCommand_radiovolumeset(Train, newCommand);
+ }
Train->ggRadioVolumePrevious.UpdateValue(1.0);
}
else if (Command.action == GLFW_RELEASE)
@@ -10106,7 +10104,7 @@ void TTrain::update_sounds_radio()
std::erase_if(m_radiomessages, [](auto const &source) { return false == source.second->is_playing(); });
}
// adjust audibility of remaining messages based on current radio conditions
- auto const radioenabled{true == mvOccupied->Radio && (mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable)};
+ auto const radioenabled{mvOccupied->RadioPowered()};
for (auto const &[messagechannel, messagesound] : m_radiomessages)
{
auto const volume{true == radioenabled && Dynamic()->Mechanik != nullptr && messagechannel == RadioChannel() ? m_radiovolume : 0.0};
@@ -10893,7 +10891,7 @@ void TTrain::radio_message(sound_source *Message, int const Channel)
m_radiomessages.emplace_back(Channel, std::make_shared(m_radiosound));
// assign sound to the template and play it
auto &message = *m_radiomessages.back().second.get();
- auto const radioenabled{true == mvOccupied->Radio && (mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable)};
+ auto const radioenabled{mvOccupied->RadioPowered()};
auto const volume{true == radioenabled && Dynamic()->Mechanik != nullptr && Channel == RadioChannel() ? 1.0 : 0.0};
message.copy_sounds(*Message).gain(static_cast(volume)).play();
}
diff --git a/vehicle/Train.h b/vehicle/Train.h
index 4fc11dc2..94ada767 100644
--- a/vehicle/Train.h
+++ b/vehicle/Train.h
@@ -195,6 +195,9 @@ class TTrain {
bool initialize_button(cParser &Parser, std::string const &Label, int const Cabindex);
// helper, returns true for EMU with oerlikon brake
bool is_eztoer() const;
+ // helper, returns true if both horn tones are operated with the same lever, without separate low and high tone buttons
+ bool has_single_horn_lever() const {
+ return ggHornButton.SubModel != nullptr && ggHornLowButton.SubModel == nullptr && ggHornHighButton.SubModel == nullptr; }
// locates nearest vehicle belonging to the consist
TDynamicObject *find_nearest_consist_vehicle(bool freefly, glm::vec3 pos) const;
// mover master controller to specified position
diff --git a/vehicle/gaugepositions.h b/vehicle/gaugepositions.h
new file mode 100644
index 00000000..fc82ed44
--- /dev/null
+++ b/vehicle/gaugepositions.h
@@ -0,0 +1,35 @@
+/*
+This Source Code Form is subject to the
+terms of the Mozilla Public License, v.
+2.0. If a copy of the MPL was not
+distributed with this file, You can
+obtain one at
+http://mozilla.org/MPL/2.0/.
+*/
+
+#pragma once
+
+#include