From 13ded6cdfa33c444acd48097db428440a7a4b8f7 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sun, 8 Nov 2026 07:12:30 +0100 Subject: [PATCH] feat: separate engine start switches for the vehicles in multiple (eu07/maszyna#80) In vehicles like the ST43, the SP32 or the SN61 every engine of a train in multiple has its own start and stop buttons in the cab. Until now the cab's line breaker / engine start switch (main_sw, main_on_bt, main_off_bt) always started and stopped the engines of all the vehicles under its control. - .fiz, Engine: JointEngineStart=Yes|No (default Yes, as before). With No the cab's main_* work the cab's own vehicle only, and an engine stopping on its own (stalled, out of fuel) stops only its own vehicle instead of every engine in the train. - .mmd: mainb_sw, mainb_on_bt, mainb_off_bt work the second vehicle with an engine under the cab's control, mainc_sw, mainc_on_bt, mainc_off_bt the third one. They count along the control cable from the cab's vehicle, behind the cab first. Like main_*, the start switch has to be held for the vehicle's IniCDelay with the vehicle ready (MainSwitchCheck); an impulse switch of an electric locomotive with series motors closes on release. - New commands, added at the end of the command list: linebreakertoggleb, linebreakeropenb, linebreakercloseb, linebreakertogglec, linebreakeropenc, linebreakerclosec. No default keys. - The logic is in vehicle/enginestart.{h,cpp} (core library), tested in tests/unit/test_engine_start.cpp. Nothing changes for existing vehicles: the new key and the new cab controls are optional, and without them the code does what it did before. Game data (SVN, not changed here, to be done separately; the code works without it): for the SN61 (dynamic/pkp/sn61_v2), whose cab models already have the buttons start02/stop02/start03/stop03, - sn61.fiz, sn61-86.fiz, sn61_v1.fiz and the other SN61 .fiz: add JointEngineStart=No to the Engine: line; - sn61.mmd.inc (and the other SN61 includes): after main_on_bt add mainb_off_bt: { stop02 mov -0.005 0 0.1 ... } mainb_on_bt: { start02 mov -0.005 0 0.1 ... } mainc_off_bt: { stop03 mov -0.005 0 0.1 ... } mainc_on_bt: { start03 mov -0.005 0 0.1 ... } with the sounds of main_off_bt / main_on_bt. The tooltips "line breaker B" / "line breaker C" are new strings for lang/*.po. --- CMakeLists.txt | 1 + McZapkie/MOVER.h | 9 ++ McZapkie/Mover.cpp | 7 +- input/command.cpp | 12 ++ input/command.h | 6 + input/drivermouseinput.cpp | 18 +++ tests/unit/test_engine_start.cpp | 248 +++++++++++++++++++++++++++++++ utilities/translation.cpp | 6 + vehicle/Train.cpp | 193 +++++++++++++++++++++++- vehicle/Train.h | 17 +++ vehicle/enginestart.cpp | 107 +++++++++++++ vehicle/enginestart.h | 45 ++++++ vr/openvr_imp.cpp | 18 +++ 13 files changed, 682 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_engine_start.cpp create mode 100644 vehicle/enginestart.cpp create mode 100644 vehicle/enginestart.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f34ad61..05e24b42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,7 @@ set(CORE_SOURCES "utilities/parser.cpp" "utilities/utilities.cpp" "scripting/ladderlogic.cpp" +"vehicle/enginestart.cpp" "vehicle/handover.cpp" "vehicle/horn.cpp" "vehicle/smoke.cpp" diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index 16bb207f..3d0f7b77 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -2093,6 +2093,9 @@ class TMoverParameters double MainsInitTime{0.0}; // config, initialization time (in seconds) of the main circuit after it receives power, before it can be closed double MainsInitTimeCountdown{0.0}; // current state of main circuit initialization, remaining time (in seconds) until it's ready start_t MainsStart{start_t::manual}; + // config, Engine:JointEngineStart. whether the cab's line breaker / engine start switch works the vehicles in multiple + // too; when it doesn't, the cab has separate switches for them (mainb_*, mainc_*) + bool JointEngineStart{true}; bool LineBreakerClosesOnlyAtNoPowerPos{false}; bool ControlPressureSwitch{false}; // activates if the main pipe and/or brake cylinder pressure aren't within operational levels bool HasControlPressureSwitch{true}; @@ -2475,6 +2478,12 @@ class TMoverParameters // whether the cab command opening the main circuit opens it where the only cab switch of the circuit is the button // closing it: on a diesel that's the starter button, and the command is the one way to stop an engine whose fuel pump // starts on its own (eu07/maszyna#427). the other vehicles with such a button keep ignoring the command + // the vehicles the line breaker / engine start switch of this vehicle works, and its engine stopping on its own + // stops: the whole consist, or with JointEngineStart=No this vehicle alone (eu07/maszyna#80) + range_t MainSwitchRange() const + { + return JointEngineStart ? range_t::consist : range_t::local; + } bool MainCircuitOpensWithStarterOnly() const { return EngineType == TEngineType::DieselEngine || EngineType == TEngineType::DieselElectric; diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 20761bac..e48ae4e1 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -7869,7 +7869,8 @@ bool TMoverParameters::dizel_Update(double dt) { // knock out the engine if the fuel pump isn't feeding it // TBD, TODO: grace period before the engine is starved for fuel and knocked out - MainSwitch(false); + // an engine started on its own stops on its own + MainSwitch(false, MainSwitchRange()); } bool DU{false}; @@ -8160,7 +8161,8 @@ double TMoverParameters::dizel_Momentum(double Fill, double n, double dt) if (enrot <= 0 && false == dizel_spinup) { - MainSwitch(false); + // a stopped engine doesn't stop those started on their own + MainSwitch(false, MainSwitchRange()); enrot = 0; } @@ -11158,6 +11160,7 @@ void TMoverParameters::LoadFIZ_Engine(std::string const &Input) { EngineType = LoadFIZ_EngineDecode(extract_value("EngineType", Input)); + extract_value(JointEngineStart, "JointEngineStart", Input, ""); if (std::string transmission = extract_value("Trans", Input); false == transmission.empty()) { diff --git a/input/command.cpp b/input/command.cpp index 0ae67cfa..ad208ce9 100644 --- a/input/command.cpp +++ b/input/command.cpp @@ -406,6 +406,12 @@ commanddescription_sequence Commands_descriptions = { {"wiperswitchdecrease", command_target::vehicle, command_mode::oneoff}, {"lightsset", command_target::vehicle, command_mode::oneoff}, {"oilfuelpumptoggle", command_target::vehicle, command_mode::oneoff}, + {"linebreakertoggleb", command_target::vehicle, command_mode::oneoff}, + {"linebreakeropenb", command_target::vehicle, command_mode::oneoff}, + {"linebreakercloseb", command_target::vehicle, command_mode::oneoff}, + {"linebreakertogglec", command_target::vehicle, command_mode::oneoff}, + {"linebreakeropenc", command_target::vehicle, command_mode::oneoff}, + {"linebreakerclosec", command_target::vehicle, command_mode::oneoff}, }; // Maps of command and coresponding strings @@ -791,6 +797,12 @@ std::unordered_map const commandMap = { {"wiperswitchdecrease", user_command::wiperswitchdecrease}, {"lightsset", user_command::lightsset}, {"oilfuelpumptoggle", user_command::oilfuelpumptoggle}, + {"linebreakertoggleb", user_command::linebreakertoggleb}, + {"linebreakeropenb", user_command::linebreakeropenb}, + {"linebreakercloseb", user_command::linebreakercloseb}, + {"linebreakertogglec", user_command::linebreakertogglec}, + {"linebreakeropenc", user_command::linebreakeropenc}, + {"linebreakerclosec", user_command::linebreakerclosec}, {"none", user_command::none}}; } // namespace simulation diff --git a/input/command.h b/input/command.h index ddacef4a..ea51a68c 100644 --- a/input/command.h +++ b/input/command.h @@ -404,6 +404,12 @@ enum class user_command wiperswitchdecrease, lightsset, oilfuelpumptoggle, + linebreakertoggleb, + linebreakeropenb, + linebreakercloseb, + linebreakertogglec, + linebreakeropenc, + linebreakerclosec, none = -1 }; diff --git a/input/drivermouseinput.cpp b/input/drivermouseinput.cpp index 22562b85..c7a9aeb9 100644 --- a/input/drivermouseinput.cpp +++ b/input/drivermouseinput.cpp @@ -597,6 +597,18 @@ drivermouse_input::default_bindings() { { "main_on_bt:",{ linebreakerclose, none } }, + { "mainb_off_bt:", { + linebreakeropenb, + none } }, + { "mainb_on_bt:", { + linebreakercloseb, + none } }, + { "mainc_off_bt:", { + linebreakeropenc, + none } }, + { "mainc_on_bt:", { + linebreakerclosec, + none } }, { "security_reset_bt:", { alerteracknowledge, none } }, @@ -774,6 +786,12 @@ drivermouse_input::default_bindings() { { "main_sw:", { linebreakertoggle, none } }, + { "mainb_sw:", { + linebreakertoggleb, + none } }, + { "mainc_sw:", { + linebreakertogglec, + none } }, { "radio_sw:", { radiotoggle, none } }, diff --git a/tests/unit/test_engine_start.cpp b/tests/unit/test_engine_start.cpp new file mode 100644 index 00000000..6524075c --- /dev/null +++ b/tests/unit/test_engine_start.cpp @@ -0,0 +1,248 @@ +#include "stdafx.h" +#include "doctest.h" +#include "consist.h" + +#include "vehicle/enginestart.h" + +// starting the engines of the vehicles in multiple from the cab (eu07/maszyna#80): the cab's own switch (main_*) works +// all of them unless Engine:JointEngineStart=No keeps it to its own vehicle, and the switches mainb_* and mainc_* start +// the second and the third vehicle alone. the cab (TTrain) passes the vehicle's MainSwitchRange() to its MainSwitch(), +// finds the vehicles with engine(), and runs remote_switch for the switches of the others + +using test::consist; +using test::definition; + +namespace { + +// the SN61 railcar, whose cab has a set of switches for each of up to three railcars in multiple +definition sn61(bool const Joint = true) { + auto railcar{definition::fiz("sn61_v2/sn61")}; + if (false == Joint) { + railcar.set("Engine", "JointEngineStart", "No"); + } + return railcar; +} + +// the battery, the cab and the pumps of every railcar of the train switched on from the first one's cab, the oil +// pressure built up, and the controller at the first position giving the engines fuel, which a mechanical diesel +// starts at: everything but the engines ready to start +void prepare(consist &Train) { + auto &cab{Train[0].mover()}; + cab.BatterySwitch(true); + cab.CabActivisation(); + cab.OilPumpSwitch(true); + cab.FuelPumpSwitch(true); + while (cab.RList[cab.MainCtrlPos].R == 0.0 && cab.IncMainCtrl(1)) { + // up to the first position with fuel + } + Train.run(20.0); +} + +bool running(TMoverParameters const &Vehicle) { + return Vehicle.Mains && Vehicle.enrot > 0.5 * Vehicle.dizel_nmin; +} + +// the switch of Engine held closed for Seconds, then let go, in steps of the train +void hold(consist &Train, engine_start::remote_switch &Switch, TMoverParameters *Engine, double const Seconds, bool const Closesonrelease = false) { + auto const dt{0.01}; + for (auto time = 0.0; time < Seconds; time += dt) { + Switch.update(Engine, true, false, dt, Closesonrelease); + Train.step({}, dt); + } + Switch.update(Engine, false, false, dt, Closesonrelease); +} + +} // namespace + +TEST_CASE("engine start: the joint start is the default, and JointEngineStart=No keeps the cab's switch to its vehicle") { + consist train; + auto const &joint{train.add(sn61(), 1).mover()}; + auto const &separate{train.add(sn61(false)).mover()}; + CHECK(joint.JointEngineStart); + CHECK_FALSE(separate.JointEngineStart); + CHECK(joint.MainSwitchRange() == range_t::consist); + CHECK(separate.MainSwitchRange() == range_t::local); +} + +TEST_CASE("engine start: the vehicles in multiple, counted from the cab's own one, those behind the cab first") { + consist train; + auto &first{train.add(sn61(), 1).mover()}; + auto &second{train.add(sn61()).mover()}; + auto &third{train.add(sn61()).mover()}; + CHECK(engine_start::engine(first, first, 1) == &first); + CHECK(engine_start::engine(first, first, 2) == &second); + CHECK(engine_start::engine(first, first, 3) == &third); + CHECK(engine_start::engine(first, first, 4) == nullptr); + CHECK(engine_start::engine(first, first, 0) == nullptr); + // from the rear cab of the first railcar the others are in front of the cab, in the same order + first.CabOccupied = -1; + CHECK(engine_start::engine(first, first, 2) == &second); + CHECK(engine_start::engine(first, first, 3) == &third); + // from the middle one, the one behind its cab comes first + second.CabOccupied = 1; + CHECK(engine_start::engine(second, second, 2) == &third); + CHECK(engine_start::engine(second, second, 3) == &first); + second.CabOccupied = -1; + CHECK(engine_start::engine(second, second, 2) == &first); + CHECK(engine_start::engine(second, second, 3) == &third); +} + +TEST_CASE("engine start: only the vehicles with an engine and under control through the cable count") { + consist train; + auto &first{train.add(sn61(), 1).mover()}; + // a vehicle without an engine between the railcars is passed over + auto &trailer{train.add(definition::fiz("ed72_v1/5bs-ra")).mover()}; + auto &second{train.add(sn61()).mover()}; + REQUIRE(trailer.Power < 0.01); + CHECK(engine_start::engine(first, first, 2) == &second); + // the vehicle with the cab doesn't drive itself: its engine is in the vehicle it controls, which isn't counted twice + trailer.CabOccupied = 1; + CHECK(engine_start::engine(trailer, first, 1) == &first); + CHECK(engine_start::engine(trailer, first, 2) == &second); + CHECK(engine_start::engine(trailer, first, 3) == nullptr); + + // coupled without the control cable, the other railcar isn't under the cab's control + consist apart; + auto &alone{apart.add(sn61(), 1).mover()}; + apart.add(sn61(), 0, coupling::coupler | coupling::brakehose); + CHECK(engine_start::engine(alone, alone, 2) == nullptr); +} + +TEST_CASE("engine start: by default the cab's switch starts and stops the engines of all the vehicles in multiple") { + consist train; + auto &first{train.add(sn61(), 1).mover()}; + auto const &second{train.add(sn61()).mover()}; + prepare(train); + REQUIRE_FALSE(first.Mains); + REQUIRE_FALSE(second.Mains); + first.MainSwitch(true, first.MainSwitchRange()); + train.run(15.0); + CHECK(running(first)); + CHECK(running(second)); + first.MainSwitch(false, first.MainSwitchRange()); + train.run(5.0); + CHECK_FALSE(first.Mains); + CHECK_FALSE(second.Mains); +} + +TEST_CASE("engine start: with JointEngineStart=No each engine has its own switch") { + consist train; + auto &first{train.add(sn61(false), 1).mover()}; + auto &second{train.add(sn61(false)).mover()}; + auto &third{train.add(sn61(false)).mover()}; + prepare(train); + // the cab's own switch starts its own engine only + first.MainSwitch(true, first.MainSwitchRange()); + train.run(15.0); + REQUIRE(running(first)); + CHECK_FALSE(second.Mains); + CHECK_FALSE(third.Mains); + + // the second one's switch, mainb_*, starts the second engine + engine_start::remote_switch switchb; + engine_start::remote_switch switchc; + auto *engineb{engine_start::engine(first, first, 2)}; + auto *enginec{engine_start::engine(first, first, 3)}; + REQUIRE(engineb == &second); + REQUIRE(enginec == &third); + hold(train, switchb, engineb, 2.0); // past the SN61's delay of 1.5 s + train.run(15.0); + CHECK(running(second)); + CHECK_FALSE(third.Mains); + // and the third one's, mainc_*, the third + hold(train, switchc, enginec, 2.0); + train.run(15.0); + CHECK(running(third)); + + // the switches open their own vehicles' circuits only + CHECK(engine_start::remote_switch::open(second)); + train.run(5.0); + CHECK_FALSE(second.Mains); + CHECK(running(first)); + CHECK(running(third)); + first.MainSwitch(false, first.MainSwitchRange()); + train.run(5.0); + CHECK_FALSE(first.Mains); + CHECK(running(third)); + CHECK_FALSE(engine_start::remote_switch::open(second)); // already stopped +} + +TEST_CASE("engine start: the switch of another vehicle is held for the vehicle's delay, with the vehicle ready") { + consist train; + train.add(sn61(false).set("Cntrl", "IniCDelay", "2.0"), 1); + auto &second{train.add(sn61(false).set("Cntrl", "IniCDelay", "2.0")).mover()}; + REQUIRE(second.InitialCtrlDelay == doctest::Approx(2.0)); + engine_start::remote_switch switchb; + + // without the battery the switch does nothing, however long it's held + hold(train, switchb, &second, 5.0); + train.run(5.0); + CHECK_FALSE(second.Mains); + CHECK_FALSE(second.dizel_startup); + + prepare(train); + // let go too early + hold(train, switchb, &second, 1.0); + CHECK_FALSE(engine_start::remote_switch::is_running(second)); + // the switch opening the circuit held along, the closing one does nothing + auto const dt{0.01}; + for (auto time = 0.0; time < 3.0; time += dt) { + switchb.update(&second, true, true, dt, false); + train.step({}, dt); + } + CHECK_FALSE(engine_start::remote_switch::is_running(second)); + // held long enough + hold(train, switchb, &second, 2.5); + CHECK(engine_start::remote_switch::is_running(second)); + train.run(15.0); + CHECK(running(second)); +} + +TEST_CASE("engine start: a switch closing on release, as an electric locomotive's impulse switch, waits to be let go") { + consist train; + train.add(sn61(false), 1); + auto &second{train.add(sn61(false).set("Cntrl", "IniCDelay", "1.0")).mover()}; + prepare(train); + engine_start::remote_switch switchb; + auto const dt{0.01}; + // held past the delay, the circuit stays open while the switch is held + for (auto time = 0.0; time < 2.0; time += dt) { + switchb.update(&second, true, false, dt, true); + train.step({}, dt); + } + CHECK_FALSE(engine_start::remote_switch::is_running(second)); + // and closes when let go + switchb.update(&second, false, false, dt, true); + CHECK(engine_start::remote_switch::is_running(second)); + train.run(15.0); + REQUIRE(running(second)); + // let go before the delay, it doesn't + CHECK(engine_start::remote_switch::open(second)); + train.run(5.0); + REQUIRE_FALSE(engine_start::remote_switch::is_running(second)); + hold(train, switchb, &second, 0.5, true); + CHECK_FALSE(engine_start::remote_switch::is_running(second)); +} + +TEST_CASE("engine start: an engine stopping on its own stops the others only when they started together") { + for (auto const joint : {true, false}) { + CAPTURE(joint); + consist train; + auto &first{train.add(sn61(joint), 1).mover()}; + auto const &second{train.add(sn61(joint)).mover()}; + prepare(train); + engine_start::remote_switch switchb; + first.MainSwitch(true, first.MainSwitchRange()); + hold(train, switchb, engine_start::engine(first, first, 2), 2.0); + train.run(15.0); + REQUIRE(running(first)); + REQUIRE(running(second)); + // the first one's fuel cut off, its engine stops + first.FuelPump.is_disabled = true; + train.run(15.0); + CHECK_FALSE(first.Mains); + CHECK(first.enrot == 0.0); + // started on their own, the others keep running + CHECK(running(second) == (false == joint)); + } +} diff --git a/utilities/translation.cpp b/utilities/translation.cpp index c0dbeb1a..d05ee13e 100644 --- a/utilities/translation.cpp +++ b/utilities/translation.cpp @@ -225,6 +225,10 @@ std::string locale::label_cab_control(std::string const &Label) { "coolingfans_sw:", STRN("cooling fans") }, { "main_off_bt:", STRN("line breaker") }, { "main_on_bt:", STRN("line breaker") }, + { "mainb_off_bt:", STRN("line breaker B") }, + { "mainb_on_bt:", STRN("line breaker B") }, + { "mainc_off_bt:", STRN("line breaker C") }, + { "mainc_on_bt:", STRN("line breaker C") }, { "security_reset_bt:", STRN("alerter") }, { "shp_reset_bt:", STRN("shp") }, { "releaser_bt:", STRN("independent brake releaser") }, @@ -270,6 +274,8 @@ std::string locale::label_cab_control(std::string const &Label) { "converterlocal_sw:", STRN("local converter") }, { "converteroff_sw:", STRN("converter") }, { "main_sw:", STRN("line breaker") }, + { "mainb_sw:", STRN("line breaker B") }, + { "mainc_sw:", STRN("line breaker C") }, { "radio_sw:", STRN("radio") }, { "radiochannel_sw:", STRN("radio channel") }, { "radiochannelprev_sw:", STRN("radio channel") }, diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index e30635f4..ab60371c 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -337,6 +337,12 @@ TTrain::commandhandler_map const TTrain::m_commandhandlers = { {user_command::linebreakertoggle, &TTrain::OnCommand_linebreakertoggle}, {user_command::linebreakeropen, &TTrain::OnCommand_linebreakeropen}, {user_command::linebreakerclose, &TTrain::OnCommand_linebreakerclose}, + {user_command::linebreakertoggleb, &TTrain::OnCommand_linebreakertoggleremote}, + {user_command::linebreakeropenb, &TTrain::OnCommand_linebreakeropenremote}, + {user_command::linebreakercloseb, &TTrain::OnCommand_linebreakercloseremote}, + {user_command::linebreakertogglec, &TTrain::OnCommand_linebreakertoggleremote}, + {user_command::linebreakeropenc, &TTrain::OnCommand_linebreakeropenremote}, + {user_command::linebreakerclosec, &TTrain::OnCommand_linebreakercloseremote}, {user_command::fuelpumptoggle, &TTrain::OnCommand_fuelpumptoggle}, {user_command::fuelpumpenable, &TTrain::OnCommand_fuelpumpenable}, {user_command::fuelpumpdisable, &TTrain::OnCommand_fuelpumpdisable}, @@ -3718,7 +3724,7 @@ void TTrain::OnCommand_linebreakeropen(TTrain *Train, command_data const &Comman return; } // already in the desired state - if (true == Train->mvControlled->MainSwitch(false)) + if (true == Train->mvControlled->MainSwitch(false, Train->mvControlled->MainSwitchRange())) { Train->m_linebreakerstate = 0; } @@ -3793,13 +3799,157 @@ void TTrain::OnCommand_linebreakerclose(TTrain *Train, command_data const &Comma if (Train->m_linebreakerstate == 2 && Train->mvControlled->EngineType == TEngineType::ElectricSeriesMotor) { // try to finalize state change of the line breaker, set the state based on the outcome - Train->m_linebreakerstate = Train->mvControlled->MainSwitch(true) ? 1 : 0; + Train->m_linebreakerstate = Train->mvControlled->MainSwitch(true, Train->mvControlled->MainSwitchRange()) ? 1 : 0; } // on button release reset the closing timer Train->fMainRelayTimer = 0.0f; } } +namespace +{ + +// which of the switches of the other vehicles in multiple a command works: 0 the second vehicle's, 1 the third one's +std::size_t remote_linebreaker_index(user_command const Command) +{ + using enum user_command; + return (Command == linebreakertogglec || Command == linebreakeropenc || Command == linebreakerclosec) ? 1 : 0; +} + +// the impulse switches of another vehicle's line breaker spring back when let go, whichever way they were pressed +void release_remote_linebreaker(TTrain::remote_linebreaker &Controls, std::optional &Sound) +{ + if (Controls.on_button.SubModel != nullptr) + { + Controls.on_button.UpdateValue(0.0, Sound); + } + if (Controls.off_button.SubModel != nullptr) + { + Controls.off_button.UpdateValue(0.0, Sound); + } + if (Controls.main_switch.SubModel != nullptr && Controls.main_switch.type() != TGaugeType::toggle) + { + Controls.main_switch.UpdateValue(0.5, Sound); + } + Controls.held_without_switch = false; +} + +} // namespace + +TMoverParameters *TTrain::remote_engine(std::size_t const Index) const +{ + return engine_start::engine(*mvOccupied, *mvControlled, static_cast(Index) + 2); +} + +void TTrain::OnCommand_linebreakertoggleremote(TTrain *Train, command_data const &Command) +{ + auto const index{remote_linebreaker_index(Command.command)}; + auto &controls{Train->m_remotelinebreakers[index]}; + + if (Command.action == GLFW_PRESS) + { + // a two-state switch goes the other way than it stands, an impulse one the other way than the engine is + bool close{false}; + if (controls.main_switch.SubModel != nullptr && controls.main_switch.type() == TGaugeType::toggle) + { + close = controls.main_switch.GetDesiredValue() < 0.5; + } + else + { + auto const *engine{Train->remote_engine(index)}; + close = engine == nullptr || false == engine_start::remote_switch::is_running(*engine); + } + if (close) + { + OnCommand_linebreakercloseremote(Train, Command); + } + else + { + OnCommand_linebreakeropenremote(Train, Command); + } + } + else if (Command.action == GLFW_RELEASE) + { + if (controls.on_button.SubModel != nullptr || controls.main_switch.type() != TGaugeType::toggle) + { + // only impulse switches react to release events + release_remote_linebreaker(controls, Train->dsbSwitch); + } + controls.held_without_switch = false; + } +} + +void TTrain::OnCommand_linebreakeropenremote(TTrain *Train, command_data const &Command) +{ + auto const index{remote_linebreaker_index(Command.command)}; + auto &controls{Train->m_remotelinebreakers[index]}; + + if (Command.action == GLFW_PRESS) + { + // visual feedback + if (controls.off_button.SubModel != nullptr) + { + controls.off_button.UpdateValue(1.0, Train->dsbSwitch); + } + else if (controls.main_switch.SubModel != nullptr) + { + controls.main_switch.UpdateValue(0.0, Train->dsbSwitch); + } + controls.held_without_switch = false; + if (auto *engine{Train->remote_engine(index)}; engine != nullptr) + { + engine_start::remote_switch::open(*engine); + } + } + else if (Command.action == GLFW_RELEASE) + { + release_remote_linebreaker(controls, Train->dsbSwitch); + } +} + +void TTrain::OnCommand_linebreakercloseremote(TTrain *Train, command_data const &Command) +{ + auto const index{remote_linebreaker_index(Command.command)}; + auto &controls{Train->m_remotelinebreakers[index]}; + + if (Command.action == GLFW_PRESS) + { + // visual feedback; the line breaker closes in the train update, once the switch was held long enough + if (controls.on_button.SubModel != nullptr) + { + controls.on_button.UpdateValue(1.0, Train->dsbSwitch); + } + else if (controls.main_switch.SubModel != nullptr) + { + controls.main_switch.UpdateValue(1.0, Train->dsbSwitch); + } + else + { + // no switch in the cab, the command alone holds it + controls.held_without_switch = true; + } + } + else if (Command.action == GLFW_RELEASE) + { + release_remote_linebreaker(controls, Train->dsbSwitch); + } +} + +void TTrain::update_remote_linebreakers(double const Deltatime) +{ + for (std::size_t index = 0; index < m_remotelinebreakers.size(); ++index) + { + auto &controls{m_remotelinebreakers[index]}; + auto *engine{remote_engine(index)}; + auto const closeheld{controls.held_without_switch || (controls.main_switch.SubModel != nullptr && controls.main_switch.GetDesiredValue() > 0.95) || + (controls.on_button.SubModel != nullptr && controls.on_button.GetDesiredValue() > 0.95)}; + auto const openheld{controls.off_button.SubModel != nullptr && controls.off_button.GetDesiredValue() > 0.95}; + // as with the cab's own switch, an electric locomotive's impulse switch closes the line breaker when let go + auto const closesonrelease{engine != nullptr && controls.on_button.SubModel != nullptr && engine->EngineType == TEngineType::ElectricSeriesMotor}; + controls.state.update(engine, closeheld, openheld, Deltatime, closesonrelease); + } +} + void TTrain::OnCommand_fuelpumptoggle(TTrain *Train, command_data const &Command) { @@ -8442,8 +8592,10 @@ bool TTrain::Update(double const Deltatime) if (m_linebreakerstate == 2 && (ggMainOnButton.SubModel == nullptr || mvControlled->EngineType != TEngineType::ElectricSeriesMotor)) { // try to finalize state change of the line breaker, set the state based on the outcome - m_linebreakerstate = mvControlled->MainSwitch(true) ? 1 : 0; + m_linebreakerstate = mvControlled->MainSwitch(true, mvControlled->MainSwitchRange()) ? 1 : 0; } + // the switches of the other vehicles in multiple + update_remote_linebreakers(Deltatime); // door permits for (auto idx = 0; idx < 2; ++idx) { @@ -9543,6 +9695,12 @@ bool TTrain::Update(double const Deltatime) ggMainOffButton.Update(); ggMainOnButton.Update(); ggMainButton.Update(); + for (auto &remotelinebreaker : m_remotelinebreakers) + { + remotelinebreaker.main_switch.Update(); + remotelinebreaker.on_button.Update(); + remotelinebreaker.off_button.Update(); + } ggSecurityResetButton.Update(); ggSHPResetButton.Update(); ggReleaserButton.Update(); @@ -11036,6 +11194,13 @@ void TTrain::clear_cab_controls() ggMaxCurrentCtrl.Clear(); ggMainOffButton.Clear(); ggMainOnButton.Clear(); + for (auto &remotelinebreaker : m_remotelinebreakers) + { + remotelinebreaker.main_switch.Clear(); + remotelinebreaker.on_button.Clear(); + remotelinebreaker.off_button.Clear(); + remotelinebreaker.held_without_switch = false; + } ggSecurityResetButton.Clear(); ggSHPResetButton.Clear(); ggReleaserButton.Clear(); @@ -11287,6 +11452,22 @@ void TTrain::set_cab_controls(int const Cab) { // instead of single main button there can be on/off pair ggMainButton.PutValue(ggMainButton.type() == TGaugeType::push ? 0.5f : m_linebreakerstate > 0 ? 1.f : 0.f); } + // line breakers of the other vehicles in multiple + for (std::size_t index = 0; index < m_remotelinebreakers.size(); ++index) + { + auto &mainswitch{m_remotelinebreakers[index].main_switch}; + if (mainswitch.SubModel == nullptr) + { + continue; + } + if (mainswitch.type() == TGaugeType::push) + { + mainswitch.PutValue(0.5f); + continue; + } + auto const *engine{remote_engine(index)}; + mainswitch.PutValue(engine != nullptr && engine_start::remote_switch::is_running(*engine) ? 1.f : 0.f); + } if (ggModernLightDimSw.SubModel != nullptr) { @@ -11778,6 +11959,12 @@ bool TTrain::initialize_gauge(cParser &Parser, std::string const &Label, int con {"maxcurrent_sw:", ggMaxCurrentCtrl}, {"main_off_bt:", ggMainOffButton}, {"main_on_bt:", ggMainOnButton}, + {"mainb_sw:", m_remotelinebreakers[0].main_switch}, + {"mainb_off_bt:", m_remotelinebreakers[0].off_button}, + {"mainb_on_bt:", m_remotelinebreakers[0].on_button}, + {"mainc_sw:", m_remotelinebreakers[1].main_switch}, + {"mainc_off_bt:", m_remotelinebreakers[1].off_button}, + {"mainc_on_bt:", m_remotelinebreakers[1].on_button}, {"security_reset_bt:", ggSecurityResetButton}, {"shp_reset_bt:", ggSHPResetButton}, {"releaser_bt:", ggReleaserButton}, diff --git a/vehicle/Train.h b/vehicle/Train.h index 009b349b..a834d8ff 100644 --- a/vehicle/Train.h +++ b/vehicle/Train.h @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/Driver.h" #include "vehicle/Button.h" #include "vehicle/Gauge.h" +#include "vehicle/enginestart.h" #include "audio/sound.h" #include "scripting/PyInt.h" #include "input/command.h" @@ -214,6 +215,10 @@ class TTrain { void change_pantograph_selection( int const Change ); // helper, common part of pantograh valves state update methods void update_pantograph_valves(); + // the Index-th other vehicle in multiple whose engine the cab starts with its own switches (0: mainb_*, 1: mainc_*) + TMoverParameters *remote_engine( std::size_t const Index ) const; + // the switches starting the engines of the other vehicles in multiple, for Deltatime seconds + void update_remote_linebreakers( double const Deltatime ); // update function subroutines void update_sounds( double const Deltatime ); void update_sounds_runningnoise( sound_source &Sound ); @@ -338,6 +343,9 @@ class TTrain { static void OnCommand_linebreakertoggle( TTrain *Train, command_data const &Command ); static void OnCommand_linebreakeropen( TTrain *Train, command_data const &Command ); static void OnCommand_linebreakerclose( TTrain *Train, command_data const &Command ); + static void OnCommand_linebreakertoggleremote( TTrain *Train, command_data const &Command ); + static void OnCommand_linebreakeropenremote( TTrain *Train, command_data const &Command ); + static void OnCommand_linebreakercloseremote( TTrain *Train, command_data const &Command ); static void OnCommand_fuelpumptoggle( TTrain *Train, command_data const &Command ); static void OnCommand_fuelpumpenable( TTrain *Train, command_data const &Command ); static void OnCommand_fuelpumpdisable( TTrain *Train, command_data const &Command ); @@ -570,6 +578,15 @@ public: // reszta może by?publiczna TGauge ggMainOffButton; TGauge ggMainOnButton; TGauge ggMainButton; // EZT + // the line breaker / engine start switches of the second and the third vehicle in multiple (mainb_*, mainc_*) + struct remote_linebreaker { + TGauge main_switch; // mainb_sw: + TGauge on_button; // mainb_on_bt: + TGauge off_button; // mainb_off_bt: + bool held_without_switch{ false }; // closed by a command, in a cab without a switch for it + engine_start::remote_switch state; + }; + std::array m_remotelinebreakers; TGauge ggSecurityResetButton; TGauge ggSHPResetButton; TGauge ggReleaserButton; diff --git a/vehicle/enginestart.cpp b/vehicle/enginestart.cpp new file mode 100644 index 00000000..4b6da2e0 --- /dev/null +++ b/vehicle/enginestart.cpp @@ -0,0 +1,107 @@ +/* +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/enginestart.h" + +#include "McZapkie/MOVER.h" +#include "utilities/utilities.h" + +namespace engine_start { + +namespace { + +// whether the vehicle has an engine the cab can start, the same test the tachometers of the vehicles in multiple make +bool has_engine(TMoverParameters const &Vehicle) { + return Vehicle.Power > 0.01; +} + +// walks from Start through its coupler Side along the control cable, and returns the Number-th vehicle with an engine +// other than Skipped it meets; Number is lowered by the ones it met when it doesn't get that far +TMoverParameters *find_along(TMoverParameters const &Start, int Side, TMoverParameters const &Skipped, int &Number) { + auto const *vehicle{&Start}; + // a train can't loop back on itself, the limit only guards against a broken chain of couplers + for (auto steps = 0; steps < 1000; ++steps) { + auto const &coupler{vehicle->Couplers[Side]}; + if (coupler.Connected == nullptr || false == TestFlag(coupler.CouplingFlag, coupling::control)) { + return nullptr; + } + // onwards through the other end of the next vehicle, whichever way round it stands + Side = 1 - coupler.ConnectedNr; + vehicle = coupler.Connected; + if (vehicle == &Skipped || false == has_engine(*vehicle)) { + continue; + } + if (--Number == 0) { + return coupler.Connected; + } + } + return nullptr; +} + +} // namespace + +TMoverParameters *engine(TMoverParameters const &Occupied, TMoverParameters &Controlled, int Number) { + if (Number < 1) { + return nullptr; + } + if (Number == 1) { + return &Controlled; + } + // the cab's own vehicle is the first one + auto others{Number - 1}; + // behind the cab is the rear end of a vehicle driven from its front cab, and the other way round + int const behind{Occupied.CabOccupied < 0 ? end::front : end::rear}; + if (auto *found{find_along(Occupied, behind, Controlled, others)}; found != nullptr) { + return found; + } + return find_along(Occupied, 1 - behind, Controlled, others); +} + +void remote_switch::update(TMoverParameters *Engine, bool const Closeheld, bool const Openheld, double const Deltatime, bool const Closesonrelease) { + if (Engine != m_engine) { + // another vehicle under the switch, after a change in the train + m_engine = Engine; + m_heldtime = 0.0; + m_ready = false; + } + if (Engine == nullptr) { + return; + } + if (Openheld || is_running(*Engine)) { + // the switch opening the circuit is held down, or there's nothing left to start + m_heldtime = 0.0; + m_ready = false; + return; + } + if (Closeheld && Engine->MainSwitchCheck()) { + m_heldtime += Deltatime; + } + if (m_heldtime > Engine->InitialCtrlDelay) { + m_ready = true; + } + if (m_ready && false == (Closesonrelease && Closeheld)) { + Engine->MainSwitch(true, range_t::local); + m_ready = false; + m_heldtime = 0.0; + } + if (false == Closeheld) { + m_heldtime = 0.0; + } +} + +bool remote_switch::open(TMoverParameters &Engine) { + return Engine.MainSwitch(false, range_t::local); +} + +bool remote_switch::is_running(TMoverParameters const &Engine) { + return Engine.Mains || Engine.dizel_startup; +} + +} // namespace engine_start diff --git a/vehicle/enginestart.h b/vehicle/enginestart.h new file mode 100644 index 00000000..beedab10 --- /dev/null +++ b/vehicle/enginestart.h @@ -0,0 +1,45 @@ +/* +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 + +class TMoverParameters; + +// starting the engines (closing the line breakers) of the vehicles in multiple from the cab (eu07/maszyna#80). in +// vehicles like the ST43, the SP32 or the SN61 every engine of the train has its own switches in the cab: main_* work +// the cab's own vehicle, mainb_* the second one and mainc_* the third one under its control. Engine:JointEngineStart=No +// in the .fiz of the cab's vehicle keeps main_* to that vehicle alone (TMoverParameters::MainSwitchRange()); by +// default it works all of them, as before. +namespace engine_start { + +// the Number-th vehicle with an engine under the cab's control: 1 is Controlled, the cab's own; then the others +// connected through the control cable, those behind the cab of Occupied first, the nearest first, then those in front +// of it. nullptr when there are fewer of them +TMoverParameters *engine(TMoverParameters const &Occupied, TMoverParameters &Controlled, int Number); + +// the switch starting the engine of another vehicle in multiple. like the cab's own switch, it has to be held closed +// for the vehicle's InitialCtrlDelay, with the vehicle ready for it, before the engine starts (the line breaker closes) +class remote_switch { +public: + // one step of Deltatime seconds of the switch of Engine, with the switch held closed (Closeheld) or open (Openheld). + // with Closesonrelease, the line breaker closes when the switch is let go after the delay, as the impulse switch + // of an electric locomotive with series motors does + void update(TMoverParameters *Engine, bool Closeheld, bool Openheld, double Deltatime, bool Closesonrelease); + // opens the line breaker / stops the engine of Engine, the vehicle alone; returns whether it was running + static bool open(TMoverParameters &Engine); + // whether the line breaker of Engine is closed or its engine starting or running + static bool is_running(TMoverParameters const &Engine); + +private: + TMoverParameters const *m_engine{nullptr}; // the vehicle the switch worked the last step + double m_heldtime{0.0}; // how long the switch is held closed with the vehicle ready to start + bool m_ready{false}; // held long enough, the line breaker closes now or when the switch is let go +}; + +} // namespace engine_start diff --git a/vr/openvr_imp.cpp b/vr/openvr_imp.cpp index 15467d00..02044d6e 100644 --- a/vr/openvr_imp.cpp +++ b/vr/openvr_imp.cpp @@ -495,6 +495,18 @@ std::unordered_map vr_openvr::m_buttonb { "main_on_bt:",{ user_command::linebreakerclose, user_command::none } }, + { "mainb_off_bt:", { + user_command::linebreakeropenb, + user_command::none } }, + { "mainb_on_bt:", { + user_command::linebreakercloseb, + user_command::none } }, + { "mainc_off_bt:", { + user_command::linebreakeropenc, + user_command::none } }, + { "mainc_on_bt:", { + user_command::linebreakerclosec, + user_command::none } }, { "security_reset_bt:", { user_command::alerteracknowledge, user_command::none } }, @@ -660,6 +672,12 @@ std::unordered_map vr_openvr::m_buttonb { "main_sw:", { user_command::linebreakertoggle, user_command::none } }, + { "mainb_sw:", { + user_command::linebreakertoggleb, + user_command::none } }, + { "mainc_sw:", { + user_command::linebreakertogglec, + user_command::none } }, { "radio_sw:", { user_command::radiotoggle, user_command::none } },