17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-25 17:25:13 +01:00
Files
maszyna/vehicle/Gauge.cpp
jerrrrycho db220a1a98 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.
2026-10-17 16:15:56 +02:00

512 lines
18 KiB
C++

/*
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/.
*/
/*
MaSzyna EU07 locomotive simulator
Copyright (C) 2001-2004 Marcin Wozniak
*/
#include "stdafx.h"
#include "vehicle/Gauge.h"
#include "vehicle/gaugepositions.h"
#include "utilities/parser.h"
#include "model/Model3d.h"
#include "vehicle/DynObj.h"
#include "utilities/Timer.h"
#include "utilities/Logs.h"
#include "rendering/renderer.h"
TGauge::TGauge( sound_source const &Soundtemplate ) :
m_soundtemplate( Soundtemplate )
{
m_soundfxincrease = m_soundtemplate;
m_soundfxdecrease = m_soundtemplate;
m_soundfxon = m_soundtemplate;
m_soundfxoff = m_soundtemplate;
}
void TGauge::Init(TSubModel *Submodel, TSubModel *Submodelon, TGaugeAnimation Type, float Scale, float Offset, float Friction, float Value, float const Endvalue, float const Endscale, bool const Interpolatescale )
{ // ustawienie parametrów animacji submodelu
SubModel = Submodel;
SubModelOn = Submodelon;
m_value = Value;
m_animation = Type;
m_scale = Scale;
m_offset = Offset;
m_friction = Friction;
m_interpolatescale = Interpolatescale;
m_endvalue = Endvalue;
m_endscale = Endscale;
if( m_animation == TGaugeAnimation::gt_Digital ) {
auto *sm { ( SubModel ? SubModel->ChildGet() : nullptr ) };
while( sm != nullptr ) {
// pętla po submodelach potomnych i obracanie ich o kąt zależy od cyfry w (fValue)
if( sm->pName.size() // musi mieć niepustą nazwę
&& std::isdigit(sm->pName[0]) ) {
sm->WillBeAnimated(); // wyłączenie optymalizacji
}
sm = sm->NextGet();
}
// do the same for the active version
sm = SubModelOn ? SubModelOn->ChildGet() : nullptr;
while( sm != nullptr ) {
// pętla po submodelach potomnych i obracanie ich o kąt zależy od cyfry w (fValue)
if( sm->pName.size() // musi mieć niepustą nazwę
&& std::isdigit(sm->pName[0]) ) {
sm->WillBeAnimated(); // wyłączenie optymalizacji
}
sm = sm->NextGet();
}
}
else {
// wyłączenie ignowania jedynkowego transformu
// a banan może być z optymalizacją?
if( SubModel != nullptr ) { SubModel->WillBeAnimated(); }
if( SubModelOn != nullptr ) { SubModelOn->WillBeAnimated(); }
}
// pass submodel location to defined sounds
auto const nulloffset { glm::vec3{} };
auto const offset { model_offset() };
{
std::vector soundfxs = {
&m_soundfxincrease,
&m_soundfxdecrease,
&m_soundfxon,
&m_soundfxoff
};
for( auto *soundfx : soundfxs ) {
if( soundfx->offset() == nulloffset ) {
soundfx->offset( offset );
}
}
}
for( auto &[soundfxkey, soundfxsource] : m_soundfxvalues ) {
if( soundfxsource.offset() == nulloffset ) {
soundfxsource.offset( offset );
}
}
};
void TGauge::Load( cParser &Parser, TDynamicObject const *Owner, double const mul ) {
std::string submodelname;
std::string gaugetypename;
float scale;
float endscale;
float endvalue;
float offset;
float friction;
endscale = -1;
endvalue = -1;
bool interpolatescale { false };
Parser.getTokens();
if( Parser.peek() != "{" ) {
// old fixed size config
Parser >> submodelname;
gaugetypename = Parser.getToken<std::string>( true );
Parser.getTokens( 3, false );
Parser
>> scale
>> offset
>> friction;
if( gaugetypename == "rotvar"
|| gaugetypename == "movvar" ) {
interpolatescale = true;
Parser.getTokens( 2, false );
Parser
>> endvalue
>> endscale;
}
}
else {
// new, block type config
// TODO: rework the base part into yaml-compatible flow style mapping
submodelname = Parser.getToken<std::string>( false );
gaugetypename = Parser.getToken<std::string>( true );
Parser.getTokens( 3, false );
Parser
>> scale
>> offset
>> friction;
if( gaugetypename == "rotvar"
|| gaugetypename == "movvar" ) {
interpolatescale = true;
Parser.getTokens( 2, false );
Parser
>> endvalue
>> endscale;
}
// new, variable length section
{
scratch_data scratchpad;
while( true == Load_mapping( Parser, scratchpad ) ) {
// all work done by while()
}
// post-deserialization cleanup
// set provided custom soundproofing to assigned sounds (for sounds without their own custom soundproofing)
if( scratchpad.soundproofing ) {
if( !m_soundfxincrease.soundproofing() ) {
m_soundfxincrease.soundproofing() = scratchpad.soundproofing;
}
if( !m_soundfxdecrease.soundproofing() ) {
m_soundfxdecrease.soundproofing() = scratchpad.soundproofing;
}
if( !m_soundfxon.soundproofing() ) {
m_soundfxon.soundproofing() = scratchpad.soundproofing;
}
if( !m_soundfxoff.soundproofing() ) {
m_soundfxoff.soundproofing() = scratchpad.soundproofing;
}
for( auto &[soundfxkey, soundfxsource] : m_soundfxvalues ) {
if( !soundfxsource.soundproofing() ) {
soundfxsource.soundproofing() = scratchpad.soundproofing;
}
}
}
}
}
// bind defined sounds with the button owner
m_soundfxincrease.owner( Owner );
m_soundfxdecrease.owner( Owner );
m_soundfxon.owner( Owner );
m_soundfxoff.owner( Owner );
for( auto &[soundfxkey, soundfxsource] : m_soundfxvalues ) {
soundfxsource.owner( Owner );
}
scale *= static_cast<float>(mul);
if( interpolatescale ) {
endscale *= static_cast<float>(mul);
}
TSubModel *submodel { nullptr };
std::array<TModel3d *, 2> sources { Owner->mdKabina, Owner->mdLowPolyInt };
for( auto const *source : sources ) {
if( source != nullptr
&& ( submodel = source->GetFromName( submodelname ) ) != nullptr ) {
// got what we wanted, bail out
break;
}
// there's a possibility the default submodel was named with _off suffix
if( source != nullptr
&& ( submodel = source->GetFromName( submodelname + "_off" ) ) != nullptr ) {
// got what we wanted, bail out
break;
}
}
if( submodel == nullptr ) {
ErrorLog( "Bad model: failed to locate sub-model \"" + submodelname + "\" in 3d model(s) of \"" + Owner->name() + "\"", logtype::model );
}
// see if we can locate optional submodel for active state, with _on suffix
TSubModel *submodelon { nullptr };
for( auto const *source : sources ) {
if( source != nullptr
&& ( submodelon = source->GetFromName( submodelname + "_on" ) ) != nullptr ) {
// got what we wanted, bail out
break;
}
}
std::map<std::string, TGaugeAnimation, std::less<>> gaugetypes {
{ "rot", TGaugeAnimation::gt_Rotate },
{ "rotvar", TGaugeAnimation::gt_Rotate },
{ "mov", TGaugeAnimation::gt_Move },
{ "movvar", TGaugeAnimation::gt_Move },
{ "wip", TGaugeAnimation::gt_Wiper },
{ "dgt", TGaugeAnimation::gt_Digital }
};
auto lookup = gaugetypes.find( gaugetypename );
auto const type = lookup != gaugetypes.end() ? lookup->second : TGaugeAnimation::gt_Unknown;
Init( submodel, submodelon, type, scale, offset, friction, 0, endvalue, endscale, interpolatescale );
// return md2 != nullptr; // true, gdy podany model zewnętrzny, a w kabinie nie było
};
bool
TGauge::Load_mapping( cParser &Input, TGauge::scratch_data &Scratchpad ) {
// token can be a key or block end
auto const key { Input.getToken<std::string>( true, "\n\r\t ,;" ) };
if( true == key.empty() || key == "}" ) { return false; }
// if not block end then the key is followed by assigned value or sub-block
if( key == "type:" ) {
using enum TGaugeType;
auto const gaugetype { Input.getToken<std::string>( true, "\n\r\t ,;" ) };
m_type = gaugetype == "push" ? push :
gaugetype == "impulse" ? push :
gaugetype == "return" ? push :
gaugetype == "delayed" ? push_delayed :
gaugetype == "pushtoggle" ? pushtoggle :
toggle; // "toggle" and default
}
else if( key == "soundinc:" ) {
m_soundfxincrease.deserialize( Input, sound_type::single );
}
else if( key == "sounddec:" ) {
m_soundfxdecrease.deserialize( Input, sound_type::single );
}
else if( key == "soundon:" ) {
m_soundfxon.deserialize( Input, sound_type::single );
}
else if( key == "soundoff:" ) {
m_soundfxoff.deserialize( Input, sound_type::single );
}
else if( key == "soundproofing:" ) {
// custom soundproofing in format [ p1, p2, p3, p4, p5, p6 ]
Input.getTokens( 6, false, "\n\r\t ,;[]" );
std::array<float, 6> soundproofing;
Input
>> soundproofing[ 0 ]
>> soundproofing[ 1 ]
>> soundproofing[ 2 ]
>> soundproofing[ 3 ]
>> soundproofing[ 4 ]
>> soundproofing[ 5 ];
Scratchpad.soundproofing = soundproofing;
}
else if( key.starts_with("sound") ) {
// sounds assigned to specific gauge values, defined by key soundX: where X = value
auto const indexstart { key.find_first_of( "-1234567890" ) };
auto const indexend { key.find_first_not_of( "-1234567890", indexstart ) };
if( indexstart != std::string::npos ) {
m_soundfxvalues.try_emplace(
std::stoi( key.substr( indexstart, indexend - indexstart ) ),
sound_source( m_soundtemplate ).deserialize( Input, sound_type::single ) );
}
}
return true; // return value marks a key: value pair was extracted, nothing about whether it's recognized
}
// ustawienie wartości docelowej. plays provided fallback sound, if no sound was defined in the control itself
bool
TGauge::UpdateValue( float fNewDesired, std::optional<sound_source> &Fallbacksound ) {
if (false == UpdateValue( fNewDesired ) && Fallbacksound) {
Fallbacksound->play( m_soundtype );
return true;
}
return false;
}
bool
TGauge::UpdateValue( float fNewDesired ) {
auto const desiredtimes100 = static_cast<int>( std::round( 100.0 * fNewDesired ) );
auto const previoustimes100 = static_cast<int>( std::round( 100.0 * m_targetvalue ) );
if( desiredtimes100 == previoustimes100 ) {
return true;
}
m_targetvalue = fNewDesired;
if( desiredtimes100 % 100 != 0 ) {
// toggle the control to continous range/exclusive sound mode from now on
m_soundtype = sound_flags::exclusive;
}
// if there's any sound associated with a position reached on the way to the new requested value, play it
// check value-specific table first...
if( auto const reached = gauge_positions::last_reached( m_soundfxvalues, previoustimes100, desiredtimes100 );
reached != m_soundfxvalues.end() ) {
reached->second.play();
return true;
}
// ...and if there isn't any, fall back on the basic set...
// HACK: crude way to discern controls with continuous and quantized value range
auto const currentvalue = GetValue();
if( currentvalue < fNewDesired && false == m_soundfxincrease.empty() ) {
// shift up
m_soundfxincrease.play( m_soundtype );
return true;
}
if( currentvalue > fNewDesired && false == m_soundfxdecrease.empty() ) {
// shift down
m_soundfxdecrease.play( m_soundtype );
return true;
}
return false; // no suitable sound was found
}
void TGauge::PutValue(float fNewDesired)
{ // McZapkie-281102: natychmiastowe wpisanie wartosci
m_targetvalue = fNewDesired;
m_value = m_targetvalue;
};
float TGauge::GetValue() const {
// we feed value in range 0-1 so we should be getting it reported in the same range
return m_value;
}
float TGauge::GetDesiredValue() const {
// we feed value in range 0-1 so we should be getting it reported in the same range
return m_targetvalue;
}
void TGauge::Update( bool const Power ) {
// update value
// TODO: remove passing manually power state when LD is in place
if( m_value != m_targetvalue ) {
auto dt = static_cast<float>(Timer::GetDeltaTime());
if( m_friction > 0 && dt < 0.5 * m_friction ) {
// McZapkie-281102: zabezpieczenie przed oscylacjami dla dlugich czasow
m_value += dt * ( m_targetvalue - m_value ) / m_friction;
if( std::abs( m_targetvalue - m_value ) <= 0.0001 ) {
// close enough, we can stop updating the model
m_value = m_targetvalue; // set it exactly as requested just in case it matters
}
}
else {
m_value = m_targetvalue;
}
}
// update submodel visibility
auto const state { Power && ( m_stateinput ? *m_stateinput : false ) };
if( state != m_state ) {
// on state change play assigned sound
if( true == state ) { m_soundfxon.play(); }
else { m_soundfxoff.play(); }
}
m_state = state;
// toggle submodel visibility only if the active state submodel is present,
// keep the default model always visible otherwise
if( SubModelOn != nullptr ) {
SubModelOn->iVisible = m_state;
if( SubModel != nullptr ) {
SubModel->iVisible = !m_state;
}
}
// update submodel animations
UpdateAnimation( SubModel );
UpdateAnimation( SubModelOn );
};
void TGauge::AssignFloat(float *fValue)
{
m_datatype = 'f';
fData = fValue;
};
void TGauge::AssignDouble(double *dValue)
{
m_datatype = 'd';
dData = dValue;
};
void TGauge::AssignInt(int *iValue)
{
m_datatype = 'i';
iData = iValue;
};
void TGauge::AssignBool(bool *bValue)
{
m_datatype = 'b';
bData = bValue;
};
void TGauge::UpdateValue()
{ // ustawienie wartości docelowej z parametru
switch (m_datatype)
{ // to nie jest zbyt optymalne, można by zrobić osobne funkcje
case 'f': {
UpdateValue( *fData );
break;
}
case 'd': {
UpdateValue( static_cast<float>(*dData) );
break;
}
case 'i': {
UpdateValue( static_cast<float>(*iData) );
break;
}
case 'b': {
UpdateValue( *bData ? 1.f : 0.f );
break;
}
default: {
break;
}
}
};
void TGauge::AssignState( bool const *State ) {
m_stateinput = State;
}
float TGauge::GetScaledValue() const {
return false == m_interpolatescale ? m_value * m_scale + m_offset : m_value * std::lerp(m_scale, m_endscale, std::clamp(m_value / m_endvalue, 0.f, 1.f)) + m_offset;
}
void
TGauge::UpdateAnimation( TSubModel *Submodel ) const {
if( Submodel == nullptr ) { return; }
switch (m_animation) {
case TGaugeAnimation::gt_Rotate: {
Submodel->SetRotate( float3( 0, 1, 0 ), static_cast<float>(GetScaledValue() * 360.0) );
break;
}
case TGaugeAnimation::gt_Move: {
Submodel->SetTranslate( float3( 0, 0, GetScaledValue() ) );
break;
}
case TGaugeAnimation::gt_Wiper: {
auto const scaledvalue { GetScaledValue() };
Submodel->SetRotate( float3( 0, 1, 0 ), static_cast<float>(scaledvalue * 360.0) );
if( auto *sm = Submodel->ChildGet(); sm ) {
sm->SetRotate( float3( 0, 1, 0 ), static_cast<float>(scaledvalue * 360.0) );
sm = sm->ChildGet();
if( sm )
sm->SetRotate( float3( 0, 1, 0 ), static_cast<float>(scaledvalue * 360.0) );
}
break;
}
case TGaugeAnimation::gt_Digital: {
// Ra 2014-07: licznik cyfrowy
auto *sm = Submodel->ChildGet();
/* std::string n = FormatFloat( "0000000000", floor( fValue ) ); // na razie tak trochę bez sensu
*/ std::string n( "000000000" + std::to_string( static_cast<int>( std::floor( GetScaledValue() ) ) ) );
if( n.length() > 10 ) { n.erase( 0, n.length() - 10 ); } // also dumb but should work for now
do { // pętla po submodelach potomnych i obracanie ich o kąt zależy od cyfry w (fValue)
if( sm->pName.size()
&& std::isdigit(sm->pName[0]) ) {
sm->SetRotate(
float3( 0, 1, 0 ),
static_cast<float>(-36.0 * ( n[ '0' + 9 - sm->pName[ 0 ] ] - '0' )) );
}
sm = sm->NextGet();
} while( sm );
break;
}
default: {
break;
}
}
}
// returns offset of submodel associated with the button from the model centre
glm::vec3
TGauge::model_offset() const {
return SubModel != nullptr ? SubModel->offset(1.f) : glm::vec3();
}
TGaugeType
TGauge::type() const {
return m_type;
}
//---------------------------------------------------------------------------