From aaccd7b9b77fb21842eda60236b72922d0a0c327 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 6 Sep 2026 10:16:24 +0200 Subject: [PATCH] Keep one primary per consist and let a secondary AI claim it. A driver without movePrimary no longer just idles. UpdateSecondary() cuts BrakeValveActive so a secondary AI stops fighting the primary over the brake pipe, and - as long as no driver in the consist has iEngineActive - keeps scanning ahead of CabOccupied, so it can take movePrimary and start the engine once a signal permits. scan_route() orients by CabOccupied when DirActive is 0, and ClaimConsistPrimary() clears movePrimary on every other driver in the consist, then calls TableClear() and CheckVehicles(), so sSpeedTable is rebuilt for the new front and ctOwner follows it. - walk the consist with FirstFind()/Neighbour() instead of Next()/Prev(), which stopped at the starting vehicle and mishandled reversed members; - ReassignConsistPrimary() picks a single driver after any coupling change, both from TDynamicObject::couple() and from the AI path; - IsOccupiedByAnotherConsist() decides membership with BelongsToConsist() and takes a signed distance, so a consist left behind no longer cuts the scan short. DistanceToAnotherConsist() hands back the gap it already computes, so OtherConsistAtSignal() counts a foreign vehicle only while it stands closer than the signal itself, instead of comparing track starts; - TSpeedPos::Set() marks an entry with spNotOurShuntSignal when a spShuntSemaphor was already permitting as the entry was added. A shunting signal does not address its permission to a particular consist, so a dormant driver does not wake up on a permission it did not see light up; the flag drops once the signal shows stop, so its own turn is never blocked. spSemaphor is excluded: a semaphore sets a route for us and permits us even when found already lit. The check reads SemPermitIndex - the entry that produced the permitting command - because SemNextIndex can point at an earlier signal. The flag is also stamped whenever the permission turns out to belong to a consist standing in front of us - both where TableUpdateEvent() already knows the signal is for the vehicle ahead, and in check_route_ahead() - so the addressee pulling away does not hand the still-lit signal over to whoever waited behind it; - the scantable debug panel lists the flags by name (spNotOurShuntSignal shows as not-ours) and folds the nearest vehicle ahead into the table by its own distance. --- McZapkie/MOVER.h | 2 + McZapkie/Mover.cpp | 88 ++-- application/application.cpp | 5 +- application/driveruipanels.cpp | 27 +- simulation/simulationstateserializer.cpp | 112 +++-- vehicle/Driver.cpp | 585 +++++++++++++++++++---- vehicle/Driver.h | 65 ++- vehicle/DynObj.cpp | 48 ++ vehicle/DynObj.h | 2 + 9 files changed, 759 insertions(+), 175 deletions(-) diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index a6658494..14d173c1 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -2028,6 +2028,7 @@ class TMoverParameters int BrakeCtrlPos = -2; /*nastawa hamulca zespolonego*/ double BrakeCtrlPosR = 0.0; /*nastawa hamulca zespolonego - plynna dla FV4a*/ + bool BrakeValveActive{true}; // only a driver with the Primary flag drives the brake pipe double BrakeCtrlPos2 = 0.0; /*nastawa hamulca zespolonego - kapturek dla FV4a*/ int ManualBrakePos = 0; /*nastawa hamulca recznego*/ double LocalBrakePosA = 0.0; /*nastawa hamulca pomocniczego*/ @@ -2408,6 +2409,7 @@ class TMoverParameters void UpdateBrakePressure(double dt); void UpdatePipePressure(double dt); void CompressorCheck(double dt); /*wlacza, wylacza kompresor, laduje zbiornik*/ + double GetDPMainValve(double dt, double hp) const; // flow through the driver's brake valve void UpdatePantVolume(double dt); // Ra void UpdateScndPipePressure(double dt); void UpdateSpringBrake(double dt); diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 83c918e2..37411d1f 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -4468,6 +4468,54 @@ void TMoverParameters::CompressorCheck(double dt) } } +double TMoverParameters::GetDPMainValve(double dt, double hp) const +{ + if (!BrakeValveActive) + { + return 0.0; + } + + auto const caboccupied{CabOccupied != 0}; + + auto const use_handle{ + (BrakeOpModes & bom_PS) == 0 + || (caboccupied && BrakeOpModeFlag != bom_PS)}; + + if (!use_handle) + { + if (BrakeCtrlPos == Handle->GetPos(bh_EB)) + { + return Handle->GetPF(BrakeCtrlPosR, PipePress, hp, dt, EqvtPipePress); + } + return 0.0; + } + + auto const mhzepidle{ + BrakeOpModeFlag >= bom_EP + && BrakeCtrlPosR <= Handle->GetPos(bh_EB) - 0.5 + && (BrakeHandle == TBrakeHandle::MHZ_EN57 || BrakeHandle == TBrakeHandle::MHZ_K8P)}; + + if (mhzepidle) + { + return Handle->GetPF(0, PipePress, hp, dt, EqvtPipePress); + } + + double pos{BrakeCtrlPosR}; + if (SpeedCtrlUnit.IsActive + && SpeedCtrlUnit.BrakeIntervention + && !SpeedCtrlUnit.Standby + && BrakeCtrlPos != Handle->GetPos(bh_EB)) + { + pos = Handle->GetPos(bh_NP); + if (SpeedCtrlUnit.BrakeInterventionBraking) + pos = Handle->GetPos(bh_FB); + if (SpeedCtrlUnit.BrakeInterventionUnbraking) + pos = Handle->GetPos(bh_RP); + } + + return Handle->GetPF(pos, PipePress, hp, dt, EqvtPipePress); +} + // ************************************************************************************************* // Q: 20160712 // aktualizacja ciśnienia w przewodzie głównym @@ -4484,7 +4532,7 @@ void TMoverParameters::UpdatePipePressure(double dt) const double kL = 0.5; // double dV; // TMoverParameters *c; // T_MoverParameters - double temp; + double hp{0.0}; // int b; PipePress = Pipe->P(); @@ -4537,39 +4585,15 @@ void TMoverParameters::UpdatePipePressure(double dt) if (lock_old || lock_new) { - temp = PipePress + 0.00001; + hp = PipePress + 0.00001; } else { - temp = ScndPipePress; + hp = ScndPipePress; } Handle->SetReductor(BrakeCtrlPos2); - if ((BrakeOpModes & bom_PS) == 0 || (CabOccupied != 0 && BrakeOpModeFlag != bom_PS)) - { - - if (BrakeOpModeFlag < bom_EP || Handle->GetPos(bh_EB) - 0.5 < BrakeCtrlPosR || (BrakeHandle != TBrakeHandle::MHZ_EN57 && BrakeHandle != TBrakeHandle::MHZ_K8P)) - { - double pos = BrakeCtrlPosR; - if (SpeedCtrlUnit.IsActive && SpeedCtrlUnit.BrakeIntervention && !SpeedCtrlUnit.Standby && BrakeCtrlPos != Handle->GetPos(bh_EB)) - { - pos = Handle->GetPos(bh_NP); - if (SpeedCtrlUnit.BrakeInterventionBraking) - pos = Handle->GetPos(bh_FB); - if (SpeedCtrlUnit.BrakeInterventionUnbraking) - pos = Handle->GetPos(bh_RP); - } - dpMainValve = Handle->GetPF(pos, PipePress, temp, dt, EqvtPipePress); - } - else - { - dpMainValve = Handle->GetPF(0, PipePress, temp, dt, EqvtPipePress); - } - } - else if (BrakeCtrlPos == Handle->GetPos(bh_EB)) - { - dpMainValve = Handle->GetPF(BrakeCtrlPosR, PipePress, temp, dt, EqvtPipePress); - } + dpMainValve = GetDPMainValve(dt, hp); if (dpMainValve < 0) // && (PipePressureVal > 0.01) //50 if (Compressor > ScndPipePress) @@ -4740,11 +4764,9 @@ void TMoverParameters::UpdatePipePressure(double dt) Pipe->Act(); PipePress = Pipe->P(); - if ((Hamulec->GetBrakeStatus() & b_dmg) == b_dmg) // jesli hamulec wyłączony - temp = 0.0; // odetnij - else - temp = 1.0; // połącz - Pipe->Flow(temp * Hamulec->GetPF(temp * PipePress, dt, Vel) + GetDVc(dt)); + // a damaged distributor cuts the car off from the pipe (as temp=0/1 used to) + auto const distributor_open{(Hamulec->GetBrakeStatus() & b_dmg) != b_dmg ? 1.0 : 0.0}; + Pipe->Flow(distributor_open * Hamulec->GetPF(distributor_open * PipePress, dt, Vel) + GetDVc(dt)); if (ASBType == 128) Hamulec->ASB(int(SlippingWheels && Vel > 1) * (1 + 2 * int(nrot_eps < -0.01))); diff --git a/application/application.cpp b/application/application.cpp index e5d0b6ba..47e6c79b 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -681,6 +681,10 @@ void eu07_application::exit() for (auto &mode : m_modes) mode.reset(); + // workers hold hidden windows from window(-1), kept in m_windows and sharing the main GL + // context - join them before Shutdown() and glfwDestroyWindow() destroy those contexts + m_taskqueue.exit(); + GfxRenderer->Shutdown(); m_network.reset(); @@ -693,7 +697,6 @@ void eu07_application::exit() { glfwDestroyWindow(window); } - m_taskqueue.exit(); glfwPollEvents(); // TODO: This fixes a segfault on Wayland when closing. Remove after updating glfw to 3.5. glfwTerminate(); diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index 729b854c..ba8db4fe 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -895,12 +895,13 @@ debug_panel::update_section_vehicle( std::vector &Output ) { std::snprintf( m_buffer.data(), m_buffer.size(), - STR_C("Brakes:\n train: %.2f (mode: %d, delay: %s, load flag: %d)\n independent: %.2f (%.2f), manual: %.2f, spring: %.2f\nBrake cylinder pressures:\n train: %.2f, independent: %.2f, status: 0x%.2x\nPipe pressures:\n brake: %.2f (hat: %.2f), main: %.2f, control: %.2f\nTank pressures:\n auxiliary: %.2f, main: %.2f, control: %.2f"), + STR_C("Brakes:\n train: %.2f (mode: %d, delay: %s, load flag: %d, valve: %s)\n independent: %.2f (%.2f), manual: %.2f, spring: %.2f\nBrake cylinder pressures:\n train: %.2f, independent: %.2f, status: 0x%.2x\nPipe pressures:\n brake: %.2f (hat: %.2f), main: %.2f, control: %.2f\nTank pressures:\n auxiliary: %.2f, main: %.2f, control: %.2f"), // brakes mover.fBrakeCtrlPos, mover.BrakeOpModeFlag, update_vehicle_brake().c_str(), mover.LoadFlag, + mover.BrakeValveActive ? "active" : "cut off", mover.LocalBrakePosA, mover.LocalBrakePosAEIM, mover.ManualBrakePos / static_cast(ManualBrakePosNo), @@ -1266,17 +1267,29 @@ debug_panel::update_section_scantable( std::vector &Output ) { if( m_input.mechanik == nullptr ) { return; } - Output.emplace_back( "Flags: Dist: Vel: Name:", Global.UITextColor ); + Output.emplace_back( " Dist: Vel: Name:", Global.UITextColor ); auto const &mechanik{ *m_input.mechanik }; - std::size_t i = 0; std::size_t const speedtablesize = std::clamp( static_cast( mechanik.TableSize() ) - 1, 0, 30 ); - do { + // the nearest vehicle ahead slots in by its own distance, last if further than every entry + auto const obstacleline { mechanik.ObstacleText() }; + auto obstaclepending { false == obstacleline.empty() }; + auto emitobstacle = [&]() { + Output.emplace_back( Bezogonkow( obstacleline ), Global.UITextColor ); + obstaclepending = false; + }; + + std::size_t const speedtablesize = std::clamp( static_cast( mechanik.TableSize() ), 0, 30 ); + for( std::size_t i = 0; i < speedtablesize; ++i ) { auto const scanline = mechanik.TableText( i ); - if( scanline.empty() ) { break; } + // disabled entries give an empty string - skip, don't break: gaps happen mid-table + if( scanline.empty() ) { continue; } + if( obstaclepending && mechanik.Obstacle.distance < mechanik.TableDistance( i ) ) { + emitobstacle(); + } Output.emplace_back( Bezogonkow( scanline ), Global.UITextColor ); - ++i; - } while( i < speedtablesize ); + } + if( obstaclepending ) { emitobstacle(); } if( Output.size() == 1 ) { Output.front().data = "(no points of interest)"; } diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index ef7cd7db..11a63c86 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -831,59 +831,83 @@ state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data } } +namespace { + +using trainset_data = scene::scratch_data::trainset_data; + +void couple_trainset( trainset_data &trainset ) +{ // couple before picking the primary - reassign sees uncoupled members as separate consists + for( std::size_t i = 1; i < trainset.vehicles.size(); ++i ) { + trainset.vehicles[ i - 1 ]->AttachNext( + trainset.vehicles[ i ], + trainset.couplings[ i - 1 ] ); + } +} + +TDynamicObject *select_trainset_driver( trainset_data &trainset ) +{ // picks the primary after coupling and hands it its orders, if any + trainset.vehicles.front()->ReassignConsistPrimary(); + + for( auto *vehicle : trainset.vehicles ) { + if( vehicle->Mechanik == nullptr || !vehicle->Mechanik->primary() ) { + continue; + } + auto const lookup { trainset.assignment.find( Global.asLang ) }; + if( lookup != trainset.assignment.end() ) { + vehicle->Mechanik->assignment() = lookup->second; + } + return vehicle; + } + return nullptr; +} + +void init_trainset_driver( trainset_data const &trainset ) +{ + if( trainset.driver == nullptr || trainset.driver->Mechanik == nullptr ) { + return; + } + auto *controller { trainset.driver->Mechanik }; + controller->DirectionInitial(); + controller->PutCommand( + "Timetable:" + trainset.name, + trainset.velocity, + 0, + nullptr ); +} + +void apply_trainset_end_signals( trainset_data const &trainset ) +{ + // coupler 0 on the last vehicle: plate end signals (the AI fixes them once it starts) + if( trainset.couplings.back() != coupling::faux ) { + return; + } + // with a driver: markers + plates; without one: plates only (the AI corrects it on startup) + auto const rearlights { + trainset.driver != nullptr + ? light::redmarker_left | light::redmarker_right | light::rearendsignals + : light::rearendsignals }; + trainset.vehicles.back()->RaLightsSet( -1, rearlights ); +} + +} + void state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad ) { - if( false == Scratchpad.trainset.is_open - || true == Scratchpad.trainset.vehicles.empty() ) { - // not bloody likely but we better check for it just the same + if( !Scratchpad.trainset.is_open || Scratchpad.trainset.vehicles.empty() ) { ErrorLog( "Bad trainset: empty trainset defined in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); Scratchpad.trainset.is_open = false; return; } - std::size_t vehicleindex { 0 }; - for( auto *vehicle : Scratchpad.trainset.vehicles ) { - // go through list of vehicles in the trainset, coupling them together and checking for potential driver - if( vehicle->Mechanik != nullptr - && vehicle->Mechanik->primary() ) { - // primary driver will receive the timetable for this trainset - Scratchpad.trainset.driver = vehicle; - // they'll also receive assignment data if there's any - auto const lookup { Scratchpad.trainset.assignment.find( Global.asLang ) }; - if( lookup != Scratchpad.trainset.assignment.end() ) { - vehicle->Mechanik->assignment() = lookup->second; - } - } - if( vehicleindex > 0 ) { - // from second vehicle on couple it with the previous one - Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( - vehicle, - Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); - } - ++vehicleindex; - } + auto &trainset { Scratchpad.trainset }; - if( Scratchpad.trainset.driver != nullptr ) { - // if present, send timetable to the driver - // wysłanie komendy "Timetable" ustawia odpowiedni tryb jazdy - auto *controller = Scratchpad.trainset.driver->Mechanik; - controller->DirectionInitial(); - controller->PutCommand( - "Timetable:" + Scratchpad.trainset.name, - Scratchpad.trainset.velocity, - 0, - nullptr ); - } - if( Scratchpad.trainset.couplings.back() == coupling::faux ) { - // jeśli ostatni pojazd ma sprzęg 0 to założymy mu końcówki blaszane (jak AI się odpali, to sobie poprawi) - // place end signals only on trains without a driver, activate markers otherwise - Scratchpad.trainset.vehicles.back()->RaLightsSet( - -1, - Scratchpad.trainset.driver != nullptr ? light::redmarker_left | light::redmarker_right | light::rearendsignals : light::rearendsignals ); - } - // all done - Scratchpad.trainset.is_open = false; + couple_trainset( trainset ); + trainset.driver = select_trainset_driver( trainset ); + init_trainset_driver( trainset ); + apply_trainset_end_signals( trainset ); + + trainset.is_open = false; } // creates path and its wrapper, restoring class data from provided stream diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 43f2f924..18255a6f 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -339,6 +339,9 @@ bool TSpeedPos::Update() // odczyt komórki pamięci najlepiej by było zrobić jako notyfikację, // czyli zmiana komórki wywoła jakąś podaną funkcję CommandCheck(); // sprawdzenie typu komendy w evencie i określenie prędkości + if( fVelNext == 0.0 ) { + iFlags &= ~spNotOurShuntSignal; // saw "stop" - the next permission will light up with us here + } } } return false; @@ -359,13 +362,24 @@ std::string TSpeedPos::GetName() const } std::string TSpeedPos::TableText() const -{ // pozycja tabelki pr?dko?ci - if (iFlags & spEnabled) - { // o ile pozycja istotna - return to_hex_str(iFlags, 8) + " " + to_string(fDist, 1, 6) + - " " + (fVelNext == -1.0 ? " -" : to_string(static_cast(fVelNext), 0, 3)) + " " + GetName(); +{ // one speed table entry - distance/velocity/name plus readable flag tags instead of hex + if( ( iFlags & spEnabled ) == 0 ) { + return ""; // disabled - empty string, the F2 panel skips such an entry } - return "Empty"; + + std::string tags; + for( auto const &description : SpeedPosFlagDescriptions ) { + if( iFlags & description.flag ) { + if( false == tags.empty() ) { tags += " "; } + tags += description.name; + } + } + + return + to_string( fDist, 1, 8 ) + + " " + ( fVelNext == -1.0 ? " -" : to_string( static_cast( fVelNext ), 0, 3 ) ) + + " " + GetName() + + ( tags.empty() ? "" : " [" + tags + "]" ); } bool TSpeedPos::IsProperSemaphor(TOrders order) @@ -385,6 +399,14 @@ bool TSpeedPos::IsProperSemaphor(TOrders order) return false; // true gdy zatrzymanie, wtedy nie ma po co skanować dalej } +bool TSpeedPos::AllowsDeparture() const +{ // signal ahead permits - a dormant AI may start without waiting for a change + if( fDist <= 0.0 ) { return false; } + if( ( iFlags & spEnabled ) == 0 ) { return false; } + if( ( iFlags & ( spSemaphor | spShuntSemaphor ) ) == 0 ) { return false; } + return fVelNext < 0.0 || fVelNext >= 1.0; +} + bool TSpeedPos::Set(basic_event *event, double dist, double length, TOrders order) { // zapamiętanie zdarzenia fDist = dist; @@ -398,6 +420,11 @@ bool TSpeedPos::Set(basic_event *event, double dist, double length, TOrders orde } iFlags |= spEnabled; CommandCheck(); // sprawdzenie typu komendy w evencie i określenie prędkości + if( ( iFlags & spShuntSemaphor ) != 0 + && fVelNext != 0.0 ) { + // a shunting signal addresses nobody in particular; a semaphore sets a route for us + iFlags |= spNotOurShuntSignal; // permission found already lit - we did not see who it was given to + } // zależnie od trybu sprawdzenie czy jest tutaj gdzieś semafor lub tarcza manewrowa // jeśli wskazuje stop wtedy wystawiamy true jako koniec sprawdzania // WriteLog("EventSet: Vel=" + AnsiString(fVelNext) + " iFlags=" + AnsiString(iFlags) + " order="+AnsiString(order)); @@ -451,6 +478,7 @@ void TController::TableClear() fLastVel = -1.0; SemNextIndex = -1; SemNextStopIndex = -1; + SemPermitIndex = std::size_t( -1 ); eSignSkip = nullptr; // nic nie pomijamy }; @@ -503,7 +531,10 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) double fCurrentDistance{ 0.0 }; // aktualna przeskanowana długość double fLastDir{ 0.0 }; - if (iTableDirection != iDirection ) { + // with no direction set (dormant AI) we scan by the occupied cab + int const scandirection{ iDirection != 0 ? iDirection : mvOccupied->CabOccupied }; + + if (iTableDirection != scandirection ) { // jeśli zmiana kierunku, zaczynamy od toru ze wskazanym pojazdem TableClear(); /* @@ -519,10 +550,19 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) // don't allow potential red light overrun keep us from reversing VelSignalLast = -1.0; } - iTableDirection = iDirection; // ustalenie w jakim kierunku jest wypełniana tabelka względem pojazdu + iTableDirection = scandirection; pTrack = pVehicle->GetTrack(); // odcinek, na którym stoi fTrackLength = pVehicle->RaTranslationGet(); // pozycja na tym torze (odległość od Point1) fLastDir = pVehicle->DirectionGet() * pVehicle->RaDirectionGet(); // ustalenie kierunku skanowania na torze + if( pVehicles[ end::front ] != nullptr + && pVehicles[ end::front ] != pVehicle ) { + // driver not in the front vehicle: DirectionGet() may point backwards relative to the consist + auto const towardfront{ pVehicles[ end::front ]->GetPosition() - pVehicle->GetPosition() }; + auto const nose{ pVehicle->VectorFront() * static_cast( pVehicle->DirectionGet() ) }; + if( towardfront.x * nose.x + towardfront.z * nose.z < 0.0 ) { + fLastDir = -fLastDir; + } + } if( fLastDir < 0.0 ) { // jeśli w kierunku Point2 toru fTrackLength = pTrack->Length() - fTrackLength; // przeskanowana zostanie odległość do Point2 @@ -635,6 +675,10 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) + ")"); } } + if( iEngineActive ) { + // only a dormant driver acts on it, so do not tag it for anybody else + newspeedpoint.iFlags &= ~spNotOurShuntSignal; + } } } } // event dodajemy najpierw, żeby móc sprawdzić, czy tor został dodany po odczytaniu prędkości następnego @@ -700,6 +744,16 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) } } + auto const distancetotrackend{ fCurrentDistance + fTrackLength }; + // sign of the distance encodes the scanning direction (fLastDir < 0 = towards Point2) + if( distancetotrackend > 0.0 // the track still reaches ahead of us + && IsOccupiedByAnotherConsist( pTrack, distancetotrackend * ( fLastDir < 0.0 ? 1.0 : -1.0 ) ) ) { + // another consist stands here: it owns the signals beyond it, so we scan no further. The track + // stays in the table, marked checked - scanning resumes from the last entry, no dup control + tLast = pTrack; + break; + } + fCurrentDistance += fTrackLength; // doliczenie kolejnego odcinka do przeskanowanej długości tLast = pTrack; // odhaczenie, że sprawdzony fLastVel = pTrack->VelocityGet(); // prędkość na poprzednio sprawdzonym odcinku @@ -772,7 +826,9 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) void TController::TableCheck(double fDistance) { // przeliczenie odległości w tabelce, ewentualnie doskanowanie (bez analizy prędkości itp.) - if( iTableDirection != iDirection ) { + // as in TableTraceRoute: a dormant AI has no iDirection, so we orient by the cab + int const scandirection{ iDirection != 0 ? iDirection : mvOccupied->CabOccupied }; + if( iTableDirection != scandirection ) { // jak zmiana kierunku, to skanujemy od końca składu TableTraceRoute( fDistance, pVehicles[ end::rear ] ); TableSort(); @@ -859,6 +915,7 @@ TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fN auto d { 0.0 }; // droga auto d_to_next_sem { 10000.0 }; //ustaiwamy na pewno dalej niż widzi AI auto go { TCommandType::cm_Unknown }; + SemPermitIndex = std::size_t( -1 ); // filled in by whichever point yields a permitting command auto speedlimitiscontinuous { true }; // stays true if potential active speed limit is unbroken to the last (relevant) point in scan table eSignNext = nullptr; IsAtPassengerStop = false; @@ -1546,6 +1603,13 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo if( isforsomeoneelse ) { VelSignalNext = 0.0; Velocity = 0.0; + // it permits, but for the vehicle standing in front of us. Record that now - + // once it pulls away the signal alone no longer says whose permission it was + if( false == iEngineActive + && ( Point.iFlags & spShuntSemaphor ) + && Point.fVelNext != 0.0 ) { + Point.iFlags |= spNotOurShuntSignal; + } } else { VelSignalNext = Point.fVelNext; @@ -1553,6 +1617,16 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo Velocity = fVelMax; VelSignal = fVelMax; } + if( Command == TCommandType::cm_Unknown && Point.AllowsDeparture() ) { + // already permitting as it entered the FOV - wake up without waiting for a change + Command = ( Point.iFlags & spShuntSemaphor ) != 0 ? + TCommandType::cm_ShuntVelocity : + TCommandType::cm_SetVelocity; + SemPermitIndex = Pointindex; + if( std::fabs( VelSignal ) < 1.0 ) { + VelSignal = Point.fVelNext < 0.0 ? -1.0 : Point.fVelNext; + } + } } } } @@ -1630,6 +1704,7 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo if( Velocity != 0.0 ) { // jeśli nie było komendy wcześniej - pierwsza się liczy - ustawianie VelSignal Command = TCommandType::cm_ShuntVelocity; // w trybie pociągowym tylko jeśli włącza tryb manewrowy (v!=0.0) + SemPermitIndex = Pointindex; // Ra 2014-06: (VelSignal) nie może być tu ustawiane, bo Tm może być daleko if( VelSignal == 0.0 ) { // aby stojący ruszył @@ -1653,6 +1728,7 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo || Velocity >= 1.0 ) { // bo wartość 0.1 służy do hamowania tylko Command = TCommandType::cm_SetVelocity; // może odjechać + SemPermitIndex = Pointindex; // Ra 2014-06: (VelSignal) nie może być tu ustawiane, bo semafor może być daleko // VelSignal=v; //nie do końca tak, to jest druga prędkość; -1 nie wpisywać... if( VelSignal == 0.0 ) { @@ -1820,6 +1896,15 @@ void TController::TableSort() { //--------------------------------------------------------------------------- +bool TController::primary( bool const Primary ) +{ + SetFlag( iDrivigFlags, Primary ? movePrimary : -movePrimary ); + if( mvOccupied != nullptr ) { + mvOccupied->BrakeValveActive = Primary; + } + return primary(); +} + TController::TController(bool AI, TDynamicObject *NewControll, bool InitPsyche, bool Primary) :// czy ma aktywnie prowadzić? AIControllFlag( AI ), pVehicle( NewControll ) { @@ -1870,9 +1955,7 @@ TController::TController(bool AI, TDynamicObject *NewControll, bool InitPsyche, // OrderValue=0; OrdersClear(); - if( true == Primary ) { - iDrivigFlags |= movePrimary; // aktywnie prowadzące pojazd - } + primary( Primary ); SetDriverPsyche(); // na końcu, bo wymaga ustawienia zmiennych TableClear(); @@ -2035,6 +2118,7 @@ void TController::Activation() // odcięcie na zaworze maszynisty, FVel6 po drugiej stronie nie luzuje mvOccupied->BrakeLevelSet( mvOccupied->Handle->GetPos(bh_NP) ); // odcięcie na zaworze maszynisty BrakeLevelSet( mvOccupied->BrakeCtrlPos ); //ustawienie zmiennej GBH + mvOccupied->BrakeValveActive = false; } // przejście AI na drugą stronę EN57, ET41 itp. // TODO: clean this up, there's lot of redundancy with TMoverParameters::ChangeCab() and TTrain::MoveToVehicle() @@ -2049,6 +2133,7 @@ void TController::Activation() if( targetvehicle->DirectionGet() != pVehicle->DirectionGet() ) { // jeśli są przeciwne do siebie to będziemy jechać w drugą stronę względem zasiedzianego pojazdu iDirection = -iDirection; + iDirectionOrder = iDirection; // so that DirectionChange does not flip it back } // move the other driver to our old vehicle pVehicle->Mechanik = targetvehicledriver; // wsadzamy tego, co ewentualnie był (podwójna trakcja) @@ -2100,6 +2185,7 @@ void TController::Activation() } CheckVehicles(); // sprawdzenie składu, AI zapali światła + sync_consist_reversers(); // after a cab change the members' reversers have to agree TableClear(); // resetowanie tabelki skanowania torów } }; @@ -2343,6 +2429,10 @@ int TController::CheckDirection() { // jeśli nie ma ustalonego kierunku to jedziemy wg aktualnej kabiny d = mvOccupied->CabActive; } + if( !d ) { + // dormant: reverser/cab inactive - orient by the occupied cab + d = mvOccupied->CabOccupied; + } return d; } @@ -2368,14 +2458,14 @@ bool TController::CheckVehicles(TOrders user) { // sprawdzanie, czy jest głównym sterującym, żeby nie było konfliktu if( p->Mechanik // jeśli ma obsadę && p->Mechanik != this ) { // ale chodzi o inny pojazd, niż aktualnie sprawdzający - if( p->Mechanik->iDrivigFlags & movePrimary ) { + if( p->Mechanik->primary() ) { // a tamten ma priorytet // TODO: take into account drivers' operating modes, one or more of them might be on banking duty - if( iDrivigFlags & movePrimary + if( primary() && mvOccupied->DirAbsolute && mvOccupied->BrakeCtrlPos >= -1 ) { // jeśli rządzi i ma kierunek - p->Mechanik->primary( false ); // dezaktywuje tamtego + p->Mechanik->primary( false ); p->Mechanik->ZeroLocalBrake(); p->MoverParameters->BrakeLevelSet( p->MoverParameters->Handle->GetPos( bh_NP ) ); // odcięcie na zaworze maszynisty p->Mechanik->BrakeLevelSet( p->MoverParameters->BrakeCtrlPos ); //ustawienie zmiennej GBH @@ -2398,12 +2488,12 @@ bool TController::CheckVehicles(TOrders user) p = p->Neighbour(dir); // pojazd podłączony od wskazanej strony } if( main ) { - iDrivigFlags |= movePrimary; // nie znaleziono innego, można się porządzić + primary( true ); } ControllingSet(); // ustalenie członu do sterowania (może być inny niż zasiedziany) - if (iDrivigFlags & movePrimary) + if( primary() ) { // jeśli jest aktywnie prowadzącym pojazd, może zrobić własny porządek auto pantmask = 1; p = pVehicles[end::front]; @@ -2565,15 +2655,19 @@ void TController::Lights(int head, int rear) } void TController::DirectionInitial() -{ // ustawienie kierunku po wczytaniu trainset (może jechać na wstecznym - mvOccupied->CabActivisationAuto(); // załączenie rozrządu (wirtualne kabiny) - if (mvOccupied->Vel > EU07_AI_NOMOVEMENT) - { // jeśli na starcie jedzie - iDirection = iDirectionOrder = - mvOccupied->V > 0 ? 1 : -1; // początkowa prędkość wymusza kierunek jazdy - DirectionForward(mvOccupied->V * mvOccupied->CabActive >= 0.0); // a dalej ustawienie nawrotnika +{ // sets the direction after a trainset is loaded + mvOccupied->CabActivisationAuto(); + if( mvOccupied->Vel >= EU07_AI_MOVEMENT ) { + // actually moving - direction from the sign of V + iDirection = iDirectionOrder = mvOccupied->V > 0 ? 1 : -1; + DirectionForward( mvOccupied->V * mvOccupied->CabActive >= 0.0 ); } - CheckVehicles(); // sprawdzenie świateł oraz skrajnych pojazdów do skanowania + else { + // standing: reverser forward relative to the cab, not from the sign of a micro V + DirectionForward( true ); + iDirection = iDirectionOrder = CheckDirection(); + } + CheckVehicles(); }; void TController::DirectionChange() { @@ -3977,7 +4071,7 @@ void TController::SpeedCntrl(double DesiredSpeed) void TController::SetTimeControllers() { // TBD, TODO: rework this method to use hint system and regardless of driver type - if( false == AIControllFlag || 0 == mvOccupied->CabActive ) { return; } + if( !primary() || !AIControllFlag || mvOccupied->CabActive == 0 ) { return; } //1. Check the type of Main Brake Handle if( BrakeSystem == TBrakeSystem::Pneumatic || ForcePNBrake ) @@ -4188,7 +4282,7 @@ void TController::SetTimeControllers() void TController::CheckTimeControllers() { // TODO: rework this method to use hint system and regardless of driver type - if( false == AIControllFlag || 0 == mvControlling->CabActive ) { return; } + if( !primary() || !AIControllFlag || mvControlling->CabActive == 0 ) { return; } // a secondary crew does not touch the controllers //1. Check the type of Main Brake Handle if( BrakeSystem == TBrakeSystem::ElectroPneumatic && mvOccupied->Handle->TimeEP && !ForcePNBrake ) @@ -4389,8 +4483,99 @@ void TController::PutCommand(std::string NewCommand, double NewValue1, double Ne mvOccupied->PutCommand(NewCommand, NewValue1, NewValue2, NewLocation); } +bool TController::CommandClaimsPrimary( std::string const &Command, double const Value ) +{ // a driving/startup order wakes a secondary crew up to lead, otherwise nobody moves the consist + auto const isdrivingorder = [&Command]( std::initializer_list const names ) { + for( auto const *name : names ) { + if( Command == name ) { return true; } + } + return false; + }; + return + Command.compare( 0, 10, "Timetable:" ) == 0 + || ( Command == "SetVelocity" && Value != 0.0 ) + || isdrivingorder( { "Prepare_engine", "ShuntVelocity", "Obey_train", "Bank", "Shunt", "Loose_shunt" } ); +} + +namespace { + +// front-to-back walk like CheckVehicles(); Neighbour() flips the coupler side on reversed members +template +void ForEachInConsist( TDynamicObject *Vehicle, Callable Function ) { + if( Vehicle == nullptr ) { return; } + int side { end::front }; + auto *vehicle { Vehicle->FirstFind( side ) }; + if( vehicle == nullptr ) { vehicle = Vehicle; } + auto dir { 1 - side }; + while( vehicle != nullptr ) { + if( false == Function( vehicle ) ) { return; } // false stops the walk + vehicle = vehicle->Neighbour( dir ); + } +} + +} + +void TController::ClaimConsistPrimary() +{ // this driver is to be the only primary left in the consist + ForEachInConsist( + pVehicle, + [this]( TDynamicObject *Vehicle ) { + auto *other { Vehicle->Mechanik }; + if( other == nullptr + || other == this ) { return true; } + if( other->primary() ) { + other->ZeroLocalBrake(); + if( other->mvOccupied->BrakeCtrlPosNo > 0 ) { // only vehicles with a driver's brake valve (otherwise Handle == null) + other->mvOccupied->BrakeLevelSet( other->mvOccupied->Handle->GetPos( bh_NP ) ); + other->BrakeLevelSet( other->mvOccupied->BrakeCtrlPos ); + } + } + // always - sync the valve on the already secondary ones too + other->primary( false ); + return true; + } ); + primary( true ); + TableClear(); // after a change of primary the table has to be built from scratch + CheckVehicles(); // otherwise the members' ctOwner stays with the previous primary +} + +TController * +TController::ActiveEnginePrimary() const { + // a primary with the engine already running - then we do not steal primary + TController *active { nullptr }; + ForEachInConsist( + pVehicle, + [&active]( TDynamicObject *Vehicle ) { + auto *driver { Vehicle->Mechanik }; + if( driver != nullptr + && driver->primary() + && driver->iEngineActive ) { + active = driver; + return false; + } + return true; + } ); + return active; +} + bool TController::PutCommand( std::string NewCommand, double NewValue1, double NewValue2, glm::dvec3 const *NewLocation, TStopReason reason ) { // analiza komendy + auto const shouldtakeprimary{ + AIControllFlag + && !primary() + && CommandClaimsPrimary( NewCommand, NewValue1 ) + && ActiveEnginePrimary() == nullptr }; // steal only while the engines are down + if( shouldtakeprimary ) { + ClaimConsistPrimary(); + if( mvOccupied->CabActive == 0 ) { + mvOccupied->CabActivisationAuto(); + } + if( mvOccupied->DirActive == 0 ) { + DirectionForward( true ); + } + DirectionChange(); + } + if (NewCommand == "CabSignal") { // SHP wyzwalane jest przez człon z obsadą, ale obsługiwane przez silnikowy // nie jest to najlepiej zrobione, ale bez symulacji obwodów lepiej nie będzie @@ -4459,7 +4644,7 @@ bool TController::PutCommand( std::string NewCommand, double NewValue1, double N TrainParams.StationStart = TrainParams.StationIndex; asNextStop = TrainParams.NextStop(); m_lastannouncement = announcement_t::idle; - iDrivigFlags |= movePrimary; // skoro dostał rozkład, to jest teraz głównym + primary( true ); // NewCommand = Global.asCurrentSceneryPath + NewCommand; auto lookup = FileExists( @@ -4882,13 +5067,78 @@ void TController::PhysicsLog() } }; +void +TController::SettleSecondaryAI() { + // no force, no brake, vigilance kept quiet; runs every tick so nothing is remembered + mvOccupied->BrakeValveActive = false; // the primary drives the brake pipe + // without a NN acknowledging alone is not enough (see TSecuritySystem::is_braking) + mvOccupied->SecuritySystem.acknowledge_press(); + mvOccupied->SecuritySystem.acknowledge_release(); + if( false == ( mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable ) ) { + mvOccupied->SecuritySystem.set_enabled( false ); + } + ZeroSpeed( true ); + ZeroLocalBrake(); + if( mvOccupied->SpringBrake.Activate ) { + mvOccupied->SpringBrakeActivate( false ); + } +} + +void +TController::UpdateSecondary( double const Awarenessrange ) { + // does not drive; scans while the consist is shut down (wake-up / taking over on a signal) + if( AIControllFlag ) { + SettleSecondaryAI(); + } + else { + // a human sits in the cab - they handle it themselves + mvOccupied->BrakeValveActive = true; + } + if( pVehicle->ctOwner != nullptr + && pVehicle->ctOwner != this + && pVehicle->ctOwner->iEngineActive ) { + return; // the consist runs under an active primary - do not duplicate the scan + } + // a secondary needs no responsiveness - scans rarely, cheap with many shut down consists + auto const scaninterval{ std::max( ReactionTime, EU07_AI_SECONDARYSCANINTERVAL ) }; + if( LastReactionTime < scaninterval ) { return; } + LastReactionTime -= scaninterval; + OrientScanToCab(); + scan_route( Awarenessrange ); + // no obstacles - a dormant driver only waits for a signal; the primary computes them later + ScanForWakeup( Awarenessrange ); +} + +void +TController::ScanForWakeup( double const Range ) { + // a permission may wake a dormant AI up (Prepare_engine in the mask) + if( AIControllFlag + && ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank | Prepare_engine ) ) == 0 ) { + check_route_ahead( Range ); + } +} + void TController::Update( double const Timedelta ) { // uruchamiać przynajmniej raz na sekundę - if( ( iDrivigFlags & movePrimary ) == 0 ) { return; } // pasywny nic nie robi - if( false == simulation::is_ready ) { return; } + if( !simulation::is_ready ) { return; } update_timers( Timedelta ); + + auto const awarenessrange { + std::max( + 750.0, + mvOccupied->Vel > EU07_AI_MOVEMENT ? + 400 + fBrakeDist : + 30.0 * fDriverDist ) }; + + if( !primary() ) { + UpdateSecondary( awarenessrange ); + return; + } + + mvOccupied->BrakeValveActive = true; // opens the valve, may have been cut off while secondary + update_logs( Timedelta ); auto const reactiontime { std::min( ReactionTime, 2.0 ) }; @@ -4913,16 +5163,9 @@ TController::Update( double const Timedelta ) { determine_consist_state(); determine_braking_distance(); determine_proximity_ranges(); - // vicinity check - auto const awarenessrange { - std::max( - 750.0, - mvOccupied->Vel > EU07_AI_MOVEMENT ? - 400 + fBrakeDist : - 30.0 * fDriverDist ) }; // 1500m dla stojących pociągów; - if( is_active() ) { - scan_route( awarenessrange ); - } + + // we scan while dormant too, to see a permission and wake up + scan_route( awarenessrange ); scan_obstacles( awarenessrange ); // generic actions control_security_system( reactiontime ); @@ -5015,8 +5258,12 @@ TController::PrepareDirection() { if( iDirection == 0 ) { // jeśli nie ma ustalonego kierunku - if( mvOccupied->Vel < EU07_AI_NOMOVEMENT ) { // ustalenie kierunku, gdy stoi + // as in DirectionInitial / TakeControl: below 1 km/h counts as standing + if( mvOccupied->Vel < EU07_AI_MOVEMENT ) { // determine the direction while standing iDirection = mvOccupied->CabActive; // wg wybranej kabiny + if( iDirection == 0 ) { + iDirection = mvOccupied->CabOccupied; + } /* if( iDirection == 0 ) { // jeśli nie ma ustalonego kierunku @@ -5262,28 +5509,46 @@ std::string TController::StopReasonText() const //- rozpoznają tylko zerową prędkość (jako koniec toru i brak podstaw do dalszego skanowania) //---------------------------------------------------------------------------------------------------------------------- -bool TController::IsOccupiedByAnotherConsist( TTrack *Track, double const Distance = 0 ) -{ // najpierw sprawdzamy, czy na danym torze są pojazdy z innego składu - if( false == Track->Dynamics.empty() ) { - for( auto dynamic : Track->Dynamics ) { - if( dynamic->ctOwner != this ) { - // jeśli jest jakiś cudzy to tor jest zajęty i skanowanie nie obowiązuje - if( Distance == 0 ) { - return true; - } - else { - // based on provided position of scanning vehicle and scan direction, filter out vehicles on irrelevant end - auto const scandirection { Distance > 0 ? 1 : -1 }; // positive value means scan towards point2 end of the track - auto const obstaclelocation { scandirection > 0 ? Track->Length() - dynamic->RaTranslationGet() : dynamic->RaTranslationGet() }; - // if detected vehicle is closer to the end of the track in scanned direction than we are, it means it's blocking our way - if( obstaclelocation < std::abs( Distance ) ) { - return true; - } - } - } +bool TController::BelongsToConsist( TDynamicObject const *Vehicle ) const +{ // ctOwner alone is not enough: cleared after uncoupling until CheckVehicles + if( Vehicle == nullptr ) { return false; } + if( Vehicle->ctOwner == this ) { return true; } + if( Vehicle->Mechanik == this ) { return true; } + if( Vehicle->ctOwner != nullptr + && Vehicle->ctOwner == pVehicle->ctOwner ) { return true; } + return pVehicle->is_connected( Vehicle ); // with no owner settled, the coupling is all that is left +} + +double TController::DistanceToAnotherConsist( TTrack *Track, double const Distance ) const +{ // Distance is signed by the scan direction and reaches the far end of the track + auto nearest { -1.0 }; + auto const scandirection { Distance > 0 ? 1 : -1 }; // positive value means scan towards point2 end of the track + for( auto *dynamic : Track->Dynamics ) { + if( BelongsToConsist( dynamic ) ) { continue; } + // how far the foreign vehicle sits from that same far end + auto const obstaclelocation { scandirection > 0 ? Track->Length() - dynamic->RaTranslationGet() : dynamic->RaTranslationGet() }; + // what is left is the gap between us and it; a negative one means it stands behind us + auto const distance { std::abs( Distance ) - obstaclelocation }; + if( distance <= 0.0 ) { continue; } + if( nearest < 0.0 + || distance < nearest ) { + nearest = distance; } } - return false; // wolny + return nearest; +} + +bool TController::IsOccupiedByAnotherConsist( TTrack *Track, double const Distance ) const +{ // whether the track carries vehicles of another consist + if( Track->Dynamics.empty() ) { return false; } + if( Distance == 0.0 ) { + // no scan reference given - any foreign vehicle on the track counts + for( auto *dynamic : Track->Dynamics ) { + if( false == BelongsToConsist( dynamic ) ) { return true; } + } + return false; + } + return DistanceToAnotherConsist( Track, Distance ) >= 0.0; }; basic_event * TController::CheckTrackEventBackward(double fDirection, TTrack *Track, TDynamicObject *Vehicle, int const Eventdirection, end const End) @@ -5614,16 +5879,26 @@ void TController::UpdateDelayFlag() { //-----------koniec skanowania semaforow +void TController::ReleaseTransientControls() +{ + mvOccupied->WarningSignal = 0; + mvControlling->SandboxManual( false ); + fWarningDuration = 0.0; + iDrivigFlags &= ~( moveStartHornNow | moveStartHornDone ); +} + void TController::TakeControl( bool const Aidriver, bool const Forcevehiclecheck ) { // przejęcie kontroli przez AI albo oddanie if (AIControllFlag == Aidriver && !Forcevehiclecheck) return; // już jest jak ma być + ReleaseTransientControls(); if (Aidriver) //żeby nie wykonywać dwa razy { // teraz AI prowadzi AIControllFlag = AIdriver; pVehicle->Controller = AIdriver; control_lights(); // reinicjalizacja swiatel mvOccupied->CabActivisation(true); + sync_consist_reversers(); // after taking over from a human the reversers may have drifted apart iDirection = 0; // kierunek jazdy trzeba dopiero zgadnąć TableClear(); // ponowne utworzenie tabelki, bo człowiek mógł pojechać niezgodnie z sygnałami if( action() != TAction::actSleep ) { @@ -5781,13 +6056,14 @@ TController::TrackObstacle() const { void TController::MoveTo(TDynamicObject *to) { // przesunięcie AI do innego pojazdu (przy zmianie kabiny) + auto takeprimary{false}; // we take the lead only once the predecessor is gone if( to->Mechanik != nullptr && to->Mechanik != this ) { // ai controller thunderdome, there can be only one if( to->Mechanik->AIControllFlag ) { - if( to->Mechanik->primary() ) { - // take over boss duties - primary( true ); + takeprimary = to->Mechanik->primary(); + if( takeprimary ) { + to->Mechanik->primary( false ); // cut the valve before the delete, so it is not left open } SafeDelete( to->Mechanik ); } @@ -5803,7 +6079,9 @@ void TController::MoveTo(TDynamicObject *to) pVehicle = to; ControllingSet(); // utworzenie połączenia do sterowanego pojazdu pVehicle->Mechanik = this; - + if( takeprimary ) { + primary( true ); + } }; void TController::ControllingSet() @@ -5823,14 +6101,32 @@ void TController::ControllingSet() std::string TController::TableText( std::size_t const Index ) const { // pozycja tabelki prędkości - if( Index < sSpeedTable.size() ) { - return sSpeedTable[ Index ].TableText(); - } - else { + if( Index >= sSpeedTable.size() ) { return ""; } + return sSpeedTable[ Index ].TableText(); }; +double TController::TableDistance( std::size_t const Index ) const +{ // for slotting the obstacle into the table by distance + return + Index < sSpeedTable.size() ? + sSpeedTable[ Index ].fDist : + std::numeric_limits::max(); +} + +std::string TController::ObstacleText() const +{ // a row shaped like TableText, so the panel shows the vehicle at the right distance + if( Obstacle.vehicle == nullptr ) { + return ""; + } + + return + to_string( Obstacle.distance, 1, 8 ) + + " - " + Obstacle.vehicle->asName + + " [vehicle" + ( Obstacle.vehicle->ctOwner == this ? " own-consist]" : "]" ); +} + int TController::CrossRoute(TTrack *tr) { // zwraca numer segmentu dla skrzyżowania (tr) // pożądany numer segmentu jest określany podczas skanowania drogi @@ -6532,6 +6828,27 @@ TController::scan_route( double const Range ) { TableCheck( Range ); } +void +TController::OrientScanToCab() { + // like the start of CheckVehicles, without primary / lights / ControllingSet + iDirection = CheckDirection(); + if( iDirection == 0 ) { return; } + + auto d{ iDirection >= 0 ? 0 : 1 }; + auto *p{ pVehicle->FirstFind( d ) }; + if( p == nullptr ) { + p = pVehicle; + } + pVehicles[ end::front ] = p; + auto dir{ 1 - d }; // Neighbour() advances this index to the next member, so it cannot be const + fLength = 0.0; + while( p != nullptr ) { + pVehicles[ end::rear ] = p; + fLength += p->MoverParameters->Dim.L; + p = p->Neighbour( dir ); + } +} + // check for potential collisions void TController::scan_obstacles( double const Range ) { @@ -6540,11 +6857,16 @@ TController::scan_obstacles( double const Range ) { // we cast to int to avoid getting confused by microstutters auto *frontvehicle { pVehicles[ ( static_cast( mvOccupied->V ) * iDirection >= 0 ? end::front : end::rear ) ] }; + // no direction set (dormant AI): orient physically by the occupied cab, else both tests below + // hold and the scan always runs towards coupler 0 - half the time behind us, taking our own + // members for an obstacle and missing the consist standing ahead + auto const scandirection { iDirection != 0 ? iDirection : mvOccupied->CabOccupied }; + int routescandirection; // for moving vehicle determine heading from velocity; for standing fall back on the set direction if( std::abs(frontvehicle->MoverParameters->V) > 0.5 ? // ignore potential micro-stutters in oposite direction during "almost stop" frontvehicle->MoverParameters->V > 0.0 : - pVehicle->DirectionGet() == frontvehicle->DirectionGet() ? iDirection >= 0 : iDirection <= 0 ) { + pVehicle->DirectionGet() == frontvehicle->DirectionGet() ? scandirection >= 0 : scandirection <= 0 ) { // towards coupler 0 routescandirection = end::front; } @@ -7118,12 +7440,21 @@ TController::UpdateDisconnect() { void TController::handle_engine() { - // HACK: activate route scanning if an idling vehicle is activated by a human user if( OrderCurrentGet() == Wait_for_orders - && false == iEngineActive -// && ( false == AIControllFlag ) - && true == mvOccupied->Power24vIsAvailable ) { - OrderNext( Prepare_engine ); + && false == iEngineActive ) { + // scanning is geometric: the driver sees a signal also with the battery off + iDrivigFlags |= moveActive; + if( AIControllFlag + && primary() + && mvOccupied->DirActive == 0 + && SemNextIndex != -1 ) { + // there is a signal ahead and no direction set yet - set it, so the scan runs forwards + if( mvOccupied->CabActive == 0 ) { + mvOccupied->CabActivisationAuto(); + } + DirectionForward( true ); + DirectionChange(); + } } // basic engine preparation if( OrderCurrentGet() == Prepare_engine ) { @@ -7248,8 +7579,8 @@ TController::pick_optimal_speed( double const Range ) { SwitchClearDist = -1; ActualProximityDist = Range; // funkcja Update() może pozostawić wartości bez zmian - // if we're idling bail out early if( false == is_active() ) { + ScanForWakeup( Range ); VelDesired = 0.0; VelNext = 0.0; AccDesired = std::min( AccDesired, EU07_AI_NOACCELERATION ); @@ -8179,11 +8510,98 @@ void TController::control_main_pipe() { } } +bool +TController::OtherConsistAtSignal() const { + // permission goes to the consist in front of the signal, so a foreign vehicle counts only + // while it stands closer to us than the signal itself + if( SemPermitIndex >= sSpeedTable.size() ) { return false; } + + auto const signaldistance { sSpeedTable[ SemPermitIndex ].fDist }; + + for( auto const &point : sSpeedTable ) { + // a track starting past the signal cannot hold anything standing in front of it + if( point.fDist > signaldistance ) { break; } + if( ( point.iFlags & spTrack ) == 0 ) { continue; } + if( point.trTrack == nullptr ) { continue; } + // only ahead of our front; a consist behind us would make every permission someone else's + auto const distancetotrackend { point.fDist + point.trTrack->Length() }; + if( distancetotrackend <= 0.0 ) { continue; } + auto const signeddistance { + distancetotrackend * ( ( point.iFlags & spReverse ) != 0 ? 1.0 : -1.0 ) }; + auto const obstacledistance { DistanceToAnotherConsist( point.trTrack, signeddistance ) }; + if( obstacledistance < 0.0 ) { continue; } // nothing foreign ahead here + if( obstacledistance <= signaldistance ) { return true; } + } + + return false; +} + +bool +TController::SignalPermissionIsOurs( bool const Signalgivesgo ) const { + // the permission belongs to the consist standing closest to the signal + if( false == Signalgivesgo ) { return true; } + // a shunting permission found already lit was handed to whoever stood here before us; it only + // keeps a dormant driver from waking up, and drops as soon as the signal shows stop + if( false == iEngineActive + && SemPermitIndex < sSpeedTable.size() // std::size_t( -1 ) never is + && ( sSpeedTable[ SemPermitIndex ].iFlags & spNotOurShuntSignal ) ) { + return false; + } + // the addressee usually cuts the scan short and the signal never makes it into the table; + // this closes the case where it did, with the addressee still standing in front of it + return false == OtherConsistAtSignal(); +} + +TController * +TController::DepartureDriver() const { + // after OrientScanToCab / CheckVehicles the front of the consist is pVehicles[front] + if( pVehicles[ end::front ] != nullptr + && pVehicles[ end::front ]->Mechanik != nullptr ) { + return pVehicles[ end::front ]->Mechanik; + } + return const_cast( this ); +} + +void +TController::WakeForDeparture( std::string const &Command, double const Value1, double const Value2 ) { + // the primary already has the engine running - we do not steal, we just pass the order on + if( auto *active { ActiveEnginePrimary() } ) { + active->PutCommand( Command, Value1, Value2, nullptr ); + return; + } + // engines down - the lead goes to the driver at the front (scanning direction) + auto *target { primary() ? this : DepartureDriver() }; + if( target == nullptr ) { + target = this; + } + if( target != this ) { + if( false == target->primary() ) { + target->ClaimConsistPrimary(); + } + target->PutCommand( Command, Value1, Value2, nullptr ); + return; + } + PutCommand( Command, Value1, Value2, nullptr ); +} + void TController::check_route_ahead( double const Range ) { auto const comm { TableUpdate( VelDesired, ActualProximityDist, VelNext, AccDesired ) }; + auto const signalpermits { comm == TCommandType::cm_SetVelocity || comm == TCommandType::cm_ShuntVelocity }; + if( false == SignalPermissionIsOurs( signalpermits ) ) { + // the permission was given to another consist, we wait for our own. Remember it on the + // entry, so the addressee pulling away does not hand the signal over to us; the flag is + // dropped when it shows stop, and only a dormant driver ever carries one + if( false == iEngineActive + && SemPermitIndex < sSpeedTable.size() + && ( sSpeedTable[ SemPermitIndex ].iFlags & spShuntSemaphor ) ) { + sSpeedTable[ SemPermitIndex ].iFlags |= spNotOurShuntSignal; + } + return; + } + switch (comm) { // ustawienie VelSignal - trochę proteza = do przemyślenia case TCommandType::cm_Ready: { // W4 zezwolił na jazdę @@ -8198,17 +8616,17 @@ TController::check_route_ahead( double const Range ) { } break; } - case TCommandType::cm_SetVelocity: { // od wersji 357 semafor nie budzi wyłączonej lokomotywy - if( ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank ) ) == 0 ) { // jedzie w dowolnym trybie albo Wait_for_orders + case TCommandType::cm_SetVelocity: { + if( ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank | Prepare_engine ) ) == 0 ) { if( std::fabs( VelSignal ) >= 1.0 ) { // 0.1 nie wysyła się do samochodow, bo potem nie ruszą - PutCommand( "SetVelocity", VelSignal, VelNext, nullptr ); // komenda robi dodatkowe operacje + WakeForDeparture( "SetVelocity", VelSignal, VelNext ); } } break; } - case TCommandType::cm_ShuntVelocity: { // od wersji 357 Tm nie budzi wyłączonej lokomotywy - if( ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank ) ) == 0 ) { // jedzie w dowolnym trybie albo Wait_for_orders - PutCommand( "ShuntVelocity", VelSignal, VelNext, nullptr ); + case TCommandType::cm_ShuntVelocity: { + if( ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank | Prepare_engine ) ) == 0 ) { + WakeForDeparture( "ShuntVelocity", VelSignal, VelNext ); } else if( iCoupler ) { // jeśli jedzie w celu połączenia SetVelocity( VelSignal, VelNext ); @@ -8216,8 +8634,7 @@ TController::check_route_ahead( double const Range ) { break; } case TCommandType::cm_Command: { // komenda z komórki - if( ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank ) ) == 0 ) { - // jedzie w dowolnym trybie albo Wait_for_orders + if( ( OrderCurrentGet() & ~( Shunt | Loose_shunt | Obey_train | Bank | Prepare_engine ) ) == 0 ) { if( mvOccupied->Vel < 0.1 ) { // dopiero jak stanie /* diff --git a/vehicle/Driver.h b/vehicle/Driver.h index e7c4fac0..6222db62 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -24,6 +24,7 @@ auto const EU07_AI_NOACCELERATION = -0.05; auto const EU07_AI_BRAKINGTESTACCELERATION = -0.06; auto const EU07_AI_NOMOVEMENT = 0.05; // standstill velocity threshold auto const EU07_AI_MOVEMENT = 1.0; // deliberate movement velocity threshold +auto const EU07_AI_SECONDARYSCANINTERVAL = 5.0; // s - a secondary scans for a wake-up less often than the primary auto const EU07_AI_SPEEDLIMITEXTENDSBEYONDSCANRANGE = 10000.0; enum TOrders @@ -144,10 +145,36 @@ enum TSpeedPosFlag spSemaphor = 0x4000, // semafor pociągowy spRoadVel = 0x8000, // zadanie prędkości drogowej spSectionVel = 0x20000, // odcinek z ograniczeniem - spProximityVelocity = 0x40000 // odcinek z ograniczeniem i podaną jego długościa + spProximityVelocity = 0x40000, // odcinek z ograniczeniem i podaną jego długościa + spNotOurShuntSignal = 0x80000 // shunting signal a dormant driver found already permitting - not for us // spDontApplySpeedLimit = 0x10000 // this point won't apply its speed limit. potentially set by the scanning vehicle }; +// names of speed table entry flags, for the debug panel; spEnabled is skipped, every visible entry has it +struct TSpeedPosFlagDescription { + int flag; + char const *name; +}; + +inline constexpr TSpeedPosFlagDescription SpeedPosFlagDescriptions[] { + { spSemaphor, "semaphore" }, + { spShuntSemaphor, "shunt" }, + { spPassengerStopPoint, "W4" }, + { spOutsideStation, "W5" }, + { spSwitch, "switch" }, + { spSwitchStatus, "diverging" }, + { spTrack, "track" }, + { spCurve, "curve" }, + { spEnd, "end" }, + { spSectionVel, "section" }, + { spProximityVelocity, "proximity" }, + { spRoadVel, "road" }, + { spStopOnSBL, "SBL" }, + { spReverse, "reverse" }, + { spElapsed, "passed" }, + { spCommandSent, "sent" }, + { spNotOurShuntSignal, "not-ours" } }; + class TSpeedPos { // pozycja tabeli prędkości dla AI public: @@ -181,6 +208,8 @@ class TSpeedPos std::string TableText() const; std::string GetName() const; bool IsProperSemaphor(TOrders order = Wait_for_orders); + // signal ahead permits - wakes a dormant AI without waiting for the next change + bool AllowsDeparture() const; }; //---------------------------------------------------------------------------- @@ -222,13 +251,15 @@ public: void Update( double dt ); // uruchamiac przynajmniej raz na sekundę void MoveTo( TDynamicObject *to ); void TakeControl( bool const Aidriver, bool const Forcevehiclecheck = false ); - inline - bool primary( bool const Primary ) { - SetFlag( iDrivigFlags, Primary ? movePrimary : -movePrimary ); - return primary(); } + void ReleaseTransientControls(); // clears the warning signal and the sandbox on a change of primary + bool primary( bool const Primary ); inline bool primary() const { return (iDrivigFlags & movePrimary) != 0; }; + // drops primary from the others in the consist and takes the lead (valve / scan table) + void ClaimConsistPrimary(); + // a primary with iEngineActive in the consist; nullptr = free to take the lead + TController *ActiveEnginePrimary() const; inline TMoverParameters const *Controlling() const { return mvControlling; } @@ -317,7 +348,12 @@ private: void determine_braking_distance(); void determine_proximity_ranges(); void scan_route( double const Range ); + void ScanForWakeup( double const Range ); // dormant/secondary AI: checks the signal ahead for a wake-up + // front/rear/fLength per CheckDirection, CabOccupied fallback; no CheckVehicles side effects + void OrientScanToCab(); void scan_obstacles( double const Range ); + void UpdateSecondary( double const Awarenessrange ); // scans for a wake-up, does not drive + void SettleSecondaryAI(); // drops force and brake, keeps vigilance quiet void control_wheelslip(); void control_pantographs(); void control_horns( double const Timedelta ); @@ -355,6 +391,13 @@ private: void control_braking_force(); void apply_independent_brake_only(); void check_route_ahead( double const Range ); + // the driver at the front (scanning direction) - the signal hands the lead to them + TController *DepartureDriver() const; + // engines down: primary for the front; running: the order goes to the current primary + void WakeForDeparture( std::string const &Command, double const Value1, double const Value2 ); + // false when the permission at the nearest signal was given to another consist + bool SignalPermissionIsOurs( bool const Signalgivesgo ) const; + bool OtherConsistAtSignal() const; // another consist in front of the signal = the addressee of the permission void check_route_behind( double const Range ); void UpdateBrakingHelper(); void hint( driver_hint const Value, hintpredicate const Predicate, float const Predicateparameter = 0.f ); @@ -477,6 +520,8 @@ public: std::string OrderCurrent() const; private: void RecognizeCommand(); // odczytuje komende przekazana lokomotywie + // whether the command wakes a secondary crew up to lead (a driving or startup order) + static bool CommandClaimsPrimary( std::string const &Command, double const Value ); void JumpToNextOrder( bool const Skipmergedchangedirection = false ); void JumpToFirstOrder(); void OrderPush( TOrders NewOrder ); @@ -526,12 +571,18 @@ private: void TableClear(); int TableDirection() { return iTableDirection; } // Ra: stare funkcje skanujące, używane do szukania sygnalizatora z tyłu - bool IsOccupiedByAnotherConsist( TTrack *Track, double const Distance ); + bool IsOccupiedByAnotherConsist( TTrack *Track, double const Distance = 0.0 ) const; + // distance to the nearest vehicle of another consist ahead on the track; -1.0 when there is none + double DistanceToAnotherConsist( TTrack *Track, double const Distance ) const; + bool BelongsToConsist( TDynamicObject const *Vehicle ) const; // whether the vehicle is part of our consist basic_event *CheckTrackEventBackward( double fDirection, TTrack *Track, TDynamicObject *Vehicle, int const Eventdirection = 1, end const End = end::rear ); TTrack *BackwardTraceRoute( double &fDistance, double &fDirection, TDynamicObject *Vehicle, basic_event *&Event, int const Eventdirection = 1, end const End = end::rear, bool const Untiloccupied = true ); void SetProximityVelocity( double dist, double vel, glm::dvec3 const *pos ); TCommandType BackwardScan( double const Range ); std::string TableText( std::size_t const Index ) const; + double TableDistance( std::size_t const Index ) const; // distance of the entry; max when out of range + // scan table row describing the nearest vehicle on the route; empty when the way is clear + std::string ObstacleText() const; /* void RouteSwitch(int d); */ @@ -544,6 +595,8 @@ private: basic_event *eSignSkip = nullptr; // można pominąć ten SBL po zatrzymaniu std::size_t SemNextIndex{ std::size_t( -1 ) }; std::size_t SemNextStopIndex{ std::size_t( -1 ) }; + // entry that produced the permitting command; SemNextIndex can point at a different one + std::size_t SemPermitIndex{ std::size_t( -1 ) }; double dMoveLen = 0.0; // odległość przejechana od ostatniego sprawdzenia tabelki basic_event *eSignNext = nullptr; // sygnał zmieniający prędkość, do pokazania na [F2] neighbour_data Obstacle; // nearest vehicle detected ahead on current route diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 184d432a..2e95a7ad 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -1616,6 +1616,8 @@ int TDynamicObject::Dettach(int dir) d = d->Next(); // i w drugą stronę } } + // the neighbour before uncoupling - afterwards Neighbours no longer points at it + auto *const severedneighbour { MoverParameters->Neighbours[ dir ].vehicle }; if( MoverParameters->Couplers[ dir ].CouplingFlag ) { // odczepianie, o ile coś podłączone MoverParameters->Dettach( dir ); @@ -1628,10 +1630,54 @@ int TDynamicObject::Dettach(int dir) }; */ } + // after uncoupling every part of the consist must have at most one primary + ReassignConsistPrimary(); + if( severedneighbour != nullptr ) { + severedneighbour->ReassignConsistPrimary(); + } // sprzęg po rozłączaniu (czego się nie da odpiąć return MoverParameters->Couplers[dir].CouplingFlag; } +void TDynamicObject::ReassignConsistPrimary() +{ // picks the primary; ClaimConsistPrimary does the actual primary / valve / table change + std::vector drivers; + TController *existingprimary = nullptr; + int primarycount = 0; + // walk from the front; Next()/Prev() are not enough, reversed members need the coupler side flipped + int side { end::front }; + auto *front { FirstFind( side ) }; + if( front == nullptr ) { front = this; } + auto dir { 1 - side }; + auto *farend { front }; + for( auto *d = front; d != nullptr; d = d->Neighbour( dir ) ) + { + if( d->Mechanik != nullptr ) + { + drivers.emplace_back( d->Mechanik ); + if( d->Mechanik->primary() ) { + ++primarycount; + existingprimary = d->Mechanik; + } + } + farend = d; + } + if( drivers.empty() ) { return; } + // a human-driven cab takes priority, so the AI does not take the consist from them + TController *humandriver = nullptr; + for( auto *drv : drivers ) { + if( false == drv->AIControllFlag ) { humandriver = drv; break; } + } + // with a single primary it stays; otherwise: the human, the end vehicles, the first on the list + TController *newprimary = + ( primarycount == 1 ) ? existingprimary : + ( humandriver != nullptr ) ? humandriver : + ( front->Mechanik != nullptr ) ? front->Mechanik : + ( farend->Mechanik != nullptr ) ? farend->Mechanik : + drivers.front(); + newprimary->ClaimConsistPrimary(); +} + void TDynamicObject::couple( int const Side ) { @@ -1653,6 +1699,7 @@ TDynamicObject::couple( int const Side ) { Side, neighbour.vehicle_end, othervehicleparams, coupling::coupler ) ) { + ReassignConsistPrimary(); // coupled consists are one consist, with one primary // one coupling type per key press return; } @@ -3118,6 +3165,7 @@ bool TDynamicObject::Update(double dt, double dt1) MoverParameters->InsideConsist = false; } if( TestFlag( MoverParameters->AIFlag, sound::attachcoupler ) ) { + ReassignConsistPrimary(); // coupled consists are one consist, with one primary auto *driver{ ctOwner ? ctOwner : Mechanik }; if( driver != nullptr ) { driver->CheckVehicles( Connect ); diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index f8f2ce78..5e69468d 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -791,6 +791,8 @@ private: return iDirection + iDirection - 1; }; int DettachStatus(int dir); int Dettach(int dir); + // after uncoupling/crewing picks one primary in the consist - two of them fight over the brake + void ReassignConsistPrimary(); TDynamicObject * Neighbour(int &dir); // updates potential collision sources void update_neighbours();