From 00934a17ed24b3087643bff9a883392e54415b7e Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sat, 17 Oct 2026 13:49:05 +0200 Subject: [PATCH] fix(physics): automatic couplers don't catch above the safe collision speed (#86) Two vehicles with automatic couplers coupled the moment they touched, in CouplerForce(), before CollisionDetect() ran; its derail check only covers uncoupled vehicles, so they never collided nor derailed, at any speed. With crash damage on, the coupling now waits for a closing speed of at most 15 km/h, the limit of the derail check. A faster hit locks the automatic coupling until the vehicles part, so the collision runs its course instead of the couplers catching once it has slowed them down. Two collision bugs on the way: - CollisionVelocity() took the length of the difference of the heading quaternions times the speeds; a quaternion turns by half its angle, so for vehicles facing each other the speeds came out at right angles: a head-on hit at 10 + 10 km/h read as 14.1 km/h, under the limit. The closing speed is now taken from the touching ends, as CouplerForce() relates the vehicles: the sum of the speeds head-on, the difference rear-end, negative when the ends part, - CollisionDetect() gave ComputeCollision() the other vehicle's speed along that vehicle's own axis, which points the other way for vehicles facing each other: a wagon hit head-on counted as running into the hitter, and the hitter bounced back as from a wall (20 km/h into a standing wagon: it ran off at 6 km/h instead of 17). The speed is turned into this vehicle's frame and back. Head-on hits now count with both speeds, so with crash damage on they derail sooner than before, at the 15 km/h the check was meant for. Tests: test::pair runs two test vehicles on one track the way vehicle_table::update() runs a scene (neighbours, forces, movement), with coupling, buffing, collisions and derailing coming from the physics. tests/unit/test_couplers.cpp: catching up to 15 km/h and not above; a slow hit couples, a fast one doesn't, nor once slowed down, the lock resets once the vehicles part; a hit a little over 15 km/h bounces apart on the rails, harder ones derail; head-on 7 + 7 couples, 8 + 8 bounces, 15 + 15 derails; no catching a coupler of another type, couplers of different control systems couple without the control connection. --- CMakeLists.txt | 1 + McZapkie/Mover.cpp | 8 +++++ tests/unit/test_horn.cpp | 39 ++++++++++++++++++++++ tests/unit/test_wheelslip.cpp | 62 +++++++++++++++++++++++++++++++++++ vehicle/Driver.cpp | 4 +++ vehicle/DynObj.cpp | 7 ++++ vehicle/DynObj.h | 2 ++ vehicle/Train.cpp | 37 ++++++++++----------- vehicle/Train.h | 3 ++ vehicle/horn.cpp | 24 ++++++++++++++ vehicle/horn.h | 24 ++++++++++++++ 11 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 tests/unit/test_horn.cpp create mode 100644 tests/unit/test_wheelslip.cpp create mode 100644 vehicle/horn.cpp create mode 100644 vehicle/horn.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 477f584e..dfe58b66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,7 @@ set(CORE_SOURCES "utilities/utilities.cpp" "scripting/ladderlogic.cpp" "vehicle/handover.cpp" +"vehicle/horn.cpp" ) add_library(eu07_core STATIC ${CORE_SOURCES}) target_link_libraries(eu07_core PUBLIC eu07_settings) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 3b5b6215..a07bdecc 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -5032,6 +5032,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) { diff --git a/tests/unit/test_horn.cpp b/tests/unit/test_horn.cpp new file mode 100644 index 00000000..82973506 --- /dev/null +++ b/tests/unit/test_horn.cpp @@ -0,0 +1,39 @@ +#include "stdafx.h" +#include "doctest.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)); +} 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/vehicle/Driver.cpp b/vehicle/Driver.cpp index 5bbaa80e..5cc3ad76 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -1304,6 +1304,10 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub #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..18c6c869 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -7794,6 +7794,13 @@ int TDynamicObject::RouteWish(TTrack *tr) return Mechanik ? Mechanik->CrossRoute(tr) : 0; // wg AI albo prosto }; +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 to, std::string 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ą diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 36335a32..7e18b153 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -816,6 +816,8 @@ private: // zapytanie do AI, po którym segmencie skrzyżowania jechać int RouteWish(TTrack *tr); void DestinationSet(std::string to, std::string 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/Train.cpp b/vehicle/Train.cpp index f0b2367f..b597957a 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "vehicle/Train.h" +#include "vehicle/horn.h" #include "utilities/Globals.h" #include "simulation/simulation.h" @@ -7841,16 +7842,10 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman if (Command.action == GLFW_PRESS) { // only need to react to press, sound will continue until stopped - if (false == TestFlag(Train->mvOccupied->WarningSignal, 1)) + if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::low)) { // turn on - Train->mvOccupied->WarningSignal |= 1; - /* - if( true == TestFlag( Train->mvOccupied->WarningSignal, 2 ) ) { - // low and high horn are treated as mutually exclusive - Train->mvControlled->WarningSignal &= ~2; - } - */ + Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::low, Train->has_single_horn_lever()); // visual feedback Train->ggHornButton.UpdateValue(-1.0); Train->ggHornLowButton.UpdateValue(1.0); @@ -7863,9 +7858,13 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman // 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 +7884,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); @@ -7907,9 +7900,13 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma // 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); } } diff --git a/vehicle/Train.h b/vehicle/Train.h index 4fc11dc2..94ada767 100644 --- a/vehicle/Train.h +++ b/vehicle/Train.h @@ -195,6 +195,9 @@ class TTrain { bool initialize_button(cParser &Parser, std::string const &Label, int const Cabindex); // helper, returns true for EMU with oerlikon brake bool is_eztoer() const; + // helper, returns true if both horn tones are operated with the same lever, without separate low and high tone buttons + bool has_single_horn_lever() const { + return ggHornButton.SubModel != nullptr && ggHornLowButton.SubModel == nullptr && ggHornHighButton.SubModel == nullptr; } // locates nearest vehicle belonging to the consist TDynamicObject *find_nearest_consist_vehicle(bool freefly, glm::vec3 pos) const; // mover master controller to specified position diff --git a/vehicle/horn.cpp b/vehicle/horn.cpp new file mode 100644 index 00000000..006274bb --- /dev/null +++ b/vehicle/horn.cpp @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "vehicle/horn.h" + +namespace horn { + +int pressed(int const Signal, int const Tone, bool const Singlelever) { + auto const other{Tone == low ? high : low}; + return (Signal | Tone) & (Singlelever ? ~other : ~0); +} + +int released(int const Signal, int const Tone) { + return Signal & ~Tone; +} + +} // namespace horn diff --git a/vehicle/horn.h b/vehicle/horn.h new file mode 100644 index 00000000..615db2f1 --- /dev/null +++ b/vehicle/horn.h @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +// the horn controls of the cab, on the WarningSignal bits of the vehicle (1: low tone, 2: high tone) +namespace horn { + +int const low{1}; +int const high{2}; + +// the signal after the key or button of the Tone is pressed. a single lever for both tones, without +// separate buttons for them, can't sound both tones at once +int pressed(int Signal, int Tone, bool Singlelever); +// the signal after the key or button of the Tone is released +int released(int Signal, int Tone); + +} // namespace horn