diff --git a/CMakeLists.txt b/CMakeLists.txt index 22982e64..477f584e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -461,6 +461,7 @@ set(CORE_SOURCES "utilities/parser.cpp" "utilities/utilities.cpp" "scripting/ladderlogic.cpp" +"vehicle/handover.cpp" ) add_library(eu07_core STATIC ${CORE_SOURCES}) target_link_libraries(eu07_core PUBLIC eu07_settings) diff --git a/tests/unit/README.md b/tests/unit/README.md index 5eebe563..bca46e83 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -52,6 +52,9 @@ TEST_CASE("brakes: service braking stops a heavier wagon too") { and a test on a half-defined vehicle tests the definition rather than the simulation. - The random numbers of the simulation are reseeded before every test case, so a test gives the same result alone or among the others. +- Logic which lives in classes too big to set up in a test (the AI driver, the cab) goes into free + functions on the vehicle state the class calls, and the test calls them the same way: see + `vehicle/handover.h`, which `TController::TakeControl()` uses, and `test_handover.cpp`. - `vehicle` creates the vehicle the way `TDynamicObject::Init()` does and steps it the way the simulation loop does, on a straight track of the given friction and gradient. Coupled vehicles aren't supported yet. diff --git a/tests/unit/data/cab_vehicle.json b/tests/unit/data/cab_vehicle.json new file mode 100644 index 00000000..7bb32421 --- /dev/null +++ b/tests/unit/data/cab_vehicle.json @@ -0,0 +1,34 @@ +{ + "_comment": "The wagon with the cab devices the driver uses: sandbox, light preset selector and modern headlight dimmer, both tables made after the Vectron's. No traction: enough for the logic of the cab controls.", + "Param": { "Category": "train", "M": 42200, "Vmax": 120, "SandCap": 500 }, + "Dimensions": { "L": 26.8, "H": 4.7, "W": 2.88, "Cx": 0.15 }, + "Wheels": { "D": 0.96, "Tw": 1.435, "Axle": "2'2'", "Ad": 2.5, "Bd": 19.5, "Rmin": 80, "BearingType": "Roll" }, + "Brake": { + "BrakeValve": "KE", "NBpA": 6, "MBF": 15, "MaxBP": 3.8, "TareMaxBP": 2.3, + "BCN": 12, "BCR": 0.127, "BCD": 0.04, "BCM": 1.5, "BCS": 1.2, "BSA": 0, "BM": "Disk1", "BVV": 125 + }, + "BuffCoupl": { "CType": "Screw", "kB": 9200, "DmaxB": 0.11, "FmaxB": 320, "kC": 4500, "DmaxC": 0.1, "FmaxC": 850, "beta": 0.58, "AllowedFlag": 115 }, + "Cntrl": { "BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "P", "MaxBPMass": 63 }, + "Switches": { "ModernDimmer": "Yes" }, + "_lights": "light bits: 1 headlight left, 2 red marker left, 4 headlight upper, 16 headlight right, 32 red marker right", + "LightsList": { + "Size": 4, "Wrap": "No", "Default": 1, + "rows": [ + [0, 0], + [21, 34], + [34, 0], + [17, 0] + ] + }, + "_dimmer": "rows: high beam, dimmed, off", + "DimmerList": { + "Size": 5, "Cycle": "no", "DefaultPos": 3, + "rows": [ + ["yes", "no", "no"], + ["yes", "yes", "no"], + ["no", "no", "yes"], + ["no", "no", "no"], + ["no", "yes", "no"] + ] + } +} diff --git a/tests/unit/test_handover.cpp b/tests/unit/test_handover.cpp new file mode 100644 index 00000000..ff4a0df8 --- /dev/null +++ b/tests/unit/test_handover.cpp @@ -0,0 +1,169 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +#include "vehicle/handover.h" + +using test::definition; +using test::vehicle; + +// what the human driver gets when taking a vehicle over from the AI (Q key). the AI operates some controls +// directly and only the AI ever released them, so the player inherited e.g. a horn blowing forever. + +namespace { + +int const pc1{light::headlight_left | light::headlight_upper | light::headlight_right}; +int const allheadlights{pc1 | light::redmarker_left | light::redmarker_right}; + +vehicle cab_vehicle() { + vehicle cab{definition::load("cab_vehicle")}; + cab->DirActive = 1; // the AI drives: reverser set + return cab; +} + +} // namespace + +TEST_CASE("handover: the horn the AI was sounding goes quiet (eu07/maszyna#511)") { + auto cab{cab_vehicle()}; + SUBCASE("low tone") { cab->WarningSignal = 1; } + SUBCASE("high tone") { cab->WarningSignal = 2; } + SUBCASE("both tones") { cab->WarningSignal = 1 | 2; } + REQUIRE(cab->WarningSignal != 0); + + handover::release_ai_controls(cab.mover(), cab.mover()); + + CHECK(cab->WarningSignal == 0); +} + +TEST_CASE("handover: sanding the AI started stops") { + auto cab{cab_vehicle()}; + cab->Sandbox(true); // the way the AI sands, bypassing the sanding button + REQUIRE(cab->SandDose); + + handover::release_ai_controls(cab.mover(), cab.mover()); + + CHECK_FALSE(cab->SandDose); +} + +TEST_CASE("handover: sanding the player or the automatic system asked for goes on") { + auto cab{cab_vehicle()}; + SUBCASE("sanding button held") { cab->SandboxManual(true); } + SUBCASE("automatic sanding") { + cab->SandboxAutoAllow(true); + cab->SandboxAuto(true); + } + REQUIRE(cab->SandDose); + + handover::release_ai_controls(cab.mover(), cab.mover()); + + CHECK(cab->SandDose); +} + +TEST_CASE("handover: the departure signal the AI was giving stops") { + auto cab{cab_vehicle()}; + cab->signal_departure(true); + REQUIRE(cab->DepartureSignal); + + handover::release_ai_controls(cab.mover(), cab.mover()); + + CHECK_FALSE(cab->DepartureSignal); +} + +TEST_CASE("handover: the brake releaser the AI was holding lets go") { + auto cab{cab_vehicle()}; + cab->BrakeReleaser(1); + REQUIRE(cab->Hamulec->Releaser()); + + handover::release_ai_controls(cab.mover(), cab.mover()); + + CHECK_FALSE(cab->Hamulec->Releaser()); +} + +TEST_CASE("handover: nothing the AI didn't latch changes") { + auto cab{cab_vehicle()}; + + handover::release_ai_controls(cab.mover(), cab.mover()); + + CHECK(cab->WarningSignal == 0); + CHECK_FALSE(cab->SandDose); + CHECK_FALSE(cab->DepartureSignal); + CHECK_FALSE(cab->Hamulec->Releaser()); +} + +TEST_CASE("handover: the light selector moves to the position giving the head lights the AI has set") { + auto cab{cab_vehicle()}; + REQUIRE(cab->LightsPos == 1); // untouched selector: lights off + + SUBCASE("Pc1") { + CHECK(handover::light_selector_position(cab.mover(), end::front, pc1, allheadlights) == 2); + } + SUBCASE("lights off") { + CHECK(handover::light_selector_position(cab.mover(), end::front, 0, allheadlights) == 1); + } + SUBCASE("vehicle without the upper headlight, which the AI's Pc1 can't light") { + auto const inventory{allheadlights & ~light::headlight_upper}; + CHECK(handover::light_selector_position(cab.mover(), end::front, pc1 & inventory, inventory) == 2); + } + SUBCASE("no position matches: the selector stays") { + CHECK(handover::light_selector_position(cab.mover(), end::front, light::headlight_left, allheadlights) == 1); + } +} + +TEST_CASE("handover: the light selector stays within its table") { + auto cab{cab_vehicle()}; + + SUBCASE("selector bigger than the table, as a vehicle definition can declare") { + // only the positions the table holds are looked at, the rest can't match + for (auto &table : cab->Lights) { + std::fill(std::begin(table) + cab->LightsPosNo, std::end(table), 0); + } + cab->LightsPosNo = 100; + CHECK(handover::light_selector_position(cab.mover(), end::front, pc1, allheadlights) == 2); + CHECK(handover::light_selector_position(cab.mover(), end::front, light::headlight_left, allheadlights) == 1); + } + SUBCASE("table other than the front or rear one: the selector stays") { + CHECK(handover::light_selector_position(cab.mover(), -1, pc1, allheadlights) == 1); + CHECK(handover::light_selector_position(cab.mover(), 2, pc1, allheadlights) == 1); + } +} + +TEST_CASE("handover: the light selector already matching the lights stays") { + auto cab{cab_vehicle()}; + cab->LightsPos = 2; // Pc1 + + CHECK(handover::light_selector_position(cab.mover(), end::front, pc1, allheadlights) == 2); +} + +TEST_CASE("handover: the headlight dimmer leaves the off position when the head lights are lit") { + auto cab{cab_vehicle()}; + REQUIRE(cab->enableModernDimmer); + REQUIRE(cab->dimPositions.size() == 5); + cab->modernDimmerPosition = 2; // off, left there by the player before the AI took over + + SUBCASE("to the default position") { + CHECK(handover::dimmer_position(cab.mover(), true) == 3); + } + SUBCASE("to the first regular position, when the default one is off as well") { + cab->modernDimmerDefaultPosition = 2; + CHECK(handover::dimmer_position(cab.mover(), true) == 3); + } + SUBCASE("unless the head lights are off anyway") { + CHECK(handover::dimmer_position(cab.mover(), false) == 2); + } +} + +TEST_CASE("handover: the headlight dimmer in any other position stays") { + auto cab{cab_vehicle()}; + for (auto const position : {0, 1, 3, 4}) { + CAPTURE(position); + cab->modernDimmerPosition = position; + CHECK(handover::dimmer_position(cab.mover(), true) == position); + } +} + +TEST_CASE("handover: a vehicle without the modern dimmer keeps its dimmer state") { + vehicle wagon{definition::load("wagon")}; + REQUIRE_FALSE(wagon->enableModernDimmer); + + CHECK(handover::dimmer_position(wagon.mover(), true) == wagon->modernDimmerPosition); +} diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 75bbb29e..5bbaa80e 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include "utilities/Logs.h" #include "vehicle/Train.h" +#include "vehicle/handover.h" #include "world/mtable.h" #include "vehicle/DynObj.h" #include "world/Event.h" @@ -5561,6 +5562,12 @@ void TController::TakeControl( bool const Aidriver, bool const Forcevehiclecheck { // teraz AI prowadzi AIControllFlag = AIdriver; pVehicle->Controller = AIdriver; + // the AI doesn't operate the headlight dimmer, drop the state left by the human driver + // so the lights set by the AI aren't hidden (dimmer in off position) or dimmed + pVehicle->HeadlightsAoff = false; + pVehicle->HeadlightsBoff = false; + pVehicle->DimHeadlights = false; + pVehicle->HighBeamLights = false; control_lights(); // reinicjalizacja swiatel mvOccupied->CabActivisation(true); iDirection = 0; // kierunek jazdy trzeba dopiero zgadnąć @@ -5598,10 +5605,23 @@ void TController::TakeControl( bool const Aidriver, bool const Forcevehiclecheck } else { // a teraz użytkownik - if (!is_train() || !is_car()) // gasimy swiatla jesli przejmujemy odstawione + if (eAction == TAction::actSleep || OrderCurrentGet() == Wait_for_orders) // gasimy swiatla jesli przejmujemy odstawione pVehicle->RaLightsSet(0, 0); + auto const aiwasdriving { AIControllFlag == AIdriver }; + if( aiwasdriving ) { + // the horn, sandbox, departure signal and brake releaser the AI may have latched are momentary for the human driver + handover::release_ai_controls( *mvOccupied, *mvControlling ); + fWarningDuration = 0.0; + remove_hint( driver_hint::hornon ); + // the AI sets the lights directly, leave the selector in matching position so it doesn't revert them later + sync_lights_selector(); + } AIControllFlag = Humandriver; pVehicle->Controller = Humandriver; + if( aiwasdriving ) { + // same with the headlight dimmer, which is only applied for the human driver + sync_lights_dimmer(); + } if( eAction == TAction::actSleep ) { eAction = TAction::actUnknown; } @@ -6329,6 +6349,57 @@ TController::control_handles() { } } // enginetype } +// moves the light preset selector to the position matching the lights set directly by the AI, +// otherwise the next SetLights() call (e.g. from CheckVehicles()) reverts them to the untouched selector position +void +TController::sync_lights_selector() { + + if( mvOccupied->LightsPosNo <= 0 ) { return; } + + auto const *headvehicle { pVehicles[ end::front ] }; + if( headvehicle == nullptr ) { return; } + // same choice of the light table as in TDynamicObject::SetLights() + auto const lightstable { ( mvOccupied->CabOccupied * pVehicle->DirectionGet() >= 0 ? end::front : end::rear ) }; + // same choice of the lit vehicle end as in TDynamicObject::RaLightsSet() + auto const headend { ( headvehicle->iDirection > 0 ? end::front : end::rear ) }; + auto const position { handover::light_selector_position( + *mvOccupied, lightstable, headvehicle->MoverParameters->iLights[ headend ], headvehicle->LightList( headend ) ) }; + if( position == mvOccupied->LightsPos ) { return; } + + mvOccupied->LightsPos = position; + // visual feedback + auto *train { simulation::Trains.find( pVehicle->name() ) }; + if( train != nullptr ) { + train->ggLightsButton.PutValue( static_cast( position - 1 ) ); + } +} + +// re-applies the headlight dimmer state after the AI handed the vehicle over to the human driver, +// moving the dimmer out of the off position if it'd hide the head lights set by the AI +void +TController::sync_lights_dimmer() { + + if( mvOccupied->dimPositions.empty() ) { return; } + + auto const *headvehicle { pVehicles[ end::front ] }; + auto const headend { ( headvehicle != nullptr && headvehicle->iDirection > 0 ? end::front : end::rear ) }; + auto const headlightson { + headvehicle != nullptr + && ( headvehicle->MoverParameters->iLights[ headend ] & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 }; + + auto const position { handover::dimmer_position( *mvOccupied, headlightson ) }; + if( position != mvOccupied->modernDimmerPosition ) { + mvOccupied->modernDimmerPosition = position; + // visual feedback + auto *train { simulation::Trains.find( pVehicle->name() ) }; + if( train != nullptr ) { + train->ggModernLightDimSw.PutValue( static_cast( position ) ); + } + } + if( position < 0 || position >= static_cast( mvOccupied->dimPositions.size() ) ) { return; } // SetLightDimmings() doesn't check the range + pVehicle->SetLightDimmings(); +} + // activate consist lamp codes void TController::control_lights() { diff --git a/vehicle/Driver.h b/vehicle/Driver.h index da14b1b3..f88e115a 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -324,6 +324,8 @@ private: void control_security_system( double const Timedelta ); void control_handles(); void control_lights(); + void sync_lights_selector(); + void sync_lights_dimmer(); void control_compartment_lights(); void control_doors(); void UpdateCommand(); diff --git a/vehicle/handover.cpp b/vehicle/handover.cpp new file mode 100644 index 00000000..a36ccde8 --- /dev/null +++ b/vehicle/handover.cpp @@ -0,0 +1,76 @@ +/* +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/handover.h" + +#include "McZapkie/MOVER.h" + +namespace handover { + +void release_ai_controls(TMoverParameters &Occupied, TMoverParameters &Controlling) { + // the AI's hornoff action touches the horn only while the AI drives + Occupied.WarningSignal = 0; + // the AI drives the sandbox directly, bypassing the manual and automatic sanding states + if (Controlling.SandDose && false == (Controlling.SandDoseManual || Controlling.SandDoseAuto)) { + Controlling.Sandbox(false); + } + if (Occupied.DepartureSignal) { + Occupied.signal_departure(false); + } + if (Occupied.Hamulec->Releaser()) { + Occupied.BrakeReleaser(0); + } +} + +int light_selector_position(TMoverParameters const &Occupied, int const Lightstable, int const Headlights, int const Inventory) { + // the selector size comes from the vehicle definition, the table holds a fixed number of positions + auto const positioncount{std::min(Occupied.LightsPosNo, static_cast(std::size(Occupied.Lights[0])))}; + if (positioncount <= 0 || Lightstable < 0 || Lightstable >= static_cast(std::size(Occupied.Lights))) { + return Occupied.LightsPos; + } + auto const &lights{Occupied.Lights[Lightstable]}; + auto const lightsmatch{[&lights, Inventory, Headlights](int const Position) { return (lights[Position - 1] & Inventory) == Headlights; }}; + if (Occupied.LightsPos >= 1 && Occupied.LightsPos <= positioncount && lightsmatch(Occupied.LightsPos)) { + return Occupied.LightsPos; + } + for (auto position = 1; position <= positioncount; ++position) { + if (lightsmatch(position)) { + return position; + } + } + return Occupied.LightsPos; +} + +int dimmer_position(TMoverParameters const &Occupied, bool const Headlightson) { + auto const &positions{Occupied.dimPositions}; + auto const current{Occupied.modernDimmerPosition}; + auto const count{static_cast(positions.size())}; + auto const isoff{[&positions, count](int const Position) { return Position < 0 || Position >= count || positions[Position].isOff; }}; + + if (false == Occupied.enableModernDimmer || false == Headlightson || false == isoff(current)) { + return current; + } + if (false == isoff(Occupied.modernDimmerDefaultPosition)) { + return Occupied.modernDimmerDefaultPosition; + } + for (auto position = 0; position < count; ++position) { + if (false == (positions[position].isOff || positions[position].isDimmed || positions[position].isHighBeam)) { + return position; + } + } + for (auto position = 0; position < count; ++position) { + if (false == positions[position].isOff) { + return position; + } + } + return current; +} + +} // namespace handover diff --git a/vehicle/handover.h b/vehicle/handover.h new file mode 100644 index 00000000..32ed57d0 --- /dev/null +++ b/vehicle/handover.h @@ -0,0 +1,33 @@ +/* +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; + +// what changes about a vehicle when the AI hands it over to the human driver. the AI operates some +// controls directly, bypassing the cab controls a human uses, and only the AI ever releases them; +// the human gets them in the state matching the cab controls instead. +namespace handover { + +// releases what the AI latches, but which is held only while pressed for the human driver: +// the horn, the sandbox, the departure signal and the brake releaser +void release_ai_controls(TMoverParameters &Occupied, TMoverParameters &Controlling); + +// position of the light preset selector [1..LightsPosNo] for the human driver: the current one when it +// gives the head lights the AI has set, else the first one which does, else the current one. +// Lightstable is the end of the selector's table in use, Inventory the lights the head end has +int light_selector_position(TMoverParameters const &Occupied, int Lightstable, int Headlights, int Inventory); + +// position of the headlight dimmer for the human driver: the current one, unless it's the off position +// which would hide the head lights the AI has lit. then the default one, else the first regular one, +// else any other than off +int dimmer_position(TMoverParameters const &Occupied, bool Headlightson); + +} // namespace handover