From d7c66fa7bcdcb4af351591cf2d1b7dd51273f29b Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sat, 7 Nov 2026 18:10:59 +0100 Subject: [PATCH] feat: bistable hold coil lever of the line breaker (eu07/maszyna#364) main_off_bt declared with "type: toggle" in the cab's .mmd is now the line breaker's hold coil lever of the ET22 and EP09: it stays where it is put. Off, it opens the line breaker and keeps it from closing (also cancelling a closing armed by a held main_on_bt); back on, it only feeds the coil again. The single keyboard key puts the lever back on before closing, and a line breaker closed by the AI or through the control cable puts the lever back on to match. TGauge now remembers whether type: was given, since every control defaults to toggle. Without type: on main_off_bt (all vehicles today) nothing changes. Data (SVN, not part of this commit): add "type: toggle" to main_off_bt: in dynamic/pkp/et22_v3/*.mmd and dynamic/pkp/ep09_v1, ep09_v2/*.mmd. Older exes ignore it. --- tests/unit/test_hold_coil_lever.cpp | 30 +++++++++++++++++++ vehicle/Gauge.cpp | 1 + vehicle/Gauge.h | 4 +++ vehicle/Train.cpp | 45 +++++++++++++++++++++++++++-- vehicle/holdcoillever.h | 34 ++++++++++++++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_hold_coil_lever.cpp create mode 100644 vehicle/holdcoillever.h diff --git a/tests/unit/test_hold_coil_lever.cpp b/tests/unit/test_hold_coil_lever.cpp new file mode 100644 index 00000000..cd3a3fee --- /dev/null +++ b/tests/unit/test_hold_coil_lever.cpp @@ -0,0 +1,30 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle/holdcoillever.h" + +// the lever of the line breaker's hold coil on the ET22 and the EP09 stays where it's put (eu07/maszyna#364), when the +// cab declares its main_off_bt control with "type: toggle"; otherwise main_off_bt is the push button it always was + +TEST_CASE("hold coil lever: only a main_off_bt declared as a toggle switch is the bistable lever (eu07/maszyna#364)") { + CHECK(hold_coil_lever::is_bistable(true, true)); + // declared as a push button + CHECK_FALSE(hold_coil_lever::is_bistable(true, false)); + // no type given: a control defaults to the toggle type, which existing cabs relied on for the push button + CHECK_FALSE(hold_coil_lever::is_bistable(false, true)); +} + +TEST_CASE("hold coil lever: operated, it goes over to the other position and stays there") { + // on: the hold coil fed, the line breaker can close + CHECK_FALSE(hold_coil_lever::cuts_off(0.f)); + auto const off{hold_coil_lever::operated(0.f)}; + CHECK(off == doctest::Approx(1.f)); + CHECK(hold_coil_lever::cuts_off(off)); + // operated again it goes back on + auto const on{hold_coil_lever::operated(off)}; + CHECK(on == doctest::Approx(0.f)); + CHECK_FALSE(hold_coil_lever::cuts_off(on)); + // caught on the way between the positions it goes to the far one + CHECK(hold_coil_lever::operated(0.3f) == doctest::Approx(1.f)); + CHECK(hold_coil_lever::operated(0.7f) == doctest::Approx(0.f)); +} diff --git a/vehicle/Gauge.cpp b/vehicle/Gauge.cpp index af8e9a06..080efaef 100644 --- a/vehicle/Gauge.cpp +++ b/vehicle/Gauge.cpp @@ -249,6 +249,7 @@ TGauge::Load_mapping( cParser &Input, TGauge::scratch_data &Scratchpad ) { gaugetype == "delayed" ? push_delayed : gaugetype == "pushtoggle" ? pushtoggle : toggle; // "toggle" and default + m_typedeclared = true; } else if( key == "soundinc:" ) { m_soundfxincrease.deserialize( Input, sound_type::single ); diff --git a/vehicle/Gauge.h b/vehicle/Gauge.h index fdef9c58..54d8504d 100644 --- a/vehicle/Gauge.h +++ b/vehicle/Gauge.h @@ -58,6 +58,9 @@ public: // returns offset of submodel associated with the button from the model centre glm::vec3 model_offset() const; TGaugeType type() const; + // whether the definition of the control declared its type, rather than left it at the default + bool type_declared() const { + return m_typedeclared; } inline bool is_push() const { return ( static_cast( type() ) & static_cast( TGaugeType::push ) ) != 0; } @@ -87,6 +90,7 @@ private: // members TGaugeAnimation m_animation { TGaugeAnimation::gt_Unknown }; // typ ruchu TGaugeType m_type { TGaugeType::toggle }; // switch type + bool m_typedeclared { false }; // the type was given with type: in the definition float m_friction { 0.f }; // hamowanie przy zliżaniu się do zadanej wartości float m_targetvalue { 0.f }; // wartość docelowa float m_value { 0.f }; // wartość obecna diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index 201b8972..5265d755 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "vehicle/Train.h" #include "vehicle/horn.h" +#include "vehicle/holdcoillever.h" #include "utilities/Globals.h" #include "simulation/simulation.h" @@ -3646,6 +3647,15 @@ void TTrain::OnCommand_pantographcompressoractivate(TTrain *Train, command_data } } +namespace +{ +// whether the cab's main_off_bt is the bistable lever of the line breaker's hold coil (eu07/maszyna#364) +bool is_hold_coil_lever(TGauge const &Button) +{ + return Button.SubModel != nullptr && hold_coil_lever::is_bistable(Button.type_declared(), Button.type() == TGaugeType::toggle); +} +} // namespace + void TTrain::OnCommand_linebreakertoggle(TTrain *Train, command_data const &Command) { @@ -3655,6 +3665,11 @@ void TTrain::OnCommand_linebreakertoggle(TTrain *Train, command_data const &Comm if (Train->m_linebreakerstate == 0) { // ...to close the circuit + if (is_hold_coil_lever(Train->ggMainOffButton) && hold_coil_lever::cuts_off(Train->ggMainOffButton.GetDesiredValue())) + { + // the one key works both controls: the hold coil lever goes back on first, or the line breaker couldn't close + Train->ggMainOffButton.UpdateValue(0.0, Train->dsbSwitch); + } // NOTE: bit of a dirty shortcut here OnCommand_linebreakerclose(Train, Command); } @@ -3694,7 +3709,18 @@ void TTrain::OnCommand_linebreakeropen(TTrain *Train, command_data const &Comman if (Command.action == GLFW_PRESS) { // visual feedback - if (Train->ggMainOffButton.SubModel != nullptr) + if (is_hold_coil_lever(Train->ggMainOffButton)) + { + // the bistable lever goes over to its other position and stays there + auto const position{hold_coil_lever::operated(Train->ggMainOffButton.GetDesiredValue())}; + Train->ggMainOffButton.UpdateValue(position, Train->dsbSwitch); + if (false == hold_coil_lever::cuts_off(position)) + { + // the hold coil is fed again, which by itself doesn't close the line breaker + return; + } + } + else if (Train->ggMainOffButton.SubModel != nullptr) { Train->ggMainOffButton.UpdateValue(1.0, Train->dsbSwitch); } @@ -3731,8 +3757,9 @@ void TTrain::OnCommand_linebreakeropen(TTrain *Train, command_data const &Comman { Train->ggMainOnButton.UpdateValue(0.0, Train->dsbSwitch); } - if (Train->ggMainOffButton.SubModel != nullptr) + if (Train->ggMainOffButton.SubModel != nullptr && false == is_hold_coil_lever(Train->ggMainOffButton)) { + // (the bistable hold coil lever stays where it was put) Train->ggMainOffButton.UpdateValue(0.0, Train->dsbSwitch); } // and the two-state switch too, for good measure @@ -8365,6 +8392,20 @@ bool TTrain::Update(double const Deltatime) // because the state of the line breaker isn't changed to match, we need to do it here manually m_linebreakerstate = 0; } + if (is_hold_coil_lever(ggMainOffButton) && hold_coil_lever::cuts_off(ggMainOffButton.GetDesiredValue())) + { + if (true == mvControlled->Mains) + { + // the line breaker was closed with the hold coil lever off, by the AI or through the control cable: + // put the lever back on to match, like the line breaker state above + ggMainOffButton.UpdateValue(0.0); + } + else if (m_linebreakerstate == 2) + { + // the hold coil was cut off while the closing button was held, the line breaker won't close on its release + m_linebreakerstate = 0; + } + } if ((ggMainButton.SubModel != nullptr && ggMainButton.GetDesiredValue() > 0.95) || ggMainOnButton.SubModel != nullptr && ggMainOnButton.GetDesiredValue() > 0.95 || diff --git a/vehicle/holdcoillever.h b/vehicle/holdcoillever.h new file mode 100644 index 00000000..005bccbd --- /dev/null +++ b/vehicle/holdcoillever.h @@ -0,0 +1,34 @@ +/* +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 lever of the line breaker's hold coil (eu07/maszyna#364), as on the ET22 and the EP09: the cab's main_off_bt +// control declared with "type: toggle" stays in the position it's put in. Position 0 keeps the hold coil fed, so the +// line breaker can be closed; position 1 cuts the coil off, which opens the line breaker and keeps it from closing +// until the lever is put back. Without the declared type main_off_bt stays the push button it always was, which +// opens the line breaker and springs back. +namespace hold_coil_lever { + +// whether the main_off_bt control is the bistable lever, from the type its definition declares, if it declares one +inline bool is_bistable(bool const Typedeclared, bool const Toggletype) { + return Typedeclared && Toggletype; +} + +// whether the lever in the given position cuts off the hold coil +inline bool cuts_off(float const Position) { + return Position > 0.5f; +} + +// the position the lever goes to when it's operated: the other one +inline float operated(float const Position) { + return cuts_off(Position) ? 0.f : 1.f; +} + +} // namespace hold_coil_lever