mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 08:05:55 +02:00
fix(ai): release what the AI was holding when the player takes over
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
34
tests/unit/data/cab_vehicle.json
Normal file
34
tests/unit/data/cab_vehicle.json
Normal 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"]
|
||||
]
|
||||
}
|
||||
}
|
||||
151
tests/unit/test_handover.cpp
Normal file
151
tests/unit/test_handover.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
#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 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);
|
||||
}
|
||||
@@ -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,10 @@ 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 = pVehicle->HeadlightsBoff = false;
|
||||
pVehicle->DimHeadlights = pVehicle->HighBeamLights = false;
|
||||
control_lights(); // reinicjalizacja swiatel
|
||||
mvOccupied->CabActivisation(true);
|
||||
iDirection = 0; // kierunek jazdy trzeba dopiero zgadnąć
|
||||
@@ -5598,10 +5603,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 +6347,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<float>( 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<float>( position ) );
|
||||
}
|
||||
}
|
||||
if( position < 0 || position >= static_cast<int>( mvOccupied->dimPositions.size() ) ) { return; } // SetLightDimmings() doesn't check the range
|
||||
pVehicle->SetLightDimmings();
|
||||
}
|
||||
|
||||
// activate consist lamp codes
|
||||
void
|
||||
TController::control_lights() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
73
vehicle/handover.cpp
Normal file
73
vehicle/handover.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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) {
|
||||
if (Occupied.LightsPosNo <= 0) {
|
||||
return Occupied.LightsPos;
|
||||
}
|
||||
auto const lightsmatch{[&](int const Position) { return (Occupied.Lights[Lightstable][Position - 1] & Inventory) == Headlights; }};
|
||||
if (Occupied.LightsPos >= 1 && Occupied.LightsPos <= Occupied.LightsPosNo && lightsmatch(Occupied.LightsPos)) {
|
||||
return Occupied.LightsPos;
|
||||
}
|
||||
for (auto position = 1; position <= Occupied.LightsPosNo; ++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<int>(positions.size())};
|
||||
auto const isoff{[&](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
|
||||
33
vehicle/handover.h
Normal file
33
vehicle/handover.h
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user