17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-19 03:49:19 +02:00

fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds

- automatic couplers don't catch above the safe collision speed (#86): with crash damage on they couple at a
  closing speed of 15 km/h at most, a faster hit locks the coupling until the vehicles part and the collision
  runs its course. On the way: CollisionVelocity() read a head-on 10 + 10 km/h as 14.1 km/h (quaternions turn
  by half the angle), it's now taken from the touching ends; CollisionDetect() took the other vehicle's speed
  the wrong way round for vehicles facing each other. Head-on hits now derail sooner, at the intended limit,
- a single horn lever can't sound both tones at once (eu07/maszyna#392),
- a skid ends once the vehicle stops on locked wheels (eu07/maszyna#479),
- electronic destination signs are blanked at the end of the timetable (eu07/maszyna#446),
- horns need the air or the voltage to sound (eu07/maszyna#290, #76): HornType=[Pneumatic]|Electric and
  Horn3Type in Cntrl.; a pneumatic horn needs 0.2 MPa in the main reservoir (or the main pipe without one),
  an electric one the low voltage; the lever stays where it was put,
- compressor start delay (#67): CompressorStartDelay [s] in Brake., counted from the compressor getting its
  power, after the engine starts say; none by default,
- the channel and volume buttons of a radio without power do nothing (eu07/maszyna#429); the power check of
  the radio is TMoverParameters::RadioPowered(),
- a lever moved smoothly sounds the positions it passes (#83).

Sonar findings in the functions changed: nesting, nested conditionals, commented-out code, std::format,
references to const. Unit tests: test::pair for two vehicles running into each other, test_couplers,
test_horn, test_wheelslip, test_compressor, test_radio, test_gauge_positions.
This commit is contained in:
jerrrrycho
2026-10-17 14:41:59 +02:00
parent c7f9796387
commit ec345f94cb
21 changed files with 1235 additions and 171 deletions

View File

@@ -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).

View File

@@ -0,0 +1,80 @@
#include "stdafx.h"
#include "doctest.h"
#include "vehicle.h"
using test::definition;
using test::vehicle;
// the compressor waiting a while after getting its power before it starts (CompressorStartDelay,
// MaSzyna-EU07/maszyna#67): in a diesel it starts some time after the engine, rather than with it
namespace {
double const step{0.01};
// a compressor fed from the main circuit, which starts on its own, and an empty main reservoir for it to fill
vehicle compressor_vehicle(double const Delay) {
auto definition{definition::load("wagon")};
definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 0.65).set("Brake", "MaxCP", 0.8);
definition.set("Brake", "CompressorSpeed", 0.01).set("Brake", "CompressorPower", "Main");
definition.set("Cntrl", "CompressorStart", "Automatic");
if (Delay > 0.0) {
definition.set("Brake", "CompressorStartDelay", Delay);
}
vehicle vehicle{definition};
vehicle->CompressedVolume = 0.0;
vehicle->LastSwitchingTime = 10.0; // nothing was switched lately
return vehicle;
}
// runs the compressor for Seconds, the main circuit as it is, the time since the last switching going on as in
// ComputeTotalForce()
void run_compressor(vehicle &Vehicle, double const Seconds) {
for (auto i{std::lround(Seconds / step)}; i > 0; --i) {
Vehicle->LastSwitchingTime += step;
Vehicle->CompressorCheck(step);
}
}
} // namespace
TEST_CASE("compressor: without a start delay the compressor starts with its power") {
auto vehicle{compressor_vehicle(0.0)};
run_compressor(vehicle, 1.0);
REQUIRE_FALSE(vehicle->CompressorFlag); // no power, no compressor
vehicle->Mains = true;
run_compressor(vehicle, step);
CHECK(vehicle->CompressorFlag);
}
TEST_CASE("compressor: the compressor starts the delay after getting its power") {
auto vehicle{compressor_vehicle(5.0)};
vehicle->Mains = true;
run_compressor(vehicle, 4.9);
CHECK_FALSE(vehicle->CompressorFlag);
CHECK(vehicle->Compressor == doctest::Approx(0.0));
run_compressor(vehicle, 0.2);
CHECK(vehicle->CompressorFlag);
run_compressor(vehicle, 10.0);
CHECK(vehicle->Compressor > 0.0); // and fills the reservoir
}
TEST_CASE("compressor: the delay starts over once the power is gone") {
auto vehicle{compressor_vehicle(5.0)};
vehicle->Mains = true;
run_compressor(vehicle, 10.0);
REQUIRE(vehicle->CompressorFlag);
vehicle->Mains = false; // the engine stopped
run_compressor(vehicle, 1.0);
CHECK_FALSE(vehicle->CompressorFlag);
vehicle->Mains = true; // and started again
run_compressor(vehicle, 4.9);
CHECK_FALSE(vehicle->CompressorFlag);
run_compressor(vehicle, 0.2);
CHECK(vehicle->CompressorFlag);
}

View File

@@ -0,0 +1,422 @@
#include "stdafx.h"
#include "doctest.h"
#include "vehicle.h"
#include "utilities/Globals.h"
using test::definition;
using test::vehicle;
namespace {
// the wagon from data/wagon.json with automatic couplers at both ends, which couple the given connections
definition automatic_definition(int Flag = coupling::coupler | coupling::brakehose) {
return definition::load("wagon").set("BuffCoupl", "CType", "Automatic").set("BuffCoupl", "AutomaticFlag", Flag);
}
// the wagon from data/wagon.json with automatic couplers at both ends, running at the given speed [km/h]
vehicle automatic_wagon(double Velocity, std::string const &Name) {
return vehicle{automatic_definition(), Velocity, Name};
}
// the same wagon, twice as heavy
vehicle heavy_automatic_wagon(double Velocity, std::string const &Name) {
return vehicle{automatic_definition().set("Param", "M", 2 * 42200), Velocity, Name};
}
bool derailed(vehicle &Vehicle) {
return TestFlag(Vehicle->DamageFlag, dtrain_out);
}
// runs the pair for Seconds, returns whether the vehicles were coupled at any point
bool couple_during(test::pair &Pair, double Seconds) {
auto coupled{false};
for (auto i{std::lround(Seconds / 0.01)}; i > 0; --i) {
Pair.step();
coupled = coupled || Pair.coupled();
}
return coupled;
}
// sets the crash damage setting for the test, restores it after
class crash_damage_setting {
public:
explicit crash_damage_setting(bool Enabled) : m_previous{Global.crash_damage} { Global.crash_damage = Enabled; }
~crash_damage_setting() { Global.crash_damage = m_previous; }
crash_damage_setting(crash_damage_setting const &) = delete;
crash_damage_setting &operator=(crash_damage_setting const &) = delete;
private:
bool m_previous;
};
} // namespace
// issue #86: automatic couplers used to catch at any closing speed, so the vehicles never collided nor derailed
TEST_CASE("couplers: automatic couplers catch a standing wagon hit at a walking pace") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(10.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
REQUIRE(moving->Couplers[end::front].type() == TCouplerType::Automatic);
REQUIRE(moving.speed() == doctest::Approx(10.0));
// the front of the moving wagon runs into the rear of the standing one
CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(10.0));
CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
CHECK(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front));
}
TEST_CASE("couplers: automatic couplers don't catch a wagon hit too fast, the wagons collide instead") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(30.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
REQUIRE(moving.speed() == doctest::Approx(30.0));
CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) > TMoverParameters::SafeCollisionVelocity);
CHECK_FALSE(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
CHECK_FALSE(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front));
}
TEST_CASE("couplers: automatic couplers catch a wagon running ahead only a little slower") {
crash_damage_setting const crashdamage{true};
auto faster{automatic_wagon(60.0, "faster")};
auto slower{automatic_wagon(50.0, "slower")};
CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(10.0));
CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(10.0));
CHECK(faster->AutomaticCouplingSafe(end::front, slower.mover(), end::rear));
}
TEST_CASE("couplers: without crash damage automatic couplers catch at any speed") {
crash_damage_setting const crashdamage{false};
auto moving{automatic_wagon(30.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
}
// the closing speed used to treat the speed of each vehicle as a vector turned by half its heading, so for vehicles
// facing each other it came out as the root of the sum of the squares of their speeds, rather than the sum
TEST_CASE("couplers: the closing speed of a rear-end collision is the difference of the speeds") {
auto faster{automatic_wagon(40.0, "faster")};
auto slower{automatic_wagon(25.0, "slower")};
// running forwards, the front of the faster wagon hits the rear of the slower one
CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(15.0));
CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(15.0));
// the wagons running ahead of each other are parting
CHECK(slower->CollisionVelocity(end::front, faster.mover(), end::rear) == doctest::Approx(-15.0));
// backing up, the rear of the faster wagon hits the front of the slower one
auto backingfaster{automatic_wagon(-40.0, "backing faster")};
auto backingslower{automatic_wagon(-25.0, "backing slower")};
CHECK(backingfaster->CollisionVelocity(end::rear, backingslower.mover(), end::front) == doctest::Approx(15.0));
CHECK(backingslower->CollisionVelocity(end::front, backingfaster.mover(), end::rear) == doctest::Approx(15.0));
}
TEST_CASE("couplers: the closing speed of a head-on collision is the sum of the speeds") {
crash_damage_setting const crashdamage{true};
auto first{automatic_wagon(10.0, "first")};
auto second{automatic_wagon(10.0, "second")};
// both wagons run forwards, their fronts meet
CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) == doctest::Approx(20.0));
CHECK(second->CollisionVelocity(end::front, first.mover(), end::front) == doctest::Approx(20.0));
// 20 km/h is above the limit, the couplers don't catch (the old formula gave 14.1 km/h and coupled them)
CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) > TMoverParameters::SafeCollisionVelocity);
CHECK_FALSE(first->AutomaticCouplingSafe(end::front, second.mover(), end::front));
CHECK_FALSE(second->AutomaticCouplingSafe(end::front, first.mover(), end::front));
// both wagons back up, their rears meet
auto firstbacking{automatic_wagon(-10.0, "first backing")};
auto secondbacking{automatic_wagon(-10.0, "second backing")};
CHECK(firstbacking->CollisionVelocity(end::rear, secondbacking.mover(), end::rear) == doctest::Approx(20.0));
// running back to back, the wagons part
CHECK(first->CollisionVelocity(end::rear, second.mover(), end::rear) == doctest::Approx(-20.0));
CHECK(first->AutomaticCouplingSafe(end::rear, second.mover(), end::rear));
// slow enough, the couplers catch
auto slowfirst{automatic_wagon(5.0, "slow first")};
auto slowsecond{automatic_wagon(5.0, "slow second")};
CHECK(slowfirst->CollisionVelocity(end::front, slowsecond.mover(), end::front) == doctest::Approx(10.0));
CHECK(slowfirst->AutomaticCouplingSafe(end::front, slowsecond.mover(), end::front));
}
TEST_CASE("couplers: the closing speed of a hit on a standing vehicle is the speed of the moving one, whichever way it faces") {
auto moving{automatic_wagon(20.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
// the standing wagon faces away from the moving one
CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(20.0));
CHECK(standing->CollisionVelocity(end::rear, moving.mover(), end::front) == doctest::Approx(20.0));
// the standing wagon faces the moving one
CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::front) == doctest::Approx(20.0));
CHECK(standing->CollisionVelocity(end::front, moving.mover(), end::front) == doctest::Approx(20.0));
// the moving wagon backs up into the standing one
auto backing{automatic_wagon(-20.0, "backing")};
CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(20.0));
CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::rear) == doctest::Approx(20.0));
// the moving wagon runs away from the standing one
CHECK(moving->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(-20.0));
}
// the coupling as the simulation runs it: two vehicles on a track, closing in on each other until the couplers (or
// buffers) meet, the coupler forces and the collision computed by the physics, step by step
TEST_CASE("couplers: automatic couplers catch right up to the safe closing speed") {
crash_damage_setting const crashdamage{true};
auto standing{automatic_wagon(0.0, "standing")};
auto below{automatic_wagon(14.9, "below")};
CHECK(below->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(14.9));
CHECK(below->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
auto above{automatic_wagon(15.1, "above")};
CHECK(above->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(15.1));
CHECK_FALSE(above->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
auto faster{automatic_wagon(15.5, "faster")};
CHECK_FALSE(faster->AutomaticCouplingSafe(end::front, standing.mover(), end::rear));
}
TEST_CASE("couplers: a wagon rolling slowly into a standing one couples with it") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(5.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
test::pair pair{moving, end::front, standing, end::rear, 1.0};
pair.run(3.0);
REQUIRE(pair.coupled());
CHECK(moving->Couplers[end::front].CouplingFlag == (coupling::coupler | coupling::brakehose));
CHECK(standing->Couplers[end::rear].CouplingFlag == (coupling::coupler | coupling::brakehose));
CHECK(moving->Couplers[end::front].Connected == &standing.mover());
CHECK_FALSE(derailed(moving));
CHECK_FALSE(derailed(standing));
// coupled, they run on together
pair.run(2.0);
CHECK(moving.speed() == doctest::Approx(standing.speed()).epsilon(0.05));
CHECK(pair.gap() < 0.2);
}
TEST_CASE("couplers: a wagon hitting a standing one too fast doesn't couple, nor once the hit has slowed it down") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(30.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
test::pair pair{moving, end::front, standing, end::rear, 1.0};
auto touched{false};
auto parted{false};
auto coupled{false};
auto locked{true};
for (int i = 0; i < 1000; ++i) {
pair.step();
// as far as the couplers know: the distance between them at the start of the step
auto const distance{moving->Neighbours[end::front].distance};
touched = touched || distance < 0.0;
parted = parted || (touched && distance > 0.05);
coupled = coupled || pair.coupled();
if (touched && false == parted) {
locked = locked && false == moving->Couplers[end::front].AutomaticCouplingAllowed && false == standing->Couplers[end::rear].AutomaticCouplingAllowed;
}
}
REQUIRE(touched);
CHECK_FALSE(coupled);
// the hit derailed and stopped them, the couplers next to each other, the automatic coupling locked all along
CHECK_FALSE(parted);
CHECK(locked);
CHECK(derailed(moving));
CHECK(derailed(standing));
CHECK(std::abs(moving.speed()) < 3.0);
CHECK(std::abs(standing.speed()) < 3.0);
CHECK_FALSE(couple_during(pair, 5.0));
}
TEST_CASE("couplers: the automatic coupling locked by a hit is unlocked once the vehicles part") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(3.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
// as a hit too fast or manual uncoupling leaves them
moving->Couplers[end::front].AutomaticCouplingAllowed = false;
standing->Couplers[end::rear].AutomaticCouplingAllowed = false;
SUBCASE("parted by more than 5 cm, they couple on the next approach") {
test::pair pair{moving, end::front, standing, end::rear, 0.5};
pair.step();
CHECK(moving->Couplers[end::front].AutomaticCouplingAllowed);
CHECK(couple_during(pair, 3.0));
}
SUBCASE("kept together, they don't") {
test::pair pair{moving, end::front, standing, end::rear, 0.0};
auto coupled{false};
auto parted{false};
for (int i = 0; i < 300 && false == parted; ++i) {
pair.step();
parted = pair.gap() > 0.05;
coupled = coupled || (false == parted && pair.coupled());
}
CHECK_FALSE(coupled);
}
}
// the couplers refuse a hit by the closing speed when they meet, the derail check (CollisionDetect()) takes the one
// left when the buffers are pressed in all the way, after their springs have slowed the hit down: for the wagons here
// by about 7 km/h. so a hit a little over the safe closing speed only bounces the vehicles apart
TEST_CASE("couplers: a hit a little too fast for the couplers bounces the vehicles apart, uncoupled and on the rails") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(20.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
test::pair pair{moving, end::front, standing, end::rear, 1.0};
CHECK_FALSE(couple_during(pair, 3.0));
CHECK_FALSE(derailed(moving));
CHECK_FALSE(derailed(standing));
CHECK(pair.gap() > 0.5);
CHECK(standing.speed() > moving.speed());
}
TEST_CASE("couplers: a hit too fast for the couplers derails the vehicles over the limit adjusted for their mass ratio") {
crash_damage_setting const crashdamage{true};
SUBCASE("equal masses: both derail") {
auto moving{automatic_wagon(30.0, "moving")};
auto standing{automatic_wagon(0.0, "standing")};
test::pair pair{moving, end::front, standing, end::rear, 1.0};
CHECK_FALSE(couple_during(pair, 5.0));
CHECK(derailed(moving));
CHECK(derailed(standing));
}
SUBCASE("half the mass: between the safe closing speed and twice that, only the lighter one derails") {
auto light{automatic_wagon(27.0, "light")};
auto heavy{heavy_automatic_wagon(0.0, "heavy")};
test::pair pair{light, end::front, heavy, end::rear, 1.0};
CHECK_FALSE(couple_during(pair, 5.0));
CHECK(derailed(light));
CHECK_FALSE(derailed(heavy));
}
SUBCASE("half the mass: above twice the safe closing speed both derail") {
auto light{automatic_wagon(45.0, "light")};
auto heavy{heavy_automatic_wagon(0.0, "heavy")};
test::pair pair{light, end::front, heavy, end::rear, 1.0};
CHECK_FALSE(couple_during(pair, 5.0));
CHECK(derailed(light));
CHECK(derailed(heavy));
}
SUBCASE("half the mass: below the safe closing speed the couplers catch, nothing derails") {
auto light{automatic_wagon(12.0, "light")};
auto heavy{heavy_automatic_wagon(0.0, "heavy")};
test::pair pair{light, end::front, heavy, end::rear, 1.0};
CHECK(couple_during(pair, 5.0));
CHECK_FALSE(derailed(light));
CHECK_FALSE(derailed(heavy));
}
}
TEST_CASE("couplers: wagons running head-on into each other couple only up to the safe closing speed") {
crash_damage_setting const crashdamage{true};
SUBCASE("7 + 7 km/h, the couplers catch") {
auto first{automatic_wagon(7.0, "first")};
auto second{automatic_wagon(7.0, "second")};
test::pair pair{first, end::front, second, end::front, 1.0};
CHECK(couple_during(pair, 3.0));
CHECK_FALSE(derailed(first));
CHECK_FALSE(derailed(second));
}
SUBCASE("8 + 8 km/h, the wagons bounce apart") {
auto first{automatic_wagon(8.0, "first")};
auto second{automatic_wagon(8.0, "second")};
test::pair pair{first, end::front, second, end::front, 1.0};
CHECK_FALSE(couple_during(pair, 3.0));
CHECK_FALSE(derailed(first));
CHECK_FALSE(derailed(second));
// both backwards, away from each other
CHECK(first.speed() < 0.0);
CHECK(second.speed() < 0.0);
}
SUBCASE("backing up, 8 + 8 km/h, the wagons bounce apart") {
auto first{automatic_wagon(-8.0, "first")};
auto second{automatic_wagon(-8.0, "second")};
test::pair pair{first, end::rear, second, end::rear, 1.0};
CHECK_FALSE(couple_during(pair, 3.0));
CHECK(first.speed() > 0.0);
CHECK(second.speed() > 0.0);
}
SUBCASE("15 + 15 km/h, both derail") {
auto first{automatic_wagon(15.0, "first")};
auto second{automatic_wagon(15.0, "second")};
test::pair pair{first, end::front, second, end::front, 1.0};
CHECK_FALSE(couple_during(pair, 5.0));
CHECK(derailed(first));
CHECK(derailed(second));
}
}
// the collision used to take the speed of a vehicle facing the other way as if it faced the same way, so a wagon
// pushed away by the buffers of the vehicle hitting it head-on counted as running into it: the hit slowed it down,
// and the hitter bounced back as from a wall
TEST_CASE("couplers: a wagon hit head-on runs off the same as one hit from behind") {
// without automatic couplers or crash damage, the collision alone
crash_damage_setting const crashdamage{false};
auto speeds_after_hit = [](int StruckEnd) {
auto moving{vehicle{definition::load("wagon"), 20.0, "moving"}};
auto struck{vehicle{definition::load("wagon"), 0.0, "struck"}};
test::pair pair{moving, end::front, struck, StruckEnd, 0.5};
auto touched{false};
for (int i = 0; i < 300 && false == (touched && pair.gap() > 0.3); ++i) {
pair.step();
touched = touched || pair.gap() < 0.0;
}
REQUIRE(touched);
REQUIRE(pair.gap() > 0.3);
REQUIRE_FALSE(pair.coupled());
return std::make_pair(moving.speed(), struck.speed());
};
auto const [hitterfrombehind, struckfrombehind]{speeds_after_hit(end::rear)};
auto const [hitterheadon, struckheadon]{speeds_after_hit(end::front)};
// hit from behind, the struck wagon runs off forwards, faster than the hitter
CHECK(struckfrombehind > 10.0);
CHECK(hitterfrombehind < struckfrombehind);
// hit head-on, it runs off backwards, as fast
CHECK(struckheadon == doctest::Approx(-struckfrombehind).epsilon(0.02));
CHECK(hitterheadon == doctest::Approx(hitterfrombehind).epsilon(0.05));
}
// the other conditions of automatic coupling
TEST_CASE("couplers: an automatic coupler doesn't catch a coupler of another type") {
crash_damage_setting const crashdamage{true};
auto moving{automatic_wagon(5.0, "moving")};
auto standing{vehicle{definition::load("wagon"), 0.0, "standing"}};
REQUIRE(standing->Couplers[end::rear].type() == TCouplerType::Screw);
test::pair pair{moving, end::front, standing, end::rear, 1.0};
CHECK_FALSE(couple_during(pair, 3.0));
}
TEST_CASE("couplers: automatic couplers of different control systems couple without the control connection") {
crash_damage_setting const crashdamage{true};
auto const flag{coupling::coupler | coupling::brakehose | coupling::control};
auto moving{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 5.0, "moving"}};
SUBCASE("the same control system") {
auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 0.0, "standing"}};
REQUIRE(standing->Couplers[end::rear].control_type == "first");
test::pair pair{moving, end::front, standing, end::rear, 1.0};
REQUIRE(couple_during(pair, 3.0));
CHECK(pair.coupling() == flag);
}
SUBCASE("a different one") {
auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "second"), 0.0, "standing"}};
REQUIRE(standing->Couplers[end::rear].control_type == "second");
test::pair pair{moving, end::front, standing, end::rear, 1.0};
REQUIRE(couple_during(pair, 3.0));
CHECK(pair.coupling() == (coupling::coupler | coupling::brakehose));
}
}

View File

@@ -0,0 +1,46 @@
#include "stdafx.h"
#include "doctest.h"
#include "vehicle/gaugepositions.h"
#include <string>
// 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<int, std::string> 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) == "");
}

99
tests/unit/test_horn.cpp Normal file
View File

@@ -0,0 +1,99 @@
#include "stdafx.h"
#include "doctest.h"
#include "vehicle.h"
#include "vehicle/horn.h"
// the horn keys of the cab: low tone (A) and high tone (shift+A), held to sound
TEST_CASE("horn: a single lever for both tones sounds one tone at a time (eu07/maszyna#392)") {
auto signal{0};
signal = horn::pressed(signal, horn::low, true);
REQUIRE(signal == horn::low);
// the lever moved over to the high tone while the low tone key is still held
signal = horn::pressed(signal, horn::high, true);
CHECK(signal == horn::high);
// letting go of the low tone key leaves the high tone sounding
signal = horn::released(signal, horn::low);
CHECK(signal == horn::high);
signal = horn::released(signal, horn::high);
CHECK(signal == 0);
}
TEST_CASE("horn: separate buttons for the tones can sound both at once") {
auto signal{0};
signal = horn::pressed(signal, horn::low, false);
signal = horn::pressed(signal, horn::high, false);
CHECK(signal == (horn::low | horn::high));
signal = horn::released(signal, horn::low);
CHECK(signal == horn::high);
}
TEST_CASE("horn: releasing a tone leaves the other bits of the signal alone") {
auto const third{4}; // the third horn, operated separately
CHECK(horn::released(horn::low | third, horn::low) == third);
CHECK(horn::pressed(third, horn::high, true) == (horn::high | third));
}
// what the horns need to sound (eu07/maszyna#290): a pneumatic horn the air of the main reservoir, an electric one
// the low voltage. the horn lever stays where the driver put it, only the horn is silent
namespace {
test::vehicle horn_vehicle(test::definition Definition) {
// a main reservoir of its own
return test::vehicle{Definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 0.65).set("Brake", "MaxCP", 0.8)};
}
int const allhorns{horn::low | horn::high | 4};
} // namespace
TEST_CASE("horn: a pneumatic horn sounds on the air of the main reservoir only") {
auto vehicle{horn_vehicle(test::definition::load("wagon"))};
REQUIRE(vehicle->HornSupply == horn_supply::pneumatic); // the default
vehicle->Compressor = 0.65;
CHECK(vehicle->SoundingHorns(allhorns) == allhorns);
vehicle->Compressor = 0.0; // the reservoir is empty
CHECK(vehicle->SoundingHorns(allhorns) == 0);
}
TEST_CASE("horn: a vehicle without a main reservoir takes the horn air from the main pipe") {
test::vehicle vehicle{test::definition::load("wagon")};
REQUIRE(vehicle->VeselVolume == 0.0);
vehicle->ScndPipePress = 0.5;
CHECK(vehicle->SoundingHorns(horn::low) == horn::low);
vehicle->ScndPipePress = 0.0;
CHECK(vehicle->SoundingHorns(horn::low) == 0);
}
TEST_CASE("horn: an electric horn sounds on the low voltage, whatever the air") {
auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "HornType", "Electric"))};
REQUIRE(vehicle->HornSupply == horn_supply::electric);
REQUIRE(vehicle->Horn3Supply == horn_supply::electric); // the third horn follows the others unless told otherwise
vehicle->Compressor = 0.0;
vehicle->Power24vIsAvailable = true;
CHECK(vehicle->SoundingHorns(allhorns) == allhorns);
vehicle->Power24vIsAvailable = false;
CHECK(vehicle->SoundingHorns(allhorns) == 0);
}
TEST_CASE("horn: the third horn can have a supply of its own, an electric horn of a diesel say") {
auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "Horn3Type", "Electric"))};
REQUIRE(vehicle->HornSupply == horn_supply::pneumatic);
REQUIRE(vehicle->Horn3Supply == horn_supply::electric);
vehicle->Compressor = 0.0;
vehicle->Power24vIsAvailable = true;
CHECK(vehicle->SoundingHorns(allhorns) == 4);
vehicle->Compressor = 0.65;
vehicle->Power24vIsAvailable = false;
CHECK(vehicle->SoundingHorns(allhorns) == (horn::low | horn::high));
}

28
tests/unit/test_radio.cpp Normal file
View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -1,5 +1,6 @@
#include "stdafx.h"
#include "vehicle.h"
#include "vehicle/DynObj.h"
#include <fstream>
#include <stdexcept>
@@ -159,6 +160,18 @@ vehicle::vehicle(definition const &Definition, double Velocity, std::string cons
}
void vehicle::step(track const &Track, double Dt) {
compute_forces(Dt);
compute_movement(Track, Dt, TLocation{0.0, 0.0, 0.0});
}
void vehicle::compute_forces(double Dt) {
if (m_pipepressure >= 0.0) {
m_mover->Pipe->CreatePress(m_pipepressure);
}
m_mover->ComputeTotalForce(Dt);
}
double vehicle::compute_movement(track const &Track, double Dt, TLocation const &Location) {
TTrackShape shape;
shape.Len = 1000.0;
shape.dHtrack = Track.gradient;
@@ -167,13 +180,8 @@ void vehicle::step(track const &Track, double Dt) {
param.friction = Track.friction;
param.CategoryFlag = 1;
TTractionParam const traction;
TLocation const location{0.0, 0.0, 0.0};
TRotation const rotation{0.0, 0.0, 0.0};
if (m_pipepressure >= 0.0) {
m_mover->Pipe->CreatePress(m_pipepressure);
}
m_mover->ComputeTotalForce(Dt);
m_mover->ComputeMovement(Dt, Dt, shape, param, traction, location, rotation);
return m_mover->ComputeMovement(Dt, Dt, shape, param, traction, Location, rotation);
}
void vehicle::run(double Seconds, track const &Track, double Dt) {
@@ -184,4 +192,88 @@ void vehicle::run(double Seconds, track const &Track, double Dt) {
}
}
// the physics reaches the vehicle next to it through TDynamicObject::MoverParameters only (neighbour_data). the
// tests can't make a TDynamicObject, whose constructor and members come with the rest of the engine, so the
// neighbour is a zeroed block of its size with just that pointer set: enough for the physics, which reads nothing
// else of it, and never used as a whole object
static_assert(alignof(TDynamicObject) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__);
namespace {
std::unique_ptr<std::byte[]> make_standin(TMoverParameters &Mover) {
auto standin{std::make_unique<std::byte[]>(sizeof(TDynamicObject))}; // zeroed
reinterpret_cast<TDynamicObject *>(standin.get())->MoverParameters = &Mover; // NOSONAR see above
return standin;
}
TDynamicObject *as_neighbour(std::byte *Standin) {
return reinterpret_cast<TDynamicObject *>(Standin); // NOSONAR see above
}
} // namespace
pair::pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap) {
auto const &first{First.mover()};
auto const &second{Second.mover()};
// the first vehicle in the middle, its FirstEnd towards the second one
m_vehicles[0] = {&First, FirstEnd, 0.0, FirstEnd == end::front ? 1.0 : -1.0, make_standin(First.mover())};
m_vehicles[1] = {&Second, SecondEnd, 0.5 * first.Dim.L + Gap + 0.5 * second.Dim.L, SecondEnd == end::front ? -1.0 : 1.0, make_standin(Second.mover())};
}
pair::~pair() {
for (auto const &placed : m_vehicles) {
// the vehicles outlive the pair, they mustn't point at its stand-ins
for (auto &neighbour : placed.owner->mover().Neighbours) {
neighbour = neighbour_data();
}
}
}
void pair::find_neighbours() {
// as TDynamicObject::update_neighbours()
for (auto const &placed : m_vehicles) {
placed.owner->mover().Loc = TLocation{placed.position, 0.0, 0.0};
}
for (int i = 0; i < 2; ++i) {
auto const &self{m_vehicles[i]};
auto const &other{m_vehicles[1 - i]};
auto &mover{self.owner->mover()};
auto const &othermover{other.owner->mover()};
auto &neighbour{mover.Neighbours[self.end]};
neighbour.vehicle = as_neighbour(other.standin.get());
neighbour.vehicle_end = other.end;
neighbour.distance = static_cast<float>(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

View File

@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <memory>
#include <stdexcept>
#include <string>
@@ -64,8 +65,57 @@ public:
double speed() const { return m_mover->V * 3.6; }
private:
friend class pair;
// the two halves of a step: the forces, taken by all the vehicles in the scene first, then the movement,
// the vehicle placed at Location. returns the distance travelled [m], positive forwards
void compute_forces(double Dt);
double compute_movement(track const &Track, double Dt, TLocation const &Location);
std::unique_ptr<TMoverParameters> m_mover;
double m_pipepressure{-1.0};
};
// two vehicles on a straight track, the FirstEnd of the first one facing the SecondEnd of the second one Gap metres
// away (from coupler to coupler), with nothing else around. a step runs them the way the simulation runs the
// vehicles of a scene (vehicle_table::update()): each one finds its neighbour, then both take the forces, the
// coupler and buffer ones among them (CouplerForce()), then both move and handle the collisions (CollisionDetect()).
// they couple and part on their own, as the couplers let them; the speed of each runs along its own axis
// test::pair pair{moving, end::front, standing, end::rear, 1.0 /* m */};
// pair.run(5.0 /* s */);
// CHECK(pair.coupled());
class pair {
public:
pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap);
~pair();
pair(pair const &) = delete;
pair &operator=(pair const &) = delete;
// the vehicles point at the stand-ins of the pair, which stay where they are
pair(pair &&) = delete;
pair &operator=(pair &&) = delete;
// advances both vehicles by one step of Dt
void step(track const &Track = {}, double Dt = 0.01);
// advances both vehicles by Seconds in steps of Dt
void run(double Seconds, track const &Track = {}, double Dt = 0.01);
// distance between the facing couplers [m], negative while the buffers are pressed in
double gap() const;
// type of the coupling between the vehicles, coupling::faux when they aren't coupled
int coupling() const;
bool coupled() const { return coupling() != coupling::faux; }
private:
struct placement {
vehicle *owner{nullptr};
int end{0}; // the end facing the other vehicle
double position{0.0}; // of the middle of the vehicle along the track [m]
double heading{1.0}; // +1 when the vehicle runs forwards towards the growing position, -1 otherwise
std::unique_ptr<std::byte[]> standin; // what the other vehicle sees as this one, see vehicle.cpp
};
void find_neighbours();
std::array<placement, 2> m_vehicles;
};
} // namespace test