17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00

fix(ai): release horn when human driver takes over from AI (#210)

* fix(ai): release what the AI was holding when the player takes over

* fix(ai): keep the light selector lookup within its table, Sonar findings of the handover

- the light selector size comes from the vehicle definition (LightsList Size)
  and isn't limited, while the table holds 18 positions; a bigger selector
  read past the table, and so did a table index other than the front or
  rear one. Only the positions the table holds are looked at now, and the
  selector stays where it is for a bad table index (cpp:S2259),
- the lambdas of the handover capture what they use explicitly (cpp:S3608),
- the chained assignments resetting the headlight state on the AI taking
  over are split (cpp:S1121).
This commit is contained in:
jerrrrycho
2026-10-16 14:44:30 +02:00
committed by GitHub
parent b400255dae
commit c7f9796387
8 changed files with 390 additions and 1 deletions

View File

@@ -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.

View File

@@ -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"]
]
}
}

View File

@@ -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);
}