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