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();