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

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

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

fix(cab): the line contactors lamp goes out with the pressure switch (eu07/maszyna#470)

The lamp of the open line contactors, which tells the start can be made, went out at 1 bar in the brake
cylinders, while the pressure switch cuts the start off only at 2 bar (or below 3.6 bar in the main pipe), so
with the brakes partly applied the start could be made with the lamp out. With a pressure switch fitted the lamp
now follows it; without one it goes out at 1 bar as before. The same holds for the lamp of the second unit, which
used to keep its last state between 1 bar and the cut-off. The condition is
TMoverParameters::LineContactorsLampLit().

Tests: tests/unit/test_line_contactors.cpp.

refactor: Sonar findings of PR 217

- horn_supply::pneumatic shadowed the pneumatic enumerator of the sound flags (cpp:S1117), it's horn_supply::air;
  the supplies read with using enum (S6177),
- CouplerForce(): the automatic coupling refused or caught without another level of nested ifs (S134), the
  same outcome: too fast, the coupling is locked; caught, coupled and locked; otherwise left to try again,
- CompressorCheck(): the power the compressor doesn't draw yet noted without a TODO (S1135),
- tests: the previous crash damage setting in an in-class initializer (S3230), the loops which stop early with
  break rather than in their conditions (S886), the stand-ins of test::pair in a std::vector rather than an array
  (S5945), units in the comments of the example without /* */ (S1103).

test: the cases the changes of the branch left without a test

- Cntrl.: BDelay1 to BDelay4 reach the brake delays (now through std::format), the default start types of the
  ground and converter overload relays of a multiple unit and of other vehicles (out of the nested conditionals),
- the compressor: fed from the low voltage it waits from the low voltage coming on; the pressure switch stops it
  above the cut-out pressure, without one the excess air is vented (the vented fraction rewritten),
- the horns: a vehicle without any air sounds as before, the threshold pressure, an electric horn on 110 V,
- the positions of a cab control below zero, and a control without position sounds.

refactor(cab): the kind of the train heating switch as a flag

Read once with the vehicle definition, rather than compared as text in each of the switch commands.

feat(cab): train heating switch springing back (MaSzyna-EU07/maszyna#85)

TrainHeating=[toggle]|impulse in the Switches: section of the vehicle definition makes the train heating switch
an unstable one: it goes up to switch the heating on, down to switch it off, and back to the middle once let go,
instead of staying where it was put. The position of such a switch isn't set from the heating state when the cab
is entered.

Tests: tests/unit/test_switches.cpp.

feat(sound): mirror sounds (MaSzyna-EU07/maszyna#84, eu07/maszyna#322)

The sound definitions of a vehicle (.mmd) take mirroropen: and mirrorclose:, played as a mirror sets off
unfolding or folding, and the locations section mirrors: <offset>... end, the offsets along the vehicle of the
mirror pairs, as for the doors (negative ahead of the centre), each pair at the sides of the vehicle. As the
animation, only the mirrors at the occupied cab sound. The start of a move is mirror_moves::motion in
vehicle/mirrors.h.

Tests: tests/unit/test_mirrors.cpp.

feat(cab): joint oil and fuel pump switch (MaSzyna-EU07/maszyna#81)

A cab control oilfuelpump_sw: switches both the oil and the fuel pump at once, with the new command
oilfuelpumptoggle (no key by default). As a two-state switch it turns both pumps off when both are on and both on
otherwise; as an impulse switch it's held to start them, like the separate pump switches. Its position follows
the pumps when the cab is entered.

refactor(editor): text fields filled without strncpy (MaSzyna-EU07/maszyna#112)

The last three strncpy calls Sonar flags (cpp:S5816), in the road and track editors, copy the text into the
ImGui field buffers with std::string::copy and terminate it right after; the text gets cut to the buffer as
before.

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.

* fix: review findings of PR 217

- the pressures of the mover are in bar (MinCP=7.0 in the vehicle definitions), so the 0.2 the pneumatic horns
  needed was 0.2 bar; it's 2 bar now, and the tests take their pressures in bar too,
- the lamps of the ground relay and of the motor overload relays follow the pressure switch as the line
  contactors one does, TMoverParameters::MainCircuitLampsEnabled(), so the lamps of one cab go out together,
- the joint oil and fuel pump switch leaves out a pump started automatically, which ignores the switching:
  with one, it couldn't turn the other off, and showed off with both running (OilFuelPumpsEnabled()),
- the kind of the train heating switch comes from the occupied vehicle on entering the cab too, as in the
  switch commands; in a multiple unit these are different vehicles,
- CouplerForce() put the force taken back by the other vehicle on its far coupler when the vehicles faced each
  other (Couplers[1 - End] rather than the coupler it's coupled with),
- the velocity difference along the coupler axis computed once, CouplerVelocityDifference(), for CouplerForce()
  and CollisionVelocity(),
- the attachcoupler flag added without the SetFlag() call with 0, the index of the mirror moves noted as by side.

Tests: the coupler force of vehicles facing each other, the joint switch with an automatic pump, the lamps of
the main circuit, the horn and compressor pressures in bar.

* fix: coupler force of vehicles facing each other taken back along the other vehicle's axis

CForce runs along the axis of its own vehicle, so the force the other vehicle takes back on its coupler of the pair
is turned around once more when the vehicles face each other (-CF * DirPatch()), as that vehicle computes it itself.

Tests: the coupler forces of the pair facing each other and facing the same way.
This commit is contained in:
jerrrrycho
2026-11-01 09:12:36 +01:00
committed by GitHub
parent 66a940d4f5
commit d995674bee
34 changed files with 1765 additions and 202 deletions

View File

@@ -16,6 +16,8 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/Logs.h"
#include "utilities/parser.h"
#include "simulation/simulation.h"
#include <format>
//---------------------------------------------------------------------------
// Ra: tu należy przenosić funcje z mover.pas, które nie są z niego wywoływane.
@@ -1176,6 +1178,56 @@ double TMoverParameters::EngineMaxRPM() const
std::numeric_limits<double>::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
{
auto const velocitydifference{CouplerVelocityDifference(End, Other, 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
}
double TMoverParameters::CouplerVelocityDifference(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)
return V - Other.V * DirPatch(End, OtherEnd);
}
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 +1244,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 +1279,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 +1306,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 +4378,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 +4403,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 +4433,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 +5050,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 +5074,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)
{
@@ -5286,7 +5336,7 @@ double TMoverParameters::CouplerForce(int const End, double dt)
auto const newdistance{initialdistance + 10.0 * distancedelta};
auto const dV{V - othervehicle->V * DirPatch(End, otherend)};
auto const dV{CouplerVelocityDifference(End, *othervehicle, otherend)};
auto const absdV{std::abs(dV)};
// potentially generate sounds on clash or stretch
@@ -5399,25 +5449,23 @@ 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
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;
}
}
}
if (thiscoupler.CouplingFlag != coupling::faux)
{
// uzgadnianie prawa Newtona
othervehicle->Couplers[1 - End].CForce = -CF;
// uzgadnianie prawa Newtona, on the coupler of the other vehicle this one is coupled with, along that vehicle's
// axis: turned around once more when the vehicles face each other
othervehicle->Couplers[otherend].CForce = -CF * DirPatch(End, otherend);
}
return CF;
@@ -10468,6 +10516,7 @@ void TMoverParameters::LoadFIZ_Brake(std::string const &line)
auto lookup = compressorpowers.find(extract_value("CompressorPower", line));
CompressorPower = lookup != compressorpowers.end() ? lookup->second : 1;
}
extract_value(CompressorStartDelay, "CompressorStartDelay", line, "");
if (true == extract_value(AirLeakRate, "AirLeakRate", line, ""))
{
@@ -10684,7 +10733,7 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line)
for (int idx = 0; idx < 4; ++idx)
{
extract_value(BrakeDelay[idx], "BDelay" + std::to_string(idx + 1), line, "");
extract_value(BrakeDelay[idx], std::format("BDelay{}", idx + 1), line, "");
}
// brakedelays, brakedelayflag
{
@@ -10780,6 +10829,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<std::string, horn_supply, std::less<>> 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<std::string, int, std::less<>> dynamicbrakes{{"Passive", dbrake_passive}, {"Switch", dbrake_switch}, {"Reversal", dbrake_reversal}, {"Automatic", dbrake_automatic}};
@@ -10939,14 +10998,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 +11399,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);