17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-06 00:02:26 +01:00

fix(physics): automatic couplers don't catch above the safe collision speed (#86)

This commit is contained in:
jerrrrycho
2026-10-15 21:20:35 +02:00
parent b400255dae
commit 71be884ed9
11 changed files with 192 additions and 20 deletions

View File

@@ -461,6 +461,7 @@ set(CORE_SOURCES
"utilities/parser.cpp" "utilities/parser.cpp"
"utilities/utilities.cpp" "utilities/utilities.cpp"
"scripting/ladderlogic.cpp" "scripting/ladderlogic.cpp"
"vehicle/horn.cpp"
) )
add_library(eu07_core STATIC ${CORE_SOURCES}) add_library(eu07_core STATIC ${CORE_SOURCES})
target_link_libraries(eu07_core PUBLIC eu07_settings) target_link_libraries(eu07_core PUBLIC eu07_settings)

View File

@@ -5032,6 +5032,14 @@ void TMoverParameters::ComputeTotalForce(double dt)
{ {
SlippingWheels = true; 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; double temp_nrot = nrot;
if (true == SlippingWheels) if (true == SlippingWheels)
{ {

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

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

View File

@@ -0,0 +1,63 @@
#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 (double t = 0.0; t < 600.0; t += 0.01) {
result.skidded |= Coach->SlippingWheels;
auto const speed{std::abs(Coach.speed())};
if (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

@@ -1303,6 +1303,10 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub
#endif #endif
asNextStop = TrainParams.NextStop(); // informacja o końcu trasy asNextStop = TrainParams.NextStop(); // informacja o końcu trasy
TrainParams.NewName("none"); // czyszczenie nieaktualnego rozkładu 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ć // ma nie podjeżdżać pod W4 i ma je pomijać
iDrivigFlags &= ~moveStopCloser; iDrivigFlags &= ~moveStopCloser;
if( false == TestFlag( iDrivigFlags, movePushPull ) ) { if( false == TestFlag( iDrivigFlags, movePushPull ) ) {

View File

@@ -7794,6 +7794,13 @@ int TDynamicObject::RouteWish(TTrack *tr)
return Mechanik ? Mechanik->CrossRoute(tr) : 0; // wg AI albo prosto 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) { void TDynamicObject::DestinationSet(std::string to, std::string numer) {
// ustawienie stacji docelowej oraz wymiennej tekstury 4, jeśli istnieje plik // ustawienie stacji docelowej oraz wymiennej tekstury 4, jeśli istnieje plik
// w zasadzie, to każdy wagon mógłby mieć inną stację docelową // w zasadzie, to każdy wagon mógłby mieć inną stację docelową

View File

@@ -816,6 +816,8 @@ private:
// zapytanie do AI, po którym segmencie skrzyżowania jechać // zapytanie do AI, po którym segmencie skrzyżowania jechać
int RouteWish(TTrack *tr); int RouteWish(TTrack *tr);
void DestinationSet(std::string to, std::string numer); 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 ); material_handle DestinationFind( std::string Destination );
void OverheadTrack(float o); void OverheadTrack(float o);
glm::dvec3 get_future_movement() const; glm::dvec3 get_future_movement() const;

View File

@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h" #include "stdafx.h"
#include "vehicle/Train.h" #include "vehicle/Train.h"
#include "vehicle/horn.h"
#include "utilities/Globals.h" #include "utilities/Globals.h"
#include "simulation/simulation.h" #include "simulation/simulation.h"
@@ -7841,16 +7842,10 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman
if (Command.action == GLFW_PRESS) if (Command.action == GLFW_PRESS)
{ {
// only need to react to press, sound will continue until stopped // 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 // turn on
Train->mvOccupied->WarningSignal |= 1; Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::low, Train->has_single_horn_lever());
/*
if( true == TestFlag( Train->mvOccupied->WarningSignal, 2 ) ) {
// low and high horn are treated as mutually exclusive
Train->mvControlled->WarningSignal &= ~2;
}
*/
// visual feedback // visual feedback
Train->ggHornButton.UpdateValue(-1.0); Train->ggHornButton.UpdateValue(-1.0);
Train->ggHornLowButton.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 // 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 | 2 );
*/ */
Train->mvOccupied->WarningSignal &= ~1; Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::low);
// visual feedback // 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); Train->ggHornLowButton.UpdateValue(0.0);
} }
} }
@@ -7885,16 +7884,10 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma
if (Command.action == GLFW_PRESS) if (Command.action == GLFW_PRESS)
{ {
// only need to react to press, sound will continue until stopped // 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 // turn on
Train->mvOccupied->WarningSignal |= 2; Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::high, Train->has_single_horn_lever());
/*
if( true == TestFlag( Train->mvOccupied->WarningSignal, 1 ) ) {
// low and high horn are treated as mutually exclusive
Train->mvControlled->WarningSignal &= ~1;
}
*/
// visual feedback // visual feedback
Train->ggHornButton.UpdateValue(1.0); Train->ggHornButton.UpdateValue(1.0);
Train->ggHornHighButton.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 // 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 | 2 );
*/ */
Train->mvOccupied->WarningSignal &= ~2; Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::high);
// visual feedback // 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); Train->ggHornHighButton.UpdateValue(0.0);
} }
} }

View File

@@ -195,6 +195,9 @@ class TTrain {
bool initialize_button(cParser &Parser, std::string const &Label, int const Cabindex); bool initialize_button(cParser &Parser, std::string const &Label, int const Cabindex);
// helper, returns true for EMU with oerlikon brake // helper, returns true for EMU with oerlikon brake
bool is_eztoer() const; 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 // locates nearest vehicle belonging to the consist
TDynamicObject *find_nearest_consist_vehicle(bool freefly, glm::vec3 pos) const; TDynamicObject *find_nearest_consist_vehicle(bool freefly, glm::vec3 pos) const;
// mover master controller to specified position // mover master controller to specified position

24
vehicle/horn.cpp Normal file
View File

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

24
vehicle/horn.h Normal file
View File

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