From 00bb44a7898fa22bcd8e31f39647e57036a2d0db Mon Sep 17 00:00:00 2001 From: combolek <4743344+combolek@users.noreply.github.com> Date: Sun, 26 Jul 2026 17:56:44 -0700 Subject: [PATCH 01/38] state_manager: Optimize computation of pressure flow for Oerlikon ESt valves TNESt3::GetPF() is a *major* hotspot in simulation state update. The leading cause is the use of typeid (RTTI) which is implemented in the C++ runtime essentially as a string compare. This change eliminates the use of typeid and instead caches statically typed pointers for the few TPrzekladnik subclass objects that need their parameters adjusted in TNESt3::GetPF(). As a bonus some virtual methods on TPrzekladnik can now be removed. Overall this change reduces the TNESt3::GetPF() contribution from about 25% to 16%. --- McZapkie/Oerlikon_ESt.cpp | 64 ++++++++++++++++++++++----------------- McZapkie/Oerlikon_ESt.h | 14 ++++----- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index 1df5fe81..a787a09d 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -237,26 +237,24 @@ double TNESt3::GetPF( double const PP, double const dt, double const Vel ) // pr BrakeRes->Flow(dV); for (int i = 1; i < 4; ++i) - { Przekladniki[i]->Update(dt); - if (typeid(*Przekladniki[i]) == typeid(TRapid)) - { - RapidStatus = (BrakeDelayFlag & bdelay_R) == bdelay_R && (std::abs(Vel) > 70.0 || (std::abs(Vel) > 50.0 && RapidStatus) || RapidStaly); - Przekladniki[i]->SetRapidStatus(RapidStatus); - } - else if( typeid( *Przekladniki[i] ) == typeid( TPrzeciwposlizg ) ) - Przekladniki[i]->SetPoslizg((BrakeStatus & b_asb) == b_asb); - else if( typeid( *Przekladniki[ i ] ) == typeid( TPrzekED ) ) { - if( Vel < -15.0 ) - Przekladniki[ i ]->SetP( 0.0 ); - else - Przekladniki[ i ]->SetP( MaxBP * 3.0 ); - } - else if( typeid( *Przekladniki[i] ) == typeid( TPrzekCiagly ) ) - Przekladniki[i]->SetMult(LoadC); - else if( typeid( *Przekladniki[i] ) == typeid( TPrzek_PZZ ) ) - Przekladniki[i]->SetLBP(LBP); + + if( PrzekRapid != nullptr ) { + RapidStatus = (BrakeDelayFlag & bdelay_R) == bdelay_R && (std::abs(Vel) > 70.0 || (std::abs(Vel) > 50.0 && RapidStatus) || RapidStaly); + PrzekRapid->SetRapidStatus(RapidStatus); } + if( PrzekPoslizg != nullptr ) + PrzekPoslizg->SetPoslizg((BrakeStatus & b_asb) == b_asb); + if( PrzekED != nullptr ) { + if( Vel < -15.0 ) + PrzekED->SetP( 0.0 ); + else + PrzekED->SetP( MaxBP * 3.0 ); + } + if( PrzekCiagly != nullptr ) + PrzekCiagly->SetMult(LoadC); + if( PrzekPZZ != nullptr ) + PrzekPZZ->SetLBP(LBP); // przeplyw testowy miedzypojemnosci dV = PF(MPP, VVP, BVs(BCP)) + PF(MPP, CVP, CVs(BCP)); @@ -458,6 +456,12 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie static double const dOO1l = 0.907; static double const dOT1l = 0.524; + PrzekRapid.reset(); + PrzekPoslizg.reset(); + PrzekED.reset(); + PrzekCiagly.reset(); + PrzekPZZ.reset(); + if (contains( params, "ESt3" ) ) { Podskok = 0.7; @@ -467,23 +471,27 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie else { Podskok = -1.0; - Przekladniki[1] = std::make_shared(); - if (contains( params, "-s216" ) ) - Przekladniki[1]->SetRapidParams(2, 16); - else - Przekladniki[1]->SetRapidParams(2, 0); - Przekladniki[3] = std::make_shared(); + Przekladniki[1] = PrzekRapid = std::make_shared(); + if (contains( params,"-ED") ) { - Przekladniki[1]->SetRapidParams(2, 18); - Przekladniki[3] = std::make_shared(); + PrzekRapid->SetRapidParams(2, 18); + Przekladniki[3] = PrzekED = std::make_shared(); + } + else + { + if (contains( params, "-s216" ) ) + PrzekRapid->SetRapidParams(2, 16); + else + PrzekRapid->SetRapidParams(2, 0); + Przekladniki[3] = PrzekPoslizg = std::make_shared(); } } if (contains(params,"AL2") ) - Przekladniki[2] = std::make_shared(); + Przekladniki[2] = PrzekCiagly = std::make_shared(); else if (contains(params,"PZZ") ) - Przekladniki[2] = std::make_shared(); + Przekladniki[2] = PrzekPZZ = std::make_shared(); else Przekladniki[2] = std::make_shared(); diff --git a/McZapkie/Oerlikon_ESt.h b/McZapkie/Oerlikon_ESt.h index a6b6f3d0..51d787be 100644 --- a/McZapkie/Oerlikon_ESt.h +++ b/McZapkie/Oerlikon_ESt.h @@ -76,12 +76,6 @@ class TPrzekladnik : public TReservoir // przekladnik (powtarzacz) TPrzekladnik() : TReservoir() {}; virtual void Update(double dt); - virtual void SetPoslizg(bool flag) {}; - virtual void SetP(double P) {}; - virtual void SetMult(double m) {}; - virtual void SetLBP(double P) {}; - virtual void SetRapidParams(double mult, double size) {}; - virtual void SetRapidStatus(bool rs) {}; }; class TRura : public TPrzekladnik // nieprzekladnik, rura laczaca @@ -185,7 +179,13 @@ class TNESt3 : public TBrake { private: std::shared_ptr CntrlRes; // zbiornik sterujacy std::shared_ptr Miedzypoj; // pojemnosc posrednia (urojona) do napelniania ZP i ZS - std::shared_ptr Przekladniki[ 4 ]; + std::shared_ptr Przekladniki[ 4 ]; // indeksowane od 1, indeks jest zarazem pojemnoscia + // bezposrednie wskazniki do przekladnikow dostrajanych w GetPF, inicjowane przy tworzeniu + std::shared_ptr PrzekRapid; + std::shared_ptr PrzekPoslizg; + std::shared_ptr PrzekED; + std::shared_ptr PrzekCiagly; + std::shared_ptr PrzekPZZ; double Nozzles[ dMAX ]; // dysze double BVM = 0.0; // przelozenie PG-CH // ValveFlag: byte; //polozenie roznych zaworkow From 12c583f41894bec2a680dea2f1063205a362591e Mon Sep 17 00:00:00 2001 From: combolek <4743344+combolek@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:54:33 -0700 Subject: [PATCH 02/38] Fix some SonarQube warnings --- McZapkie/Oerlikon_ESt.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/McZapkie/Oerlikon_ESt.cpp b/McZapkie/Oerlikon_ESt.cpp index a787a09d..9559d305 100644 --- a/McZapkie/Oerlikon_ESt.cpp +++ b/McZapkie/Oerlikon_ESt.cpp @@ -471,12 +471,14 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie else { Podskok = -1.0; - Przekladniki[1] = PrzekRapid = std::make_shared(); + PrzekRapid = std::make_shared(); + Przekladniki[1] = PrzekRapid; if (contains( params,"-ED") ) { PrzekRapid->SetRapidParams(2, 18); - Przekladniki[3] = PrzekED = std::make_shared(); + PrzekED = std::make_shared(); + Przekladniki[3] = PrzekED; } else { @@ -484,14 +486,21 @@ void TNESt3::SetSize( int const size, std::string const ¶ms ) // ustawianie PrzekRapid->SetRapidParams(2, 16); else PrzekRapid->SetRapidParams(2, 0); - Przekladniki[3] = PrzekPoslizg = std::make_shared(); + PrzekPoslizg = std::make_shared(); + Przekladniki[3] = PrzekPoslizg; } } if (contains(params,"AL2") ) - Przekladniki[2] = PrzekCiagly = std::make_shared(); + { + PrzekCiagly = std::make_shared(); + Przekladniki[2] = PrzekCiagly; + } else if (contains(params,"PZZ") ) - Przekladniki[2] = PrzekPZZ = std::make_shared(); + { + PrzekPZZ = std::make_shared(); + Przekladniki[2] = PrzekPZZ; + } else Przekladniki[2] = std::make_shared(); From 19441b5e91331b0441a577d7e2db4498f709b339 Mon Sep 17 00:00:00 2001 From: combolek <4743344+combolek@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:01:21 -0700 Subject: [PATCH 03/38] Convert Przekladniki to std::array --- McZapkie/Oerlikon_ESt.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/McZapkie/Oerlikon_ESt.h b/McZapkie/Oerlikon_ESt.h index 51d787be..c1269185 100644 --- a/McZapkie/Oerlikon_ESt.h +++ b/McZapkie/Oerlikon_ESt.h @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. Copyright (C) 2007-2014 Maciej Cierniak */ +#include #include #include "hamulce.h" // Pascal unit #include "friction.h" // Pascal unit @@ -179,7 +180,7 @@ class TNESt3 : public TBrake { private: std::shared_ptr CntrlRes; // zbiornik sterujacy std::shared_ptr Miedzypoj; // pojemnosc posrednia (urojona) do napelniania ZP i ZS - std::shared_ptr Przekladniki[ 4 ]; // indeksowane od 1, indeks jest zarazem pojemnoscia + std::array, 4> Przekladniki; // indeksowane od 1, indeks jest zarazem pojemnoscia // bezposrednie wskazniki do przekladnikow dostrajanych w GetPF, inicjowane przy tworzeniu std::shared_ptr PrzekRapid; std::shared_ptr PrzekPoslizg; From 72f932e5d9b3d101c6b8a5044ef855910a9cb65f Mon Sep 17 00:00:00 2001 From: Ryba04 <71170857+Ryba04@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:34:22 +0200 Subject: [PATCH 04/38] Add independent dynamic brake support (eg. Vectron) to UART controllers --- utilities/Globals.h | 3 ++- utilities/uart.cpp | 13 +++++++++++++ utilities/uart.h | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/utilities/Globals.h b/utilities/Globals.h index 729b7cbc..fc6b5a7f 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -224,7 +224,8 @@ struct global_settings { {"train", &uart_conf.trainenable}, {"local", &uart_conf.localenable}, {"radiovolume", &uart_conf.radiovolumeenable}, - {"radiochannel", &uart_conf.radiochannelenable} + {"radiochannel", &uart_conf.radiochannelenable}, + {"dynamicbrake", &uart_conf.dynamicenable}, }; #endif #ifdef WITH_ZMQ diff --git a/utilities/uart.cpp b/utilities/uart.cpp index caa644aa..cac7a198 100644 --- a/utilities/uart.cpp +++ b/utilities/uart.cpp @@ -506,6 +506,19 @@ void uart_input::poll() 0 ); } + if (true == conf.dynamicenable) + { + // dynamic brake 8 bit + double const position{(float)(buffer[13] - conf.dynamicbrakemin) / (conf.dynamicbrakemax - conf.dynamicbrakemin)}; + + relay.post( + user_command::dynamicbrakecontrollerset, + position, + 0, + GLFW_PRESS, + 0 + ); + } old_packet = buffer; } diff --git a/utilities/uart.h b/utilities/uart.h index a418dc22..1a0f344b 100644 --- a/utilities/uart.h +++ b/utilities/uart.h @@ -53,6 +53,8 @@ public: float lvmax = 150.0f; float lvuart = 65535.0f; float tachoscale = 1.0f; + float dynamicbrakemin = 0.0f; + float dynamicbrakemax = 255.0f; bool mainenable = true; bool scndenable = true; @@ -60,6 +62,7 @@ public: bool localenable = true; bool radiovolumeenable = false; bool radiochannelenable = false; + bool dynamicenable = false; bool debug = false; From 766a957527b1ac5e4ec7279aa97a8428598ddc9d Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 1 Sep 2026 20:41:06 +0200 Subject: [PATCH 05/38] Fix relfections on default shader and tonemapping --- shaders/mat_default.frag | 4 ++-- shaders/mat_reflmap.frag | 2 +- shaders/tonemapping.glsl | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shaders/mat_default.frag b/shaders/mat_default.frag index 826dedee..42f1adaa 100644 --- a/shaders/mat_default.frag +++ b/shaders/mat_default.frag @@ -38,10 +38,10 @@ void main() vec3 fragcolor = ambient; vec3 fragnormal = normalize(f_normal); float reflectivity = param[1].z; - float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + //float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; glossiness = abs(param[1].w); - fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0, shadow_tone); vec4 color = vec4(apply_fog(fragcolor), tex_color.a * alpha_mult); /* float distance = dot(f_pos.xyz, f_pos.xyz); diff --git a/shaders/mat_reflmap.frag b/shaders/mat_reflmap.frag index 3b02e092..5695ee3f 100644 --- a/shaders/mat_reflmap.frag +++ b/shaders/mat_reflmap.frag @@ -44,7 +44,7 @@ void main() float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; glossiness = abs(param[1].w); - fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0, shadow_tone); if(alphatestfail) fragcolor.r = 1.0; diff --git a/shaders/tonemapping.glsl b/shaders/tonemapping.glsl index 9dc74a72..863fe0fe 100644 --- a/shaders/tonemapping.glsl +++ b/shaders/tonemapping.glsl @@ -44,7 +44,7 @@ vec3 filmic(vec3 x) // AgX tonemapping based on nxrighthere / Missing Deadlines implementation. // 0: Default, 1: Golden, 2: Punchy #ifndef AGX_LOOK -#define AGX_LOOK 2 +#define AGX_LOOK 0 #endif vec3 AgxDefaultContrastApprox(vec3 x) @@ -109,8 +109,8 @@ vec3 AgxLook(vec3 val) vec3 offset = vec3(0.0); vec3 slope = vec3(1.0); - vec3 power = vec3(1.0); - float sat = 1.0; + vec3 power = vec3(1.6); + float sat = 0.9; #if AGX_LOOK == 1 // Golden From 4f47233a89e3ab3693dc2be4db604430f135bb4a Mon Sep 17 00:00:00 2001 From: Piotr Warszawski Date: Fri, 4 Sep 2026 21:01:33 +0200 Subject: [PATCH 06/38] whois: fall back to the consist owner in the +0/+7 mode too The +8/+16/+24/+32/+40 (and the new +48) modes all pick the reporting vehicle the same way: the activator's own Mechanik if it's the primary driver, otherwise the consist's controlling vehicle (ctOwner) - so the event still resolves correctly if a wagon happens to trigger it ahead of its locomotive. +0 (train name / stations left / stop flag) predates that pattern and never got it: it required m_activator->Mechanik directly and silently wrote nothing otherwise. A whois hook placed where the loco doesn't necessarily lead (e.g. a train that reverses direction, or one being propelled) can have a wagon trigger the event first and the memcell stays empty, with no error - hard to diagnose from scenery side. Same owner-selection, same guard semantics (still nothing written if neither the activator nor its consist has a controller), only the source of the vehicle it reads from is widened. --- world/Event.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/world/Event.cpp b/world/Event.cpp index eb679e72..09afec61 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1129,20 +1129,28 @@ whois_event::run_() { + "[engine power: " + to_string( m_activator->MoverParameters->Power, 2 ) + "]" ); } // +0 - else if( m_activator->Mechanik ) { - if( m_activator->Mechanik->primary() ) { // tylko jeśli ktoś tam siedzi - nie powinno dotyczyć pasażera! + else { + // same owner selection as the +8/+16/+24/+32/+40/+48 branches above: fall back to + // the consist's controlling vehicle if the activator itself isn't one (e.g. a wagon + // triggered the event ahead of its locomotive) - +0 used to require the activator's + // own Mechanik and silently produce nothing for a wagon-triggered whois + auto const *owner { ( + m_activator->Mechanik != nullptr && m_activator->Mechanik->primary() ? + m_activator->Mechanik : + m_activator->ctOwner ) }; + if( owner != nullptr ) { targetcell->UpdateValues( - m_activator->Mechanik->TrainName(), - m_activator->Mechanik->StationCount() - m_activator->Mechanik->StationIndex(), // ile przystanków do końca - m_activator->Mechanik->IsStop() ? + owner->TrainName(), + owner->StationCount() - owner->StationIndex(), // ile przystanków do końca + owner->IsStop() ? 1 : 0, // 1, gdy ma tu zatrzymanie m_input.flags & ( flags::text | flags::value1 | flags::value2 ) ); WriteLog( "Type: WhoIs (" + std::to_string( m_input.flags ) + ") - " - + "[train: " + m_activator->Mechanik->TrainName() + "], " - + "[stations left: " + std::to_string( m_activator->Mechanik->StationCount() - m_activator->Mechanik->StationIndex() ) + "], " - + "[stop at next station: " + ( m_activator->Mechanik->IsStop() ? "yes" : "no") + "]" ); + + "[train: " + owner->TrainName() + "], " + + "[stations left: " + std::to_string( owner->StationCount() - owner->StationIndex() ) + "], " + + "[stop at next station: " + ( owner->IsStop() ? "yes" : "no") + "]" ); } } } From 7ead4f11b4d87384a5894947c0efe440bed80642 Mon Sep 17 00:00:00 2001 From: Piotr Warszawski Date: Fri, 4 Sep 2026 22:09:23 +0200 Subject: [PATCH 07/38] whois: guard against a null activator whois_event::run_() dereferenced m_activator unconditionally in every branch (this predates this PR). getvalues_event::run_() and putvalues_event::run_() already guard the same way elsewhere in this file; whois_event was missing it. Single early return covers every branch, old and new. --- world/Event.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/world/Event.cpp b/world/Event.cpp index 09afec61..73f10592 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1014,6 +1014,8 @@ whois_event::deserialize_( cParser &Input, scene::scratch_data &Scratchpad ) { void whois_event::run_() { + if( m_activator == nullptr ) { return; } + for( auto &target : m_targets ) { auto *targetcell { static_cast( std::get( target ) ) }; if( targetcell == nullptr ) { continue; } From 1ce576c0c602ebe0bf8629279fd75177fa4f8e80 Mon Sep 17 00:00:00 2001 From: Piotr Warszawski Date: Fri, 4 Sep 2026 22:22:25 +0200 Subject: [PATCH 08/38] whois: log the no-activator case instead of failing silently A whois with no triggering vehicle previously just wrote nothing to the target cell with no trace in the log, which is a real pain to diagnose from the scenery side (this is exactly the kind of thing that cost real debugging time on a station script recently). Co-Authored-By: Claude Sonnet 5 --- world/Event.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/world/Event.cpp b/world/Event.cpp index 73f10592..8e89740e 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1014,7 +1014,12 @@ whois_event::deserialize_( cParser &Input, scene::scratch_data &Scratchpad ) { void whois_event::run_() { - if( m_activator == nullptr ) { return; } + if( m_activator == nullptr ) { + // no triggering vehicle to report on - most likely whois was launched from a + // scenery event chain rather than a track/isolation crossing + WriteLog( "Type: WhoIs (" + std::to_string( m_input.flags ) + ") - no activator, event ignored" ); + return; + } for( auto &target : m_targets ) { auto *targetcell { static_cast( std::get( target ) ) }; From 2dcae05c77bfd4b2b427b9f188bf792da3df7cec Mon Sep 17 00:00:00 2001 From: Piotr Warszawski Date: Sat, 5 Sep 2026 19:10:58 +0200 Subject: [PATCH 09/38] event names: drop redundant locale-dependent ToLower() at registration make_event() lowercased the event name twice: cParser::getToken() already folds ASCII via toLowerChar() (which intentionally leaves bytes >= 0x80 alone), then ToLower() ran std::tolower() over every byte on top of that. With a non-"C" global locale, that second pass remaps cp1250 uppercase accented letters (e.g. 0xAF -> 0xBF). Every lookup path - TTrack::AssignEvents(), child-event resolution in multi_event, event-launcher name resolution - normalizes only through the parser, so a registered name containing such a letter can no longer be found: the track logs "can't find assigned event" and the binding is silently dropped. Removing the extra ToLower() makes the registration key match every lookup site (ASCII-only folding, non-ASCII bytes compared verbatim), and matches the direction toLowerChar()'s own comment already points at for UTF-8. --- world/Event.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/world/Event.cpp b/world/Event.cpp index eb679e72..34617196 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -2195,7 +2195,13 @@ message_event::export_as_text_( std::ostream &Output ) const { basic_event * make_event( cParser &Input, scene::scratch_data &Scratchpad ) { - auto const name = ToLower( Input.getToken() ); + // getToken() already lowercases ASCII letters via the parser's toLowerChar(), + // which deliberately leaves bytes >= 0x80 untouched. The extra ToLower() here + // ran std::tolower() over every byte, so under a non-"C" global locale it + // remapped cp1250 uppercase accented letters (e.g. 0xAF -> 0xBF) - producing a + // registered name that no lookup site (Track::AssignEvents, child-event and + // launcher resolution) could match, since those normalize with the parser only. + auto const name = Input.getToken(); auto const type = Input.getToken(); basic_event *event { nullptr }; From c3007e32374aad9bf64c043769387618769656dc Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sun, 6 Sep 2026 15:01:52 +0200 Subject: [PATCH 10/38] ci: skip Sonar coverage gate --- sonar-project.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sonar-project.properties b/sonar-project.properties index 89a6e780..2e3fc88b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,3 +7,7 @@ sonar.sourceEncoding=UTF-8 # Exclusions sonar.exclusions=betterRenderer/**,ref/**,imgui/**,stb/**,build/**,bw-output/** + +# Excluding all files from coverage making the gate skip that +# instead of failing it. +sonar.coverage.exclusions=**/* From ff3daea78ce99491df0c245ea9fcff944bf3c9a9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 14 Sep 2026 17:34:59 +0200 Subject: [PATCH 11/38] seed the random engines from the system pool, not std::random_device --- application/application.cpp | 17 ++++++++++++++--- application/editoruipanels.cpp | 3 +-- utilities/utilities.cpp | 29 +++++++++++++++++++++++++---- utilities/utilities.h | 11 +++++++++++ utilities/uuid.hpp | 3 ++- 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index e5d0b6ba..49d493c2 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -321,7 +321,7 @@ int eu07_application::init(int Argc, char *Argv[]) } if (!Global.random_seed) - Global.random_seed = std::random_device{}(); + Global.random_seed = clock_seed(); Global.random_engine.seed(Global.random_seed); // configure the OS console according to Globals.ShowSystemConsole. @@ -1140,10 +1140,21 @@ int eu07_application::init_settings(int Argc, char *Argv[]) Global.local_start_vehicle = ToLower(Argv[++i]); } } + else if (token == "-seed") + { + // a number is the seed; anything else is a word that stands for one. said in the + // log as it was given, because that is what gets passed on to somebody else + if (i + 1 < Argc) + { + Global.random_seed = seed_of(Argv[++i]); + WriteLog("seed given on the command line: " + std::string(Argv[i]) + " -> " + std::to_string(Global.random_seed)); + } + } else { std::cout << "usage: " << std::string(Argv[0]) << " [-s sceneryfilepath]" - << " [-v vehiclename]" << std::endl; + << " [-v vehiclename]" + << " [-seed number|word]" << std::endl; return -1; } } @@ -1421,7 +1432,7 @@ int eu07_application::init_data() int eu07_application::init_modes() { - Global.local_random_engine.seed(std::random_device{}()); + Global.local_random_engine.seed(clock_seed()); if ((!Global.network_servers.empty() || Global.network_client) && Global.SceneryFile.empty()) { diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index f15f791e..cbfe0ae7 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -403,10 +403,9 @@ std::string *brush_object_list::GetRandomObject() if (Objects.empty()) return ∅ - static std::mt19937 rng{std::random_device{}()}; std::uniform_int_distribution dist(0, Objects.size() - 1); - return &Objects[dist(rng)]; + return &Objects[dist(Global.local_random_engine)]; } void brush_object_list::render() diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 156d16cc..1f5f0682 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -15,6 +15,8 @@ Copyright (C) 2007-2014 Maciej Cierniak // //#include //#include +#include +#include #include //#ifndef WIN32 //#include @@ -30,7 +32,6 @@ Copyright (C) 2007-2014 Maciej Cierniak #include "utilities/U8.h" -//#include "utilities/Logs.h" // TODO: This shouldn't be in Globals? bool DebugModeFlag = false; @@ -137,15 +138,35 @@ int Random(int min, int max) return dist(Global.random_engine); } +std::uint32_t clock_seed() +{ + static std::atomic nth{0}; + return static_cast(std::time({})) + nth.fetch_add(1, std::memory_order_relaxed); +} + +std::uint32_t seed_of(std::string const &Text) +{ + std::uint32_t number{}; + if (std::from_chars(Text.data(), Text.data() + Text.size(), number).ec == std::errc{}) + { + return number; + } + std::uint32_t hash{}; + for (auto const character : Text) + { + hash = hash * 31u + static_cast(static_cast(character)); + } + return hash; +} + std::string generate_uuid_v4() { - std::random_device rd; - std::mt19937 gen(rd()); + static thread_local std::mt19937 generator{clock_seed()}; std::uniform_int_distribution dist(0, 255); std::array bytes; for (auto &b : bytes) - b = static_cast(dist(gen)); + b = static_cast(dist(generator)); // UUID v4 (RFC 4122) bytes[6] = bytes[6] & 0x0F | 0x40; diff --git a/utilities/utilities.h b/utilities/utilities.h index bc38b125..d013fade 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -62,6 +62,17 @@ inline long Round(double const f) double Random(double a, double b); int Random(int min, int max); + +// Seed for the random engines: the clock, plus a counter so two engines seeded in +// the same second do not come out the same. +std::uint32_t clock_seed(); + +// The seed a word stands for. A number is taken as it is written; anything else is +// hashed the way Java's String.hashCode does it, so the same word gives the same +// world on every platform and every build - which is the whole point of telling +// somebody a seed. +std::uint32_t seed_of(std::string const &Text); + std::string generate_uuid_v4(); double LocalRandom(double a, double b); diff --git a/utilities/uuid.hpp b/utilities/uuid.hpp index d94fd0cc..ff32f5d2 100644 --- a/utilities/uuid.hpp +++ b/utilities/uuid.hpp @@ -6,6 +6,7 @@ #include #include #include +#include "utilities/utilities.h" class UID { public: @@ -13,7 +14,7 @@ public: static UID random() { - static thread_local std::mt19937_64 gen(std::random_device{}()); + static thread_local std::mt19937_64 gen{clock_seed()}; UID u; uint64_t a = gen(); uint64_t b = gen(); From 1c2f397fc544480a99a2c7494cc7f3a2f57bf577 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 14 Sep 2026 18:36:07 +0200 Subject: [PATCH 12/38] seed from ini can be word --- utilities/Globals.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 0ab064b4..f45b6b3e 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -726,7 +726,9 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string& if (token == "randomseed") { - ParseOne(Parser, Global.random_seed, 1); + std::string text; + ParseOne(Parser, text); + Global.random_seed = seed_of(text); return true; } From 2945f3f9b2e00a87f238d512c888a87a862eab26 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 14 Sep 2026 19:11:07 +0200 Subject: [PATCH 13/38] use random_device for local generator --- application/application.cpp | 5 +++-- utilities/utilities.cpp | 9 +-------- utilities/utilities.h | 4 ---- utilities/uuid.hpp | 2 +- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index 49d493c2..718380c7 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -320,9 +320,11 @@ int eu07_application::init(int Argc, char *Argv[]) return result; } + // the run's seed, settled once and kept in Globals: the engines come out of it if (!Global.random_seed) - Global.random_seed = clock_seed(); + Global.random_seed = std::random_device{}(); Global.random_engine.seed(Global.random_seed); + Global.local_random_engine.seed(Global.random_seed); // configure the OS console according to Globals.ShowSystemConsole. // must run AFTER init_settings (so the ini-loaded value is honoured) and @@ -1432,7 +1434,6 @@ int eu07_application::init_data() int eu07_application::init_modes() { - Global.local_random_engine.seed(clock_seed()); if ((!Global.network_servers.empty() || Global.network_client) && Global.SceneryFile.empty()) { diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 1f5f0682..92aa28ed 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -16,7 +16,6 @@ Copyright (C) 2007-2014 Maciej Cierniak //#include //#include #include -#include #include //#ifndef WIN32 //#include @@ -138,12 +137,6 @@ int Random(int min, int max) return dist(Global.random_engine); } -std::uint32_t clock_seed() -{ - static std::atomic nth{0}; - return static_cast(std::time({})) + nth.fetch_add(1, std::memory_order_relaxed); -} - std::uint32_t seed_of(std::string const &Text) { std::uint32_t number{}; @@ -161,7 +154,7 @@ std::uint32_t seed_of(std::string const &Text) std::string generate_uuid_v4() { - static thread_local std::mt19937 generator{clock_seed()}; + static thread_local std::mt19937 generator{std::random_device{}()}; std::uniform_int_distribution dist(0, 255); std::array bytes; diff --git a/utilities/utilities.h b/utilities/utilities.h index d013fade..f90bd896 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -63,10 +63,6 @@ inline long Round(double const f) double Random(double a, double b); int Random(int min, int max); -// Seed for the random engines: the clock, plus a counter so two engines seeded in -// the same second do not come out the same. -std::uint32_t clock_seed(); - // The seed a word stands for. A number is taken as it is written; anything else is // hashed the way Java's String.hashCode does it, so the same word gives the same // world on every platform and every build - which is the whole point of telling diff --git a/utilities/uuid.hpp b/utilities/uuid.hpp index ff32f5d2..3326ecf1 100644 --- a/utilities/uuid.hpp +++ b/utilities/uuid.hpp @@ -14,7 +14,7 @@ public: static UID random() { - static thread_local std::mt19937_64 gen{clock_seed()}; + static thread_local std::mt19937_64 gen{std::random_device{}()}; UID u; uint64_t a = gen(); uint64_t b = gen(); From 788135cc2df158224bf5830439e4dd95c80c9812 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 14 Sep 2026 19:26:24 +0200 Subject: [PATCH 14/38] Improve random engine entropy and clean up random engine initialization --- application/application.cpp | 4 ++-- utilities/utilities.cpp | 19 ++++++++++++++----- utilities/utilities.h | 7 ++++--- utilities/uuid.hpp | 6 +++--- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index 49d493c2..1542e3d4 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -321,7 +321,7 @@ int eu07_application::init(int Argc, char *Argv[]) } if (!Global.random_seed) - Global.random_seed = clock_seed(); + Global.random_seed = true_random_seed(); Global.random_engine.seed(Global.random_seed); // configure the OS console according to Globals.ShowSystemConsole. @@ -1432,7 +1432,7 @@ int eu07_application::init_data() int eu07_application::init_modes() { - Global.local_random_engine.seed(clock_seed()); + Global.local_random_engine.seed(true_random_seed()); if ((!Global.network_servers.empty() || Global.network_client) && Global.SceneryFile.empty()) { diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 1f5f0682..f5377619 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -138,10 +138,20 @@ int Random(int min, int max) return dist(Global.random_engine); } -std::uint32_t clock_seed() +std::uint64_t true_random_seed() { - static std::atomic nth{0}; - return static_cast(std::time({})) + nth.fetch_add(1, std::memory_order_relaxed); + try + { + std::random_device rd; + if (rd.entropy() > 0.0) + { + std::uint32_t high = rd(); + std::uint32_t low = rd(); + return (high << 32) | low; + } + } + catch (...) {} + return static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); } std::uint32_t seed_of(std::string const &Text) @@ -161,12 +171,11 @@ std::uint32_t seed_of(std::string const &Text) std::string generate_uuid_v4() { - static thread_local std::mt19937 generator{clock_seed()}; std::uniform_int_distribution dist(0, 255); std::array bytes; for (auto &b : bytes) - b = static_cast(dist(generator)); + b = static_cast(dist(Global.random_engine)); // UUID v4 (RFC 4122) bytes[6] = bytes[6] & 0x0F | 0x40; diff --git a/utilities/utilities.h b/utilities/utilities.h index d013fade..70cf1c07 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -63,9 +63,10 @@ inline long Round(double const f) double Random(double a, double b); int Random(int min, int max); -// Seed for the random engines: the clock, plus a counter so two engines seeded in -// the same second do not come out the same. -std::uint32_t clock_seed(); +/// @author lxvia +/// @brief Returns a random seed from the system's entropy source, if available. Otherwise, falls back to a clock-based seed. +/// @return A random seed as 64 bit unsigned integer +std::uint64_t true_random_seed(); // The seed a word stands for. A number is taken as it is written; anything else is // hashed the way Java's String.hashCode does it, so the same word gives the same diff --git a/utilities/uuid.hpp b/utilities/uuid.hpp index ff32f5d2..5eeb89f4 100644 --- a/utilities/uuid.hpp +++ b/utilities/uuid.hpp @@ -7,6 +7,7 @@ #include #include #include "utilities/utilities.h" +#include "utilities/Globals.h" class UID { public: @@ -14,10 +15,9 @@ public: static UID random() { - static thread_local std::mt19937_64 gen{clock_seed()}; UID u; - uint64_t a = gen(); - uint64_t b = gen(); + uint64_t a = Global.random_engine(); + uint64_t b = Global.random_engine(); for (int i = 0; i < 8; ++i) u.bytes[i] = uint8_t(a >> (i * 8) & 0xFF); for (int i = 0; i < 8; ++i) u.bytes[8 + i] = uint8_t(b >> (i * 8) & 0xFF); From 8df81eb46d9cc4d951680a3d1627157ac38aaed9 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 14 Sep 2026 21:11:08 +0200 Subject: [PATCH 15/38] Improve skybox --- environment/skydome.cpp | 5 ++++- shaders/color.frag | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/environment/skydome.cpp b/environment/skydome.cpp index f5b936f5..830eb145 100644 --- a/environment/skydome.cpp +++ b/environment/skydome.cpp @@ -218,7 +218,10 @@ void CSkyDome::RebuildColors() { vertex = glm::normalize( m_vertices[ i ] ); // angle between sun and vertex - const float gamma = std::acos( glm::dot( vertex, m_sundirection ) ); + // NOTE: the dot product can exceed the [ -1, 1 ] range by a rounding error, + // which would turn the vertex - and, through the averages, the fog and ambient + // light - into a NaN, so it gets clamped before feeding it to acos() + const float gamma = std::acos( std::clamp( glm::dot( vertex, m_sundirection ), -1.0f, 1.0f ) ); // warning : major hack!!! .. i had to do something with values under horizon //vertex.y = Clamp( vertex.y, 0.05f, 1.0f ); diff --git a/shaders/color.frag b/shaders/color.frag index 45c0829e..2f7fabc5 100644 --- a/shaders/color.frag +++ b/shaders/color.frag @@ -10,9 +10,26 @@ layout(location = 0) out vec4 out_color; layout(location = 1) out vec4 out_motion; #endif +// the skydome vertex colours carry Perez luminance scaled by a fixed exposure +// (see CSkyDome::SetExposure), so the circumsolar region and the horizon band +// routinely land well above 1.0. hard-clipping them, as this shader used to do, +// flattened everything within ~20 degrees of the sun - and most of the lower sky - +// into one solid white patch. compress the top end instead: below the knee the +// response is unchanged, above it it rolls off towards SKY_SHOULDER_WHITE, so the +// brightness gradient around the sun survives and the patch keeps a soft edge. +const float SKY_SHOULDER_KNEE = 0.25; +const float SKY_SHOULDER_WHITE = 0.95; + +vec3 sky_shoulder(vec3 color) +{ + const float range = SKY_SHOULDER_WHITE - SKY_SHOULDER_KNEE; + vec3 excess = max(color - vec3(SKY_SHOULDER_KNEE), vec3(0.0)); + return min(color, vec3(SKY_SHOULDER_KNEE)) + range * (vec3(1.0) - exp(-excess / range)); +} + void main() { - vec3 col = clamp(pow(f_color.rgb, vec3(2.2)),0, 1); + vec3 col = sky_shoulder(pow(max(f_color.rgb, vec3(0.0)), vec3(2.2))); #if POSTFX_ENABLED out_color = vec4(apply_fog(col), 1.0f); #else From 3fa3853018120bd8c439417e792b20e99d37e2e4 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 14 Sep 2026 22:51:33 +0200 Subject: [PATCH 16/38] fix for sonar --- application/application.cpp | 11 +++++++---- utilities/utilities.cpp | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index f5bec7cf..a6b88ae8 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/dictionary.h" #include "version_info.h" #include +#include #include "utilities/translation.h" #if WITH_DISCORD_RPC @@ -320,9 +321,11 @@ int eu07_application::init(int Argc, char *Argv[]) return result; } - // the run's seed, settled once and kept in Globals: the engines come out of it + // the run's seed, settled once and kept in Globals: the engines come out of it. + // narrowed on purpose - mt19937 and seed_of() both work on 32 bits, and that is + // the seed somebody can write down and pass on if (!Global.random_seed) - Global.random_seed = true_random_seed(); + Global.random_seed = static_cast(true_random_seed()); Global.random_engine.seed(Global.random_seed); Global.local_random_engine.seed(Global.random_seed); @@ -1149,7 +1152,7 @@ int eu07_application::init_settings(int Argc, char *Argv[]) if (i + 1 < Argc) { Global.random_seed = seed_of(Argv[++i]); - WriteLog("seed given on the command line: " + std::string(Argv[i]) + " -> " + std::to_string(Global.random_seed)); + WriteLog(std::format("seed given on the command line: {} -> {}", Argv[i], Global.random_seed)); } } else @@ -1434,7 +1437,7 @@ int eu07_application::init_data() int eu07_application::init_modes() { - Global.local_random_engine.seed(true_random_seed()); + Global.local_random_engine.seed(static_cast(true_random_seed())); if ((!Global.network_servers.empty() || Global.network_client) && Global.SceneryFile.empty()) { diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 5ff79092..c1d0b73d 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -29,6 +29,7 @@ Copyright (C) 2007-2014 Maciej Cierniak #include "utilities/Globals.h" #include "utilities/parser.h" #include "utilities/U8.h" +#include "utilities/Logs.h" @@ -142,21 +143,28 @@ std::uint64_t true_random_seed() try { std::random_device rd; - if (rd.entropy() > 0.0) + if (rd.entropy() > 0.0) { - std::uint32_t high = rd(); - std::uint32_t low = rd(); + // NOTE: both halves have to be 64 bit wide before the shift - shifting a std::uint32_t + // by 32 is undefined behaviour, and drops the high half of the seed + std::uint64_t const high{rd()}; + std::uint64_t const low{rd()}; return (high << 32) | low; } } - catch (...) {} + catch (std::exception const &Error) + { + // std::random_device throws when the machine has no usable hardware entropy source; + // the clock below stands in for it, which is worth saying out loud since it is a + // weaker seed than the caller asked for + WriteLog(std::format("random: no hardware entropy source ({}), seeding from the clock instead", Error.what())); + } return static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); } std::uint32_t seed_of(std::string const &Text) { - std::uint32_t number{}; - if (std::from_chars(Text.data(), Text.data() + Text.size(), number).ec == std::errc{}) + if (std::uint32_t number{}; std::from_chars(Text.data(), Text.data() + Text.size(), number).ec == std::errc{}) { return number; } From 468ea1d747657fdb9c75969f3127f3136c21288d Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 19 Sep 2026 16:56:39 +0200 Subject: [PATCH 17/38] Add shader diffuse multiply --- shaders/light_common.glsl | 24 +++++++++++++++------ shaders/mat_colored.frag | 5 +++-- shaders/mat_sunlessnormalmap.frag | 11 +++++----- shaders/mat_sunlessnormalmap_specgloss.frag | 11 +++++----- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 56be0ae1..3bfd1066 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -256,17 +256,29 @@ vec3 EnvBRDFApprox(vec3 F0, float roughness, float NoV) return F0 * AB.x + AB.y; } +// Diffuse colour of the submodel (t3d/e3d) or shape, used as the albedo multiplier: +// untextured materials take it as their colour, textured ones multiply the base texture by it +// (same as the legacy renderer's GL_MODULATE). param[0].rgb holds the authored sRGB colour, +// param[1].x its intensity (HSV value unless a material overrides it). +vec3 material_diffuse() +{ + vec3 diffusecolor = pow(max(param[0].rgb, vec3(0.0)), vec3(2.2)); + float value = max(param[0].r, max(param[0].g, param[0].b)); + return (value > 0.0 ? diffusecolor * (param[1].x / value) : vec3(0.0)); +} + // [0] - diffuse, [1] - specular // do magic here vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone) { - vec3 basecolor = param[0].rgb; - // Scale ambient before it gets tinted by basecolor / texture. + // Diffuse colour is applied once, to the albedo; the light terms below are not scaled by it again + texturecolor *= material_diffuse(); + // Scale ambient before it gets tinted by the texture. // Sun, headlights and emission are added afterwards so they are NOT // attenuated by AMBIENT_SCALE - this only dims the indirect term. - fragcolor *= basecolor * AMBIENT_SCALE; + fragcolor *= AMBIENT_SCALE; - vec3 emissioncolor = basecolor * emission; + vec3 emissioncolor = vec3(emission); vec3 view_dir = normalize(-f_pos.xyz); float NdotV = max(dot(fragnormal, view_dir), 0.0); @@ -302,7 +314,7 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl // panels, vehicle bodies and terrain reads as a clear edge rather // than a soft Lambertian ramp. Tunable via SUN_NDOTL_SHARPNESS. float sun_NdotL = pow(sunlight.x, SUN_NDOTL_SHARPNESS); - float diffuseamount = sun_NdotL * param[1].x * lights[0].intensity; + float diffuseamount = sun_NdotL * lights[0].intensity; float shadow1 = 0.0; if (shadowtone < 1.0) @@ -317,7 +329,7 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl { light_s light = lights[i]; vec2 part = calc_headlights(light, fragnormal); - fragcolor += light.color * (part.x * param[1].x + part.y * param[1].y) * light.intensity; + fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity; } float specularamount = sunlight.y * param[1].y * specularity * lights[0].intensity diff --git a/shaders/mat_colored.frag b/shaders/mat_colored.frag index 80d1d546..620ec43c 100644 --- a/shaders/mat_colored.frag +++ b/shaders/mat_colored.frag @@ -24,7 +24,8 @@ layout(location = 1) out vec4 out_motion; void main() { - vec4 tex_color = vec4(pow(param[0].rgb, vec3(2.2)), param[0].a); + // no texture: the diffuse colour alone makes the albedo, apply_lights() applies it to the white base + vec4 tex_color = vec4(1.0, 1.0, 1.0, param[0].a); // if (tex_color.a < opacity) // discard; @@ -32,7 +33,7 @@ void main() vec3 fragcolor = ambient; vec3 fragnormal = normalize(f_normal); float reflectivity = param[1].z; - float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + float specularity = dot(pow(param[0].rgb, vec3(2.2)), vec3(0.5)); glossiness = abs(param[1].w); fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); diff --git a/shaders/mat_sunlessnormalmap.frag b/shaders/mat_sunlessnormalmap.frag index c0f20f5f..0b3fc7f6 100644 --- a/shaders/mat_sunlessnormalmap.frag +++ b/shaders/mat_sunlessnormalmap.frag @@ -32,14 +32,15 @@ uniform sampler2D normalmap; vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone) { - vec3 basecolor = param[0].rgb; + // diffuse colour applied once, to the albedo (see material_diffuse() in light_common.glsl) + texturecolor *= material_diffuse(); // Cab interior: dim ambient less than the exterior path because // ambient is the dominant indoor illumination. 0.80 trims the // brightest faces without making the cab feel under-lit. - fragcolor *= basecolor * 0.80; + fragcolor *= 0.80; - vec3 emissioncolor = basecolor * emission; + vec3 emissioncolor = vec3(emission); vec3 envcolor = envmap_color(fragnormal); // yuv path @@ -60,7 +61,7 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl // Sharpen N.L for stronger contrast between lit and shaded cab // surfaces (uses SUN_NDOTL_SHARPNESS from light_common.glsl). float sun_NdotL = pow(sunlight.x, SUN_NDOTL_SHARPNESS); - float diffuseamount = (sun_NdotL * param[1].x) * lights[0].intensity; + float diffuseamount = sun_NdotL * lights[0].intensity; fragcolor += envcolor * reflectivity; float specularamount = (sunlight.y * param[1].y * specularity) * lights[0].intensity; glossiness = abs(param[1].w); @@ -79,7 +80,7 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl // else if (light.type == LIGHT_HEADLIGHTS) part = calc_headlights(light, fragnormal); - fragcolor += light.color * (part.x * param[1].x + part.y * param[1].y) * light.intensity; + fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity; } if (shadowtone < 1.0) diff --git a/shaders/mat_sunlessnormalmap_specgloss.frag b/shaders/mat_sunlessnormalmap_specgloss.frag index 48e71435..21cc9f6e 100644 --- a/shaders/mat_sunlessnormalmap_specgloss.frag +++ b/shaders/mat_sunlessnormalmap_specgloss.frag @@ -36,12 +36,13 @@ uniform sampler2D specgloss; vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone) { - vec3 basecolor = param[0].rgb; + // diffuse colour applied once, to the albedo (see material_diffuse() in light_common.glsl) + texturecolor *= material_diffuse(); // Cab interior: dim ambient less than the exterior path - ambient // is the dominant indoor illumination, but it was still too bright. - fragcolor *= basecolor * 0.80; + fragcolor *= 0.80; - vec3 emissioncolor = basecolor * emission; + vec3 emissioncolor = vec3(emission); vec3 view_dir = normalize(-f_pos.xyz); float NdotV = max(dot(fragnormal, view_dir), 0.0); @@ -71,7 +72,7 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl // Sharpen N.L for stronger contrast between lit and shaded cab // surfaces (uses SUN_NDOTL_SHARPNESS from light_common.glsl). float sun_NdotL = pow(sunlight.x, SUN_NDOTL_SHARPNESS); - float diffuseamount = sun_NdotL * param[1].x * lights[0].intensity; + float diffuseamount = sun_NdotL * lights[0].intensity; float specularamount = sunlight.y * param[1].y * specularity * lights[0].intensity; @@ -79,7 +80,7 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl { light_s light = lights[i]; vec2 part = calc_headlights(light, fragnormal); - fragcolor += light.color * (part.x * param[1].x + part.y * param[1].y) * light.intensity; + fragcolor += light.color * (part.x + part.y * param[1].y) * light.intensity; } if (shadowtone < 1.0) From 306c7eaf26092bc4ebe27e6a55d13c36741d0122 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 19 Sep 2026 17:09:58 +0200 Subject: [PATCH 18/38] Adjust param values --- shaders/mat_colored.frag | 6 +++--- shaders/mat_default.frag | 2 +- shaders/mat_reflmap.frag | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shaders/mat_colored.frag b/shaders/mat_colored.frag index 620ec43c..46de50f0 100644 --- a/shaders/mat_colored.frag +++ b/shaders/mat_colored.frag @@ -9,9 +9,9 @@ in vec4 f_clip_future_pos; #param (color, 0, 0, 4, diffuse) #param (diffuse, 1, 0, 1, diffuse) -#param (specular, 0.2, 0.2, 0.2, specular) -#param (reflection, 0.5, 0.5, 0.5, zero) -#param (glossiness, 0.5, 0.5, 0.5, glossiness) +#param (specular, 1, 1, 1, specular) +#param (reflection, 1, 2, 1, zero) +#param (glossiness, 1, 3, 1, glossiness) layout(location = 0) out vec4 out_color; #if MOTIONBLUR_ENABLED diff --git a/shaders/mat_default.frag b/shaders/mat_default.frag index 42f1adaa..bc48d939 100644 --- a/shaders/mat_default.frag +++ b/shaders/mat_default.frag @@ -41,7 +41,7 @@ void main() //float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; glossiness = abs(param[1].w); - fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0, shadow_tone); + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0.0, shadow_tone); vec4 color = vec4(apply_fog(fragcolor), tex_color.a * alpha_mult); /* float distance = dot(f_pos.xyz, f_pos.xyz); diff --git a/shaders/mat_reflmap.frag b/shaders/mat_reflmap.frag index 5695ee3f..bd4c4ae7 100644 --- a/shaders/mat_reflmap.frag +++ b/shaders/mat_reflmap.frag @@ -44,7 +44,7 @@ void main() float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; glossiness = abs(param[1].w); - fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0, shadow_tone); + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0.0, shadow_tone); if(alphatestfail) fragcolor.r = 1.0; From cb6b97b9041c5600ecbd1e39221638681805c19b Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 19 Sep 2026 21:15:49 +0200 Subject: [PATCH 19/38] light_common: weight textureGather shadow PCF by sub-texel position --- shaders/light_common.glsl | 41 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 3bfd1066..f212bac7 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -64,29 +64,28 @@ float calc_shadow() radius = 0.5; #if defined(GL_ARB_gpu_shader5) || defined(GL_EXT_gpu_shader5) || __VERSION__ >= 400 - // Fast path -- replace the original 4x4 grid of individual hardware-PCF - // lookups with 4 textureGather() calls. Each gather returns the 4 raw - // shadow comparisons of a 2x2 texel footprint, so 4 gathers laid out at - // (+-1, +-1) * radius * texel from the sample center cover the same 4x4 - // sample area as the original kernel; summing all 16 comparisons and - // dividing by 16 reproduces the original loop's averaging. The cost on - // the TMUs drops from 16 hardware-PCF samples to 4 gathers (the gather - // path returns 4 values per fetch where the original needed 4 fetches), - // roughly a 4x reduction in shadow-sample work. The only thing dropped - // vs. the hardware-PCF path is the implicit bilinear blending inside - // each 2x2 footprint -- effectively turning a tent-weighted kernel into - // a box-weighted one of the same extent, which is imperceptible in - // motion. calc_shadow() is by far the heaviest piece of the lighting - // shader, so this is a measurable GPU saving on every shaded fragment. + // Fast path: 4 textureGather() calls cover a 4x4 texel block. Raw gathered + // comparisons must be weighted by the sub-texel position (bilinear weights), + // otherwise the result is constant across each shadow map texel and shadow + // edges turn into texel-sized stair steps. With per-axis weights + // (1-f, 1, 1, f) this equals a 3x3 grid of hardware bilinear PCF taps. + vec2 size = vec2(textureSize(shadowmap, 0).xy); + vec2 uv = coords.xy * size - 0.5; + vec2 base = floor(uv); + vec2 f = uv - base; float refz = coords.z + bias; float layer = float(cascade); - vec2 off = radius * texel; - vec4 g0 = textureGather(shadowmap, vec3(coords.xy + vec2(-off.x, -off.y), layer), refz); - vec4 g1 = textureGather(shadowmap, vec3(coords.xy + vec2( off.x, -off.y), layer), refz); - vec4 g2 = textureGather(shadowmap, vec3(coords.xy + vec2(-off.x, off.y), layer), refz); - vec4 g3 = textureGather(shadowmap, vec3(coords.xy + vec2( off.x, off.y), layer), refz); - float shadow = dot(g0 + g1 + g2 + g3, vec4(1.0 / 16.0)); - return shadow; + // sampling at texel corners selects blocks with lower-left texel base-1 and base+1 + vec4 g00 = textureGather(shadowmap, vec3((base + vec2(0.0, 0.0)) / size, layer), refz); + vec4 g10 = textureGather(shadowmap, vec3((base + vec2(2.0, 0.0)) / size, layer), refz); + vec4 g01 = textureGather(shadowmap, vec3((base + vec2(0.0, 2.0)) / size, layer), refz); + vec4 g11 = textureGather(shadowmap, vec3((base + vec2(2.0, 2.0)) / size, layer), refz); + // gather component order: x=(0,1) y=(1,1) z=(1,0) w=(0,0) + float shadow = dot(g00, vec4(1.0 - f.x, 1.0, 1.0 - f.y, (1.0 - f.x) * (1.0 - f.y))) + + dot(g10, vec4(1.0, f.x, f.x * (1.0 - f.y), 1.0 - f.y)) + + dot(g01, vec4((1.0 - f.x) * f.y, f.y, 1.0, 1.0 - f.x)) + + dot(g11, vec4(f.y, f.x * f.y, f.x, 1.0)); + return shadow / 9.0; #else // Fallback for drivers without textureGather on shadow samplers // (notably GLES 3.0 and any 3.3 desktop driver that doesn't expose From 1d9c608396e5b5c2cffb84bee30d111affbc2aad Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 19 Sep 2026 21:56:02 +0200 Subject: [PATCH 20/38] light_common: add normal offset to shadow lookups --- shaders/light_common.glsl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index f212bac7..0801b09d 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -45,7 +45,20 @@ float calc_shadow() if (distance <= cascade_end[cascade]) break; float dist_casc = distance / cascade_end[cascade]; - vec3 coords = f_light_pos[cascade].xyz / f_light_pos[cascade].w; + // Normal offset: sample the shadow map from a point pushed along the surface normal + // by a fraction of the cascade's texel footprint. A constant depth bias can't keep up + // with large texels (far cascades, small shadow maps) on surfaces at a grazing angle + // to the sun, which then show shadow acne stripes. lightview maps view space metres + // to [0,1] shadow map coordinates, so its x/y row lengths give texels per metre. + vec3 shadownormal = normalize(f_normal); + float shadowNdotL = clamp(dot(shadownormal, normalize(-lights[0].dir)), 0.0, 1.0); + mat4 shadowview = lightview[cascade]; + float texelspermetre = min(length(vec3(shadowview[0][0], shadowview[1][0], shadowview[2][0])), + length(vec3(shadowview[0][1], shadowview[1][1], shadowview[2][1]))) * float(textureSize(shadowmap, 0).x); + float texelsize = 1.0 / texelspermetre; + vec3 shadowpos = f_pos.xyz + shadownormal * (texelsize * 1.5 * (1.0 - shadowNdotL)); + vec4 lightpos = shadowview * vec4(shadowpos, 1.0); + vec3 coords = lightpos.xyz / lightpos.w; if (coords.z < 0.0) return 0.0f; From 95f41f069f9f918c517410f9ed865d8178da0cf2 Mon Sep 17 00:00:00 2001 From: pawelw Date: Wed, 23 Sep 2026 01:02:11 +0200 Subject: [PATCH 21/38] Speed table: keep it sorted and keep the signal indices valid (#178) * Fix the speed table index remap in TablePurger() TablePurger() rebuilds the speed table and has to move SemNextIndex and SemNextStopIndex along with it. Two cases came out wrong. A trimmed point was never invalidated: the index was first overwritten with the position the next kept point would take, and the guard that should reset it to -1 compared the loop counter against that new value, so it only matched when nothing had been trimmed before it. The index then pointed at an unrelated record, and if that record happens to sit at zero velocity the placement can no longer be updated either, because the update requires the recorded one to be non-zero. An index pointing at the last entry was left untouched, because the loop stops one short of it and that entry is appended afterwards. The stale value ended up past the end of the trimmed table, which is what the provisory check in TableTraceRoute() catches. The placements are now recorded separately and assigned once the new table is complete, with the always-kept last entry handled too. The trim condition is shared by the counting and the copying pass instead of being spelled out twice. TableCheck() drops entries beyond a switch that changed under the scan without adjusting the placements either, so they are invalidated there as well. The check in TableTraceRoute() stays as a safety net, without the TODO. * Sort the whole speed table in TableSort() TableSort() made a single bubble pass over the table. That only moves an entry one place towards the front, but TableTraceRoute() adds the scanned events of a track before the track itself, so a track carrying k scanned events lands k places too far and needs k passes. With two or more events in one slot - a cab signal resonator next to a signal, a signal and a W4 - the track entry stays behind entries that are farther away, and TableUpdate() walks the table out of distance order until enough later scans have shifted it back. The range before the last slot is now sorted with std::stable_sort, and the semaphor placements are moved along by looking their events up again. --------- Co-authored-by: pwarszawski --- vehicle/Driver.cpp | 114 ++++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 43f2f924..9a6de859 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -540,8 +540,7 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) } else { if( iTableDirection == 0 ) { return; } - // NOTE: provisory fix for BUG: sempahor indices no longer matching table size - // TODO: find and really fix the reason it happens + // NOTE: belt and braces; TablePurger() used to leave these pointing past the end of the table if( SemNextIndex != -1 && SemNextIndex >= sSpeedTable.size() ) { SemNextIndex = -1; @@ -800,6 +799,15 @@ void TController::TableCheck(double fDistance) sSpeedTable.pop_back(); --iLast; } + // a semaphor recorded beyond the switch went away with the discarded entries + if( SemNextIndex != -1 + && SemNextIndex >= sSpeedTable.size() ) { + SemNextIndex = -1; + } + if( SemNextStopIndex != -1 + && SemNextStopIndex >= sSpeedTable.size() ) { + SemNextStopIndex = -1; + } tLast = sSpeedTable[ i ].trTrack; TableTraceRoute( fDistance, pVehicles[ end::rear ] ); TableSort(); @@ -1739,14 +1747,16 @@ void TController::TablePurger() return; } // simplest approach should be good enough for start -- just copy whatever is still relevant, then swap + auto const isexpendable = []( TSpeedPos const &Speedpoint ) { + return + 0 == ( Speedpoint.iFlags & spEnabled ) + || ( ( Speedpoint.iFlags & ( spElapsed | spTrack | spCurve | spSwitch ) ) == ( spElapsed | spTrack | spCurve ) + && Speedpoint.fVelNext < 0.0 ); }; // do a trial run first, to see if we need to bother at all std::size_t trimcount{ 0 }; for( std::size_t idx = 0; idx < sSpeedTable.size() - 1; ++idx ) { - auto const &speedpoint = sSpeedTable[ idx ]; - if( 0 == (speedpoint.iFlags & spEnabled) - || ( (speedpoint.iFlags & (spElapsed | spTrack | spCurve | spSwitch)) == (spElapsed | spTrack | spCurve) - && speedpoint.fVelNext < 0.0 ) ) { - // NOTE: we could break out early here, but running through entire thing gives us exact size needed for new table + // NOTE: we could break out early here, but running through entire thing gives us exact size needed for new table + if( isexpendable( sSpeedTable[ idx ] ) ) { ++trimcount; } } @@ -1755,34 +1765,34 @@ void TController::TablePurger() return; } std::vector trimmedtable; trimmedtable.reserve( sSpeedTable.size() - trimcount ); - // we can only update pointers safely after new table is finalized, so record their indices until then + // the indices are only valid for the old table, so the new placements are recorded separately + // and assigned once the new table is complete + auto semnextindex { std::size_t( -1 ) }; + auto semnextstopindex { std::size_t( -1 ) }; + auto const trackplacement = [&]( std::size_t const Index, bool const Kept ) { + // a trimmed point leaves the index invalid, a kept one lands where the new table ends + if( Index == SemNextIndex ) { + semnextindex = ( Kept ? trimmedtable.size() : std::size_t( -1 ) ); + } + if( Index == SemNextStopIndex ) { + semnextstopindex = ( Kept ? trimmedtable.size() : std::size_t( -1 ) ); + } }; + for( std::size_t idx = 0; idx < sSpeedTable.size() - 1; ++idx ) { - // cache placement of semaphors in the new table, if we encounter them - if( idx == SemNextIndex ) { - SemNextIndex = trimmedtable.size(); - } - if( idx == SemNextStopIndex ) { - SemNextStopIndex = trimmedtable.size(); - } auto const &speedpoint = sSpeedTable[ idx ]; - if( 0 == (speedpoint.iFlags & spEnabled) - || ( (speedpoint.iFlags & (spElapsed | spTrack | spCurve | spSwitch)) == (spElapsed | spTrack | spCurve) - && speedpoint.fVelNext < 0.0 ) ) { - // if the trimmed point happens to be currently active semaphor we need to invalidate their placements - if( idx == SemNextIndex ) { - SemNextIndex = -1; - } - if( idx == SemNextStopIndex ) { - SemNextStopIndex = -1; - } - continue; - } + auto const kept { false == isexpendable( speedpoint ) }; + trackplacement( idx, kept ); + if( false == kept ) { continue; } // we're left with useful speed point record we should copy trimmedtable.emplace_back( speedpoint ); } - // always copy the last entry + // always copy the last entry; an index pointing at it has to follow it as well + trackplacement( sSpeedTable.size() - 1, true ); trimmedtable.emplace_back( sSpeedTable.back() ); + SemNextIndex = semnextindex; + SemNextStopIndex = semnextstopindex; + if( Global.iWriteLogEnabled & 8 ) { WriteLog( "Speed table garbage collection for " + OwnerName() + " cut away " + std::to_string( trimcount ) + ( trimcount == 1 ? " record" : " records" ) ); } @@ -1797,25 +1807,35 @@ void TController::TableSort() { // we skip last slot and no point in checking if there's only one other entry return; } - TSpeedPos sp_temp = TSpeedPos(); // uzywany do przenoszenia - for( int i = 0; i < iLast - 1; ++i ) { - // pętla tylko do dwóch pozycji od końca bo ostatniej nie modyfikujemy - if (sSpeedTable[i].fDist > sSpeedTable[i + 1].fDist) - { // jesli pozycja wcześniejsza jest dalej to źle - sp_temp = sSpeedTable[i + 1]; - sSpeedTable[i + 1] = sSpeedTable[i]; // zamiana - sSpeedTable[i] = sp_temp; - // jeszcze sprawdzenie czy pozycja nie była indeksowana dla eventów - if (SemNextIndex == i) - ++SemNextIndex; - else if (SemNextIndex == i + 1) - --SemNextIndex; - if (SemNextStopIndex == i) - ++SemNextStopIndex; - else if (SemNextStopIndex == i + 1) - --SemNextStopIndex; - } - } + // the last slot holds the track the scan stopped at and stays where it is. + // a single bubble pass is not enough: TableTraceRoute() adds the events of a track before the + // track itself, so a track with k scanned events lands k places too far and needs k passes + auto const sortedcount { static_cast( iLast ) }; + auto const inrange = [&]( std::size_t const Index ) { + return Index < sortedcount; }; + // the placements point at semaphor entries; remember which ones, the sort moves them around + auto const *semnextevent { inrange( SemNextIndex ) ? sSpeedTable[ SemNextIndex ].evEvent : nullptr }; + auto const *semnextstopevent { inrange( SemNextStopIndex ) ? sSpeedTable[ SemNextStopIndex ].evEvent : nullptr }; + + std::stable_sort( + std::begin( sSpeedTable ), std::begin( sSpeedTable ) + sortedcount, + []( TSpeedPos const &Left, TSpeedPos const &Right ) { + return Left.fDist < Right.fDist; } ); + + auto const relocate = [&]( basic_event const *Event, std::size_t &Index ) { + if( Event == nullptr ) { return; } + auto const lookup { + std::find_if( + std::begin( sSpeedTable ), std::begin( sSpeedTable ) + sortedcount, + [Event]( TSpeedPos const &Point ) { + return Point.evEvent == Event; } ) }; + Index = ( + lookup != std::begin( sSpeedTable ) + sortedcount ? + static_cast( std::distance( std::begin( sSpeedTable ), lookup ) ) : + std::size_t( -1 ) ); + }; + relocate( semnextevent, SemNextIndex ); + relocate( semnextstopevent, SemNextStopIndex ); } //--------------------------------------------------------------------------- From b1bdd4217818ba82521015968ee18901a4a14cda Mon Sep 17 00:00:00 2001 From: maj00r <73280030+maj00r@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:03:01 +0200 Subject: [PATCH 22/38] Forget a buffer's bindings when it is deleted (#182) The binding tracker kept the name of a deleted buffer. When gl handed that name to a new buffer, the tracker took it for already bound and skipped the bind, so a new vao was left without its index buffer and the nvidia driver read indices from a null pointer. --- gl/buffer.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gl/buffer.cpp b/gl/buffer.cpp index 8b8bd486..b80cee3e 100644 --- a/gl/buffer.cpp +++ b/gl/buffer.cpp @@ -58,6 +58,11 @@ gl::buffer::buffer() gl::buffer::~buffer() { + // forget the bindings of this buffer; gl can hand its name to a new buffer, + // which the tracker would then treat as already bound and skip the bind + for (auto &point : binding_points) + if (point == *this) + point = 0; glDeleteBuffers(1, *this); } From 8fbb48235033729d57366444dc5470285469de6a Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sun, 27 Sep 2026 00:09:16 +0200 Subject: [PATCH 23/38] feat(materials): texture randomization for loads * feat(materials): texture randomization for loads --- .../renderer/source/materialparser.cpp | 8 + model/AnimModel.cpp | 4 + model/Model3d.cpp | 117 ++++++++++++ model/Model3d.h | 10 ++ model/material.cpp | 169 ++++++++++++------ model/material.h | 32 +++- rendering/opengl33renderer.cpp | 18 +- rendering/openglrenderer.cpp | 28 ++- utilities/utilities.cpp | 40 ++++- utilities/utilities.h | 8 + vehicle/DynObj.cpp | 4 + vehicle/DynObj.h | 4 + 12 files changed, 379 insertions(+), 63 deletions(-) diff --git a/betterRenderer/renderer/source/materialparser.cpp b/betterRenderer/renderer/source/materialparser.cpp index 20ef4322..0530916b 100644 --- a/betterRenderer/renderer/source/materialparser.cpp +++ b/betterRenderer/renderer/source/materialparser.cpp @@ -187,6 +187,14 @@ bool MaterialAdapterLegacyMatFile::ParseLevel(cParser& parser, int priority) { else if (key.compare(0, 7, "texture") == 0) { key.erase(0, 7); + // "textureN_variants:" declares a pool of textures picked per model instance. this renderer + // doesn't support per instance variants yet, so the pool is reduced to a single entry drawn + // when the material is loaded, which is how such texture used to be picked before variants + if (auto const variantmarker{key.find("_variants")}; + variantmarker != std::string::npos) { + key.erase(variantmarker, std::string("_variants").size()); + } + auto value{deserialize_random_set(parser)}; replace_slashes(value); auto& [tex_priority, texture] = m_texture_mapping[key]; diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index ef4f90a8..bc2d6e84 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -627,6 +627,10 @@ void TAnimModel::RaPrepare() } } TSubModel::iInstance = reinterpret_cast( this ); //żeby nie robić cudzych animacji + // NOTE: scenery models don't pick texture variants of their own, so we clear the seed left by the + // last rendered vehicle. instanced draws share a single material bind anyway, which would make + // per instance variants inconsistent between batched and individually drawn models + TSubModel::iVariantSeed = 0; TSubModel::pasText = &asText; // przekazanie tekstu do wyświetlacza (!!!! do przemyślenia) for (auto entry : m_animlist) { diff --git a/model/Model3d.cpp b/model/Model3d.cpp index 5f59558b..38f39080 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -30,6 +30,7 @@ using namespace Mtable; float TSubModel::fSquareDist = 0.f; std::uintptr_t TSubModel::iInstance; // numer renderowanego egzemplarza obiektu +std::uint32_t TSubModel::iVariantSeed = 0; // ziarno losowania wariantów tekstur renderowanego egzemplarza texture_handle const *TSubModel::ReplacableSkinId = nullptr; int TSubModel::iAlpha = 0x30300030; // maska do testowania flag tekstur wymiennych TModel3d *TSubModel::pRoot; // Ra: tymczasowo wskaźnik na model widoczny z submodelu @@ -2460,6 +2461,120 @@ void TModel3d::LoadFromTextFile(std::string const &FileName, bool dynamic) } } +std::uint32_t TSubModel::VariantId(TSubModel const *Submodel) +{ // the seed is already well scattered, so the group is merely added to it. this way branches of one model + // are guaranteed to land on different variants, as long as the pool holds at least as many entries as + // the model has branches -- two containers carried by the same wagon won't come up with the same number + return iVariantSeed + static_cast(Submodel != nullptr ? Submodel->m_variantgroup : 0); +} + +namespace +{ +// two branches sitting no farther than this from each other are treated as one object. +// generous, a simplified lod mesh doesn't have to sit exactly where the detailed one does +float const variantgrouptolerance{0.25f}; + +// assigns provided variant group id to the submodel and all of its children +void mark_variant_group(TSubModel *Submodel, int const Group, bool const Includesiblings) +{ + while (Submodel != nullptr) + { + Submodel->m_variantgroup = Group; + mark_variant_group(Submodel->ChildGet(), Group, true); + if (false == Includesiblings) + { + break; + } + Submodel = Submodel->NextGet(); + } +} + +// expands provided bounding area to cover geometry carried by the submodel and all of its children +void accumulate_mesh_bounds(TSubModel const *Submodel, glm::vec3 &Minimum, glm::vec3 &Maximum, bool &Isempty, bool const Includesiblings) +{ + while (Submodel != nullptr) + { + if (auto const &vertices{(Submodel->m_geometry.handle != null_handle ? GfxRenderer->Vertices(Submodel->m_geometry.handle) : Submodel->Vertices)}; + false == vertices.empty()) + { + // the transformation chain can still hold a rotation, so the vertices go through it + // rather than get taken at their face value + float4x4 parentmatrix; + Submodel->ParentMatrix(&parentmatrix); + auto const transformationmatrix{glm::make_mat4(parentmatrix.readArray())}; + for (auto const &vertex : vertices) + { + auto const position{glm::vec3{transformationmatrix * glm::vec4{vertex.position, 1}}}; + Minimum = glm::min(Minimum, position); + Maximum = glm::max(Maximum, position); + } + Isempty = false; + } + accumulate_mesh_bounds(Submodel->Child, Minimum, Maximum, Isempty, true); + if (false == Includesiblings) + { + break; + } + Submodel = Submodel->Next; + } +} + +// returns position of provided branch of the model, relative to the model root +glm::vec3 branch_position(TSubModel const *Branch) +{ + // the transformation chain is the cheap way to get there, as it doesn't touch the geometry, + // which keeps this affordable even for models made of thousands of branches... + auto const offset{Branch->offset()}; + if (glm::length2(offset) > variantgrouptolerance * variantgrouptolerance) + { + return offset; + } + // ...but it only tells us anything for branches which actually carry a transform. models loaded from + // text format can have theirs baked into the vertices instead, which leaves the whole model piled up + // at the origin and, without the fallback below, all of its branches in a single variant group. + // NOTE: it's the centre of the bounding area we're after, and not the average vertex position offset() + // would give us -- the latter drifts with vertex density, which puts each lod mesh of one object in + // a group of its own and has the object change its texture variant as the lods get swapped + glm::vec3 minimum{std::numeric_limits::max()}; + glm::vec3 maximum{std::numeric_limits::lowest()}; + bool isempty{true}; + accumulate_mesh_bounds(Branch, minimum, maximum, isempty, false); + return (false == isempty ? (minimum + maximum) * 0.5f : offset); +} +} // namespace + +void TModel3d::assign_variant_groups() +{ // each top level branch of the model gets a group id, which it passes down to all of its children. + // for load models a branch is a single load chunk, and the group id is what makes two chunks of the same + // model pick different texture variants, while keeping all parts of one chunk on the same variant. + // the id comes from the position of the branch, not from its ordinal: lod variants of one object are + // separate branches drawn in disjoint distance ranges, and grouping them by position is what keeps the + // object on the same variant no matter which of them is currently on screen + std::vector grouppositions; // position of each group, indexed by its id + auto const groupid = [&grouppositions](TSubModel const *Branch) { + auto const position{branch_position(Branch)}; + for (std::size_t index = 0; index < grouppositions.size(); ++index) + { + if (glm::length2(grouppositions[index] - position) < variantgrouptolerance * variantgrouptolerance) + { + return static_cast(index); + } + } + grouppositions.emplace_back(position); + return static_cast(grouppositions.size() - 1); + }; + + for (auto *trunk = Root; trunk != nullptr; trunk = trunk->NextGet()) + { + // root level submodels are usually mere containers, but can carry geometry of their own + trunk->m_variantgroup = groupid(trunk); + for (auto *branch = trunk->ChildGet(); branch != nullptr; branch = branch->NextGet()) + { + mark_variant_group(branch, groupid(branch), false); + } + } +} + void TModel3d::Init() { // obrócenie początkowe układu współrzędnych, dla // pojazdów wykonywane po analizie animacji @@ -2492,6 +2607,8 @@ void TModel3d::Init() Root->m_boundingradius = std::max(Root->m_boundingradius, root->m_boundingradius); } + assign_variant_groups(); + if (Global.iConvertModels & 1 && false == asBinary.empty()) { SaveToBinFile(asBinary); diff --git a/model/Model3d.h b/model/Model3d.h index 7ebb1317..0e053609 100644 --- a/model/Model3d.h +++ b/model/Model3d.h @@ -137,6 +137,9 @@ public: TSubModel *Child { nullptr }; public: material_handle m_material { null_handle }; // numer tekstury, -1 wymienna, 0 brak + // index of the top level branch of the model this submodel belongs to. submodels sharing a branch + // also share the texture variant picked for them, so a multi-mesh load chunk stays consistent + int m_variantgroup { 0 }; bool bWire { false }; // nie używane, ale wczytywane float Opacity { 1.0f }; float f_Angle { 0.0f }; @@ -168,6 +171,9 @@ public: public: static size_t iInstance; // identyfikator egzemplarza, który aktualnie renderuje model + // seed of the currently rendered model instance, combined with m_variantgroup it picks + // texture variants for materials which declare them. 0 for instances without own seed + static std::uint32_t iVariantSeed; static material_handle const *ReplacableSkinId; static int iAlpha; // maska bitowa dla danego przebiegu static float fSquareDist; @@ -211,6 +217,8 @@ public: static void ReplacableSet(material_handle const *r, int a) { ReplacableSkinId = r; iAlpha = a; }; + // returns id of texture variant to use for provided submodel of the currently rendered instance + static std::uint32_t VariantId(TSubModel const *Submodel); void Name_Material( std::string const &Name ); void Name( std::string const &Name ); // Ra: funkcje do budowania terenu z E3D @@ -291,6 +299,8 @@ public: void SaveToBinFile(std::string const &FileName); uint32_t Flags() const { return iFlags; }; void Init(); + // assigns each top level branch of the model, and everything below it, its own variant group id + void assign_variant_groups(); std::string NameGet() const { return m_filename; }; nameoffset_sequence const & smoke_sources() const { return m_smokesources; } diff --git a/model/material.cpp b/model/material.cpp index f5e32689..19fa0b37 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -64,55 +64,96 @@ std::map texture_bindings { { "normalmap", 1 } }; +int opengl_material::texture_slot( std::string const &Key ) +{ + auto key { Key }; + + if( ( key.size() > 0 ) && ( key[ 0 ] != '_' ) ) + { + // numbered binding, "textureN:" fills the slot N-1 + key.erase( key.find_first_not_of( "-1234567890" ) ); + size_t const num = std::stoi( key ) - 1; + if( num < gl::MAX_TEXTURES ) { + return static_cast( num ); + } + log_error( "invalid texture binding: " + std::to_string( num ) ); + is_good = false; + return -1; + } + + if( key.size() > 2 ) + { + // named binding, the slot it fills is up to the shader + key.erase( 0, 1 ); + key.pop_back(); + if( shader && ( shader->texture_conf.find( key ) != shader->texture_conf.end() ) ) { + return static_cast( shader->texture_conf[ key ].id ); + } + auto const lookup { texture_bindings.find( key ) }; + if( ( shader == nullptr ) && ( lookup != texture_bindings.end() ) ) { + return lookup->second; + } + // ignore unrecognized texture bindings in legacy render mode, it's most likely data for more advanced shaders + if( Global.GfxRenderer == "default" ) { + log_error( "unknown texture binding: " + key ); + is_good = false; + } + return -1; + } + + log_error( "unrecognized texture binding: " + key ); + is_good = false; + return -1; +} + +void opengl_material::build_texture_variants( std::vector const &Bindings, bool const Loadnow ) +{ + if( true == Bindings.empty() ) { return; } + + // build a texture set for each entry of the largest of the declared pools. + // pools of different size can be combined, shorter ones simply repeat from the start + std::size_t variantcount { 0 }; + for( auto const &[ slot, pool ] : Bindings ) { + variantcount = std::max( variantcount, pool->size() ); + } + texture_variants.resize( variantcount, textures ); + + for( auto const &[ slot, pool ] : Bindings ) { + for( std::size_t variantindex = 0; variantindex < variantcount; ++variantindex ) { + auto const &texturename { ( *pool )[ variantindex % pool->size() ] }; + auto const texturehandle { GfxRenderer->Fetch_Texture( texturename, Loadnow ) }; + if( texturehandle == null_handle ) { + // a single bad entry shouldn't take down the whole material, fall back to the basic texture + log_error( "missing texture variant: " + texturename ); + continue; + } + texture_variants[ variantindex ][ slot ] = texturehandle; + } + } +} + void opengl_material::finalize(bool Loadnow) { is_good = true; if (parse_info) { - for (auto it : parse_info->tex_mapping) - { - std::string key = it.first; - std::string value = it.second.name; + // texture bindings which come with a pool of alternatives, gathered while resolving regular bindings + std::vector variantbindings; - if (key.size() > 0 && key[0] != '_') - { - key.erase( key.find_first_not_of( "-1234567890" ) ); - size_t num = std::stoi(key) - 1; - if (num < gl::MAX_TEXTURES) { - textures[num] = GfxRenderer->Fetch_Texture(value, Loadnow); - } - else { - log_error("invalid texture binding: " + std::to_string(num)); - is_good = false; - } - } - else if (key.size() > 2) - { - key.erase(0, 1); - key.pop_back(); - std::map::iterator lookup; - if( shader && shader->texture_conf.find( key ) != shader->texture_conf.end() ) { - textures[ shader->texture_conf[ key ].id ] = GfxRenderer->Fetch_Texture( value, Loadnow ); - } - else if( shader == nullptr - && ( lookup = texture_bindings.find( key ) ) != texture_bindings.end() ) { - textures[ lookup->second ] = GfxRenderer->Fetch_Texture( value, Loadnow ); - } - else { - // ignore unrecognized texture bindings in legacy render mode, it's most likely data for more advanced shaders - if( Global.GfxRenderer == "default" ) { - log_error( "unknown texture binding: " + key ); - is_good = false; - } - } - } - else { - log_error("unrecognized texture binding: " + key); - is_good = false; + for( auto const &[ bindingkey, binding ] : parse_info->tex_mapping ) + { + auto const slot { texture_slot( bindingkey ) }; + if( slot < 0 ) { continue; } + + textures[ slot ] = GfxRenderer->Fetch_Texture( binding.name, Loadnow ); + if( binding.variants.size() > 1 ) { + variantbindings.emplace_back( static_cast( slot ), &( binding.variants ) ); } } + build_texture_variants( variantbindings, Loadnow ); + if (!shader) { // TODO: add error severity to logging, re-enable these errors as low severity messages @@ -289,6 +330,44 @@ bool is_weather( std::string const &String ) { } // imports member data pair from the config file +void opengl_material::deserialize_texture_mapping( cParser &Input, std::string const &Key, int const Priority ) { + + // a texture binding can be provided as a pool of alternatives, "textureN_variants: [ a b c ]". + // unlike a regular random set, which is resolved once when the material is loaded and thus shared + // by everything using it, such pool is preserved whole and one entry is picked per model instance + auto key { Key }; + auto const variantmarker { key.find( "_variants" ) }; + auto const isvariantset { variantmarker != std::string::npos }; + if( true == isvariantset ) { + // reduce the key to its plain form, "N_variants:" becomes "N:" + key.erase( variantmarker, std::string( "_variants" ).size() ); + } + + std::vector variants; + std::string value; + if( false == isvariantset ) { + value = deserialize_random_set( Input ); + replace_slashes( value ); + } + else { + variants = deserialize_set( Input ); + for( auto &variant : variants ) { + replace_slashes( variant ); + } + if( true == variants.empty() ) { + log_error( "empty texture variant pool: " + key ); + return; + } + // first entry doubles as the regular, variant-agnostic texture of the material + value = variants.front(); + } + + auto const [ it, inserted ] { parse_info->tex_mapping.try_emplace( key, parse_info_s::tex_def{ value, Priority, variants } ) }; + if( ( false == inserted ) && ( Priority > it->second.priority ) ) { + it->second = parse_info_s::tex_def{ value, Priority, variants }; + } +} + bool opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool const Loadnow ) { @@ -324,17 +403,7 @@ opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool c else if (key.compare(0, 7, "texture") == 0) { key.erase(0, 7); - - auto value { deserialize_random_set( Input ) }; - replace_slashes( value ); - auto it = parse_info->tex_mapping.find(key); - if (it == parse_info->tex_mapping.end()) - parse_info->tex_mapping.emplace(std::make_pair(key, parse_info_s::tex_def({ value, Priority }))); - else if (Priority > it->second.priority) - { - parse_info->tex_mapping.erase(it); - parse_info->tex_mapping.emplace(std::make_pair(key, parse_info_s::tex_def({ value, Priority }))); - } + deserialize_texture_mapping( Input, key, Priority ); } else if (key.compare(0, 5, "param") == 0) { key.erase(0, 5); diff --git a/model/material.h b/model/material.h index 6d3f2b8d..7e04514c 100644 --- a/model/material.h +++ b/model/material.h @@ -18,7 +18,13 @@ http://mozilla.org/MPL/2.0/. // a collection of parameters for the rendering setup. // for modern opengl this translates to set of attributes for shaders struct opengl_material : public IMaterial { - std::array textures = { null_handle }; + using texture_set = std::array; + + texture_set textures = { null_handle }; + // alternative texture sets, defined with "textureN_variants:" key. + // one of them is picked per rendered model instance, empty when the material declares no variants. + // NOTE: all variants share the remaining material properties, opacity and translucency included + std::vector texture_variants; std::array params; std::vector params_state; @@ -60,6 +66,14 @@ struct opengl_material : public IMaterial { { return textures[slot]; } + // returns texture set to use for model instance identified by provided variant id + texture_set const &GetTextures(std::uint32_t const Variant) const + { + return ( + texture_variants.empty() ? + textures : + texture_variants[Variant % texture_variants.size()]); + } // members static struct path_data { std::unordered_map index_map; @@ -71,10 +85,23 @@ struct opengl_material : public IMaterial { bool update_on_season_change{ false }; private: +// types + // texture binding which comes with a pool of alternatives: slot the binding fills, and the pool itself + using variant_binding = std::pair const *>; + // methods // imports member data pair from the config file bool deserialize_mapping( cParser &Input, int const Priority, bool const Loadnow ); + // imports texture binding, potentially carrying a pool of alternatives, from the config file + void + deserialize_texture_mapping( cParser &Input, std::string const &Key, int const Priority ); + // returns texture slot provided binding key resolves to, or -1 if it doesn't resolve to any + int + texture_slot( std::string const &Key ); + // fills the alternative texture sets from provided pools, one set per entry of the longest pool + void + build_texture_variants( std::vector const &Bindings, bool const Loadnow ); void log_error(const std::string &str); // members @@ -90,6 +117,9 @@ private: { std::string name; int priority; + // optional pool of alternative textures for the same binding, one picked per model instance. + // empty for regular, single texture bindings + std::vector variants; }; struct param_def { diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 576cf404..5419bbcd 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -1286,6 +1286,7 @@ bool opengl33_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squ if( Setup ) { TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset{ Dynamic->vPosition - m_renderpass.pass_camera.position() }; Dynamic->ABuLittleUpdate( Squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu @@ -2262,14 +2263,21 @@ void opengl33_renderer::Bind_Material( material_handle const Material, TSubModel else model_ubs.alpha_mult = 1.0f; + // materials which declare texture variants provide a different texture set per model instance, + // picked from the seed of the rendered instance and the variant group of the submodel + auto const &materialtextures{ + material.texture_variants.empty() ? + material.textures : + material.GetTextures(TSubModel::VariantId(sm))}; + if (GLAD_GL_ARB_multi_bind) { GLuint lastdiff = 0; size_t i; for (i = 0; i < gl::MAX_TEXTURES; i++) - if (material.textures[i] != null_handle) + if (materialtextures[i] != null_handle) { - opengl_texture &tex = m_textures.mark_as_used(material.textures[i]); + opengl_texture &tex = m_textures.mark_as_used(materialtextures[i]); tex.create(); if (opengl_texture::units[i] != tex.id) { @@ -2286,7 +2294,7 @@ void opengl33_renderer::Bind_Material( material_handle const Material, TSubModel else { size_t unit = 0; - for (auto &tex : material.textures) + for (auto &tex : materialtextures) { if (tex == null_handle) break; @@ -3351,6 +3359,7 @@ bool opengl33_renderer::Render(TDynamicObject *Dynamic) // setup TSubModel::iInstance = reinterpret_cast(Dynamic); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom float squaredistance; @@ -3518,10 +3527,12 @@ bool opengl33_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li { TSubModel::iInstance = 0; + TSubModel::iVariantSeed = 0; return false; } TSubModel::iInstance = reinterpret_cast(Dynamic); + TSubModel::iVariantSeed = Dynamic->variant_seed(); if ((true == FreeFlyModeFlag) || (false == Dynamic->bDisplayCab) || (Dynamic->mdKabina == Dynamic->mdModel)) { @@ -4445,6 +4456,7 @@ bool opengl33_renderer::Render_Alpha(TDynamicObject *Dynamic) // setup TSubModel::iInstance = (size_t)Dynamic; //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom float squaredistance; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index d159c31a..1da66ba5 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -801,6 +801,7 @@ bool opengl_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squar if( Setup ) { TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); Dynamic->ABuLittleUpdate( Squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu ::glPushMatrix(); @@ -1759,10 +1760,17 @@ opengl_renderer::Bind_Material( material_handle const Material, TSubModel const return; } + // materials which declare texture variants provide a different texture set per model instance, + // picked from the seed of the rendered instance and the variant group of the submodel + auto const &materialtextures { + material.texture_variants.empty() ? + material.textures : + material.GetTextures( TSubModel::VariantId( sm ) ) }; + if( false == Global.BasicRenderer ) { - m_textures.bind( m_normaltextureunit, material.textures[1] ); + m_textures.bind( m_normaltextureunit, materialtextures[1] ); } - m_textures.bind( m_diffusetextureunit, material.textures[0] ); + m_textures.bind( m_diffusetextureunit, materialtextures[0] ); } else { // null material, unbind all textures @@ -2475,6 +2483,7 @@ opengl_renderer::Render( TDynamicObject *Dynamic ) { // setup TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); Dynamic->ABuLittleUpdate( squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu ::glPushMatrix(); @@ -2574,10 +2583,12 @@ opengl_renderer::Render_cab( TDynamicObject const *Dynamic, float const Lightlev if( Dynamic == nullptr ) { TSubModel::iInstance = 0; + TSubModel::iVariantSeed = 0; return false; } TSubModel::iInstance = reinterpret_cast( Dynamic ); + TSubModel::iVariantSeed = Dynamic->variant_seed(); if( ( true == FreeFlyModeFlag ) || ( false == Dynamic->bDisplayCab ) @@ -2866,11 +2877,11 @@ opengl_renderer::Render( TSubModel *Submodel ) { // material configuration: // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( Submodel->ReplacableSkinId[ -Submodel->m_material ] ); + Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); } else { // również 0 - Bind_Material( Submodel->m_material ); + Bind_Material( Submodel->m_material, Submodel ); } // main draw call m_geometry.draw( Submodel->m_geometry.handle ); @@ -2884,11 +2895,11 @@ opengl_renderer::Render( TSubModel *Submodel ) { ::glColor3fv( glm::value_ptr( pick_color( m_pickcontrolsitems.size() ) ) ); // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( Submodel->ReplacableSkinId[ -Submodel->m_material ] ); + Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); } else { // również 0 - Bind_Material( Submodel->m_material ); + Bind_Material( Submodel->m_material, Submodel ); } // main draw call m_geometry.draw( Submodel->m_geometry.handle ); @@ -3657,6 +3668,7 @@ opengl_renderer::Render_Alpha( TDynamicObject *Dynamic ) { // setup TSubModel::iInstance = ( size_t )Dynamic; //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.camera.position(); // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom float squaredistance; @@ -3909,11 +3921,11 @@ opengl_renderer::Render_Alpha( TSubModel *Submodel ) { // material configuration: // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( Submodel->ReplacableSkinId[ -Submodel->m_material ] ); + Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); } else { // również 0 - Bind_Material( Submodel->m_material ); + Bind_Material( Submodel->m_material, Submodel ); } // main draw call m_geometry.draw( Submodel->m_geometry.handle ); diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index c1d0b73d..14e44897 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -527,4 +527,42 @@ std::string deserialize_random_set(cParser &Input, char const *Break) // shouldn't ever get here but, eh return ""; } -} \ No newline at end of file +} + +// returns hash of provided string, combined with provided seed +std::uint32_t hash_string(std::string const &String, std::uint32_t const Seed) +{ + // FNV-1a, 32 bit, with the seed mixed into the standard offset basis + std::uint32_t hash{2166136261u ^ Seed}; + for (auto const character : String) + { + hash ^= static_cast(static_cast(character)); + hash *= 16777619u; + } + return hash; +} + +// extracts a group of tokens from provided data stream, returns all of them +std::vector deserialize_set(cParser &Input, char const *Break) +{ + + std::vector tokens; + + auto token{Input.getToken(true, Break)}; + std::ranges::replace(token, '\\', '/'); + if (token != "[") + { + // simple case, single token + if (false == token.empty()) + { + tokens.emplace_back(token); + } + return tokens; + } + // '[' marks a beginning of a set, retrieve all entries until it's closed + while ((token = deserialize_random_set(Input, Break)) != "" && token != "]") + { + tokens.emplace_back(token); + } + return tokens; +} diff --git a/utilities/utilities.h b/utilities/utilities.h index 70cf1c07..91d74e67 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -377,6 +377,14 @@ glm::dvec3 LoadPoint(class cParser &Input); // extracts a group of tokens from provided data stream std::string deserialize_random_set(cParser &Input, char const *Break = "\n\r\t ;"); +// extracts a group of tokens from provided data stream, returns all of them. +// a single token, not enclosed in "[]", is returned as one element sequence +std::vector deserialize_set(cParser &Input, char const *Break = "\n\r\t ;"); + +// returns hash of provided string, combined with provided seed. FNV-1a, picked for having +// identical result regardless of platform and standard library, unlike std::hash +std::uint32_t hash_string(std::string const &String, std::uint32_t const Seed = 0); + // extracts a group of pairs from provided data stream // NOTE: expects no more than single pair per line template void deserialize_map(MapType_ &Map, cParser &Input) diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 184d432a..e84af613 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -1934,6 +1934,10 @@ TDynamicObject::Init(std::string Name, // nazwa pojazdu, np. "EU07-424" iDirection = Reversed ? 0 : 1; // Ra: 0, jeśli ma być wstawiony jako obrócony tyłem asBaseDir = paths::dynamic + BaseDir + "/"; // McZapkie-310302 asName = Name; + // derived from the name and the seed of the simulation rather than drawn at random, so that the vehicle + // keeps the same look for the whole session and looks the same for everyone in a multiplayer game, while + // another run of the same scenario, with another seed, dresses it differently + m_variantseed = hash_string( asName, Global.random_seed ); std::string asAnimName; // zmienna robocza do wyszukiwania osi i wózków // Ra: zmieniamy znaczenie obsady na jednoliterowe, żeby dosadzić kierownika if (DriverType == "headdriver") diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index f8f2ce78..c48e40e1 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -634,6 +634,10 @@ private: std::string asName; const std::string &name() const { return asName; } + // seed for texture variants of materials used by this vehicle and its load + std::uint32_t m_variantseed { 0 }; + std::uint32_t variant_seed() const { + return m_variantseed; } std::string asBaseDir; // std::ofstream PneuLogFile; //zapis parametrow pneumatycznych From 551f6dbc3bb7d756d50c5e07f5109377898b26de Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 28 Sep 2026 20:49:18 +0200 Subject: [PATCH 24/38] seasonal model variants for vehicle exterior model --- rendering/opengl33renderer.cpp | 8 ++++++-- rendering/openglrenderer.cpp | 4 +++- vehicle/DynObj.cpp | 21 +++++++++++++++++++++ vehicle/DynObj.h | 5 +++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 5419bbcd..b9937816 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -3730,7 +3730,9 @@ void opengl33_renderer::Render(TSubModel *Submodel) case rendermode::shadows: { // skip if the shadow caster rank is too low for currently set threshold - if( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) + if( ( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + || ( Submodel->fVisible <= 0.f ) ) { --m_renderpass.draw_stats.submodels; --m_renderpass.draw_stats.drawcalls; @@ -4630,7 +4632,9 @@ void opengl33_renderer::Render_Alpha(TSubModel *Submodel) case rendermode::shadows: { // skip if the shadow caster rank is too low for currently set threshold - if( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) + if( ( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + || ( Submodel->fVisible <= 0.f ) ) { --m_renderpass.draw_stats.submodels; --m_renderpass.draw_stats.drawcalls; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 1da66ba5..6f06e9b3 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -2863,7 +2863,9 @@ opengl_renderer::Render( TSubModel *Submodel ) { } case rendermode::shadows: { // skip if the shadow caster rank is too low for currently set threshold - if( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) + if( ( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + || ( Submodel->fVisible <= 0.f ) ) { --m_renderpass.draw_stats.submodels; --m_renderpass.draw_stats.drawcalls; diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index e84af613..9f8fd161 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -800,6 +800,19 @@ TDynamicObject::set_cab_lights( int const Cab, float const Level ) { } } +// aktualizujemy submodele w zaleznosci od aktualnej pory roku +void TDynamicObject::on_season_update() +{ + if (sm_winter_variant != nullptr) // pokazujemy wariant zimowy + sm_winter_variant->SetVisibilityLevel(Global.Season == "winter:" ? 1 : 0, true, false); + if (sm_spring_variant != nullptr) // pokazujemy wariant wiosenny + sm_spring_variant->SetVisibilityLevel(Global.Season == "spring:" ? 1 : 0, true, false); + if (sm_summer_variant != nullptr) // pokazujemy wariant letni + sm_summer_variant->SetVisibilityLevel(Global.Season == "summer:" ? 1 : 0, true, false); + if (sm_autumn_variant != nullptr) // pokazujemy wariant jesienny + sm_autumn_variant->SetVisibilityLevel(Global.Season == "autumn:" ? 1 : 0, true, false); +} + // ABu 29.01.05 przeklejone z render i renderalpha: ********************* void TDynamicObject::ABuLittleUpdate(double ObjSqrDist) { // ABu290105: pozbierane i uporzadkowane powtarzajace @@ -814,6 +827,10 @@ void TDynamicObject::ABuLittleUpdate(double ObjSqrDist) if (vFloor.z > 0.0) mdLoad->GetSMRoot()->SetTranslate(modelShake + vFloor); + // model jest współdzielony przez pojazdy, a pora roku może się zmienić w trakcie symulacji + if (Global.UpdateMaterials) + on_season_update(); + if (ObjSqrDist < 400 * 400 ) // gdy bliżej niż 400m { for( auto &animation : pAnimations ) { @@ -2493,6 +2510,10 @@ TDynamicObject::Init(std::string Name, // nazwa pojazdu, np. "EU07-424" smBogie[0]->WillBeAnimated(); if (smBogie[1]) smBogie[1]->WillBeAnimated(); + sm_winter_variant = mdModel->GetFromName("winter_variant"); + sm_spring_variant = mdModel->GetFromName("spring_variant"); + sm_summer_variant = mdModel->GetFromName("summer_variant"); + sm_autumn_variant = mdModel->GetFromName("autumn_variant"); } // ABu: zainicjowanie zmiennej, zeby nic sie nie ruszylo w pierwszej klatce, // potem juz liczona prawidlowa wartosc masy diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index c48e40e1..36335a32 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -310,6 +310,10 @@ private: TSubModel *smWahacze[4]; // wahacze (np. nogi, dźwignia w drezynie) TSubModel *smBrakeMode; // Ra 15-01: nastawa hamulca też TSubModel *smLoadMode; // Ra 15-01: nastawa próżny/ładowny + TSubModel *sm_winter_variant {}; // submodel zimowego wariantu + TSubModel *sm_spring_variant {}; // submodel wiosennego wariantu + TSubModel *sm_summer_variant {}; // submodel letniego wariantu + TSubModel *sm_autumn_variant {}; // submodel jesiennego wariantu double fWahaczeAmp; // Winger 160204 - pantografy double pantspeedfactor; @@ -462,6 +466,7 @@ private: void ABuLittleUpdate(double ObjSqrDist); void ABuBogies(); void ABuModelRoll(); + void on_season_update(); void TurnOff(); // update state of load exchange operation void update_exchange( double const Deltatime ); From 4137f5504ae71ee0bdd4415ac9dfba593e64d335 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Mon, 28 Sep 2026 21:07:53 +0200 Subject: [PATCH 25/38] fix: sonar sast fixes - sonar.yml, nosonar, linux chmod modification --- .github/workflows/sonar.yml | 22 +++++++++++++++++++--- application/application.cpp | 2 +- sonar-project.properties | 4 +++- utilities/Globals.h | 10 ++++++++-- utilities/uart.cpp | 4 ++-- vehicle/Driver.cpp | 2 +- vehicle/DynObj.cpp | 3 ++- 7 files changed, 36 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 8140317e..66213c23 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -70,7 +70,9 @@ jobs: pull-requests: read # verify PR head sha via the API env: BUILD_WRAPPER_OUT_DIR: bw-output - SONAR_EXCLUSIONS: betterRenderer/**,ref/**,imgui/**,stb/**,build/**,bw-output/** + # .trusted/ holds the checkout of master used to restore the scanner config; + # it is not part of the analysed change. + SONAR_EXCLUSIONS: betterRenderer/**,ref/**,imgui/**,stb/**,build/**,bw-output/**,.trusted/** SONAR_SCANNER_JAVA_OPTS: -Xmx5g steps: - name: Post pending status @@ -102,12 +104,26 @@ jobs: - name: Unpack analysis input run: | set -euo pipefail + # The archive holds the whole PR workspace and is unpacked over this one, + # .trusted/ included. Move the trusted config out of reach first, or a PR + # carrying its own .trusted/sonar-project.properties would have it planted + # here and restored below as if it came from master. + if [ -f "$GITHUB_WORKSPACE/.trusted/sonar-project.properties" ]; then + cp "$GITHUB_WORKSPACE/.trusted/sonar-project.properties" /tmp/trusted-sonar-project.properties + fi + # Nothing may be written outside the workspace: reject absolute members and + # any member reaching through a parent directory before unpacking anything. + tar -tzf /tmp/sonar-input/workspace.tar.gz > /tmp/archive-members.txt + if grep -qE '^/|(^|/)[.][.](/|$)' /tmp/archive-members.txt; then + echo "::error::archive contains paths outside the workspace" + exit 1 + fi tar -xzf /tmp/sonar-input/workspace.tar.gz -C "$GITHUB_WORKSPACE" # Strip PR-controlled scanner configuration, restore the trusted one. rm -f "$GITHUB_WORKSPACE/sonar-project.properties" \ "$GITHUB_WORKSPACE/.sonarcloud.properties" - if [ -f "$GITHUB_WORKSPACE/.trusted/sonar-project.properties" ]; then - cp "$GITHUB_WORKSPACE/.trusted/sonar-project.properties" "$GITHUB_WORKSPACE/" + if [ -f /tmp/trusted-sonar-project.properties ]; then + cp /tmp/trusted-sonar-project.properties "$GITHUB_WORKSPACE/sonar-project.properties" fi - name: Resolve and verify PR metadata diff --git a/application/application.cpp b/application/application.cpp index a6b88ae8..26af01fa 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -1105,7 +1105,7 @@ void eu07_application::init_files() #elif __unix__ unlink("log.txt"); unlink("errors.txt"); - mkdir("logs", 0755); + mkdir("logs", 0750); #endif } namespace fs = std::filesystem; diff --git a/sonar-project.properties b/sonar-project.properties index 2e3fc88b..a1e5b66c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,7 +6,9 @@ sonar.sources=. sonar.sourceEncoding=UTF-8 # Exclusions -sonar.exclusions=betterRenderer/**,ref/**,imgui/**,stb/**,build/**,bw-output/** +# .trusted/ is the copy of master the analysis workflow checks out to restore its +# own configuration; analysing it would duplicate every file of the base branch. +sonar.exclusions=betterRenderer/**,ref/**,imgui/**,stb/**,build/**,bw-output/**,.trusted/** # Excluding all files from coverage making the gate skip that # instead of failing it. diff --git a/utilities/Globals.h b/utilities/Globals.h index c4bd26f6..d0f70701 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -38,8 +38,14 @@ struct global_settings { bool shiftState{ false }; //m7todo: brzydko bool ctrlState{ false }; bool altState{ false }; - std::mt19937 random_engine; - std::mt19937 local_random_engine; + // Simulation randomness, never used for anything security related: no keys, tokens or + // credentials come out of these. Reproducibility is a requirement here, which is why both + // are plain mt19937 -- the run's seed can be written down and passed on, and it travels in + // server_hello so that every client in a session replays the same sequence. A generator + // that cannot be seeded to a known state would break both of those. + std::mt19937 random_engine; // NOSONAR shared run seed, kept in sync across the network + std::mt19937 local_random_engine; // NOSONAR re-seeded from a true random source in init_modes, + // drives purely local cosmetics (sound pitch, editor placement) bool ready_to_load{ false }; std::time_t starting_timestamp = 0; // starting time, in local timezone uint32_t random_seed = 0; diff --git a/utilities/uart.cpp b/utilities/uart.cpp index cac7a198..ab5060fa 100644 --- a/utilities/uart.cpp +++ b/utilities/uart.cpp @@ -399,7 +399,7 @@ void uart_input::poll() char buf[buffer.size() * 3 + 1]; size_t pos = 0; for (uint8_t b : buffer) - pos += sprintf(&buf[pos], "%02X ", b); + pos += snprintf(&buf[pos], sizeof(buf) - pos, "%02X ", b); WriteLog("uart: rx: " + std::string(buf)); } @@ -635,7 +635,7 @@ void uart_input::poll() buf[ buffer.size() * 3 ] = 0; size_t pos = 0; for (uint8_t b : buffer) - pos += sprintf(&buf[pos], "%02X ", b); + pos += snprintf(&buf[pos], sizeof(buf) - pos, "%02X ", b); WriteLog("uart: tx: " + std::string(buf)); } diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 9a6de859..9771f7db 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -1901,7 +1901,7 @@ TController::TController(bool AI, TDynamicObject *NewControll, bool InitPsyche, #ifdef _WIN32 CreateDirectory( "physicslog", nullptr); #elif __unix__ - mkdir( "physicslog", 0744 ); + mkdir( "physicslog", 0740 ); #endif LogFile.open( std::string( "physicslog/" + VehicleName + ".dat" ), std::ios::in | std::ios::out | std::ios::trunc ); diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index e84af613..57cc8660 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3024,7 +3024,8 @@ TDynamicObject::update_load_offset() { void TDynamicObject::shuffle_load_order() { - std::shuffle( std::begin( SectionLoadOrder ), std::end( SectionLoadOrder ), Global.random_engine ); + // order of visible load chunks, nothing security related -- see the engines in Globals.h + std::shuffle( std::begin( SectionLoadOrder ), std::end( SectionLoadOrder ), Global.random_engine ); // NOSONAR // shift chunks assigned to corridors to the end of the list, so they show up last std::stable_partition( std::begin( SectionLoadOrder ), std::end( SectionLoadOrder ), From 176427f96b26767b81d3da0303dc7890869e22da Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Mon, 28 Sep 2026 21:08:15 +0200 Subject: [PATCH 26/38] refactor: sonar c++20 changes (#185) --- application/application.cpp | 7 +--- launcher/scenery_list.cpp | 2 +- launcher/vehicle_picker.cpp | 2 +- model/Model3d.cpp | 13 +++--- model/Texture.cpp | 6 +-- network/backend/asio.cpp | 6 ++- network/message.h | 16 ++++---- rendering/screenshot.cpp | 8 ++-- scene/sn_utils.cpp | 20 +++++---- simulation/simulationstateserializer.cpp | 5 ++- simulation/simulationtime.cpp | 3 +- utilities/Globals.cpp | 20 ++------- utilities/Logs.cpp | 3 +- utilities/utilities.cpp | 22 ++++++++++ utilities/utilities.h | 4 ++ vehicle/Driver.cpp | 52 ++++++++++-------------- vehicle/Driver.h | 6 +-- widgets/trainingcard.cpp | 22 +++++----- 18 files changed, 111 insertions(+), 106 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index 26af01fa..ceb5ec6c 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -1479,11 +1479,8 @@ bool eu07_application::init_network() // TODO: sort out this timezone mess std::time_t utc_now = std::time(nullptr); - tm tm_local, tm_utc; - tm *tmp = std::localtime(&utc_now); - memcpy(&tm_local, tmp, sizeof(tm)); - tmp = std::gmtime(&utc_now); - memcpy(&tm_utc, tmp, sizeof(tm)); + std::tm const tm_local = local_tm(utc_now); + std::tm const tm_utc = utc_tm(utc_now); int64_t offset = tm_local.tm_hour * 3600 + tm_local.tm_min * 60 + tm_local.tm_sec - (tm_utc.tm_hour * 3600 + tm_utc.tm_min * 60 + tm_utc.tm_sec); diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index 0b49e80f..77f69a35 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -281,7 +281,7 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) for (auto &dyn_desc : trainset.vehicles) { deferred_image *mini = nullptr; - if (dyn_desc.skin && dyn_desc.skin->mini.get() != -1) + if (dyn_desc.skin && dyn_desc.skin->mini.get() != (GLuint)-1) mini = &dyn_desc.skin->mini; else mini = &placeholder_mini; diff --git a/launcher/vehicle_picker.cpp b/launcher/vehicle_picker.cpp index 88c40ef9..f7d3503f 100644 --- a/launcher/vehicle_picker.cpp +++ b/launcher/vehicle_picker.cpp @@ -270,7 +270,7 @@ bool ui::vehiclepicker_panel::selectable_image(const char *desc, bool selected, } GLuint tex = image->get(); - if (tex != -1) { + if (tex != (GLuint)-1) { glm::ivec2 size = image->size(); float width = 30.0f / size.y * size.x; ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); diff --git a/model/Model3d.cpp b/model/Model3d.cpp index 38f39080..fba07446 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -24,6 +24,8 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others #include "world/mtable.h" #include "scene/sn_utils.h" +#include + //--------------------------------------------------------------------------- using namespace Mtable; @@ -224,16 +226,13 @@ inline void readMatrix(cParser &parser, float4x4 &matrix) template void UserdataParse(cParser &parser, gfx::vertex_userdata &vertex) { + static_assert(sizeof(T) == sizeof(float), "userdata is stored as raw 32-bit values"); parser.getTokens(4); - union - { - T InType; - float FloatType; - } buf{}; for (int i = 0; i < 4; ++i) { - parser >> buf.InType; - vertex.data[i] = buf.FloatType; + T value{}; + parser >> value; + vertex.data[i] = std::bit_cast(value); } } diff --git a/model/Texture.cpp b/model/Texture.cpp index a5397663..5c1d852f 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -394,12 +394,12 @@ opengl_texture::make_from_memory(size_t width, size_t height, const uint8_t *raw void opengl_texture::update_from_memory(size_t width, size_t height, const uint8_t *raw) { - if (id != -1 && (width != data_width || height != data_height || GL_SRGB8_ALPHA8 != data_format || GL_RGBA != data_components)) + if (id != (GLuint)-1 && (width != data_width || height != data_height || GL_SRGB8_ALPHA8 != data_format || GL_RGBA != data_components)) { glDeleteTextures(1, &id); - id = -1; + id = (GLuint)-1; } - if (id == -1) + if (id == (GLuint)-1) { data_width = width; data_height = height; diff --git a/network/backend/asio.cpp b/network/backend/asio.cpp index 958b084e..c2d1364d 100644 --- a/network/backend/asio.cpp +++ b/network/backend/asio.cpp @@ -12,8 +12,10 @@ network::tcp::connection::connection(asio::io_context &io_ctx, bool client, size network::tcp::connection::~connection() { - m_socket.shutdown(m_socket.shutdown_both); - m_socket.close(); + // error_code overloads: the throwing ones would escape the destructor, e.g. when the peer is already gone + asio::error_code ec; + m_socket.shutdown(m_socket.shutdown_both, ec); + m_socket.close(ec); } void network::tcp::connection::disconnect() diff --git a/network/message.h b/network/message.h index 43ad13ec..871e833f 100644 --- a/network/message.h +++ b/network/message.h @@ -30,17 +30,17 @@ struct client_hello : public message virtual void serialize(std::ostream &stream) const override; virtual void deserialize(std::istream &stream) override; - int32_t version; - uint32_t start_packet; + int32_t version{}; + uint32_t start_packet{}; }; struct server_hello : public message { server_hello() : message(SERVER_HELLO) {} - uint32_t seed; - int64_t timestamp; - int64_t config; + uint32_t seed{}; + int64_t timestamp{}; + int64_t config{}; std::string scenario; virtual void serialize(std::ostream &stream) const override; @@ -62,9 +62,9 @@ struct frame_info : public request_command { frame_info() : request_command(FRAME_INFO) {} - double render_dt; - double dt; - double sync; + double render_dt{}; + double dt{}; + double sync{}; virtual void serialize(std::ostream &stream) const override; virtual void deserialize(std::istream &stream) override; diff --git a/rendering/screenshot.cpp b/rendering/screenshot.cpp index 285679b7..eb7e7aac 100644 --- a/rendering/screenshot.cpp +++ b/rendering/screenshot.cpp @@ -2,6 +2,7 @@ #include "rendering/screenshot.h" #include "utilities/Globals.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include void screenshot_manager::screenshot_save_thread( char *img, int w, int h ) @@ -29,11 +30,8 @@ void screenshot_manager::screenshot_save_thread( char *img, int w, int h ) } char datetime[64]; - time_t timer; - struct tm* tm_info; - time(&timer); - tm_info = localtime(&timer); - strftime(datetime, 64, "%Y-%m-%d_%H-%M-%S", tm_info); + std::tm const tm_info = local_tm(std::time(nullptr)); + strftime(datetime, 64, "%Y-%m-%d_%H-%M-%S", &tm_info); uint64_t perf; #ifdef _WIN32 diff --git a/scene/sn_utils.cpp b/scene/sn_utils.cpp index 71e6549c..08825d91 100644 --- a/scene/sn_utils.cpp +++ b/scene/sn_utils.cpp @@ -5,6 +5,8 @@ #include "stdafx.h" #include "scene/sn_utils.h" +#include + // sanity checks static_assert(std::numeric_limits::is_iec559, "IEEE754 required"); static_assert(sizeof(float) == 4, "Float must be 4 bytes"); @@ -17,7 +19,7 @@ uint16_t sn_utils::ld_uint16(std::istream &s) uint8_t buf[2]; s.read((char*)buf, 2); uint16_t v = buf[1] << 8 | buf[0]; - return reinterpret_cast(v); + return v; } // deserialize little endian uint32 @@ -26,7 +28,7 @@ uint32_t sn_utils::ld_uint32(std::istream &s) uint8_t buf[4]; s.read((char*)buf, 4); uint32_t v = buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0]; - return reinterpret_cast(v); + return v; } // deserialize little endian int32 @@ -35,7 +37,7 @@ int32_t sn_utils::ld_int32(std::istream &s) uint8_t buf[4]; s.read((char*)buf, 4); uint32_t v = buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0]; - return reinterpret_cast(v); + return static_cast(v); } // deserialize little endian uint64 @@ -47,7 +49,7 @@ uint64_t sn_utils::ld_uint64(std::istream &s) (uint64_t)buf[5] << 40 | (uint64_t)buf[4] << 32 | (uint64_t)buf[3] << 24 | (uint64_t)buf[2] << 16 | (uint64_t)buf[1] << 8 | (uint64_t)buf[0]; - return reinterpret_cast(v); + return v; } // deserialize little endian int64 @@ -59,7 +61,7 @@ int64_t sn_utils::ld_int64(std::istream &s) (uint64_t)buf[5] << 40 | (uint64_t)buf[4] << 32 | (uint64_t)buf[3] << 24 | (uint64_t)buf[2] << 16 | (uint64_t)buf[1] << 8 | (uint64_t)buf[0]; - return reinterpret_cast(v); + return static_cast(v); } // deserialize little endian ieee754 float32 @@ -68,7 +70,7 @@ float sn_utils::ld_float32(std::istream &s) uint8_t buf[4]; s.read((char*)buf, 4); uint32_t v = buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0]; - return reinterpret_cast(v); + return std::bit_cast(v); } // deserialize little endian ieee754 float64 @@ -80,7 +82,7 @@ double sn_utils::ld_float64(std::istream &s) (uint64_t)buf[5] << 40 | (uint64_t)buf[4] << 32 | (uint64_t)buf[3] << 24 | (uint64_t)buf[2] << 16 | (uint64_t)buf[1] << 8 | (uint64_t)buf[0]; - return reinterpret_cast(v); + return std::bit_cast(v); } // deserialize null-terminated string @@ -195,7 +197,7 @@ void sn_utils::ls_int64(std::ostream &s, int64_t v) void sn_utils::ls_float32(std::ostream &s, float t) { - uint32_t v = reinterpret_cast(t); + uint32_t v = std::bit_cast(t); uint8_t buf[4]; buf[0] = v; buf[1] = v >> 8; @@ -206,7 +208,7 @@ void sn_utils::ls_float32(std::ostream &s, float t) void sn_utils::ls_float64(std::ostream &s, double t) { - uint64_t v = reinterpret_cast(t); + uint64_t v = std::bit_cast(t); uint8_t buf[8]; buf[0] = v; buf[1] = v >> 8; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index ef7cd7db..99a733d3 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -27,6 +27,7 @@ http://mozilla.org/MPL/2.0/. #include "application/application.h" #include "rendering/renderer.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "editor/editorTerrainStreamer.hpp" namespace simulation { @@ -393,8 +394,8 @@ void state_serializer::init_time() { auto &time = simulation::Time.data(); if( true == Global.ScenarioTimeCurrent ) { // calculate time shift required to match scenario time with local clock - auto const *localtime = std::gmtime( &Global.starting_timestamp ); - Global.ScenarioTimeOffset = ( localtime->tm_hour * 60 + localtime->tm_min - ( time.wHour * 60 + time.wMinute ) ) / 60.f; + auto const localtime = utc_tm( Global.starting_timestamp ); + Global.ScenarioTimeOffset = ( localtime.tm_hour * 60 + localtime.tm_min - ( time.wHour * 60 + time.wMinute ) ) / 60.f; } else if( false == std::isnan( Global.ScenarioTimeOverride ) ) { // scenario time override takes precedence over scenario time offset diff --git a/simulation/simulationtime.cpp b/simulation/simulationtime.cpp index 52886709..f1683c53 100644 --- a/simulation/simulationtime.cpp +++ b/simulation/simulationtime.cpp @@ -32,7 +32,8 @@ scenario_time::init(std::time_t timestamp) { auto const requestedminute { requestedtime % 60 }; // cache requested elements, if any - std::tm *tms = std::gmtime(×tamp); + std::tm const tm = utc_tm(timestamp); + std::tm const *tms = &tm; m_time.wYear = tms->tm_year + 1900; m_time.wMonth = tms->tm_mon + 1; m_time.wDayOfWeek = tms->tm_wday; diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index f45b6b3e..47b04d18 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulationenvironment.h" #include "vehicle/Driver.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "Console.h" #include "scripting/PyInt.h" #include "utilities/Timer.h" @@ -26,15 +27,7 @@ http://mozilla.org/MPL/2.0/. void global_settings::LoadIniFile(std::string asFileName) { // initialize season data in case the main config file doesn't - std::time_t timenow = std::time(nullptr); - - std::tm tm{}; - -#ifdef _WIN32 - localtime_s(&tm, &timenow); -#else - localtime_r(&timenow, &tm); -#endif + std::tm const tm = local_tm(std::time(nullptr)); fMoveLight = tm.tm_yday + 1; // numer bieżącego dnia w roku simulation::Environment.compute_season(fMoveLight); @@ -513,14 +506,7 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string& if (fMoveLight == 0.f) { - std::time_t timenow = std::time(nullptr); - std::tm tm{}; - -#ifdef _WIN32 - localtime_s(&tm, &timenow); -#else - localtime_r(&timenow, &tm); -#endif + std::tm const tm = local_tm(std::time(nullptr)); fMoveLight = tm.tm_yday + 1; } diff --git a/utilities/Logs.cpp b/utilities/Logs.cpp index 554a77b9..737d25d7 100644 --- a/utilities/Logs.cpp +++ b/utilities/Logs.cpp @@ -31,7 +31,8 @@ std::string filename_date() { #ifdef __unix__ timespec ts; clock_gettime(CLOCK_REALTIME, &ts); - tm *tms = localtime(&ts.tv_sec); + std::tm const tm = local_tm(ts.tv_sec); + std::tm const *tms = &tm; st.wYear = tms->tm_year; st.wMonth = tms->tm_mon; st.wDayOfWeek = tms->tm_wday; diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 14e44897..efaf3e6c 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -426,6 +426,28 @@ std::time_t last_modified(std::string const &Filename) return 0; } +std::tm local_tm(std::time_t Time) +{ + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &Time); +#else + localtime_r(&Time, &tm); +#endif + return tm; +} + +std::tm utc_tm(std::time_t Time) +{ + std::tm tm{}; +#ifdef _WIN32 + gmtime_s(&tm, &Time); +#else + gmtime_r(&Time, &tm); +#endif + return tm; +} + // potentially erases file extension from provided file name. returns: true if extension was removed, false otherwise bool erase_extension(std::string &Filename) { diff --git a/utilities/utilities.h b/utilities/utilities.h index 91d74e67..6e761380 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -220,6 +220,10 @@ std::pair FileExists(std::vector const &N // returns time of last modification for specified file std::time_t last_modified(std::string const &Filename); +// thread-safe replacements for std::localtime and std::gmtime, which hand out a pointer to shared static storage +std::tm local_tm(std::time_t Time); +std::tm utc_tm(std::time_t Time); + // potentially erases file extension from provided file name. returns: true if extension was removed, false otherwise bool erase_extension(std::string &Filename); diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 9771f7db..9dfd6731 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -449,8 +449,8 @@ void TController::TableClear() iTableDirection = 0; // nieznany tLast = nullptr; fLastVel = -1.0; - SemNextIndex = -1; - SemNextStopIndex = -1; + SemNextIndex = npos; + SemNextStopIndex = npos; eSignSkip = nullptr; // nic nie pomijamy }; @@ -512,8 +512,8 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) sSpeedTable.clear(); iLast = -1; tLast = nullptr; //żaden nie sprawdzony - SemNextIndex = -1; - SemNextStopIndex = -1; + SemNextIndex = npos; + SemNextStopIndex = npos; */ if( VelSignalLast == 0.0 ) { // don't allow potential red light overrun keep us from reversing @@ -541,16 +541,16 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) else { if( iTableDirection == 0 ) { return; } // NOTE: belt and braces; TablePurger() used to leave these pointing past the end of the table - if( SemNextIndex != -1 + if( SemNextIndex != npos && SemNextIndex >= sSpeedTable.size() ) { - SemNextIndex = -1; + SemNextIndex = npos; } - if( SemNextStopIndex != -1 + if( SemNextStopIndex != npos && SemNextStopIndex >= sSpeedTable.size() ) { - SemNextStopIndex = -1; + SemNextStopIndex = npos; } // kontynuacja skanowania od ostatnio sprawdzonego toru (w ostatniej pozycji zawsze jest tor) - if( SemNextStopIndex != -1 + if( SemNextStopIndex != npos && sSpeedTable[SemNextStopIndex].fVelNext == 0.0 ) { // znaleziono semafor lub tarczę lub tor z prędkością zero, trzeba sprawdzić czy to nadał semafor // jeśli jest następny semafor to sprawdzamy czy to on nadał zero @@ -614,23 +614,23 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) fDistance = newspeedpoint.fDist; // jeśli sygnał stop, to nie ma potrzeby dalej skanować SemNextStopIndex = iLast; - if (SemNextIndex == -1) { + if (SemNextIndex == npos) { SemNextIndex = iLast; } if (Global.iWriteLogEnabled & 8) { WriteLog("(stop signal from " - + (SemNextStopIndex != -1 ? sSpeedTable[SemNextStopIndex].GetName() : "unknown semaphor") + + (SemNextStopIndex != npos ? sSpeedTable[SemNextStopIndex].GetName() : "unknown semaphor") + ")"); } } else { if( true == newspeedpoint.IsProperSemaphor(OrderCurrentGet()) - && SemNextIndex == -1 ) { + && SemNextIndex == npos ) { SemNextIndex = iLast; // sprawdzamy czy pierwszy na drodze } if (Global.iWriteLogEnabled & 8) { WriteLog("(forward signal for " - + (SemNextIndex != -1 ? sSpeedTable[SemNextIndex].GetName() : "unknown semaphor") + + (SemNextIndex != npos ? sSpeedTable[SemNextIndex].GetName() : "unknown semaphor") + ")"); } } @@ -800,13 +800,13 @@ void TController::TableCheck(double fDistance) --iLast; } // a semaphor recorded beyond the switch went away with the discarded entries - if( SemNextIndex != -1 + if( SemNextIndex != npos && SemNextIndex >= sSpeedTable.size() ) { - SemNextIndex = -1; + SemNextIndex = npos; } - if( SemNextStopIndex != -1 + if( SemNextStopIndex != npos && SemNextStopIndex >= sSpeedTable.size() ) { - SemNextStopIndex = -1; + SemNextStopIndex = npos; } tLast = sSpeedTable[ i ].trTrack; TableTraceRoute( fDistance, pVehicles[ end::rear ] ); @@ -1387,13 +1387,13 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo if( Global.iWriteLogEnabled & 8 ) { WriteLog( "Speed table update for " + OwnerName() + ", passed semaphor " + sSpeedTable[ SemNextIndex ].GetName() ); } - SemNextIndex = -1; // jeśli minęliśmy semafor od ograniczenia to go kasujemy ze zmiennej sprawdzającej dla skanowania w przód + SemNextIndex = npos; // jeśli minęliśmy semafor od ograniczenia to go kasujemy ze zmiennej sprawdzającej dla skanowania w przód } if( SemNextStopIndex == Pointindex ) { if( Global.iWriteLogEnabled & 8 ) { WriteLog( "Speed table update for " + OwnerName() + ", passed semaphor " + sSpeedTable[ SemNextStopIndex ].GetName() ); } - SemNextStopIndex = -1; // jeśli minęliśmy semafor od ograniczenia to go kasujemy ze zmiennej sprawdzającej dla skanowania w przód + SemNextStopIndex = npos; // jeśli minęliśmy semafor od ograniczenia to go kasujemy ze zmiennej sprawdzającej dla skanowania w przód } switch( Point.evEvent->input_command() ) { // TBD, TODO: expand emergency_brake handling to a more generic security system signal @@ -1461,13 +1461,13 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo } // jeśli jest mienięty poprzedni semafor a wcześniej // byl nowy to go dorzucamy do zmiennej, żeby cały czas widział najbliższy - if( SemNextIndex == -1 ) { + if( SemNextIndex == npos ) { SemNextIndex = Pointindex; if( Global.iWriteLogEnabled & 8 ) { WriteLog( "Speed table update for " + OwnerName() + ", next semaphor is " + sSpeedTable[ SemNextIndex ].GetName() ); } } - if( SemNextStopIndex == -1 + if( SemNextStopIndex == npos || ( sSpeedTable[SemNextStopIndex].fVelNext != 0 && Point.fVelNext == 0 ) ) { SemNextStopIndex = Pointindex; @@ -1926,17 +1926,9 @@ TController::TController(bool AI, TDynamicObject *NewControll, bool InitPsyche, fActionTime = -5.0; }; -void TController::CloseLog() -{ - if (WriteLogFlag) - { - LogFile.close(); - } -}; - TController::~TController() { // wykopanie mechanika z roboty - CloseLog(); + // LogFile is closed by its own destructor }; // zamiana kodu rozkazu na opis diff --git a/vehicle/Driver.h b/vehicle/Driver.h index e7c4fac0..b76dbf08 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -542,8 +542,9 @@ private: double fLastVel = 0.0; // prędkość na poprzednio sprawdzonym torze TTrack *tLast = nullptr; // ostatni analizowany tor 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 ) }; + static constexpr std::size_t npos{ std::size_t( -1 ) }; + std::size_t SemNextIndex{ npos }; + std::size_t SemNextStopIndex{ npos }; 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 @@ -622,7 +623,6 @@ private: // logs // methods void PhysicsLog(); - void CloseLog(); // members std::ofstream LogFile; // zapis parametrow fizycznych double LastUpdatedTime = 0.0; // czas od ostatniego logu diff --git a/widgets/trainingcard.cpp b/widgets/trainingcard.cpp index d4d11e5b..27fb0965 100644 --- a/widgets/trainingcard.cpp +++ b/widgets/trainingcard.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "widgets/trainingcard.h" #include "simulation/simulation.h" +#include "utilities/utilities.h" #ifdef __linux__ #include @@ -46,12 +47,11 @@ void trainingcard_panel::clear() void trainingcard_panel::save_thread_func() { - std::tm *tm = std::localtime(&(*start_time_wall)); - std::string date = std::to_string(tm->tm_year + 1900) + "-" + std::to_string(tm->tm_mon + 1) + "-" + std::to_string(tm->tm_mday); - std::string from = std::to_string(tm->tm_hour) + ":" + std::to_string(tm->tm_min); - std::time_t now = std::time(nullptr); - tm = std::localtime(&now); - std::string to = std::to_string(tm->tm_hour) + ":" + std::to_string(tm->tm_min); + std::tm tm = local_tm(*start_time_wall); + std::string date = std::to_string(tm.tm_year + 1900) + "-" + std::to_string(tm.tm_mon + 1) + "-" + std::to_string(tm.tm_mday); + std::string from = std::to_string(tm.tm_hour) + ":" + std::to_string(tm.tm_min); + tm = local_tm(std::time(nullptr)); + std::string to = std::to_string(tm.tm_hour) + ":" + std::to_string(tm.tm_min); std::fstream temp("reports/" + recording_timestamp + ".html", std::ios_base::out | std::ios_base::binary); std::fstream input("report_template.html", std::ios_base::in | std::ios_base::binary); @@ -121,9 +121,9 @@ void trainingcard_panel::render_contents() if (start_time_wall) { - std::tm *tm = std::localtime(&(*start_time_wall)); - std::string rep = "Czas rozpoczęcia: " + std::to_string(tm->tm_year + 1900) + "-" + std::to_string(tm->tm_mon + 1) + "-" + std::to_string(tm->tm_mday) + " " + std::to_string(tm->tm_hour) + - ":" + std::to_string(tm->tm_min); + std::tm const tm = local_tm(*start_time_wall); + std::string rep = "Czas rozpoczęcia: " + std::to_string(tm.tm_year + 1900) + "-" + std::to_string(tm.tm_mon + 1) + "-" + std::to_string(tm.tm_mday) + " " + std::to_string(tm.tm_hour) + + ":" + std::to_string(tm.tm_min); ImGui::TextUnformatted(rep.c_str()); } @@ -159,8 +159,8 @@ void trainingcard_panel::render_contents() if (ImGui::Button("Rozpocznij szkolenie")) { start_time_wall = std::time(nullptr); - std::tm *tm = std::localtime(&(*start_time_wall)); - recording_timestamp = std::to_string(tm->tm_year + 1900) + std::to_string(tm->tm_mon + 1) + std::to_string(tm->tm_mday) + std::to_string(tm->tm_hour) + std::to_string(tm->tm_min) + "_" + + std::tm const tm = local_tm(*start_time_wall); + recording_timestamp = std::to_string(tm.tm_year + 1900) + std::to_string(tm.tm_mon + 1) + std::to_string(tm.tm_mday) + std::to_string(tm.tm_hour) + std::to_string(tm.tm_min) + "_" + std::string(trainee_name.c_str()) + "_" + std::string(instructor_name.c_str()); int ret = StartRecording(); From 96be688d1374d33234869625394bbc039d78364b Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Wed, 30 Sep 2026 14:51:36 +0200 Subject: [PATCH 27/38] Send the reverser position, not the absolute direction, when activating a cab CabActivisation() passed DirAbsolute (DirActive * CabActive) as the value of the "Direction" command, which every receiving vehicle stores as its own DirActive. Every other sender passes DirActive. With the cab at the rear end (CabActive = -1) the two have opposite signs, so the other units got their reverser flipped against the controlling one. It shows whenever a rear cab is activated after the reverser has been set: - the AI takes over an occupied cab (TakeControl() calls CabActivisation(true)), e.g. an EN57 inserted with reardriver and a starting velocity: the motor car drives the train backwards; - a vehicle without automatic cab activation (SA134) driven by the AI from the start: the cab is activated by a driver hint after the reverser is set, and the two units pull against each other. Fixes #169. --- McZapkie/Mover.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 83c918e2..68b88918 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -2901,7 +2901,7 @@ bool TMoverParameters::CabActivisation(bool const Enforce) CabMaster = true; SecuritySystem.set_enabled(true); // activate the alerter TODO: make it part of control based cab selection SendCtrlToNext("CabActivisation", 1, CabActive); - SendCtrlToNext("Direction", DirAbsolute, CabActive); + SendCtrlToNext("Direction", DirActive, CabActive); if (InactiveCabFlag & activation::springbrakeoff) { SpringBrakeActivate(false); From f9deee5cfdd63db40ace5c555fdec13cf61cb24e Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 02:03:49 +0200 Subject: [PATCH 28/38] fix(editor): stuck brush after clicks on UI; add polygon area fill (#191) --- application/editormode.cpp | 619 +++++++++++++++++++++++++++++++-- application/editormode.h | 26 +- application/editoruilayer.cpp | 10 + application/editoruilayer.h | 2 + application/editoruipanels.cpp | 43 ++- application/editoruipanels.h | 7 +- 6 files changed, 674 insertions(+), 33 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 46a8edad..94b1424d 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -24,6 +24,8 @@ http://mozilla.org/MPL/2.0/. #include "model/Model3d.h" #include "utilities/Float3d.h" #include "scene/scene.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" #include "imgui/imgui.h" @@ -58,6 +60,14 @@ namespace return state == GLFW_PRESS; } + // true when the most recent left button press landed in the viewport rather than on an ImGui window. + // valid once ImGui has processed the press (i.e. from mode update onwards), so it's also used to + // discard deferred pick callbacks of a press that leaked past the application's UI filter + inline bool viewport_click() + { + return false == ImGui::GetIO().MouseDownOwned[0]; + } + // tests whether the vertical line through (Px,Pz) passes over triangle abc; if so returns the // surface height at that point through OutY. used by the "snap to ground" (END) feature. inline bool triangle_height_at(glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c, @@ -117,6 +127,158 @@ namespace } } + // world transform of a model instance, matching the renderer: translate * rotateY * rotateX * rotateZ * scale + glm::dmat4 instance_matrix(TAnimModel const &Model) + { + glm::dmat4 rootm(1.0); + rootm = glm::translate(rootm, Model.location()); + glm::vec3 const angles = Model.Angles(); + if (angles.y != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast(angles.y)), glm::dvec3(0.0, 1.0, 0.0)); + if (angles.x != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast(angles.x)), glm::dvec3(1.0, 0.0, 0.0)); + if (angles.z != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast(angles.z)), glm::dvec3(0.0, 0.0, 1.0)); + return glm::scale(rootm, glm::dvec3(Model.Scale())); + } + + // even-odd test of a point against a polygon outline projected onto the XZ plane + bool point_in_polygon_xz(std::vector const &Polygon, double const X, double const Z) + { + bool inside = false; + for (std::size_t i = 0, j = Polygon.size() - 1; i < Polygon.size(); j = i++) + { + auto const &a = Polygon[i]; + auto const &b = Polygon[j]; + if ((a.z > Z) != (b.z > Z) && X < (b.x - a.x) * (Z - a.z) / (b.z - a.z) + a.x) + inside = !inside; + } + return inside; + } + + // area of a polygon outline projected onto the XZ plane (shoelace formula) + double polygon_area_xz(std::vector const &Polygon) + { + if (Polygon.size() < 3) + return 0.0; + double sum = 0.0; + for (std::size_t i = 0, j = Polygon.size() - 1; i < Polygon.size(); j = i++) + sum += Polygon[j].x * Polygon[i].z - Polygon[i].x * Polygon[j].z; + return std::abs(sum) * 0.5; + } + + // short readable name of a node template (model file and texture), like the node bank shows it + std::string template_label(std::string const &Template) + { + cParser tokenizer(Template); + tokenizer.getTokens(9, false); // node, ranges, name, type, position, rotation + auto model{tokenizer.getToken(false)}; + auto texture{tokenizer.getToken(false)}; + replace_slashes(model); + erase_extension(model); + replace_slashes(texture); + return texture == "none" || texture.empty() ? model : model + " (" + texture + ")"; + } + + // world triangles bucketed on a uniform XZ grid, for many ground height queries over one area + class triangle_grid + { + public: + triangle_grid(std::vector Triangles, glm::dvec2 const &Min, glm::dvec2 const &Max) : m_triangles(std::move(Triangles)), m_min(Min) + { + glm::dvec2 const extent = glm::max(Max - Min, glm::dvec2(1.0)); + // ~8 m cells, coarser for big areas so large terrain triangles don't flood the grid + m_cellsize = std::max(8.0, std::max(extent.x, extent.y) / 128.0); + m_columns = static_cast(std::ceil(extent.x / m_cellsize)) + 1; + m_rows = static_cast(std::ceil(extent.y / m_cellsize)) + 1; + m_cells.resize(static_cast(m_columns) * m_rows); + + for (std::uint32_t idx = 0; idx < m_triangles.size(); ++idx) + { + auto const &t = m_triangles[idx]; + int const c0 = column(std::min({t[0].x, t[1].x, t[2].x})); + int const c1 = column(std::max({t[0].x, t[1].x, t[2].x})); + int const r0 = row(std::min({t[0].z, t[1].z, t[2].z})); + int const r1 = row(std::max({t[0].z, t[1].z, t[2].z})); + for (int r = r0; r <= r1; ++r) + for (int c = c0; c <= c1; ++c) + m_cells[static_cast(r) * m_columns + c].push_back(idx); + } + } + + // highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one + bool height_at(double const X, double const Z, double const Ceiling, double &OutY) const + { + bool found = false; + for (auto const idx : m_cells[static_cast(row(Z)) * m_columns + column(X)]) + { + auto const &t = m_triangles[idx]; + double y; + if (triangle_height_at(t[0], t[1], t[2], X, Z, y) && y <= Ceiling && (!found || y > OutY)) + { + OutY = y; + found = true; + } + } + return found; + } + + private: + int column(double const X) const + { + return std::clamp(static_cast(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1); + } + int row(double const Z) const + { + return std::clamp(static_cast(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1); + } + + std::vector m_triangles; + std::vector> m_cells; + glm::dvec2 m_min; + double m_cellsize{8.0}; + int m_columns{1}; + int m_rows{1}; + }; + + // appends triangles of a shape node which overlap specified XZ rectangle + void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) + { + auto const &data = Shape.data(); + if (data.area.radius >= 0.0f) + { + double const r = data.area.radius; + if (data.area.center.x + r < Min.x || data.area.center.x - r > Max.x || data.area.center.z + r < Min.y || data.area.center.z - r > Max.y) + return; + } + + auto const add = [&](glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c) { + if (std::max({a.x, b.x, c.x}) < Min.x || std::min({a.x, b.x, c.x}) > Max.x || std::max({a.z, b.z, c.z}) < Min.y || std::min({a.z, b.z, c.z}) > Max.y) + return; + Out.push_back({a, b, c}); + }; + + if (false == data.vertices.empty()) + { + for (std::size_t i = 0; i + 2 < data.vertices.size(); i += 3) + add(data.vertices[i].position, data.vertices[i + 1].position, data.vertices[i + 2].position); + return; + } + // once the shape is uploaded its world-space source is released; the renderer keeps an origin-relative copy + if (data.geometry.bank == 0 && data.geometry.chunk == 0) + return; + auto const &verts = GfxRenderer->Vertices(data.geometry); + auto const &indices = GfxRenderer->Indices(data.geometry); + auto const to_world = [&](gfx::basic_vertex const &v) { return data.origin + glm::dvec3(v.position); }; + if (false == indices.empty()) + { + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) + add(to_world(verts[indices[i]]), to_world(verts[indices[i + 1]]), to_world(verts[indices[i + 2]])); + } + else + { + for (std::size_t i = 0; i + 2 < verts.size(); i += 3) + add(to_world(verts[i]), to_world(verts[i + 1]), to_world(verts[i + 2])); + } + } + } bool editor_mode::editormode_input::init() @@ -306,32 +468,38 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button) GfxRenderer->Pick_Node_Callback( [this, mode, rotation_mode, fixed_rotation_value, Action, Button](scene::basic_node * /*node*/) { + // picks are resolved a few frames later; drop the ones queued before the button went up, + // or for a press that turned out to belong to the UI + if (!mouseHold || !viewport_click()) + return; + const std::string *src = ui()->get_active_node_template(); if (!src) return; std::string name = "editor_"; - glm::dvec3 newPos = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); - double distance = glm::distance(newPos, oldPos); - if (distance < ui()->getSpacing()) + // spacing is measured in world space: the mouse offset is camera-relative and the camera may move mid-stroke + glm::dvec3 const newPos = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); + if (m_brush_has_last && glm::distance(newPos, oldPos) < ui()->getSpacing()) return; - TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + newPos); + TAnimModel *cloned = simulation::State.create_model(*src, name, newPos); oldPos = newPos; + m_brush_has_last = true; if (!cloned) return; std::string new_name = "editor_" + cloned->uuid.to_string(); cloned->m_name = new_name; - + apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); + std::string as_text; cloned->export_as_text(as_text); push_snapshot(cloned, EditorSnapshot::Action::Add, as_text); m_node = cloned; - apply_rotation_for_new_node(m_node, rotation_mode, fixed_rotation_value); ui()->set_node(m_node); }); } @@ -439,6 +607,9 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) if (s.node_ptr == node) s.node_ptr = nullptr; } + + // deleted nodes also drop out of the "undo last fill" set + m_fill_last.erase(std::remove(m_fill_last.begin(), m_fill_last.end(), node), m_fill_last.end()); } void editor_mode::undo_last() @@ -626,6 +797,19 @@ bool editor_mode::update() Application.set_cursor(GLFW_CURSOR_NORMAL); } + // same for the left button (brush / sculpt / click placement). the UI filter in the application + // uses io.WantCaptureMouse from the previous frame, so a press on a panel the cursor has just + // moved onto can leak into the viewport, and its release is then swallowed by the UI; the brush + // would keep painting with the button up. by now ImGui has processed this frame's input, so + // MouseDown is the real button state and MouseDownOwned tells whether the press hit the UI. + // (pick callbacks queued by such a press are discarded separately, see viewport_click()) + if (mouseHold && (!ImGui::GetIO().MouseDown[0] || !viewport_click())) + { + mouseHold = false; + m_dragging = false; + m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE); + } + // fixed step render time routines (50 Hz) fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable while (fTime50Hz >= 1.0 / 50.0) @@ -708,6 +892,13 @@ bool editor_mode::update() // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); + // --- area fill: outline overlay and settings, while the mode is active --- + if (ui()->mode() == nodebank_panel::FILL) + { + draw_area_fill_outline(); + render_area_fill(); + } + // --- ImGui: Editor Settings & History windows --- if(m_settings_open) render_settings(); @@ -1097,18 +1288,8 @@ void editor_mode::capture_terrain() return; } - // instance world transform, matching the renderer: translate * rotateY * rotateX * rotateZ * scale - glm::dmat4 rootm(1.0); - rootm = glm::translate(rootm, model->location()); - glm::vec3 const angles = model->Angles(); - if (angles.y != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast(angles.y)), glm::dvec3(0.0, 1.0, 0.0)); - if (angles.x != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast(angles.x)), glm::dvec3(1.0, 0.0, 0.0)); - if (angles.z != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast(angles.z)), glm::dvec3(0.0, 0.0, 1.0)); - glm::vec3 const scale = model->Scale(); - rootm = glm::scale(rootm, glm::dvec3(scale)); - std::vector tris; - gather_submodel_triangles(model->pModel->Root, rootm, tris); + gather_submodel_triangles(model->pModel->Root, instance_matrix(*model), tris); if (tris.empty()) { WriteLog("Editor: selected model has no readable geometry to capture", logtype::generic); @@ -1174,6 +1355,366 @@ void editor_mode::capture_terrain() simulation::State.delete_model(model); } +void editor_mode::add_area_fill_point() +{ + // like the other placement tools, the point comes from the depth buffer under the cursor + // (ground geometry only), relative to the camera + glm::dvec3 const offset = GfxRenderer->Mouse_Position(); + if (glm::length(offset) < 1e-3) + return; // no ground under the cursor (yet) + + m_fill_points.push_back(Camera.Pos + offset); +} + +void editor_mode::undo_last_area_fill() +{ + // nullify_history_pointers() removes deleted nodes from m_fill_last, so work on a copy + auto const created = std::move(m_fill_last); + m_fill_last.clear(); + + for (TAnimModel *model : created) + { + if (model == nullptr) + continue; + if (m_node == model) + { + m_node = nullptr; + ui()->set_node(nullptr); + } + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + } + m_fill_status = "Removed " + std::to_string(created.size()) + " objects of the last fill"; +} + +void editor_mode::run_area_fill() +{ + // limits keeping a single click from freezing the editor for too long + std::size_t constexpr max_objects = 20000; + // large model instances (e.g. terrain tiles) are treated as ground, smaller ones (houses, trees) are not + float constexpr terrain_model_radius = 50.0f; + // ground is searched up to this far above the height interpolated from the outline points, + // so hills inside the outline are found but bridges or roofs high above it are not + double constexpr height_tolerance = 50.0; + + if (m_fill_points.size() < 3) + { + m_fill_status = "Outline needs at least 3 points"; + return; + } + + // model package + std::vector package; + if (m_fill_package == 0) + package = m_fill_custom; + else + package = ui()->nodebank_group_templates(static_cast(m_fill_package - 1)); + package.erase(std::remove_if(package.begin(), package.end(), [](std::string const &Template) { return Template.empty(); }), package.end()); + if (package.empty()) + { + m_fill_status = "Selected model package is empty"; + return; + } + + // area bounds + glm::dvec2 bmin(std::numeric_limits::max()); + glm::dvec2 bmax(-std::numeric_limits::max()); + for (auto const &p : m_fill_points) + { + bmin = glm::min(bmin, glm::dvec2(p.x, p.z)); + bmax = glm::max(bmax, glm::dvec2(p.x, p.z)); + } + double const area = polygon_area_xz(m_fill_points); + auto const target = static_cast(std::min(std::round(area / 10000.0 * std::max(0.0f, m_fill_density)), max_objects)); + if (target == 0) + { + m_fill_status = "Density too low for this area, nothing to place"; + return; + } + + // scatter points: random samples inside the outline, rejecting the ones closer than the minimal spacing + // to an already accepted point (checked on a hashed grid with cell size == spacing) + double const spacing = std::max(0.0f, m_fill_min_spacing); + std::unordered_map> occupancy; + auto const cell_key = [](std::int64_t const X, std::int64_t const Z) { return (X << 32) ^ (Z & 0xffffffff); }; + std::vector points; + points.reserve(target); + for (std::size_t attempt = 0; attempt < target * 30 && points.size() < target; ++attempt) + { + glm::dvec2 const p(LocalRandom(bmin.x, bmax.x), LocalRandom(bmin.y, bmax.y)); + if (false == point_in_polygon_xz(m_fill_points, p.x, p.y)) + continue; + if (spacing > 0.0) + { + auto const cx = static_cast(std::floor(p.x / spacing)); + auto const cz = static_cast(std::floor(p.y / spacing)); + bool free = true; + for (std::int64_t dz = -1; dz <= 1 && free; ++dz) + for (std::int64_t dx = -1; dx <= 1 && free; ++dx) + { + auto const lookup = occupancy.find(cell_key(cx + dx, cz + dz)); + if (lookup == occupancy.end()) + continue; + for (auto const idx : lookup->second) + if (glm::distance(points[idx], p) < spacing) + { + free = false; + break; + } + } + if (!free) + continue; + occupancy[cell_key(cx, cz)].push_back(points.size()); + } + points.push_back(p); + } + + // ground geometry within the area. shapes hold the (legacy) terrain, large instances the terrain tiles + std::vector triangles; + glm::dvec2 const center = (bmin + bmax) * 0.5; + float const radius = static_cast(glm::length(bmax - bmin) * 0.5); + auto const sections = simulation::Region->sections(glm::dvec3(center.x, 0.0, center.y), radius); // copy, the result is a scratchpad + for (auto *section : sections) + { + for (auto const &shape : section->m_shapes) + gather_shape_triangles(shape, bmin, bmax, triangles); + for (auto &cell : section->m_cells) + { + double const r = cell.m_area.radius; + if (cell.m_area.center.x + r < bmin.x || cell.m_area.center.x - r > bmax.x || cell.m_area.center.z + r < bmin.y || cell.m_area.center.z - r > bmax.y) + continue; + for (auto const &shape : cell.m_shapesopaque) + gather_shape_triangles(shape, bmin, bmax, triangles); + if (false == m_fill_models_as_ground) + continue; + for (auto *instance : cell.m_instancesopaque) + { + if (instance == nullptr || instance->Model() == nullptr || instance->radius() < terrain_model_radius) + continue; + std::vector modeltriangles; + gather_submodel_triangles(instance->Model()->Root, instance_matrix(*instance), modeltriangles); + for (auto const &t : modeltriangles) + if (std::max({t[0].x, t[1].x, t[2].x}) >= bmin.x && std::min({t[0].x, t[1].x, t[2].x}) <= bmax.x && std::max({t[0].z, t[1].z, t[2].z}) >= bmin.y && + std::min({t[0].z, t[1].z, t[2].z}) <= bmax.y) + triangles.push_back(t); + } + } + } + triangle_grid const ground(std::move(triangles), bmin, bmax); + auto const terrains = active_terrains(); + + auto const rotation_mode = m_fill_random_rotation ? functions_panel::RANDOM : ui()->rot_mode(); + auto const fixed_rotation_value = ui()->rot_val(); + float const scalemin = std::max(0.01f, std::min(m_fill_scale_min, m_fill_scale_max)); + float const scalemax = std::max(0.01f, std::max(m_fill_scale_min, m_fill_scale_max)); + + std::vector created; + created.reserve(points.size()); + std::size_t unsupported = 0; + for (auto const &p : points) + { + // reference height: the outline points, weighted by inverse squared distance + double weights = 0.0, reference = 0.0; + for (auto const &v : m_fill_points) + { + double const w = 1.0 / std::max(1e-6, glm::length2(glm::dvec2(v.x, v.z) - p)); + weights += w; + reference += w * v.y; + } + reference /= weights; + double const ceiling = reference + height_tolerance; + + double y = -std::numeric_limits::max(); + bool found = ground.height_at(p.x, p.y, ceiling, y); + for (editor_terrain *terrain : terrains) + { + if (!terrain->contains(p.x, p.y)) + continue; + double const h = terrain->height_at(p.x, p.y); + if (h <= ceiling && (!found || h > y)) + { + y = h; + found = true; + } + } + if (!found) + { + y = reference; + ++unsupported; + } + + glm::dvec3 const location(p.x, y, p.y); + if (!simulation::Region->point_inside(location)) + continue; + + auto const &src = package[package.size() > 1 ? std::min(package.size() - 1, static_cast(LocalRandom(0.0, static_cast(package.size())))) : 0]; + TAnimModel *cloned = simulation::State.create_model(src, "editor_", location); + if (!cloned) + continue; + cloned->m_name = "editor_" + cloned->uuid.to_string(); + apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); + if (scalemin != 1.0f || scalemax != 1.0f) + cloned->Scale(static_cast(LocalRandom(scalemin, scalemax))); + created.push_back(cloned); + } + + // a new fill replaces the "last fill" undo set; the previous objects stay in the scene + m_fill_last = std::move(created); + + m_fill_status = "Placed " + std::to_string(m_fill_last.size()) + " of " + std::to_string(target) + " objects"; + if (points.size() < target) + m_fill_status += " (limited by min. spacing)"; + if (unsupported > 0) + m_fill_status += ", " + std::to_string(unsupported) + " without ground found (height interpolated)"; + WriteLog("Editor: area fill - " + m_fill_status, logtype::generic); +} + +void editor_mode::draw_area_fill_outline() const +{ + if (m_fill_points.empty()) + return; + + // same camera-relative view and clean perspective the transform gizmo uses + ImGuiIO const &io = ImGui::GetIO(); + glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); + glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); + float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); + float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; + glm::mat4 const viewprojection = glm::perspective(fovy, aspect, 0.1f, 10000.0f) * view; + + auto const to_clip = [&](glm::dvec3 const &Point) { return viewprojection * glm::vec4(glm::vec3(Point - camerapos), 1.0f); }; + auto const to_screen = [&](glm::vec4 const &Clip) { + return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * io.DisplaySize.x, (0.5f - Clip.y / Clip.w * 0.5f) * io.DisplaySize.y); + }; + float constexpr nearw = 0.1f; + + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + ImU32 const edgecolor = IM_COL32(255, 200, 40, 230); + ImU32 const closingcolor = IM_COL32(255, 200, 40, 110); + + std::size_t const count = m_fill_points.size(); + for (std::size_t i = 0; i < count; ++i) + { + bool const closing = (i + 1 == count); + if (closing && count < 3) + break; + glm::vec4 a = to_clip(m_fill_points[i]); + glm::vec4 b = to_clip(m_fill_points[(i + 1) % count]); + // clip the edge against the near plane, so points behind the camera don't flip across the screen + if (a.w < nearw && b.w < nearw) + continue; + if (a.w < nearw) + a = glm::mix(a, b, (nearw - a.w) / (b.w - a.w)); + else if (b.w < nearw) + b = glm::mix(b, a, (nearw - b.w) / (a.w - b.w)); + drawlist->AddLine(to_screen(a), to_screen(b), closing ? closingcolor : edgecolor, 2.0f); + } + for (std::size_t i = 0; i < count; ++i) + { + glm::vec4 const c = to_clip(m_fill_points[i]); + if (c.w < nearw) + continue; + drawlist->AddCircleFilled(to_screen(c), i == 0 ? 6.0f : 4.0f, i == 0 ? IM_COL32(255, 90, 40, 255) : edgecolor); + } +} + +void editor_mode::render_area_fill() +{ + ImGui::Begin("Area fill", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing); + + ImGui::TextUnformatted("LMB: add outline point, Backspace: remove last point"); + double const area = polygon_area_xz(m_fill_points); + ImGui::Text("Points: %zu Area: %.0f m2 (%.2f ha)", m_fill_points.size(), area, area / 10000.0); + if (ImGui::Button("Remove last point") && !m_fill_points.empty()) + m_fill_points.pop_back(); + ImGui::SameLine(); + if (ImGui::Button("Clear outline")) + m_fill_points.clear(); + + ImGui::Separator(); + + // model package: a node bank group, or a custom set assembled from node bank templates + auto const groups = ui()->nodebank_groups(); + m_fill_package = std::clamp(m_fill_package, 0, static_cast(groups.size())); + std::string const current = m_fill_package == 0 ? std::string("Custom set") : groups[m_fill_package - 1]; + ImGui::SetNextItemWidth(250.0f); + if (ImGui::BeginCombo("Model package", current.c_str())) + { + if (ImGui::Selectable("Custom set", m_fill_package == 0)) + m_fill_package = 0; + for (int i = 0; i < static_cast(groups.size()); ++i) + { + auto const label = groups[i] + "##fillgroup" + std::to_string(i); + if (ImGui::Selectable(label.c_str(), m_fill_package == i + 1)) + m_fill_package = i + 1; + } + ImGui::EndCombo(); + } + + if (m_fill_package == 0) + { + ImGui::BeginChild("fill_custom_set", ImVec2(350.0f, 110.0f), true); + for (int i = 0; i < static_cast(m_fill_custom.size()); ++i) + { + auto const label = template_label(m_fill_custom[i]) + "##fillcustom" + std::to_string(i); + if (ImGui::Selectable(label.c_str(), m_fill_custom_idx == i)) + m_fill_custom_idx = i; + } + ImGui::EndChild(); + if (ImGui::Button("Add selected template")) + { + // the node bank selection, not a random pick from the brush set + auto const *src = ui()->get_active_node_template(true); + if (src && !src->empty()) + m_fill_custom.push_back(*src); + } + ImGui::SameLine(); + if (ImGui::Button("Remove") && m_fill_custom_idx >= 0 && m_fill_custom_idx < static_cast(m_fill_custom.size())) + { + m_fill_custom.erase(m_fill_custom.begin() + m_fill_custom_idx); + m_fill_custom_idx = -1; + } + ImGui::SameLine(); + if (ImGui::Button("Clear set")) + { + m_fill_custom.clear(); + m_fill_custom_idx = -1; + } + } + else + { + ImGui::Text("Templates in group: %zu", ui()->nodebank_group_templates(static_cast(m_fill_package - 1)).size()); + } + + ImGui::Separator(); + + ImGui::SetNextItemWidth(120.0f); + ImGui::InputFloat("Density (objects/ha)", &m_fill_density, 10.0f, 100.0f, "%.1f"); + m_fill_density = std::max(0.0f, m_fill_density); + ImGui::SetNextItemWidth(120.0f); + ImGui::InputFloat("Min. spacing (m)", &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); + m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing); + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f"); + ImGui::Checkbox("Random rotation (off: Functions panel setting)", &m_fill_random_rotation); + ImGui::Checkbox("Large models count as ground (terrain tiles)", &m_fill_models_as_ground); + + ImGui::Text("Estimated objects: %.0f", std::round(area / 10000.0 * m_fill_density)); + + if (ImGui::Button("Fill area")) + run_area_fill(); + ImGui::SameLine(); + std::string const undolabel = "Undo last fill (" + std::to_string(m_fill_last.size()) + ")"; + if (ImGui::Button(undolabel.c_str()) && !m_fill_last.empty()) + undo_last_area_fill(); + + if (!m_fill_status.empty()) + ImGui::TextUnformatted(m_fill_status.c_str()); + + ImGui::End(); +} + void editor_mode::render_gizmo() { // the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse @@ -1381,6 +1922,7 @@ void editor_mode::exit() g_redo.clear(); m_history.clear(); + m_fill_last.clear(); // drop selection so a stale/dangling node pointer isn't used on the next editor session m_node = nullptr; @@ -1496,6 +2038,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in } break; + case GLFW_KEY_BACKSPACE: + // area fill: remove the last outline point + if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) + m_fill_points.pop_back(); + break; + case GLFW_KEY_F: if (is_press(Action)) { @@ -1565,11 +2113,30 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (is_press(Action)) { mouseHold = true; - m_node = nullptr; + m_brush_has_last = false; // every brush stroke starts with a placement - // delegate node picking behaviour depending on current panel mode + // in area fill mode the left button only adds outline points, it doesn't touch the selection + if (mode == nodebank_panel::FILL) + { + GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) { + if (viewport_click()) + add_area_fill_point(); + }); + m_input.mouse.button(Button, Action); + return; + } + + // delegate node picking behaviour depending on current panel mode. + // NOTE: the pick is resolved a few frames later, often after the button was already released, + // so the callback mustn't depend on the button still being held GfxRenderer->Pick_Node_Callback( [this, mode, rotation_mode, fixed_rotation_value](scene::basic_node *node) { + // the press turned out to be meant for the UI + if (!viewport_click()) + return; + + m_node = nullptr; + // ignore picks that are beyond allowed placement distance if (node) { double const dist = glm::distance(node->location(), glm::dvec3{Global.pCamera.Pos}); @@ -1578,9 +2145,6 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const } if (mode == nodebank_panel::MODIFY) { - if (!m_dragging) - return; - m_node = node; ui()->set_node(m_node); } @@ -1607,19 +2171,16 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (!cloned) return; - // record addition for undo - std::string as_text; std::string new_name = "editor_" + cloned->uuid.to_string(); - cloned->m_name = new_name; + apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); + + // record addition for undo (after rotation, so a redo recreates the same thing) + std::string as_text; cloned->export_as_text(as_text); push_snapshot(cloned, EditorSnapshot::Action::Add, as_text); - if (!m_dragging) - return; - m_node = cloned; - apply_rotation_for_new_node(m_node, rotation_mode, fixed_rotation_value); ui()->set_node(m_node); } }); diff --git a/application/editormode.h b/application/editormode.h index 29b1ad69..413b4225 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -19,6 +19,10 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrainStreamer.hpp" #include +#include +#include + +class TAnimModel; class editor_mode : public application_mode { @@ -122,7 +126,8 @@ class editor_mode : public application_mode scene::basic_node *m_node{nullptr}; // currently selected scene node bool m_takesnapshot{true}; // helper, hints whether snapshot of selected node(s) should be taken before modification bool m_dragging = false; - glm::dvec3 oldPos; + glm::dvec3 oldPos; // world position of the last brush placement + bool m_brush_has_last{false}; // false at the start of a brush stroke, so the first placement ignores spacing bool mouseHold{false}; float kMaxPlacementDistance = 200.0f; static bool m_change_history; @@ -196,6 +201,25 @@ class editor_mode : public application_mode void render_change_history(); void render_settings(); + // area fill: scatters models from a package over a polygon outlined in the viewport + void render_area_fill(); + void draw_area_fill_outline() const; + void add_area_fill_point(); + void run_area_fill(); + void undo_last_area_fill(); + std::vector m_fill_points; // outline vertices (world space) in click order + std::vector m_fill_last; // instances created by the most recent fill, for "Undo last fill" + std::vector m_fill_custom; // user-assembled package of node templates + int m_fill_custom_idx{-1}; + int m_fill_package{0}; // 0 = custom set, N = nodebank group N-1 + float m_fill_density{200.0f}; // objects per hectare + float m_fill_min_spacing{2.0f}; // minimal distance between placed objects (m) + bool m_fill_random_rotation{true}; // random yaw; otherwise the Functions panel rotation settings apply + float m_fill_scale_min{1.0f}; + float m_fill_scale_max{1.0f}; + bool m_fill_models_as_ground{true}; // large model instances (terrain tiles) count as ground + std::string m_fill_status; // result of the last fill, shown in the panel + // ImGuizmo-based transform gizmo for the selected node enum class gizmo_operation { translate, rotate, scale }; void render_gizmo(); diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index d87dd8fa..b00b1e51 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -55,6 +55,16 @@ void editor_ui::toggleBrushSettings(bool isVisible) m_brushobjects.is_open = isVisible; } +std::vector editor_ui::nodebank_groups() const +{ + return m_nodebankpanel.group_names(); +} + +std::vector editor_ui::nodebank_group_templates(std::size_t const Group) const +{ + return m_nodebankpanel.group_templates(Group); +} + void editor_ui::set_node(scene::basic_node *Node) { m_node = Node; diff --git a/application/editoruilayer.h b/application/editoruilayer.h index ad3d5447..57d75466 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -37,6 +37,8 @@ class editor_ui : public ui_layer nodebank_panel::edit_mode mode(); float getSpacing(); void toggleBrushSettings(bool isVisible); + std::vector nodebank_groups() const; + std::vector nodebank_group_templates(std::size_t const Group) const; private: // members diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index cbfe0ae7..80eec0ba 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -434,13 +434,13 @@ void brush_object_list::render() { Objects.erase(Objects.begin() + idx); } - if (ImGui::Button("Remove all") && idx >= 0 && idx < Objects.size()) + if (ImGui::Button("Remove all")) { Objects.clear(); } } - ImGui::End(); } + ImGui::End(); } nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) @@ -535,6 +535,8 @@ void nodebank_panel::render() ImGui::SameLine(); ImGui::RadioButton("Copy to bank", (int *)&mode, COPY); ImGui::SameLine(); + ImGui::RadioButton("Area fill", (int *)&mode, FILL); + ImGui::SameLine(); if (ImGui::Button("Reload Nodebank")) { nodebank_reload(); @@ -598,6 +600,43 @@ const std::string *nodebank_panel::get_active_template() return m_selectedtemplate.get(); } +std::vector nodebank_panel::group_names() const +{ + std::vector names; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + names.emplace_back(entry.first); + else if (names.empty()) + names.emplace_back("(ungrouped)"); // templates listed before the first header + } + return names; +} + +std::vector nodebank_panel::group_templates(std::size_t const Group) const +{ + std::vector templates; + std::size_t group{0}; + bool started{false}; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + { + // header: opens the next group, unless it's the very first entry + if (started) + ++group; + started = true; + continue; + } + started = true; + if (group == Group) + templates.emplace_back(*entry.second); + else if (group > Group) + break; + } + return templates; +} + std::string nodebank_panel::generate_node_label(std::string Input) const { diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 41da7173..50e8bf99 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -81,7 +81,8 @@ class nodebank_panel : public ui_panel MODIFY, COPY, ADD, - BRUSH + BRUSH, + FILL }; edit_mode mode = MODIFY; @@ -90,6 +91,10 @@ class nodebank_panel : public ui_panel void render() override; void add_template(const std::string &desc); const std::string *get_active_template(); + // nodebank groups (collapsing headers), in file order; entries listed before the first header form an unnamed group + std::vector group_names() const; + // node templates of the group with specified index (as returned by group_names()) + std::vector group_templates(std::size_t const Group) const; private: // methods: From 99c5a0090b63d931db62fe8b801493e447f0a681 Mon Sep 17 00:00:00 2001 From: jakubg1 <24206305+jakubg1@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:10:21 +0200 Subject: [PATCH 29/38] Remove brake position check from `TController::PrepareEngine()` The function would flag the consist as not ready (`iEngineActive` set to `false`) when the brake handle was at a cutoff position. Since this is what the brake lever is exactly starting at in the EW58 train, the train when asked to move the brake lever to operating position would not listen since the `control_tractive_and_braking_force()` function which is supposed to do that is gated behind the readiness flag in the `TController::Update()` function. This makes sure EW58 trains can be driven by AI once again instead of being stuck. I don't really know why the brake position check was there, since it would fail only if: - There is a brake handle - The brake handle is in the cutoff position - The brake handle is NOT in the operating position The third check seems redundant but curiously there is one case where this matters and it's... MHZ_EN57. Its operating and cutoff positions are both defined as `0`, which might be a hack, since the actual cutoff position in the MHZ_EN57 handle is meant to be `6`. --- vehicle/Driver.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 9dfd6731..6f3c4100 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -2857,10 +2857,7 @@ bool TController::PrepareEngine() && false == IsAnyLineBreakerOpen && ( false == IsAnyConverterPresent || true == IsAnyConverterEnabled ) && ( false == IsAnyCompressorPresent || true == IsAnyCompressorEnabled ) - && ( mvControlling->ScndPipePress > 4.5 || mvControlling->VeselVolume == 0.0 ) - && ( static_cast(mvOccupied->fBrakeCtrlPos) == static_cast(mvOccupied->Handle->GetPos(bh_RP)) - || static_cast(mvOccupied->fBrakeCtrlPos) != static_cast(mvOccupied->Handle->GetPos(bh_NP)) - || mvOccupied->BrakeHandle == TBrakeHandle::NoHandle ); + && ( mvControlling->ScndPipePress > 4.5 || mvControlling->VeselVolume == 0.0 ); } if( true == isready ) { From 403a6d1f9f6ed080a01da23218a21fcd8cca1894 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 1 Oct 2026 07:58:28 +0200 Subject: [PATCH 30/38] fix: yet other sonar sast fixes --- McZapkie/Mover.cpp | 19 ++++++++----------- rendering/opengl33renderer.cpp | 8 ++++---- rendering/openglrenderer.cpp | 8 ++++---- simulation/simulationenvironment.cpp | 6 +++--- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 68b88918..61bdc17a 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -2408,7 +2408,7 @@ bool TMoverParameters::IncMainCtrl(int CtrlSpeed) { ++MainCtrlPos; OK = true; - if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1 && MainCtrlActualPos != MainCtrlPos) + if (EIMCtrlType == 0 && ( SpeedCtrlAutoTurnOffFlag & 1 ) == 1 && MainCtrlActualPos != MainCtrlPos) { DecScndCtrl(2); SpeedCtrlUnit.IsActive = false; @@ -2591,7 +2591,7 @@ bool TMoverParameters::DecMainCtrl(int CtrlSpeed) { MainCtrlPos--; OK = true; - if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1 && MainCtrlActualPos != MainCtrlPos) + if (EIMCtrlType == 0 && (SpeedCtrlAutoTurnOffFlag & 1) == 1 && MainCtrlActualPos != MainCtrlPos) { DecScndCtrl(2); SpeedCtrlUnit.IsActive = false; @@ -2780,7 +2780,7 @@ bool TMoverParameters::IncScndCtrl(int CtrlSpeed) if (OK && EngineType == TEngineType::ElectricInductionMotor && ScndCtrlPosNo == 1 && MainCtrlPos > 0) { SpeedCtrlValue = Vel; - if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1) + if (EIMCtrlType == 0 && (SpeedCtrlAutoTurnOffFlag & 1) == 1) { MainCtrlActualPos = MainCtrlPos; } @@ -4586,14 +4586,11 @@ void TMoverParameters::UpdatePipePressure(double dt) auto const lowvoltagepower{Power24vIsAvailable || Power110vIsAvailable}; - if (true == RadioStopFlag || true == AlarmChainFlag || (true == EIMCtrlEmergency && LocalBrakePosA >= 1.0) || SecuritySystem.is_braking() || - (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive) - /* - // NOTE: disabled because 32 is 'load destroyed' flag, what does this have to do with emergency brake? - // (if it's supposed to be broken coupler, such event sets alarmchainflag instead when appropriate) - || ( true == TestFlag( EngDmgFlag, 32 ) ) - */ - || (0 == CabActive && InactiveCabFlag & activation::emergencybrake) || (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive)) + // EngDmgFlag 32 (load destroyed) used to trigger this too; dropped, it has nothing to do with + // the emergency brake (a broken coupler sets AlarmChainFlag instead) + if (RadioStopFlag || AlarmChainFlag || (EIMCtrlEmergency && LocalBrakePosA >= 1.0) || SecuritySystem.is_braking() || + (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive) || + (0 == CabActive && (InactiveCabFlag & activation::emergencybrake) != 0)) { EmergencyValveFlow = PF(0, PipePress, 0.15) * dt; } diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b9937816..06fad0cd 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -81,9 +81,9 @@ bool opengl33_renderer::Init(GLFWwindow *Window) } // rgb value for 5780 kelvin - Global.DayLight.diffuse[0] = 255.0f / 255.0f; - Global.DayLight.diffuse[1] = 242.0f / 255.0f; - Global.DayLight.diffuse[2] = 231.0f / 255.0f; + Global.DayLight.diffuse[0] = 1.0f; // 255 / 255 + Global.DayLight.diffuse[1] = 0.9490196f; // 242 / 255 + Global.DayLight.diffuse[2] = 0.9058824f; // 231 / 255 Global.DayLight.is_directional = true; m_sunlight.id = opengl33_renderer::sunlight; @@ -1956,7 +1956,7 @@ bool opengl33_renderer::Render(world_environment *Environment) // moon { Bind_Texture(0, m_moontexture); - glm::vec3 mooncolor(255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f); + glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); glm::vec4 color(mooncolor.r, mooncolor.g, mooncolor.b, // fade the moon if it's near the sun in the sky, especially during the day std::max(0.f, 1.0 - 0.5 * Global.fLuminance - 0.65 * std::max(0.f, glm::dot(Environment->m_sun.getDirection(), Environment->m_moon.getDirection()))) * fogfactor); diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 6f06e9b3..dcbd1df2 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -88,9 +88,9 @@ opengl_renderer::Init( GLFWwindow *Window ) { ::glEnable( opengl_renderer::sunlight ); // rgb value for 5780 kelvin - Global.DayLight.diffuse[ 0 ] = 255.0f / 255.0f; - Global.DayLight.diffuse[ 1 ] = 242.0f / 255.0f; - Global.DayLight.diffuse[ 2 ] = 231.0f / 255.0f; + Global.DayLight.diffuse[0] = 1.0f; // 255 / 255 + Global.DayLight.diffuse[1] = 0.9490196f; // 242 / 255 + Global.DayLight.diffuse[2] = 0.9058824f; // 231 / 255 Global.DayLight.is_directional = true; m_sunlight.id = opengl_renderer::sunlight; // ::glLightf( opengl_renderer::sunlight, GL_SPOT_CUTOFF, 90.0f ); @@ -1603,7 +1603,7 @@ opengl_renderer::Render( world_environment *Environment ) { if (!m_isATI) { Bind_Texture( m_moontexture ); - glm::vec3 mooncolor( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ); + glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); ::glColor4f( mooncolor.r, mooncolor.g, mooncolor.b, // fade the moon if it's near the sun in the sky, especially during the day diff --git a/simulation/simulationenvironment.cpp b/simulation/simulationenvironment.cpp index 9837e0e8..b96cea20 100644 --- a/simulation/simulationenvironment.cpp +++ b/simulation/simulationenvironment.cpp @@ -140,7 +140,7 @@ world_environment::update() { m_lightintensity = moonlightlevel; // if the moon is up, it overrides the twilight twilightfactor = 0.0f; - keylightcolor = glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 202.0f / 255.0f ); + keylightcolor = glm::vec3( 1.0f, 0.9490196f, 0.7921569f ); } else { // regular situation with sun as the key light @@ -152,8 +152,8 @@ world_environment::update() { // include 'golden hour' effect in twilight lighting float const duskfactor = 1.25f - std::clamp( Global.SunAngle, 0.0f, 18.0f ) / 18.0f; keylightcolor = glm::mix( - glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ), - glm::vec3( 235.0f / 255.0f, 120.0f / 255.0f, 36.0f / 255.0f ), + glm::vec3( 1.0f, 0.9490196f, 0.9058824f ), // 255, 242, 231 / 255 + glm::vec3( 0.9215686f, 0.4705882f, 0.1411765f ), // 235, 120, 36 / 255 duskfactor ); } // ...retrieve current sky colour and brightness... From f05b58b0e9995991e841c6562f36f8585d761af7 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 1 Oct 2026 08:01:23 +0200 Subject: [PATCH 31/38] Revert "fix: yet other sonar sast fixes" This reverts commit 403a6d1f9f6ed080a01da23218a21fcd8cca1894. --- McZapkie/Mover.cpp | 19 +++++++++++-------- rendering/opengl33renderer.cpp | 8 ++++---- rendering/openglrenderer.cpp | 8 ++++---- simulation/simulationenvironment.cpp | 6 +++--- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 61bdc17a..68b88918 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -2408,7 +2408,7 @@ bool TMoverParameters::IncMainCtrl(int CtrlSpeed) { ++MainCtrlPos; OK = true; - if (EIMCtrlType == 0 && ( SpeedCtrlAutoTurnOffFlag & 1 ) == 1 && MainCtrlActualPos != MainCtrlPos) + if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1 && MainCtrlActualPos != MainCtrlPos) { DecScndCtrl(2); SpeedCtrlUnit.IsActive = false; @@ -2591,7 +2591,7 @@ bool TMoverParameters::DecMainCtrl(int CtrlSpeed) { MainCtrlPos--; OK = true; - if (EIMCtrlType == 0 && (SpeedCtrlAutoTurnOffFlag & 1) == 1 && MainCtrlActualPos != MainCtrlPos) + if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1 && MainCtrlActualPos != MainCtrlPos) { DecScndCtrl(2); SpeedCtrlUnit.IsActive = false; @@ -2780,7 +2780,7 @@ bool TMoverParameters::IncScndCtrl(int CtrlSpeed) if (OK && EngineType == TEngineType::ElectricInductionMotor && ScndCtrlPosNo == 1 && MainCtrlPos > 0) { SpeedCtrlValue = Vel; - if (EIMCtrlType == 0 && (SpeedCtrlAutoTurnOffFlag & 1) == 1) + if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1) { MainCtrlActualPos = MainCtrlPos; } @@ -4586,11 +4586,14 @@ void TMoverParameters::UpdatePipePressure(double dt) auto const lowvoltagepower{Power24vIsAvailable || Power110vIsAvailable}; - // EngDmgFlag 32 (load destroyed) used to trigger this too; dropped, it has nothing to do with - // the emergency brake (a broken coupler sets AlarmChainFlag instead) - if (RadioStopFlag || AlarmChainFlag || (EIMCtrlEmergency && LocalBrakePosA >= 1.0) || SecuritySystem.is_braking() || - (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive) || - (0 == CabActive && (InactiveCabFlag & activation::emergencybrake) != 0)) + if (true == RadioStopFlag || true == AlarmChainFlag || (true == EIMCtrlEmergency && LocalBrakePosA >= 1.0) || SecuritySystem.is_braking() || + (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive) + /* + // NOTE: disabled because 32 is 'load destroyed' flag, what does this have to do with emergency brake? + // (if it's supposed to be broken coupler, such event sets alarmchainflag instead when appropriate) + || ( true == TestFlag( EngDmgFlag, 32 ) ) + */ + || (0 == CabActive && InactiveCabFlag & activation::emergencybrake) || (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive)) { EmergencyValveFlow = PF(0, PipePress, 0.15) * dt; } diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 06fad0cd..b9937816 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -81,9 +81,9 @@ bool opengl33_renderer::Init(GLFWwindow *Window) } // rgb value for 5780 kelvin - Global.DayLight.diffuse[0] = 1.0f; // 255 / 255 - Global.DayLight.diffuse[1] = 0.9490196f; // 242 / 255 - Global.DayLight.diffuse[2] = 0.9058824f; // 231 / 255 + Global.DayLight.diffuse[0] = 255.0f / 255.0f; + Global.DayLight.diffuse[1] = 242.0f / 255.0f; + Global.DayLight.diffuse[2] = 231.0f / 255.0f; Global.DayLight.is_directional = true; m_sunlight.id = opengl33_renderer::sunlight; @@ -1956,7 +1956,7 @@ bool opengl33_renderer::Render(world_environment *Environment) // moon { Bind_Texture(0, m_moontexture); - glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); + glm::vec3 mooncolor(255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f); glm::vec4 color(mooncolor.r, mooncolor.g, mooncolor.b, // fade the moon if it's near the sun in the sky, especially during the day std::max(0.f, 1.0 - 0.5 * Global.fLuminance - 0.65 * std::max(0.f, glm::dot(Environment->m_sun.getDirection(), Environment->m_moon.getDirection()))) * fogfactor); diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index dcbd1df2..6f06e9b3 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -88,9 +88,9 @@ opengl_renderer::Init( GLFWwindow *Window ) { ::glEnable( opengl_renderer::sunlight ); // rgb value for 5780 kelvin - Global.DayLight.diffuse[0] = 1.0f; // 255 / 255 - Global.DayLight.diffuse[1] = 0.9490196f; // 242 / 255 - Global.DayLight.diffuse[2] = 0.9058824f; // 231 / 255 + Global.DayLight.diffuse[ 0 ] = 255.0f / 255.0f; + Global.DayLight.diffuse[ 1 ] = 242.0f / 255.0f; + Global.DayLight.diffuse[ 2 ] = 231.0f / 255.0f; Global.DayLight.is_directional = true; m_sunlight.id = opengl_renderer::sunlight; // ::glLightf( opengl_renderer::sunlight, GL_SPOT_CUTOFF, 90.0f ); @@ -1603,7 +1603,7 @@ opengl_renderer::Render( world_environment *Environment ) { if (!m_isATI) { Bind_Texture( m_moontexture ); - glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); + glm::vec3 mooncolor( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ); ::glColor4f( mooncolor.r, mooncolor.g, mooncolor.b, // fade the moon if it's near the sun in the sky, especially during the day diff --git a/simulation/simulationenvironment.cpp b/simulation/simulationenvironment.cpp index b96cea20..9837e0e8 100644 --- a/simulation/simulationenvironment.cpp +++ b/simulation/simulationenvironment.cpp @@ -140,7 +140,7 @@ world_environment::update() { m_lightintensity = moonlightlevel; // if the moon is up, it overrides the twilight twilightfactor = 0.0f; - keylightcolor = glm::vec3( 1.0f, 0.9490196f, 0.7921569f ); + keylightcolor = glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 202.0f / 255.0f ); } else { // regular situation with sun as the key light @@ -152,8 +152,8 @@ world_environment::update() { // include 'golden hour' effect in twilight lighting float const duskfactor = 1.25f - std::clamp( Global.SunAngle, 0.0f, 18.0f ) / 18.0f; keylightcolor = glm::mix( - glm::vec3( 1.0f, 0.9490196f, 0.9058824f ), // 255, 242, 231 / 255 - glm::vec3( 0.9215686f, 0.4705882f, 0.1411765f ), // 235, 120, 36 / 255 + glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ), + glm::vec3( 235.0f / 255.0f, 120.0f / 255.0f, 36.0f / 255.0f ), duskfactor ); } // ...retrieve current sky colour and brightness... From 20f536f05e68acb9e3a9fe1140cbd310b29cded2 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 11:51:41 +0200 Subject: [PATCH 32/38] fix(instancing): keep batches correct after load-time classification --- model/AnimModel.cpp | 5 + rendering/opengl33geometrybank.cpp | 35 +++++-- rendering/opengl33renderer.cpp | 156 ++++++++++++++++++++--------- rendering/opengl33renderer.h | 7 +- scene/scene.cpp | 26 ++--- 5 files changed, 158 insertions(+), 71 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index bc2d6e84..d5d856e0 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -478,6 +478,9 @@ std::shared_ptr TAnimModel::AddContainer(std::string const &Name auto tmp = std::make_shared(); tmp->Init(tsb); m_animlist.push_back(tmp); + // containers are added by events after the instance was loaded and bucketed; + // the instanced path skips RaAnimate(), so this instance has to be drawn on its own + m_instanceable = false; return tmp; } return nullptr; @@ -688,6 +691,8 @@ std::optional> > TAnimModel::L void TAnimModel::SkinSet( int const Index, material_handle const Material ) { m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = Material; + // the instance stays in the bucket of its old skin set, where it would get the textures of the batch + m_instanceable = false; } void TAnimModel::AnimUpdate(double dt) diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 91c9a8dd..506d22f1 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -215,13 +215,34 @@ opengl33_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, GLsizei const inst_count = static_cast(InstanceCount); if( chunkrecord.index_count > 0 ) { - m_vao->bind(); - ::glDrawElementsInstancedBaseVertex( - chunk.type, - chunkrecord.index_count, GL_UNSIGNED_INT, - reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), - inst_count, - chunkrecord.vertex_offset ); + // GLES 3.0 context doesn't provide the base vertex variant, same fallbacks as in draw_() + if( glDrawElementsInstancedBaseVertex ) { + m_vao->bind(); + ::glDrawElementsInstancedBaseVertex( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count, + chunkrecord.vertex_offset ); + } + else if( glDrawElementsInstancedBaseVertexOES ) { + m_vao->bind(); + ::glDrawElementsInstancedBaseVertexOES( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count, + chunkrecord.vertex_offset ); + } + else { + setup_attrib( chunkrecord.vertex_offset ); + m_vao->bind(); + ::glDrawElementsInstanced( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count ); + } } else { m_vao->bind(); diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b9937816..352d9632 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2382,8 +2382,11 @@ void opengl33_renderer::Render(scene::basic_region *Region) m_sectionqueue.clear(); m_cellqueue.clear(); - // discard last pass's accumulated instance buckets before this pass starts - m_frame_instance_buckets.clear(); + // discard last pass's accumulated instance buckets before this pass starts. + // the vectors themselves are kept, so their allocations get reused by the following passes + for( auto &bucket : m_frame_instance_buckets ) { + bucket.second.clear(); + } // build a list of region sections to render glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; auto const camerax = static_cast(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); @@ -2802,10 +2805,11 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // still happens inside Render_Instanced(), so correctness is unchanged -- only // the number of calls and glBufferSubData round-trips drops sharply. // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) - for( auto const &bucket : m_frame_instance_buckets ) { + for( auto &bucket : m_frame_instance_buckets ) { + if( bucket.second.empty() ) { continue; } Render_Instanced( bucket.first.pModel, bucket.second ); + bucket.second.clear(); } - m_frame_instance_buckets.clear(); } void opengl33_renderer::Draw_Geometry(std::vector::iterator begin, std::vector::iterator end) @@ -3024,13 +3028,13 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max(); + float farthest_distancesquared = 0.f; material_data const *batch_material { nullptr }; for( auto *Instance : Instances ) { if( Instance == nullptr ) { continue; } + // instances which lost their instanceable status after being bucketed (animation or skin + // assigned by an event) are drawn by the per-node path + if( false == Instance->m_instanceable ) { continue; } if( false == Instance->m_visible ) { continue; } if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; } @@ -3077,6 +3085,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorm_framestamp = m_framestamp; closest_distancesquared = std::min( closest_distancesquared, static_cast(distancesquared) ); + farthest_distancesquared = std::max( farthest_distancesquared, static_cast(distancesquared) ); // Build the camera-relative root modelview for this instance: // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) @@ -3095,55 +3104,106 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( distancesquared ), mv ); } - if( m_instance_modelviews.empty() ) { return; } + if( m_instance_survivors.empty() ) { return; } + + // LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) + // range, so the distinct range bounds split distance into bands with constant LOD. The bucket + // collects instances from every visible cell, so a single fSquareDist for all of them would draw + // far instances at the LOD of the nearest one. When some bound falls inside the distance span of + // the survivors they're sorted near-to-far and drawn one band at a time, like Render_Sleepers(). + m_instance_lodbounds.clear(); + m_instance_lodpending.clear(); + m_instance_lodpending.push_back( Model->Root ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + if( ( submodel->fSquareMinDist > closest_distancesquared ) && ( submodel->fSquareMinDist <= farthest_distancesquared ) ) { + m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); + } + if( ( submodel->fSquareMaxDist > closest_distancesquared ) && ( submodel->fSquareMaxDist <= farthest_distancesquared ) ) { + m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); + } + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + if( false == m_instance_lodbounds.empty() ) { + std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); + m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); + std::sort( m_instance_survivors.begin(), m_instance_survivors.end(), + []( std::pair const &Left, std::pair const &Right ) { + return Left.first < Right.first; } ); + } + // contiguous copy of the (sorted) modelview matrices, for the UBO upload + m_instance_modelviews.clear(); + m_instance_modelviews.reserve( m_instance_survivors.size() ); + for( auto const &survivor : m_instance_survivors ) { + m_instance_modelviews.emplace_back( survivor.second ); + } + + auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); + alpha ^= 0x0F0F000F; // 2. Walk the submodel tree once per sub-batch. The submodel-local matrix // stack starts at identity; the per-instance camera transform comes from // instance_modelview[gl_InstanceID] in the shader. std::size_t const total = m_instance_modelviews.size(); - std::size_t offset_idx = 0; - while( offset_idx < total ) { - std::size_t const this_batch = std::min( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); - - // 2a. Upload N modelviews to instance_ubo[0..N-1]. - instance_ubo->update( - reinterpret_cast( m_instance_modelviews.data() + offset_idx ), - 0, - static_cast( this_batch * sizeof( glm::mat4 ) ) ); - - // 2b. Push identity onto the matrix stack so submodel transforms - // accumulate from identity, not from camera/instance space. - ::glPushMatrix(); - ::glLoadIdentity(); - - // 2c. Configure the existing TModel3d/TSubModel render path. Setting - // m_current_instance_count routes draw(handle) calls to draw_instanced. - m_current_instance_count = this_batch; - - Model->Root->fSquareDist = closest_distancesquared; // shared global, used by submodel LOD - auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); - alpha ^= 0x0F0F000F; - Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); - Model->Root->pRoot = Model; - - Render( Model->Root ); - - m_current_instance_count = 0; - - ::glPopMatrix(); - - // 2d. Restore instance_modelview[0] to identity so subsequent - // non-instanced draws continue to compute identity * modelview. - { - glm::mat4 const identity( 1.0f ); - instance_ubo->update( reinterpret_cast( &identity ), 0, sizeof( identity ) ); + std::size_t band_start = 0; + while( band_start < total ) { + // the band ends at the first instance distance which reaches the next lod bound + // (without bounds inside the distance span the whole set forms a single band) + auto const upperbound = std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), m_instance_survivors[ band_start ].first ); + float const band_limit = ( upperbound == m_instance_lodbounds.end() ? std::numeric_limits::max() : *upperbound ); + std::size_t band_end = band_start; + while( ( band_end < total ) && ( m_instance_survivors[ band_end ].first < band_limit ) ) { + ++band_end; } + // every distance in the band selects the same lod; the nearest one represents the band + float const band_distancesquared = ( m_instance_lodbounds.empty() ? closest_distancesquared : m_instance_survivors[ band_start ].first ); - offset_idx += this_batch; - ++m_renderpass.draw_stats.instanced_drawcalls; + std::size_t offset_idx = band_start; + while( offset_idx < band_end ) { + std::size_t const this_batch = std::min( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); + + // 2a. Upload N modelviews to instance_ubo[0..N-1]. + instance_ubo->update( + reinterpret_cast( m_instance_modelviews.data() + offset_idx ), + 0, + static_cast( this_batch * sizeof( glm::mat4 ) ) ); + + // 2b. Push identity onto the matrix stack so submodel transforms + // accumulate from identity, not from camera/instance space. + ::glPushMatrix(); + ::glLoadIdentity(); + + // 2c. Configure the existing TModel3d/TSubModel render path. Setting + // m_current_instance_count routes draw(handle) calls to draw_instanced. + m_current_instance_count = this_batch; + + Model->Root->fSquareDist = band_distancesquared; // shared global, used by submodel LOD + Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); + Model->Root->pRoot = Model; + + Render( Model->Root ); + + m_current_instance_count = 0; + + ::glPopMatrix(); + + // 2d. Restore instance_modelview[0] to identity so subsequent + // non-instanced draws continue to compute identity * modelview. + { + glm::mat4 const identity( 1.0f ); + instance_ubo->update( reinterpret_cast( &identity ), 0, sizeof( identity ) ); + } + + offset_idx += this_batch; + ++m_renderpass.draw_stats.instanced_drawcalls; + } + band_start = band_end; } m_renderpass.draw_stats.instances += static_cast( total ); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 6fda37c3..8e5c0b14 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -376,7 +376,7 @@ class opengl33_renderer : public gfx_renderer { // cell's buckets are merged here keyed by (TModel3d*, skins), so that // Render_Instanced() runs once per unique model across the whole pass instead // of once per cell -- collapsing many tiny instanced draws into a few large - // batches. Reused every pass; cleared at the top of Render(scene::basic_region*). + // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). scene::basic_cell::instance_bucket_map m_frame_instance_buckets; renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages @@ -426,6 +426,11 @@ class opengl33_renderer : public gfx_renderer { // reused across calls -- clear() retains capacity, so once it has grown to // the largest batch seen, steady-state frames perform no allocation here. std::vector m_instance_modelviews; + // further persistent scratch buffers for Render_Instanced(): culled instances paired with their + // squared distance, and the lod bounds of the drawn model which split them into lod bands + std::vector> m_instance_survivors; + std::vector m_instance_lodbounds; + std::vector m_instance_lodpending; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; diff --git a/scene/scene.cpp b/scene/scene.cpp index 14825705..8ebff031 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -442,22 +442,18 @@ basic_cell::erase( TAnimModel *Instance ) { [=]( TAnimModel *instance ) { return instance == Instance; } ), std::end( m_instancesopaque ) ); - // also remove from the per-(pModel, skins) instance bucket if present - if( Instance->m_instanceable && Instance->Model() != nullptr ) { - instance_bucket_key key; - key.pModel = Instance->Model(); - auto const *mat = Instance->Material(); - if( mat != nullptr ) { - for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } + // also remove from the per-(pModel, skins) instance bucket if present. + // NOTE: searched by pointer rather than by key: the instance may have lost its instanceable + // status or changed its skins since it was inserted, which would leave a dangling pointer behind + for( auto bucket = m_instancebuckets_opaque.begin(); bucket != m_instancebuckets_opaque.end(); ) { + bucket->second.erase( + std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), + std::end( bucket->second ) ); + if( bucket->second.empty() ) { + bucket = m_instancebuckets_opaque.erase( bucket ); } - auto bucket = m_instancebuckets_opaque.find( key ); - if( bucket != m_instancebuckets_opaque.end() ) { - bucket->second.erase( - std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), - std::end( bucket->second ) ); - if( bucket->second.empty() ) { - m_instancebuckets_opaque.erase( bucket ); - } + else { + ++bucket; } } } From 4eabf3c3b53dfcc61ef56fc21a667c8b0e98d695 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 12:07:54 +0200 Subject: [PATCH 33/38] feat(instancing): batch models animated only by time, wind or sky --- model/AnimModel.cpp | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index d5d856e0..09f01c8c 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -403,8 +403,8 @@ namespace { // TSubModel tree, which would make batched rendering unsafe. Most animations // loaded from .t3d files are global functions of time (clocks, wind, sky) and // only transform the local modelview matrix — those are safe to share. Camera- -// relative billboards also operate purely on the local matrix using whatever -// modelview the caller pushed, which is exactly per-instance behaviour. +// relative billboards are not: they're computed on the cpu from the modelview of +// the drawn instance, which in a batch is only the submodel-local chain. // The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ / // at_Translate) are tied to per-instance iAnimOwner and are unsafe to share. // at_Undefined is the type assigned to .t3d submodels declared with `anim: true` @@ -425,17 +425,40 @@ bool anim_type_unsafe_for_instancing( TAnimType a ) { } } +// returns true if this animation type depends only on global state (simulation +// time, wind, sky) and so gives every instance of the model the same local +// transform. The batch evaluates it once and the per-instance placement comes +// from the instance matrix, the result is the same as with per-instance drawing. +bool anim_type_shared_by_instances( TAnimType a ) { + switch( a ) { + case TAnimType::at_SecondsJump: + case TAnimType::at_MinutesJump: + case TAnimType::at_HoursJump: + case TAnimType::at_Hours24Jump: + case TAnimType::at_Seconds: + case TAnimType::at_Minutes: + case TAnimType::at_Hours: + case TAnimType::at_Hours24: + case TAnimType::at_Wind: + case TAnimType::at_Sky: + return true; + default: + return false; + } +} + // recursively walks a submodel tree and returns true if any submodel declares // an animation type that's unsafe to batch, OR carries the runtime "needs -// animation matrix" flag (iFlags bit 0x4000), which is set whenever the -// submodel was tagged as animatable in the .t3d file or had WillBeAnimated() -// called on it during model load. Either signal means the submodel may receive +// animation matrix" flag (iFlags bit 0x4000) for anything else than an animation +// shared by all instances. The flag is set whenever the submodel was tagged as +// animatable in the .t3d file (any `anim:` other than `false`) or had +// WillBeAnimated() called on it during model load. Such a submodel may receive // per-instance event-driven animation commands at runtime, which the GPU- // instanced path (one shared submodel tree across all instances) cannot serve. bool submodel_tree_blocks_instancing( TSubModel const *Sub ) { if( Sub == nullptr ) { return false; } if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; } - if( ( Sub->iFlags & 0x4000 ) != 0 ) { return true; } + if( ( ( Sub->iFlags & 0x4000 ) != 0 ) && ( false == anim_type_shared_by_instances( Sub->b_Anim ) ) ) { return true; } if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; } if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; } return false; From 2130d175e941a69b47b4ce47cbda8cb379923efc Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 12:43:07 +0200 Subject: [PATCH 34/38] perf(instancing): cache instance rotation and group lod bands linearly --- model/AnimModel.cpp | 17 +++++ model/AnimModel.h | 8 ++ rendering/opengl33renderer.cpp | 135 ++++++++++++++------------------- rendering/opengl33renderer.h | 11 ++- 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 09f01c8c..cc54a2a2 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -664,6 +664,23 @@ void TAnimModel::RaPrepare() } } +glm::mat4 const &TAnimModel::rotation_scale() { + + if( ( false == m_rotationscalevalid ) || ( vAngle != m_rotationscaleangles ) || ( m_scale != m_rotationscalescale ) ) { + // same order as the per-instance render path + glm::mat4 transform( 1.0f ); + if( vAngle.y != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); } + if( vAngle.x != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); } + if( vAngle.z != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); } + if( m_scale != glm::vec3( 1.0f ) ) { transform = glm::scale( transform, m_scale ); } + m_rotationscale = transform; + m_rotationscaleangles = vAngle; + m_rotationscalescale = m_scale; + m_rotationscalevalid = true; + } + return m_rotationscale; +} + int TAnimModel::Flags() { // informacja dla TGround, czy ma być w Render, RenderAlpha, czy RenderMixed int i = pModel ? pModel->Flags() : 0; // pobranie flag całego modelu diff --git a/model/AnimModel.h b/model/AnimModel.h index 435c1dba..3c4b9f11 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -172,6 +172,8 @@ public: public: // methods void RaPrepare(); // ustawienie animacji egzemplarza na wzorcu + // rotation and scale of the instance as a matrix: rotate y * rotate x * rotate z * scale + glm::mat4 const &rotation_scale(); void RaAnimate( unsigned int const Framestamp ); // przeliczenie animacji egzemplarza // radius() subclass details, calculates node's bounding radius @@ -190,6 +192,12 @@ public: TModel3d *pModel { nullptr }; glm::vec3 vAngle; // bazowe obroty egzemplarza względem osi glm::vec3 m_scale { 1.0f, 1.0f, 1.0f }; // per-axis scale (see Scale() accessors above) + // rotation and scale part of the instance transform, cached for the instanced render path. + // rebuilt by rotation_scale() whenever the angles or the scale differ from the ones it was built from + glm::mat4 m_rotationscale { 1.0f }; + glm::vec3 m_rotationscaleangles { 0.0f, 0.0f, 0.0f }; + glm::vec3 m_rotationscalescale { 1.0f, 1.0f, 1.0f }; + bool m_rotationscalevalid { false }; material_data m_materialdata; std::string asText; // tekst dla wyświetlacza znakowego diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 352d9632..f96ddffa 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -3026,13 +3026,33 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorRoot ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + if( submodel->fSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); } + if( submodel->fSquareMaxDist < std::numeric_limits::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); } + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); + m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); + auto const bandcount { m_instance_lodbounds.size() + 1 }; + + // 1. Visibility / distance cull. Collect surviving instances with their precomputed + // camera-space root modelview matrices and lod bands. m_instance_survivors and the other + // scratch vectors are persistent members reused across every Render_Instanced() call: + // clear() drops the contents but keeps the allocated capacity, so after the first few + // frames this stops calling malloc/free entirely. m_instance_survivors.clear(); m_instance_survivors.reserve( Instances.size() ); @@ -3043,8 +3063,6 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max(); - float farthest_distancesquared = 0.f; material_data const *batch_material { nullptr }; for( auto *Instance : Instances ) { @@ -3084,64 +3102,39 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorMaterial(); } Instance->m_framestamp = m_framestamp; - closest_distancesquared = std::min( closest_distancesquared, static_cast(distancesquared) ); - farthest_distancesquared = std::max( farthest_distancesquared, static_cast(distancesquared) ); // Build the camera-relative root modelview for this instance: // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) - // The scale is folded in here so the GPU-instanced path produces visually - // identical output to the regular per-instance path: each instance gets - // its own (translate × rotate × scale) baked into instance_modelview[i], - // which the shader applies via effective_modelview = instance_mv * model_local. - glm::dvec3 const offset = Instance->location() - m_renderpass.pass_camera.position(); - glm::mat4 mv = view_matrix; - mv = glm::translate( mv, glm::vec3( offset ) ); - auto const &angle = Instance->vAngle; - if( angle.y != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); } - if( angle.x != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); } - if( angle.z != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); } - auto const &scale = Instance->Scale(); - if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) { - mv = glm::scale( mv, scale ); - } - m_instance_survivors.emplace_back( static_cast( distancesquared ), mv ); + // The rotation and scale part is cached by the instance, the translation goes into its last + // column. The scale is folded in so the GPU-instanced path produces visually identical output + // to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local. + glm::mat4 root { Instance->rotation_scale() }; + root[ 3 ] = glm::vec4( glm::vec3( Instance->location() - m_renderpass.pass_camera.position() ), 1.f ); + + auto const distance { static_cast( distancesquared ) }; + m_instance_survivors.push_back( { + view_matrix * root, + distance, + static_cast( std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), distance ) - m_instance_lodbounds.begin() ) } ); } if( m_instance_survivors.empty() ) { return; } - // LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) - // range, so the distinct range bounds split distance into bands with constant LOD. The bucket - // collects instances from every visible cell, so a single fSquareDist for all of them would draw - // far instances at the LOD of the nearest one. When some bound falls inside the distance span of - // the survivors they're sorted near-to-far and drawn one band at a time, like Render_Sleepers(). - m_instance_lodbounds.clear(); - m_instance_lodpending.clear(); - m_instance_lodpending.push_back( Model->Root ); - while( false == m_instance_lodpending.empty() ) { - auto const *submodel = m_instance_lodpending.back(); - m_instance_lodpending.pop_back(); - if( submodel == nullptr ) { continue; } - if( ( submodel->fSquareMinDist > closest_distancesquared ) && ( submodel->fSquareMinDist <= farthest_distancesquared ) ) { - m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); - } - if( ( submodel->fSquareMaxDist > closest_distancesquared ) && ( submodel->fSquareMaxDist <= farthest_distancesquared ) ) { - m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); - } - m_instance_lodpending.push_back( submodel->Child ); - m_instance_lodpending.push_back( submodel->Next ); - } - if( false == m_instance_lodbounds.empty() ) { - std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); - m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); - std::sort( m_instance_survivors.begin(), m_instance_survivors.end(), - []( std::pair const &Left, std::pair const &Right ) { - return Left.first < Right.first; } ); - } - // contiguous copy of the (sorted) modelview matrices, for the UBO upload - m_instance_modelviews.clear(); - m_instance_modelviews.reserve( m_instance_survivors.size() ); + // group the modelviews by lod band (counting sort) into a contiguous array, for the UBO upload. + // every distance in a band selects the same lod; the nearest one represents the band + m_instance_bandstarts.assign( bandcount + 1, 0 ); + m_instance_banddistances.assign( bandcount, std::numeric_limits::max() ); for( auto const &survivor : m_instance_survivors ) { - m_instance_modelviews.emplace_back( survivor.second ); + ++m_instance_bandstarts[ survivor.band + 1 ]; + m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared ); + } + for( std::size_t band = 1; band <= bandcount; ++band ) { + m_instance_bandstarts[ band ] += m_instance_bandstarts[ band - 1 ]; + } + m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 ); + m_instance_modelviews.resize( m_instance_survivors.size() ); + for( auto const &survivor : m_instance_survivors ) { + m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview; } auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); @@ -3150,21 +3143,10 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max() : *upperbound ); - std::size_t band_end = band_start; - while( ( band_end < total ) && ( m_instance_survivors[ band_end ].first < band_limit ) ) { - ++band_end; - } - // every distance in the band selects the same lod; the nearest one represents the band - float const band_distancesquared = ( m_instance_lodbounds.empty() ? closest_distancesquared : m_instance_survivors[ band_start ].first ); + for( std::size_t band = 0; band < bandcount; ++band ) { - std::size_t offset_idx = band_start; + std::size_t offset_idx = m_instance_bandstarts[ band ]; + std::size_t const band_end = m_instance_bandstarts[ band + 1 ]; while( offset_idx < band_end ) { std::size_t const this_batch = std::min( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); @@ -3183,7 +3165,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorRoot->fSquareDist = band_distancesquared; // shared global, used by submodel LOD + Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); Model->Root->pRoot = Model; @@ -3203,11 +3185,10 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( total ); - m_renderpass.draw_stats.models += static_cast( total ); + m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); + m_renderpass.draw_stats.models += static_cast( m_instance_survivors.size() ); } // Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 8e5c0b14..2f86117f 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -428,9 +428,18 @@ class opengl33_renderer : public gfx_renderer { std::vector m_instance_modelviews; // further persistent scratch buffers for Render_Instanced(): culled instances paired with their // squared distance, and the lod bounds of the drawn model which split them into lod bands - std::vector> m_instance_survivors; + struct instance_survivor { + glm::mat4 modelview; + float distancesquared; + std::uint32_t band; + }; + std::vector m_instance_survivors; std::vector m_instance_lodbounds; std::vector m_instance_lodpending; + // per lod band: first index in m_instance_modelviews (with end sentinel), fill cursor, nearest distance + std::vector m_instance_bandstarts; + std::vector m_instance_bandcursors; + std::vector m_instance_banddistances; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; From d670c4b116fe1436b30da734d4b419ad7f339cfb Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 12:57:33 +0200 Subject: [PATCH 35/38] perf(instancing): skip models without shadow casters in shadow passes --- rendering/opengl33renderer.cpp | 40 ++++++++++++++++++++++++++-------- rendering/opengl33renderer.h | 3 ++- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index f96ddffa..aba5212a 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -3032,6 +3032,12 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorMaterial() != nullptr ? Instances.front()->Material()->replacable_skins : nullptr }; + bool castsshadow { m_renderpass.draw_mode != rendermode::shadows }; m_instance_lodbounds.clear(); m_instance_lodpending.clear(); m_instance_lodpending.push_back( Model->Root ); @@ -3041,9 +3047,14 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorfSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); } if( submodel->fSquareMaxDist < std::numeric_limits::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); } + if( ( false == castsshadow ) && ( submodel->eType < TP_ROTATOR ) ) { + auto const material { submodel->m_material >= 0 ? submodel->m_material : ( skins != nullptr ? skins[ -submodel->m_material ] : null_handle ) }; + castsshadow = ( Material( material ).shadow_rank <= Global.gfx_shadow_rank_cutoff ); + } m_instance_lodpending.push_back( submodel->Child ); m_instance_lodpending.push_back( submodel->Next ); } + if( false == castsshadow ) { return; } std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); auto const bandcount { m_instance_lodbounds.size() + 1 }; @@ -3064,6 +3075,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorlocation() - m_renderpass.pass_camera.position() ), 1.f ); auto const distance { static_cast( distancesquared ) }; + farthest_distancesquared = std::max( farthest_distancesquared, distance ); m_instance_survivors.push_back( { view_matrix * root, distance, @@ -3120,16 +3133,25 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max() ); - for( auto const &survivor : m_instance_survivors ) { - ++m_instance_bandstarts[ survivor.band + 1 ]; + for( auto &survivor : m_instance_survivors ) { m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared ); + auto const slot { std::min( distanceslots - 1, static_cast( std::sqrt( survivor.distancesquared ) * slotscale ) ) }; + survivor.band = static_cast( survivor.band * distanceslots + slot ); // from now on it's the sort key + ++m_instance_bandstarts[ survivor.band + 1 ]; } - for( std::size_t band = 1; band <= bandcount; ++band ) { - m_instance_bandstarts[ band ] += m_instance_bandstarts[ band - 1 ]; + for( std::size_t key = 1; key <= keycount; ++key ) { + m_instance_bandstarts[ key ] += m_instance_bandstarts[ key - 1 ]; } m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 ); m_instance_modelviews.resize( m_instance_survivors.size() ); @@ -3145,8 +3167,8 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 2f86117f..43cd24b0 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -436,7 +436,8 @@ class opengl33_renderer : public gfx_renderer { std::vector m_instance_survivors; std::vector m_instance_lodbounds; std::vector m_instance_lodpending; - // per lod band: first index in m_instance_modelviews (with end sentinel), fill cursor, nearest distance + // per sort key (lod band and distance slot): first index in m_instance_modelviews (with end sentinel) + // and fill cursor; per lod band: nearest distance std::vector m_instance_bandstarts; std::vector m_instance_bandcursors; std::vector m_instance_banddistances; From 06078ddab928bbc2bb0c5eaf43e541dbb84b436e Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 1 Oct 2026 13:30:25 +0200 Subject: [PATCH 36/38] fix: other sonar sast fixes (#193) --- McZapkie/Mover.cpp | 19 ++++++++----------- rendering/opengl33renderer.cpp | 8 ++++---- rendering/openglrenderer.cpp | 8 ++++---- simulation/simulationenvironment.cpp | 6 +++--- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 68b88918..61bdc17a 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -2408,7 +2408,7 @@ bool TMoverParameters::IncMainCtrl(int CtrlSpeed) { ++MainCtrlPos; OK = true; - if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1 && MainCtrlActualPos != MainCtrlPos) + if (EIMCtrlType == 0 && ( SpeedCtrlAutoTurnOffFlag & 1 ) == 1 && MainCtrlActualPos != MainCtrlPos) { DecScndCtrl(2); SpeedCtrlUnit.IsActive = false; @@ -2591,7 +2591,7 @@ bool TMoverParameters::DecMainCtrl(int CtrlSpeed) { MainCtrlPos--; OK = true; - if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1 && MainCtrlActualPos != MainCtrlPos) + if (EIMCtrlType == 0 && (SpeedCtrlAutoTurnOffFlag & 1) == 1 && MainCtrlActualPos != MainCtrlPos) { DecScndCtrl(2); SpeedCtrlUnit.IsActive = false; @@ -2780,7 +2780,7 @@ bool TMoverParameters::IncScndCtrl(int CtrlSpeed) if (OK && EngineType == TEngineType::ElectricInductionMotor && ScndCtrlPosNo == 1 && MainCtrlPos > 0) { SpeedCtrlValue = Vel; - if (EIMCtrlType == 0 && SpeedCtrlAutoTurnOffFlag & 1 == 1) + if (EIMCtrlType == 0 && (SpeedCtrlAutoTurnOffFlag & 1) == 1) { MainCtrlActualPos = MainCtrlPos; } @@ -4586,14 +4586,11 @@ void TMoverParameters::UpdatePipePressure(double dt) auto const lowvoltagepower{Power24vIsAvailable || Power110vIsAvailable}; - if (true == RadioStopFlag || true == AlarmChainFlag || (true == EIMCtrlEmergency && LocalBrakePosA >= 1.0) || SecuritySystem.is_braking() || - (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive) - /* - // NOTE: disabled because 32 is 'load destroyed' flag, what does this have to do with emergency brake? - // (if it's supposed to be broken coupler, such event sets alarmchainflag instead when appropriate) - || ( true == TestFlag( EngDmgFlag, 32 ) ) - */ - || (0 == CabActive && InactiveCabFlag & activation::emergencybrake) || (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive)) + // EngDmgFlag 32 (load destroyed) used to trigger this too; dropped, it has nothing to do with + // the emergency brake (a broken coupler sets AlarmChainFlag instead) + if (RadioStopFlag || AlarmChainFlag || (EIMCtrlEmergency && LocalBrakePosA >= 1.0) || SecuritySystem.is_braking() || + (SpringBrakeDriveEmergencyVel >= 0 && Vel > SpringBrakeDriveEmergencyVel && SpringBrake.IsActive) || + (0 == CabActive && (InactiveCabFlag & activation::emergencybrake) != 0)) { EmergencyValveFlow = PF(0, PipePress, 0.15) * dt; } diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b9937816..06fad0cd 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -81,9 +81,9 @@ bool opengl33_renderer::Init(GLFWwindow *Window) } // rgb value for 5780 kelvin - Global.DayLight.diffuse[0] = 255.0f / 255.0f; - Global.DayLight.diffuse[1] = 242.0f / 255.0f; - Global.DayLight.diffuse[2] = 231.0f / 255.0f; + Global.DayLight.diffuse[0] = 1.0f; // 255 / 255 + Global.DayLight.diffuse[1] = 0.9490196f; // 242 / 255 + Global.DayLight.diffuse[2] = 0.9058824f; // 231 / 255 Global.DayLight.is_directional = true; m_sunlight.id = opengl33_renderer::sunlight; @@ -1956,7 +1956,7 @@ bool opengl33_renderer::Render(world_environment *Environment) // moon { Bind_Texture(0, m_moontexture); - glm::vec3 mooncolor(255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f); + glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); glm::vec4 color(mooncolor.r, mooncolor.g, mooncolor.b, // fade the moon if it's near the sun in the sky, especially during the day std::max(0.f, 1.0 - 0.5 * Global.fLuminance - 0.65 * std::max(0.f, glm::dot(Environment->m_sun.getDirection(), Environment->m_moon.getDirection()))) * fogfactor); diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 6f06e9b3..dcbd1df2 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -88,9 +88,9 @@ opengl_renderer::Init( GLFWwindow *Window ) { ::glEnable( opengl_renderer::sunlight ); // rgb value for 5780 kelvin - Global.DayLight.diffuse[ 0 ] = 255.0f / 255.0f; - Global.DayLight.diffuse[ 1 ] = 242.0f / 255.0f; - Global.DayLight.diffuse[ 2 ] = 231.0f / 255.0f; + Global.DayLight.diffuse[0] = 1.0f; // 255 / 255 + Global.DayLight.diffuse[1] = 0.9490196f; // 242 / 255 + Global.DayLight.diffuse[2] = 0.9058824f; // 231 / 255 Global.DayLight.is_directional = true; m_sunlight.id = opengl_renderer::sunlight; // ::glLightf( opengl_renderer::sunlight, GL_SPOT_CUTOFF, 90.0f ); @@ -1603,7 +1603,7 @@ opengl_renderer::Render( world_environment *Environment ) { if (!m_isATI) { Bind_Texture( m_moontexture ); - glm::vec3 mooncolor( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ); + glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); ::glColor4f( mooncolor.r, mooncolor.g, mooncolor.b, // fade the moon if it's near the sun in the sky, especially during the day diff --git a/simulation/simulationenvironment.cpp b/simulation/simulationenvironment.cpp index 9837e0e8..b96cea20 100644 --- a/simulation/simulationenvironment.cpp +++ b/simulation/simulationenvironment.cpp @@ -140,7 +140,7 @@ world_environment::update() { m_lightintensity = moonlightlevel; // if the moon is up, it overrides the twilight twilightfactor = 0.0f; - keylightcolor = glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 202.0f / 255.0f ); + keylightcolor = glm::vec3( 1.0f, 0.9490196f, 0.7921569f ); } else { // regular situation with sun as the key light @@ -152,8 +152,8 @@ world_environment::update() { // include 'golden hour' effect in twilight lighting float const duskfactor = 1.25f - std::clamp( Global.SunAngle, 0.0f, 18.0f ) / 18.0f; keylightcolor = glm::mix( - glm::vec3( 255.0f / 255.0f, 242.0f / 255.0f, 231.0f / 255.0f ), - glm::vec3( 235.0f / 255.0f, 120.0f / 255.0f, 36.0f / 255.0f ), + glm::vec3( 1.0f, 0.9490196f, 0.9058824f ), // 255, 242, 231 / 255 + glm::vec3( 0.9215686f, 0.4705882f, 0.1411765f ), // 235, 120, 36 / 255 duskfactor ); } // ...retrieve current sky colour and brightness... From 1474d2b773ce1510d0c2e73505c464d2616b0045 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 1 Oct 2026 13:30:36 +0200 Subject: [PATCH 37/38] fix: merge nested ifs in Driver.cpp cpp:s1066 (#194) --- vehicle/Driver.cpp | 1723 ++++++++++++++++++++------------------------ 1 file changed, 785 insertions(+), 938 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 6f3c4100..9f1b2586 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -219,13 +219,13 @@ void TSpeedPos::CommandCheck() fVelNext = value1; iFlags &= ~(spShuntSemaphor | spPassengerStopPoint | spStopOnSBL); iFlags |= spSemaphor;// nie manewrowa, nie przystanek, nie zatrzymać na SBL, ale semafor - if (value1 == 0.0) // jeśli pierwsza zerowa - if (value2 != 0.0) // a druga nie - { // S1 na SBL, można przejechać po zatrzymaniu (tu nie mamy prędkości ani odległości) - fVelNext = value2; // normalnie będzie zezwolenie na jazdę, aby się usunął z tabelki - iFlags |= spStopOnSBL; // flaga, że ma zatrzymać; na pewno nie zezwoli na manewry - } - break; + if (value1 == 0.0 && value2 != 0.0) // jeśli pierwsza zerowa + // a druga nie + { // S1 na SBL, można przejechać po zatrzymaniu (tu nie mamy prędkości ani odległości) + fVelNext = value2; // normalnie będzie zezwolenie na jazdę, aby się usunął z tabelki + iFlags |= spStopOnSBL; // flaga, że ma zatrzymać; na pewno nie zezwoli na manewry + } + break; case TCommandType::cm_SectionVelocity: // odcinek z ograniczeniem prędkości fVelNext = value1; @@ -301,46 +301,39 @@ bool TSpeedPos::Update() if( trTrack->iCategoryFlag & 1 ) { // railways - if( iFlags & spSwitch ) { + if( iFlags & spSwitch && (trTrack->GetSwitchState() & 1) != 0 != ((iFlags & spSwitchStatus) != 0) ) { // jeśli odcinek zmienny - if( (trTrack->GetSwitchState() & 1) != 0 != - ( ( iFlags & spSwitchStatus ) != 0 ) ) { - // czy stan się zmienił? - // Ra: zakładam, że są tylko 2 możliwe stany - iFlags ^= spSwitchStatus; - if( ( iFlags & spElapsed ) == 0 ) { - // jeszcze trzeba skanowanie wykonać od tego toru - // problem jest chyba, jeśli zwrotnica się przełoży zaraz po zjechaniu z niej - // na Mydelniczce potrafi skanować na wprost mimo pojechania na bok - return true; - } - } - } + // czy stan się zmienił? + // Ra: zakładam, że są tylko 2 możliwe stany + iFlags ^= spSwitchStatus; + if ((iFlags & spElapsed) == 0) + { + // jeszcze trzeba skanowanie wykonać od tego toru + // problem jest chyba, jeśli zwrotnica się przełoży zaraz po zjechaniu z niej + // na Mydelniczce potrafi skanować na wprost mimo pojechania na bok + return true; + } + } } else { // roads and others - if( iFlags & 0xF0000000 ) { + if( iFlags & 0xF0000000 && (iFlags & 0xF0000000) > 0x10000000 ) { // jeśli skrzyżowanie, ograniczyć prędkość przy skręcaniu - if( ( iFlags & 0xF0000000 ) > 0x10000000 ) { - // ±1 to jazda na wprost, ±2 nieby też, ale z przecięciem głównej drogi - chyba że jest równorzędne... - // TODO: uzależnić prędkość od promienia; - // albo niech będzie ograniczona w skrzyżowaniu (velocity z ujemną wartością) - fVelNext = 30.0; - } - } + // ±1 to jazda na wprost, ±2 nieby też, ale z przecięciem głównej drogi - chyba że jest równorzędne... + // TODO: uzależnić prędkość od promienia; + // albo niech będzie ograniczona w skrzyżowaniu (velocity z ujemną wartością) + fVelNext = 30.0; + } } } } - else if (iFlags & spEvent) { + else if (iFlags & spEvent && ((iFlags & spElapsed) == 0 || fVelNext == 0.0)) { // jeśli event - if( (iFlags & spElapsed) == 0 - || fVelNext == 0.0 ) { - // ignore already passed signals, but keep an eye on overrun stops - // 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 - } - } + // ignore already passed signals, but keep an eye on overrun stops + // 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 + } return false; }; @@ -377,11 +370,10 @@ bool TSpeedPos::IsProperSemaphor(TOrders order) else if (iFlags & spOutsideStation) return true; } - else if (order & Obey_train) + else if (order & Obey_train && iFlags & spSemaphor) { - if (iFlags & spSemaphor) - return true; - } + return true; + } return false; // true gdy zatrzymanie, wtedy nie ma po co skanować dalej } @@ -408,11 +400,10 @@ bool TSpeedPos::Set(basic_event *event, double dist, double length, TOrders orde else if (iFlags & spOutsideStation) return true; } - else if (order & Obey_train) + else if (order & Obey_train && iFlags & spSemaphor && fVelNext == 0.0) { - if (iFlags & spSemaphor && fVelNext == 0.0) - return true; - } + return true; + } return false; // true gdy zatrzymanie, wtedy nie ma po co skanować dalej }; @@ -423,10 +414,9 @@ void TSpeedPos::Set(TTrack *track, double dist, int flag) if (trTrack) { iFlags = flag | (trTrack->eType == tt_Normal ? spTrack : spTrack | spSwitch ); // zapamiętanie kierunku wraz z typem - if (iFlags & spSwitch) - if (trTrack->GetSwitchState() & 1) - iFlags |= spSwitchStatus; - fVelNext = trTrack->VelocityGet(); + if (iFlags & spSwitch && trTrack->GetSwitchState() & 1) + iFlags |= spSwitchStatus; + fVelNext = trTrack->VelocityGet(); if (trTrack->iDamageFlag & 128) fVelNext = 0.0; // jeśli uszkodzony, to też stój if (iFlags & spEnd) @@ -590,52 +580,48 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) auto const events { CheckTrackEvent( pTrack, fLastDir ) }; for( auto *pEvent : events ) { - if( pEvent != nullptr ) // jeśli jest semafor na tym torze + if( pEvent != nullptr && TableNotFound(pEvent, fCurrentDistance) ) // jeśli jest semafor na tym torze { // trzeba sprawdzić tabelkę, bo dodawanie drugi raz tego samego przystanku nie jest korzystne - if (TableNotFound(pEvent, fCurrentDistance)) // jeśli nie ma - { - TableAddNew(); // zawsze jest true + // jeśli nie ma + TableAddNew(); // zawsze jest true - if (Global.iWriteLogEnabled & 8) { - WriteLog("Speed table for " + OwnerName() + " found new event, " + pEvent->m_name); - } - auto &newspeedpoint = sSpeedTable[iLast]; -/* - if( newspeedpoint.Set( - pEvent, - fCurrentDistance + ProjectEventOnTrack( pEvent, pTrack, fLastDir ), - OrderCurrentGet() ) ) { -*/ - if( newspeedpoint.Set( - pEvent, - GetDistanceToEvent( pTrack, pEvent, fLastDir, fCurrentDistance ), - fLength, - OrderCurrentGet() ) ) { + if (Global.iWriteLogEnabled & 8) + { + WriteLog("Speed table for " + OwnerName() + " found new event, " + pEvent->m_name); + } + auto &newspeedpoint = sSpeedTable[iLast]; + /* + if( newspeedpoint.Set( + pEvent, + fCurrentDistance + ProjectEventOnTrack( pEvent, pTrack, fLastDir ), + OrderCurrentGet() ) ) { + */ + if (newspeedpoint.Set(pEvent, GetDistanceToEvent(pTrack, pEvent, fLastDir, fCurrentDistance), fLength, OrderCurrentGet())) + { - fDistance = newspeedpoint.fDist; // jeśli sygnał stop, to nie ma potrzeby dalej skanować - SemNextStopIndex = iLast; - if (SemNextIndex == npos) { - SemNextIndex = iLast; - } - if (Global.iWriteLogEnabled & 8) { - WriteLog("(stop signal from " - + (SemNextStopIndex != npos ? sSpeedTable[SemNextStopIndex].GetName() : "unknown semaphor") - + ")"); - } - } - else { - if( true == newspeedpoint.IsProperSemaphor(OrderCurrentGet()) - && SemNextIndex == npos ) { - SemNextIndex = iLast; // sprawdzamy czy pierwszy na drodze - } - if (Global.iWriteLogEnabled & 8) { - WriteLog("(forward signal for " - + (SemNextIndex != npos ? sSpeedTable[SemNextIndex].GetName() : "unknown semaphor") - + ")"); - } - } - } - } + fDistance = newspeedpoint.fDist; // jeśli sygnał stop, to nie ma potrzeby dalej skanować + SemNextStopIndex = iLast; + if (SemNextIndex == npos) + { + SemNextIndex = iLast; + } + if (Global.iWriteLogEnabled & 8) + { + WriteLog("(stop signal from " + (SemNextStopIndex != npos ? sSpeedTable[SemNextStopIndex].GetName() : "unknown semaphor") + ")"); + } + } + else + { + if (true == newspeedpoint.IsProperSemaphor(OrderCurrentGet()) && SemNextIndex == npos) + { + SemNextIndex = iLast; // sprawdzamy czy pierwszy na drodze + } + if (Global.iWriteLogEnabled & 8) + { + WriteLog("(forward signal for " + (SemNextIndex != npos ? sSpeedTable[SemNextIndex].GetName() : "unknown semaphor") + ")"); + } + } + } } // event dodajemy najpierw, żeby móc sprawdzić, czy tor został dodany po odczytaniu prędkości następnego if( pTrack->VelocityGet() == 0.0 // zatrzymanie @@ -686,17 +672,13 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) */ } } - else if ( pTrack->fRadius != 0.0 // odległość na łuku lepiej aproksymować cięciwami - || ( tLast != nullptr - && tLast->fRadius != 0.0 )) // koniec łuku też jest istotny + else if ( (pTrack->fRadius != 0.0 // odległość na łuku lepiej aproksymować cięciwami + || tLast != nullptr && tLast->fRadius != 0.0) && + TableAddNew()) // koniec łuku też jest istotny { // albo dla liczenia odległości przy pomocy cięciw - te usuwać po przejechaniu - if (TableAddNew()) { - // dodanie odcinka do tabelki - sSpeedTable[iLast].Set( - pTrack, fCurrentDistance, - fLastDir < 0 ? spEnabled | spCurve | spReverse : spEnabled | spCurve ); - } - } + // dodanie odcinka do tabelki + sSpeedTable[iLast].Set(pTrack, fCurrentDistance, fLastDir < 0 ? spEnabled | spCurve | spReverse : spEnabled | spCurve); + } } fCurrentDistance += fTrackLength; // doliczenie kolejnego odcinka do przeskanowanej długości @@ -708,31 +690,22 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) if (pTrack != nullptr ) { // jeśli kolejny istnieje - if( tLast != nullptr ) { + if( tLast != nullptr && tLast->VelocityGet() > 0 && (tLast->VelocityGet() < pTrack->VelocityGet() || pTrack->VelocityGet() < 0) ) { - if( tLast->VelocityGet() > 0 - && ( tLast->VelocityGet() < pTrack->VelocityGet() - || pTrack->VelocityGet() < 0 ) ) { - - if( iLast != -1 - && sSpeedTable[iLast].trTrack == tLast ) { - // if the track is already in the table we only need to mark it as relevant - sSpeedTable[ iLast ].iFlags |= spEnabled; - } - else { - // otherwise add it - TableAddNew(); - sSpeedTable[ iLast ].Set( - tLast, - fCurrentDistance - fTrackLength, // by now the current distance points to beginning of next track - ( fLastDir > 0 ? - pTrack->iPrevDirection : - pTrack->iNextDirection ) ? - spEnabled : - spEnabled | spReverse ); - } - } - } + if (iLast != -1 && sSpeedTable[iLast].trTrack == tLast) + { + // if the track is already in the table we only need to mark it as relevant + sSpeedTable[iLast].iFlags |= spEnabled; + } + else + { + // otherwise add it + TableAddNew(); + sSpeedTable[iLast].Set(tLast, + fCurrentDistance - fTrackLength, // by now the current distance points to beginning of next track + (fLastDir > 0 ? pTrack->iPrevDirection : pTrack->iNextDirection) ? spEnabled : spEnabled | spReverse); + } + } // zwiększenie skanowanej odległości tylko jeśli istnieje dalszy tor fTrackLength = pTrack->Length(); } @@ -749,14 +722,12 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) fLastDir < 0 ? spEnabled | spEnd | spReverse : spEnabled | spEnd); } } - else if( sSpeedTable[ iLast ].trTrack == tLast ) { + else if( sSpeedTable[iLast].trTrack == tLast && false == TestFlag(sSpeedTable[iLast].trTrack->iCategoryFlag, 0x100) ) { // otherwise just mark the last added track as the final one // TODO: investigate exactly how we can wind up not marking the last existing track as actual end - if( false == TestFlag( sSpeedTable[ iLast ].trTrack->iCategoryFlag, 0x100 ) ) { - // don't mark portals, as these aren't exactly track ends, but teleport devices - sSpeedTable[ iLast ].iFlags |= spEnabled | spEnd; - } - } + // don't mark portals, as these aren't exactly track ends, but teleport devices + sSpeedTable[iLast].iFlags |= spEnabled | spEnd; + } // to ostatnia pozycja, bo NULL nic nie da, a może się podpiąć obrotnica, czy jakieś transportery return; } @@ -820,27 +791,21 @@ void TController::TableCheck(double fDistance) { // degradacja pozycji sSpeedTable[i].iFlags &= ~spEnabled; // nie liczy się } - else if( ( sSpeedTable[ i ].iFlags & 0xF0000028 ) == spElapsed ) { + else if( (sSpeedTable[i].iFlags & 0xF0000028) == spElapsed && sSpeedTable[i].fVelNext < 0 ) { // jest z tyłu (najechany) i nie jest zwrotnicą ani skrzyżowaniem - if( sSpeedTable[ i ].fVelNext < 0 ) // a nie ma ograniczenia prędkości - { - sSpeedTable[ i ].iFlags = 0; // to nie ma go po co trzymać (odtykacz usunie ze środka) - } - } + // a nie ma ograniczenia prędkości + sSpeedTable[i].iFlags = 0; // to nie ma go po co trzymać (odtykacz usunie ze środka) + } } - else if (sSpeedTable[i].iFlags & spEvent) // jeśli event + else if (sSpeedTable[i].iFlags & spEvent && sSpeedTable[i].fDist < (typeid(*sSpeedTable[i].evEvent) == typeid(putvalues_event) ? -fLength : 0) && mvOccupied->CategoryFlag == 2 && + sSpeedTable[i].fDist < -0.75) // jeśli event { - if (sSpeedTable[i].fDist < ( - typeid( *sSpeedTable[i].evEvent ) == typeid( putvalues_event ) ? - -fLength : - 0)) // jeśli jest z tyłu - if (mvOccupied->CategoryFlag == 2 && sSpeedTable[i].fDist < -0.75) - { // pociąg staje zawsze, a samochód tylko jeśli nie przejedzie całą długością (może być zaskoczony zmianą) - // WriteLog("TableCheck: Event is behind. Delete from table: " + sSpeedTable[i].evEvent->asName); - sSpeedTable[i].iFlags &= ~spEnabled; // degradacja pozycji dla samochodu; - // semafory usuwane tylko przy sprawdzaniu, bo wysyłają komendy - } - } + // jeśli jest z tyłu + // pociąg staje zawsze, a samochód tylko jeśli nie przejedzie całą długością (może być zaskoczony zmianą) + // WriteLog("TableCheck: Event is behind. Delete from table: " + sSpeedTable[i].evEvent->asName); + sSpeedTable[i].iFlags &= ~spEnabled; // degradacja pozycji dla samochodu; + // semafory usuwane tylko przy sprawdzaniu, bo wysyłają komendy + } } } sSpeedTable[iLast].Update(); // aktualizacja ostatniego @@ -890,10 +855,10 @@ TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fN iDrivigFlags |= moveSwitchFound; // rozjazd z przodu/pod ogranicza np. sens skanowania wstecz SwitchClearDist = point.fDist + point.trTrack->Length() + fLength; } - else if ( TestFlag( point.iFlags, spEvent ) ) // W4 może się deaktywować + else if ( TestFlag(point.iFlags, spEvent) && TableUpdateEvent(v, go, point, d_to_next_sem, idx) ) // W4 może się deaktywować { // jeżeli event, może być potrzeba wysłania komendy, aby ruszył - if( TableUpdateEvent( v, go, point, d_to_next_sem, idx ) ) { continue; } - } + continue; + } auto const railwaytrackend { true == TestFlag(point.iFlags, spEnd) && is_train() }; if( v >= 0.0 // pozycje z prędkością -1 można spokojnie pomijać @@ -938,14 +903,10 @@ TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fN // points located behind can affect our current speed, but little else else if ( point.iFlags & spTrack) // jeśli tor { // tor ogranicza prędkość, dopóki cały skład nie przejedzie, - if( v >= 1.0 ) { // EU06 się zawieszało po dojechaniu na koniec toru postojowego - if( d + point.trTrack->Length() < -fLength ) { - if( false == railwaytrackend ) { - // zapętlenie, jeśli już wyjechał za ten odcinek - continue; - } - } - } + if( v >= 1.0 && d + point.trTrack->Length() < -fLength && false == railwaytrackend ) { // EU06 się zawieszało po dojechaniu na koniec toru postojowego + // zapętlenie, jeśli już wyjechał za ten odcinek + continue; + } if( v < fVelDes ) { // ograniczenie aktualnej prędkości aż do wyjechania za ograniczenie fVelDes = v; @@ -953,11 +914,9 @@ TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fN if( v < VelLimitLastDist.first ) { VelLimitLastDist.second = d + point.trTrack->Length() + fLength; } - else if( VelLimitLastDist.second > 0 ) { // the speed limit can potentially start afterwards, so don't mark it as broken too soon - if( ( point.iFlags & ( spSwitch | spPassengerStopPoint ) ) == 0 ) { - speedlimitiscontinuous = false; - } - } + else if( VelLimitLastDist.second > 0 && (point.iFlags & (spSwitch | spPassengerStopPoint)) == 0 ) { // the speed limit can potentially start afterwards, so don't mark it as broken too soon + speedlimitiscontinuous = false; + } if( false == railwaytrackend ) { continue; // i tyle wystarczy } @@ -1029,19 +988,16 @@ TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fN } // if (v>=0.0) // finding a point which doesn't impose speed limit means any potential active speed limit can't extend through entire scan table // NOTE: we test actual speed value for the given point, as events may receive (v = -1) as a way to disable them in irrelevant modes - if( point.fVelNext < 0 ) { + if( point.fVelNext < 0 && (point.iFlags & (spSwitch | spPassengerStopPoint)) == 0 ) { // NOTE: we exclude switches from this rule, as speed limits generally extend through these - if( ( point.iFlags & ( spSwitch | spPassengerStopPoint ) ) == 0 ) { - speedlimitiscontinuous = false; - } - } + speedlimitiscontinuous = false; + } if (fNext >= 0.0) { // jeśli ograniczenie - if( ( point.iFlags & ( spEnabled | spEvent ) ) == ( spEnabled | spEvent ) ) { // tylko sygnał przypisujemy - if( eSignNext == nullptr ) { // jeśli jeszcze nic nie zapisane tam - eSignNext = point.evEvent; // dla informacji - } - } + if( (point.iFlags & (spEnabled | spEvent)) == (spEnabled | spEvent) && eSignNext == nullptr ) { // tylko sygnał przypisujemy + // jeśli jeszcze nic nie zapisane tam + eSignNext = point.evEvent; // dla informacji + } if( fNext == 0.0 ) { break; // nie ma sensu analizować tabelki dalej } @@ -1408,20 +1364,13 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo auto const magnetlocation { pVehicles[ end::front ]->MoverParameters->SecuritySystem.MagnetLocation }; auto const magnetrange { 1.0 }; auto const ismagnetpassed { Point.fDist < -( magnetlocation + magnetrange ) }; - if( Point.fDist < -magnetlocation ) { + if( Point.fDist < -magnetlocation && (mvOccupied->Vel > EU07_AI_NOMOVEMENT || false == ismagnetpassed) ) { // NOTE: normally we'd activate the magnet once the leading vehicle passes it // but on a fresh scan after direction change it would be detected as long as it's under consist, and meet the (simple) activation condition // thus we're doing a more precise check in such situation (we presume direction change takes place only if the vehicle is standing still) - if( mvOccupied->Vel > EU07_AI_NOMOVEMENT - || false == ismagnetpassed ) { - mvOccupied->SecuritySystem.set_cabsignal_lock(!AIControllFlag); // don't make life difficult for the ai, but a human driver is a fair game - PutCommand( - Point.evEvent->input_text(), - Point.evEvent->input_value( 1 ), - Point.evEvent->input_value( 2 ), - nullptr ); - } - } + mvOccupied->SecuritySystem.set_cabsignal_lock(!AIControllFlag); // don't make life difficult for the ai, but a human driver is a fair game + PutCommand(Point.evEvent->input_text(), Point.evEvent->input_value(1), Point.evEvent->input_value(2), nullptr); + } if( ismagnetpassed ) { Point.Clear(); mvOccupied->SecuritySystem.set_cabsignal_lock(false); @@ -1507,21 +1456,18 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo } else if ( Point.iFlags & spStopOnSBL) { // jeśli S1 na SBL - if( mvOccupied->Vel < 2.0 ) { + if( mvOccupied->Vel < 2.0 && Point.fDist < fMaxProximityDist && Obstacle.distance > 1000 ) { // stanąć nie musi, ale zwolnić przynajmniej // jest w maksymalnym zasięgu to można go pominąć (wziąć drugą prędkosć) // as long as there isn't any obstacle in arbitrary view range - if( Point.fDist < fMaxProximityDist - && Obstacle.distance > 1000 ) { - eSignSkip = Point.evEvent; - // jazda na widoczność - skanować możliwość kolizji i nie podjeżdżać zbyt blisko - // usunąć flagę po podjechaniu blisko semafora zezwalającego na jazdę - // ostrożnie interpretować sygnały - semafor może zezwalać na jazdę pociągu z przodu! - iDrivigFlags |= moveVisibility; - // store the ordered restricted speed and don't exceed it until the flag is cleared - VelRestricted = Point.evEvent->input_value( 2 ); - } - } + eSignSkip = Point.evEvent; + // jazda na widoczność - skanować możliwość kolizji i nie podjeżdżać zbyt blisko + // usunąć flagę po podjechaniu blisko semafora zezwalającego na jazdę + // ostrożnie interpretować sygnały - semafor może zezwalać na jazdę pociągu z przodu! + iDrivigFlags |= moveVisibility; + // store the ordered restricted speed and don't exceed it until the flag is cleared + VelRestricted = Point.evEvent->input_value(2); + } if( eSignSkip != Point.evEvent ) { // jeśli ten SBL nie jest do pominięcia to ma 0 odczytywać Velocity = Point.evEvent->input_value( 1 ); @@ -1570,54 +1516,59 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo if ( Point.fDist < 0) VelRoad = Point.fVelNext; } - else if ( Point.iFlags & spSectionVel) + else if ( Point.iFlags & spSectionVel && Point.fDist < 0) { // to W27 - if ( Point.fDist < 0) // teraz trzeba sprawdzić inne warunki - { - if ( Point.fSectionVelocityDist == 0.0) { - if( Global.iWriteLogEnabled & 8 ) { - WriteLog( "TableUpdate: Event is behind. SVD = 0: " + Point.evEvent->m_name ); - } - Point.Clear(); // jeśli punktowy to kasujemy i nie dajemy ograniczenia na stałe - } - else if ( Point.fSectionVelocityDist < 0.0) { - // ograniczenie obowiązujące do następnego - if ( Point.fVelNext == min_speed(Point.fVelNext, VelLimitLast) - && Point.fVelNext != VelLimitLast) { - // jeśli ograniczenie jest mniejsze niż obecne to obowiązuje od zaraz - VelLimitLast = Point.fVelNext; - } - else if ( Point.fDist < -fLength) { - // jeśli większe to musi wyjechać za poprzednie - VelLimitLast = Point.fVelNext; - if( Global.iWriteLogEnabled & 8 ) { - WriteLog( "TableUpdate: Event is behind. SVD < 0: " + Point.evEvent->m_name ); - } - Point.Clear(); // wyjechaliśmy poza poprzednie, można skasować - } - } - else - { // jeśli większe to ograniczenie ma swoją długość - if ( Point.fVelNext == min_speed(Point.fVelNext, VelLimitLast) - && Point.fVelNext != VelLimitLast) { - // jeśli ograniczenie jest mniejsze niż obecne to obowiązuje od zaraz - VelLimitLast = Point.fVelNext; - } - else if ( Point.fDist < -fLength - && Point.fVelNext != VelLimitLast) { - // jeśli większe to musi wyjechać za poprzednie - VelLimitLast = Point.fVelNext; - } - else if (Point.fDist < -fLength - Point.fSectionVelocityDist) { - VelLimitLast = -1.0; - if( Global.iWriteLogEnabled & 8 ) { - WriteLog( "TableUpdate: Event is behind. SVD > 0: " + Point.evEvent->m_name ); - } - Point.Clear(); // wyjechaliśmy poza poprzednie, można skasować - } - } - } - } + // teraz trzeba sprawdzić inne warunki + if (Point.fSectionVelocityDist == 0.0) + { + if (Global.iWriteLogEnabled & 8) + { + WriteLog("TableUpdate: Event is behind. SVD = 0: " + Point.evEvent->m_name); + } + Point.Clear(); // jeśli punktowy to kasujemy i nie dajemy ograniczenia na stałe + } + else if (Point.fSectionVelocityDist < 0.0) + { + // ograniczenie obowiązujące do następnego + if (Point.fVelNext == min_speed(Point.fVelNext, VelLimitLast) && Point.fVelNext != VelLimitLast) + { + // jeśli ograniczenie jest mniejsze niż obecne to obowiązuje od zaraz + VelLimitLast = Point.fVelNext; + } + else if (Point.fDist < -fLength) + { + // jeśli większe to musi wyjechać za poprzednie + VelLimitLast = Point.fVelNext; + if (Global.iWriteLogEnabled & 8) + { + WriteLog("TableUpdate: Event is behind. SVD < 0: " + Point.evEvent->m_name); + } + Point.Clear(); // wyjechaliśmy poza poprzednie, można skasować + } + } + else + { // jeśli większe to ograniczenie ma swoją długość + if (Point.fVelNext == min_speed(Point.fVelNext, VelLimitLast) && Point.fVelNext != VelLimitLast) + { + // jeśli ograniczenie jest mniejsze niż obecne to obowiązuje od zaraz + VelLimitLast = Point.fVelNext; + } + else if (Point.fDist < -fLength && Point.fVelNext != VelLimitLast) + { + // jeśli większe to musi wyjechać za poprzednie + VelLimitLast = Point.fVelNext; + } + else if (Point.fDist < -fLength - Point.fSectionVelocityDist) + { + VelLimitLast = -1.0; + if (Global.iWriteLogEnabled & 8) + { + WriteLog("TableUpdate: Event is behind. SVD > 0: " + Point.evEvent->m_name); + } + Point.Clear(); // wyjechaliśmy poza poprzednie, można skasować + } + } + } //sprawdzenie eventów pasywnych przed nami { // zawalidrogi nie ma (albo pojazd jest samochodem), sprawdzić sygnał @@ -1632,81 +1583,89 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo Point.Clear(); } } - else if( Command == TCommandType::cm_Unknown ) { + else if( Command == TCommandType::cm_Unknown && Velocity != 0.0 ) { // jeśli jeszcze nie ma komendy // komenda jest tylko gdy ma jechać, bo stoi na podstawie tabelki - 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) - // Ra 2014-06: (VelSignal) nie może być tu ustawiane, bo Tm może być daleko - if( VelSignal == 0.0 ) { - // aby stojący ruszył - VelSignal = Velocity; - } - if( Point.fDist < 0.0 ) { - // jeśli przejechany - //!!! ustawienie, gdy przejechany jest lepsze niż wcale, ale to jeszcze nie to - VelSignal = Velocity; - // to można usunąć (nie mogą być usuwane w skanowaniu) - Point.Clear(); - } - } - } + // 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) + // Ra 2014-06: (VelSignal) nie może być tu ustawiane, bo Tm może być daleko + if (VelSignal == 0.0) + { + // aby stojący ruszył + VelSignal = Velocity; + } + if (Point.fDist < 0.0) + { + // jeśli przejechany + //!!! ustawienie, gdy przejechany jest lepsze niż wcale, ale to jeszcze nie to + VelSignal = Velocity; + // to można usunąć (nie mogą być usuwane w skanowaniu) + Point.Clear(); + } + } } else if( ( Point.iFlags & spSectionVel ) == 0 ) { //jeśli jakiś event pasywny ale nie ograniczenie - if( Command == TCommandType::cm_Unknown ) { - // jeśli nie było komendy wcześniej - pierwsza się liczy - ustawianie VelSignal - if( Velocity < 0.0 - || Velocity >= 1.0 ) { - // bo wartość 0.1 służy do hamowania tylko - Command = TCommandType::cm_SetVelocity; // może odjechać - // 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 ) { - // aby stojący ruszył - VelSignal = -1.0; - } - if( Point.fDist < 0.0 ) { - // jeśli przejechany - VelSignal = Velocity == 0.0 ? 0.0 : -1.0; - // ustawienie, gdy przejechany jest lepsze niż wcale, ale to jeszcze nie to - if( Point.iFlags & spEvent ) { - // jeśli event - if( Point.evEvent != eSignSkip - || Point.fVelNext != VelRestricted ) { - // ale inny niż ten, na którym minięto S1, chyba że się już zmieniło - // sygnał zezwalający na jazdę wyłącza jazdę na widoczność (po S1 na SBL) - iDrivigFlags &= ~moveVisibility; - // remove restricted speed - VelRestricted = -1.0; - } - } - // jeśli nie jest ograniczeniem prędkości to można usunąć - // (nie mogą być usuwane w skanowaniu) - Point.Clear(); - } - } - else if( Point.evEvent->is_command() ) { - // jeśli prędkość jest zerowa, a komórka zawiera komendę - eSignNext = Point.evEvent; // dla informacji - // make sure the command isn't for someone else - // TBD, TODO: revise this check for bank/loose_shunt; we might want to ignore obstacles with no owner in these modes - if( Point.fDist < Obstacle.distance ) { - if( true == TestFlag( iDrivigFlags, moveStopHere ) ) { - // jeśli ma stać, dostaje komendę od razu - Command = TCommandType::cm_Command; // komenda z komórki, do wykonania po zatrzymaniu - } - // NOTE: human drivers are likely to stop wherever and expect things to still work, so we give them more leeway - else if( Point.fDist <= ( AIControllFlag ? fMaxProximityDist : std::max( 250.0, fMaxProximityDist ) ) ) { - // jeśli ma dociągnąć, to niech dociąga - // (moveStopCloser dotyczy dociągania do W4, nie semafora) - Command = TCommandType::cm_Command; // komenda z komórki, do wykonania po zatrzymaniu - } - } - } - } - } + if( Command != TCommandType::cm_Unknown ) + { + } + else + { + // jeśli nie było komendy wcześniej - pierwsza się liczy - ustawianie VelSignal + if (Velocity < 0.0 || Velocity >= 1.0) + { + // bo wartość 0.1 służy do hamowania tylko + Command = TCommandType::cm_SetVelocity; // może odjechać + // 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) + { + // aby stojący ruszył + VelSignal = -1.0; + } + if (Point.fDist < 0.0) + { + // jeśli przejechany + VelSignal = Velocity == 0.0 ? 0.0 : -1.0; + // ustawienie, gdy przejechany jest lepsze niż wcale, ale to jeszcze nie to + if (Point.iFlags & spEvent && (Point.evEvent != eSignSkip || Point.fVelNext != VelRestricted)) + { + // jeśli event + // ale inny niż ten, na którym minięto S1, chyba że się już zmieniło + // sygnał zezwalający na jazdę wyłącza jazdę na widoczność (po S1 na SBL) + iDrivigFlags &= ~moveVisibility; + // remove restricted speed + VelRestricted = -1.0; + } + // jeśli nie jest ograniczeniem prędkości to można usunąć + // (nie mogą być usuwane w skanowaniu) + Point.Clear(); + } + } + else if (Point.evEvent->is_command()) + { + // jeśli prędkość jest zerowa, a komórka zawiera komendę + eSignNext = Point.evEvent; // dla informacji + // make sure the command isn't for someone else + // TBD, TODO: revise this check for bank/loose_shunt; we might want to ignore obstacles with no owner in these modes + if (Point.fDist < Obstacle.distance) + { + if (true == TestFlag(iDrivigFlags, moveStopHere)) + { + // jeśli ma stać, dostaje komendę od razu + Command = TCommandType::cm_Command; // komenda z komórki, do wykonania po zatrzymaniu + } + // NOTE: human drivers are likely to stop wherever and expect things to still work, so we give them more leeway + else if (Point.fDist <= (AIControllFlag ? fMaxProximityDist : std::max(250.0, fMaxProximityDist))) + { + // jeśli ma dociągnąć, to niech dociąga + // (moveStopCloser dotyczy dociągania do W4, nie semafora) + Command = TCommandType::cm_Command; // komenda z komórki, do wykonania po zatrzymaniu + } + } + } + } + } } // jeśli nie ma zawalidrogi return false; @@ -1964,12 +1923,10 @@ std::string TController::OrderCurrent() const { // pobranie aktualnego rozkazu celem wyświetlenia auto const order { OrderCurrentGet() }; // generally prioritize direction change... - if( order & Change_direction ) { + if( order & Change_direction && false == TestFlag(iDrivigFlags, moveConnect) ) { // ...but not in the middle of coupling operation - if( false == TestFlag( iDrivigFlags, moveConnect ) ) { - return STR("Change direction"); - } - } + return STR("Change direction"); + } switch( static_cast( order & ~Change_direction ) ) { case Wait_for_orders: { return STR("Wait for orders"); } @@ -2085,14 +2042,10 @@ void TController::Activation() } BrakeLevelSet( mvOccupied->BrakeCtrlPos ); } - if( mvControlling->EngineType == TEngineType::DieselEngine ) { + if( mvControlling->EngineType == TEngineType::DieselEngine && mvControlling->ShuntModeAllow ) { // dla 2Ls150 - przed ustawieniem kierunku - można zmienić tryb pracy - if( mvControlling->ShuntModeAllow ) { - mvControlling->CurrentSwitch( - (OrderCurrentGet() & (Shunt | Loose_shunt)) != 0 - || fMass > 224000.0 ); // do tego na wzniesieniu może nie dać rady na liniowym - } - } + mvControlling->CurrentSwitch((OrderCurrentGet() & (Shunt | Loose_shunt)) != 0 || fMass > 224000.0); // do tego na wzniesieniu może nie dać rady na liniowym + } // Ra: to przełączanie poniżej jest tu bez sensu mvOccupied->CabOccupied = iDirection; // aktywacja kabiny w prowadzonym pojeżdzie (silnikowy może być odwrotnie?) mvOccupied->CabActivisation(); // uruchomienie kabin w członach @@ -2379,24 +2332,22 @@ bool TController::CheckVehicles(TOrders user) while (p) { // 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 ) { - // a tamten ma priorytet - // TODO: take into account drivers' operating modes, one or more of them might be on banking duty - if( iDrivigFlags & movePrimary - && mvOccupied->DirAbsolute - && mvOccupied->BrakeCtrlPos >= -1 ) { - // jeśli rządzi i ma kierunek - p->Mechanik->primary( false ); // dezaktywuje tamtego - 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 - } - else { - main = false; // nici z rządzenia - } - } - } + && p->Mechanik != this && p->Mechanik->iDrivigFlags & movePrimary ) { // ale chodzi o inny pojazd, niż aktualnie sprawdzający + // a tamten ma priorytet + // TODO: take into account drivers' operating modes, one or more of them might be on banking duty + if (iDrivigFlags & movePrimary && mvOccupied->DirAbsolute && mvOccupied->BrakeCtrlPos >= -1) + { + // jeśli rządzi i ma kierunek + p->Mechanik->primary(false); // dezaktywuje tamtego + 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 + } + else + { + main = false; // nici z rządzenia + } + } ++iVehicles; // jest jeden pojazd więcej pVehicles[end::rear] = p; // zapamiętanie ostatniego fLength += p->MoverParameters->Dim.L; // dodanie długości pojazdu @@ -2523,19 +2474,17 @@ bool TController::CheckVehicles(TOrders user) // potentially adjust light state control_lights(); // custom ai action for disconnect mode: switch off lights on disconnected vehicle(s) - if( is_train() ) { - if( true == TestFlag( OrderCurrentGet(), Disconnect ) ) { - if( AIControllFlag ) { - // światła manewrowe (Tb1) tylko z przodu, aby nie pozostawić odczepionego ze światłem - if( mvOccupied->DirActive >= 0 ) { // jak ma kierunek do przodu - pVehicles[ end::rear ]->RaLightsSet( -1, 0 ); - } - else { // jak dociska - pVehicles[ end::front ]->RaLightsSet( 0, -1 ); - } - } - } - } + if( is_train() && true == TestFlag(OrderCurrentGet(), Disconnect) && AIControllFlag ) { + // światła manewrowe (Tb1) tylko z przodu, aby nie pozostawić odczepionego ze światłem + if (mvOccupied->DirActive >= 0) + { // jak ma kierunek do przodu + pVehicles[end::rear]->RaLightsSet(-1, 0); + } + else + { // jak dociska + pVehicles[end::front]->RaLightsSet(0, -1); + } + } // enable door locks cue_action( driver_hint::consistdoorlockson ); // potentially enable train heating @@ -2642,10 +2591,10 @@ int TController::OrderDirectionChange(int newdir, TMoverParameters *Vehicle) IncBrake(); // niech hamuje if (Vehicle->DirActive == testd * Vehicle->CabActive) VelforDriver = -1; // można jechać, bo kierunek jest zgodny z żądanym - if (is_emu()) - if (Vehicle->DirActive > 0) - // if () //tylko jeśli jazda pociągowa (tego nie wiemy w momencie odpalania silnika) - Vehicle->DirectionForward(); // Ra: z przekazaniem do silnikowego + if (is_emu() && Vehicle->DirActive > 0) + // if () //tylko jeśli jazda pociągowa (tego nie wiemy w momencie odpalania silnika) + Vehicle->DirectionForward(); + // Ra: z przekazaniem do silnikowego return (int)VelforDriver; // zwraca prędkość mechanika } @@ -3115,11 +3064,10 @@ bool TController::IncBrake() OK = false; } } - if( /*GBH mvOccupied->BrakeCtrlPos*/BrakeCtrlPosition > 0 ) { - if( mvOccupied->Hamulec->Releaser() ) { - mvOccupied->BrakeReleaser( 0 ); - } - } + if( /*GBH mvOccupied->BrakeCtrlPos*/ + /*GBH mvOccupied->BrakeCtrlPos*/ BrakeCtrlPosition > 0 && mvOccupied->Hamulec->Releaser() ) { + mvOccupied->BrakeReleaser(0); + } break; } case TBrakeSystem::ElectroPneumatic: { @@ -3230,15 +3178,13 @@ bool TController::DecBrake() { auto const pos_diff { ( is_dmu() ? 0.25 : 1.0 ) }; deltaAcc = -AccDesired * BrakeAccFactor() - ( fBrake_a0[ 0 ] + 4 * (/*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition - pos_diff )*fBrake_a1[ 0 ] ); } - if (deltaAcc < 0) { - if(/*GBH mvOccupied->BrakeCtrlPosR*/BrakeCtrlPosition > 0 ) { - OK = /*mvOccupied->*/BrakeLevelAdd( -0.25 ); - //if ((deltaAcc < 5 * fBrake_a1[0]) && (mvOccupied->BrakeCtrlPosR >= 1.2)) - // mvOccupied->BrakeLevelAdd(-1.0); - /* if (mvOccupied->BrakeCtrlPosR < 0.74) GBH */ - if( BrakeCtrlPosition < 0.74 ) - /*mvOccupied->*/BrakeLevelSet( gbh_RP ); - } + if (deltaAcc < 0 && /*GBH mvOccupied->BrakeCtrlPosR*/ BrakeCtrlPosition > 0) { + OK = /*mvOccupied->*/ BrakeLevelAdd(-0.25); + // if ((deltaAcc < 5 * fBrake_a1[0]) && (mvOccupied->BrakeCtrlPosR >= 1.2)) + // mvOccupied->BrakeLevelAdd(-1.0); + /* if (mvOccupied->BrakeCtrlPosR < 0.74) GBH */ + if (BrakeCtrlPosition < 0.74) + /*mvOccupied->*/ BrakeLevelSet(gbh_RP); } if( !OK ) { OK = mvOccupied->DecLocalBrakeLevel(2); @@ -3380,183 +3326,173 @@ bool TController::IncSpeed() if( mvControlling->EnginePowerSource.SourceType == TPowerSource::Accumulator ) { fVoltage = mvControlling->BatteryVoltage; } - if (!IsAnyMotorOverloadRelayOpen - && !mvControlling->ControlPressureSwitch) { - if (mvControlling->IsMainCtrlNoPowerPos() - || mvControlling->StLinFlag) { // youBy polecił dodać 2012-09-08 v367 - // na pozycji 0 przejdzie, a na pozostałych będzie czekać, aż się załączą liniowe (zgaśnie DelayCtrlFlag) - if (Ready || iDrivigFlags & movePress) { - auto const usehighoverloadrelaythreshold { - mvControlling->TrainType != dt_ET42 // ET42 uses these variables for different purpose. TODO: fix this - && mvControlling->ImaxHi > mvControlling->ImaxLo - && mvOccupied->Vel < (mvControlling->IsMotorOverloadRelayHighThresholdOn() ? 30.0 : 20.0) - && iVehicles - ControlledEnginesCount > 0 -// && ( std::fabs( mvControlling->Im ) > 0.85 * mvControlling->Imax ) - && mvControlling->Imax * mvControlling->EngineVoltage * ControlledEnginesCount / - (fMass * (fAccGravity == 0.025 ? -0.01 : // prevent div/0 - fAccGravity - 0.025)) < - -2.8 }; - // use series mode: - // if high threshold is set for motor overload relay, - // if the power station is heavily burdened, - // if it generates enough traction force - // to build up speed to 30/40 km/h for passenger/cargo train (10 km/h less if going uphill) - auto const sufficienttractionforce { std::abs( mvControlling->Ft ) * ControlledEnginesCount > ( IsHeavyCargoTrain ? 75 : 50 ) * 1000.0 }; - auto const sufficientacceleration { AbsAccS >= ( IsHeavyCargoTrain ? 0.03 : IsCargoTrain ? 0.06 : 0.09 ) }; - auto const seriesmodefieldshunting { mvControlling->ScndCtrlPos > 0 && mvControlling->RList[mvControlling->MainCtrlPos].Bn == 1 }; - auto const parallelmodefieldshunting { mvControlling->ScndCtrlPos > 0 && mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1 }; - auto const minvoltage { ( mvPantographUnit->EnginePowerSource.SourceType == TPowerSource::CurrentCollector ? mvPantographUnit->EnginePowerSource.CollectorParameters.MinV : 0.0 ) }; - auto const maxvoltage { ( mvPantographUnit->EnginePowerSource.SourceType == TPowerSource::CurrentCollector ? mvPantographUnit->EnginePowerSource.CollectorParameters.MaxV : 0.0 ) }; - auto const seriesmodevoltage { - std::lerp( - minvoltage, - maxvoltage, - IsHeavyCargoTrain ? 0.35 : 0.40 ) }; - auto const useseriesmode = mvControlling->Imax > mvControlling->ImaxLo || true == usehighoverloadrelaythreshold || fVoltage < seriesmodevoltage || - (true == sufficientacceleration && true == sufficienttractionforce && - mvOccupied->Vel <= (IsCargoTrain ? 40 : 30) + (seriesmodefieldshunting ? 5 : 0) - (fAccGravity < -0.025 ? 10 : 0)); - // when not in series mode use the first available parallel mode configuration until 50/60 km/h for passenger/cargo train - // (if there's only one parallel mode configuration it'll be used regardless of current speed) - auto const usefieldshunting = mvControlling->StLinFlag && mvControlling->RList[mvControlling->MainCtrlPos].R < 0.01 && - (useseriesmode ? mvControlling->RList[mvControlling->MainCtrlPos].Bn == 1 : - true == sufficientacceleration && true == sufficienttractionforce && mvOccupied->Vel <= (IsCargoTrain ? 60 : 50) + (parallelmodefieldshunting ? 5 : 0) ? - mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1 : - mvControlling->MainCtrlPos == mvControlling->MainCtrlPosNo); + if (!IsAnyMotorOverloadRelayOpen && !mvControlling->ControlPressureSwitch && (mvControlling->IsMainCtrlNoPowerPos() || mvControlling->StLinFlag) && (Ready || iDrivigFlags & movePress)) { + // youBy polecił dodać 2012-09-08 v367 + // na pozycji 0 przejdzie, a na pozostałych będzie czekać, aż się załączą liniowe (zgaśnie DelayCtrlFlag) + auto const usehighoverloadrelaythreshold{mvControlling->TrainType != dt_ET42 // ET42 uses these variables for different purpose. TODO: fix this + && mvControlling->ImaxHi > mvControlling->ImaxLo && mvOccupied->Vel < (mvControlling->IsMotorOverloadRelayHighThresholdOn() ? 30.0 : 20.0) && + iVehicles - ControlledEnginesCount > 0 + // && ( std::fabs( mvControlling->Im ) > 0.85 * mvControlling->Imax ) + && mvControlling->Imax * mvControlling->EngineVoltage * ControlledEnginesCount / + (fMass * (fAccGravity == 0.025 ? -0.01 : // prevent div/0 + fAccGravity - 0.025)) < + -2.8}; + // use series mode: + // if high threshold is set for motor overload relay, + // if the power station is heavily burdened, + // if it generates enough traction force + // to build up speed to 30/40 km/h for passenger/cargo train (10 km/h less if going uphill) + auto const sufficienttractionforce{std::abs(mvControlling->Ft) * ControlledEnginesCount > (IsHeavyCargoTrain ? 75 : 50) * 1000.0}; + auto const sufficientacceleration{AbsAccS >= (IsHeavyCargoTrain ? 0.03 : IsCargoTrain ? 0.06 : 0.09)}; + auto const seriesmodefieldshunting{mvControlling->ScndCtrlPos > 0 && mvControlling->RList[mvControlling->MainCtrlPos].Bn == 1}; + auto const parallelmodefieldshunting{mvControlling->ScndCtrlPos > 0 && mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1}; + auto const minvoltage{(mvPantographUnit->EnginePowerSource.SourceType == TPowerSource::CurrentCollector ? mvPantographUnit->EnginePowerSource.CollectorParameters.MinV : 0.0)}; + auto const maxvoltage{(mvPantographUnit->EnginePowerSource.SourceType == TPowerSource::CurrentCollector ? mvPantographUnit->EnginePowerSource.CollectorParameters.MaxV : 0.0)}; + auto const seriesmodevoltage{std::lerp(minvoltage, maxvoltage, IsHeavyCargoTrain ? 0.35 : 0.40)}; + auto const useseriesmode = mvControlling->Imax > mvControlling->ImaxLo || true == usehighoverloadrelaythreshold || fVoltage < seriesmodevoltage || + (true == sufficientacceleration && true == sufficienttractionforce && + mvOccupied->Vel <= (IsCargoTrain ? 40 : 30) + (seriesmodefieldshunting ? 5 : 0) - (fAccGravity < -0.025 ? 10 : 0)); + // when not in series mode use the first available parallel mode configuration until 50/60 km/h for passenger/cargo train + // (if there's only one parallel mode configuration it'll be used regardless of current speed) + auto const usefieldshunting = mvControlling->StLinFlag && mvControlling->RList[mvControlling->MainCtrlPos].R < 0.01 && + (useseriesmode ? mvControlling->RList[mvControlling->MainCtrlPos].Bn == 1 : + true == sufficientacceleration && true == sufficienttractionforce && mvOccupied->Vel <= (IsCargoTrain ? 60 : 50) + (parallelmodefieldshunting ? 5 : 0) ? + mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1 : + mvControlling->MainCtrlPos == mvControlling->MainCtrlPosNo); - // if needed enable high threshold for overload relay... - if( mvControlling->TrainType != dt_ET42 ) { // ET42 uses these variables for different purpose. TODO: fix this - if( usehighoverloadrelaythreshold ) { - if( mvControlling->Imax < mvControlling->ImaxHi ) { - // to enable this setting we'll typically need the main controller in series mode (which is not guaranteed) - if( mvControlling->RList[ mvControlling->MainCtrlPos ].Bn > 1 ) { - if( false == mvControlling->IsScndCtrlNoPowerPos() ) { - mvControlling->DecScndCtrl( 2 ); - } - while( false == mvControlling->IsMainCtrlNoPowerPos() - && mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1 ) { - mvControlling->DecMainCtrl( 1 ); // kręcimy nastawnik jazdy o 1 wstecz - } - } - mvControlling->CurrentSwitch( true ); // rozruch wysoki (za to może się ślizgać) - } - } - // ...or disable high threshold for overload relay if no longer needed - else { - if( mvControlling->IsMotorOverloadRelayHighThresholdOn() - && std::fabs(mvControlling->Im) < mvControlling->ImaxLo ) { - mvControlling->CurrentSwitch( false ); // rozruch wysoki wyłącz - } - } - } - - double Vs = 99999; - if( usefieldshunting ? - mvControlling->ScndCtrlPos < mvControlling->ScndCtrlPosNo : - mvControlling->MainCtrlPos < mvControlling->MainCtrlPosNo ) { - Vs = ESMVelocity( !usefieldshunting ); - } - - if( std::abs(mvControlling->Im) < (fReady < 0.4 ? mvControlling->Imin : mvControlling->IminLo) - || mvControlling->Vel > Vs ) { - // Ra: wywalał nadmiarowy, bo Im może być ujemne; jak nie odhamowany, to nie przesadzać z prądem - if( usefieldshunting ) { - // to dać bocznik - // engage the shuntfield only if there's sufficient power margin to draw from - auto const sufficientpowermargin { fVoltage - seriesmodevoltage > ( IsHeavyCargoTrain ? 100.0 : 75.0 ) * ControlledEnginesCount }; - - OK = sufficientpowermargin ? mvControlling->IncScndCtrl(1) : true; - } - else { - // jeśli ustawiony bocznik to bocznik na zero po chamsku - if( mvControlling->ScndCtrlPos ) { - mvControlling->DecScndCtrl( 2 ); - } - // kręcimy nastawnik jazdy - // don't draw too much power; - // keep from dropping into series mode when entering/using parallel mode, and from shutting down in the series mode - auto const sufficientpowermargin { - fVoltage - ( - mvControlling->RList[ std::min( mvControlling->MainCtrlPos + 1, mvControlling->MainCtrlPosNo ) ].Bn == 1 ? - minvoltage : - seriesmodevoltage ) - > ( IsHeavyCargoTrain ? 100.0 : 75.0 ) * ControlledEnginesCount }; - - OK = sufficientpowermargin && false == mvControlling->DelayCtrlFlag ? mvControlling->IncMainCtrl(1) : true; - // czekaj na 1 pozycji, zanim się nie włączą liniowe - if( true == mvControlling->StLinFlag ) { - iDrivigFlags |= moveIncSpeed; - } - else { - iDrivigFlags &= ~moveIncSpeed; - } - - if( mvControlling->Im == 0 - && mvControlling->MainCtrlPowerPos() > 1 ) { - // brak prądu na dalszych pozycjach - // nie załączona lokomotywa albo wywalił nadmiarowy - Need_TryAgain = true; - } - } + // if needed enable high threshold for overload relay... + if (mvControlling->TrainType != dt_ET42) + { // ET42 uses these variables for different purpose. TODO: fix this + if (usehighoverloadrelaythreshold) + { + if (mvControlling->Imax < mvControlling->ImaxHi) + { + // to enable this setting we'll typically need the main controller in series mode (which is not guaranteed) + if (mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1) + { + if (false == mvControlling->IsScndCtrlNoPowerPos()) + { + mvControlling->DecScndCtrl(2); + } + while (false == mvControlling->IsMainCtrlNoPowerPos() && mvControlling->RList[mvControlling->MainCtrlPos].Bn > 1) + { + mvControlling->DecMainCtrl(1); // kręcimy nastawnik jazdy o 1 wstecz + } + } + mvControlling->CurrentSwitch(true); // rozruch wysoki (za to może się ślizgać) } - else { - OK = false; - } } - } - } + // ...or disable high threshold for overload relay if no longer needed + else + { + if (mvControlling->IsMotorOverloadRelayHighThresholdOn() && std::fabs(mvControlling->Im) < mvControlling->ImaxLo) + { + mvControlling->CurrentSwitch(false); // rozruch wysoki wyłącz + } + } + } + + double Vs = 99999; + if (usefieldshunting ? mvControlling->ScndCtrlPos < mvControlling->ScndCtrlPosNo : mvControlling->MainCtrlPos < mvControlling->MainCtrlPosNo) + { + Vs = ESMVelocity(!usefieldshunting); + } + + if (std::abs(mvControlling->Im) < (fReady < 0.4 ? mvControlling->Imin : mvControlling->IminLo) || mvControlling->Vel > Vs) + { + // Ra: wywalał nadmiarowy, bo Im może być ujemne; jak nie odhamowany, to nie przesadzać z prądem + if (usefieldshunting) + { + // to dać bocznik + // engage the shuntfield only if there's sufficient power margin to draw from + auto const sufficientpowermargin{fVoltage - seriesmodevoltage > (IsHeavyCargoTrain ? 100.0 : 75.0) * ControlledEnginesCount}; + + OK = sufficientpowermargin ? mvControlling->IncScndCtrl(1) : true; + } + else + { + // jeśli ustawiony bocznik to bocznik na zero po chamsku + if (mvControlling->ScndCtrlPos) + { + mvControlling->DecScndCtrl(2); + } + // kręcimy nastawnik jazdy + // don't draw too much power; + // keep from dropping into series mode when entering/using parallel mode, and from shutting down in the series mode + auto const sufficientpowermargin{fVoltage - + (mvControlling->RList[std::min(mvControlling->MainCtrlPos + 1, mvControlling->MainCtrlPosNo)].Bn == 1 ? minvoltage : seriesmodevoltage) > + (IsHeavyCargoTrain ? 100.0 : 75.0) * ControlledEnginesCount}; + + OK = sufficientpowermargin && false == mvControlling->DelayCtrlFlag ? mvControlling->IncMainCtrl(1) : true; + // czekaj na 1 pozycji, zanim się nie włączą liniowe + if (true == mvControlling->StLinFlag) + { + iDrivigFlags |= moveIncSpeed; + } + else + { + iDrivigFlags &= ~moveIncSpeed; + } + + if (mvControlling->Im == 0 && mvControlling->MainCtrlPowerPos() > 1) + { + // brak prądu na dalszych pozycjach + // nie załączona lokomotywa albo wywalił nadmiarowy + Need_TryAgain = true; + } + } + } + else + { + OK = false; + } + } // mvControlling->AutoRelayCheck(); // sprawdzenie logiki sterowania break; case TEngineType::Dumb: - if (!IsAnyMotorOverloadRelayOpen) - if (Ready || iDrivigFlags & movePress) //{(BrakePress<=0.01*MaxBrakePress)} - { - OK = IncSpeedEIM(); - if( !OK ) - OK = mvControlling->IncMainCtrl(1); - if (!OK) - OK = mvControlling->IncScndCtrl(1); - } - break; + if (!IsAnyMotorOverloadRelayOpen && (Ready || iDrivigFlags & movePress)) + //{(BrakePress<=0.01*MaxBrakePress)} + { + OK = IncSpeedEIM(); + if (!OK) + OK = mvControlling->IncMainCtrl(1); + if (!OK) + OK = mvControlling->IncScndCtrl(1); + } + break; case TEngineType::DieselElectric: - if (!IsAnyMotorOverloadRelayOpen - && !mvControlling->ControlPressureSwitch) { - if (mvControlling->IsMainCtrlNoPowerPos() || - mvControlling->StLinFlag) { // youBy polecił dodać 2012-09-08 v367 - // na pozycji 0 przejdzie, a na pozostałych będzie czekać, aż się załączą liniowe (zgaśnie DelayCtrlFlag) - if (Ready || iDrivigFlags & movePress) //{(BrakePress<=0.01*MaxBrakePress)} - { - OK = IncSpeedEIM(); - if( !OK ) - OK = mvControlling->IncMainCtrl(1); - if (!OK) - OK = mvControlling->IncScndCtrl(1); - } - } - } + if (!IsAnyMotorOverloadRelayOpen && !mvControlling->ControlPressureSwitch && (mvControlling->IsMainCtrlNoPowerPos() || mvControlling->StLinFlag) && (Ready || iDrivigFlags & movePress)) { + // youBy polecił dodać 2012-09-08 v367 + // na pozycji 0 przejdzie, a na pozostałych będzie czekać, aż się załączą liniowe (zgaśnie DelayCtrlFlag) + //{(BrakePress<=0.01*MaxBrakePress)} + OK = IncSpeedEIM(); + if (!OK) + OK = mvControlling->IncMainCtrl(1); + if (!OK) + OK = mvControlling->IncScndCtrl(1); + } break; case TEngineType::ElectricInductionMotor: - if( !IsAnyMotorOverloadRelayOpen ) { - if( Ready || iDrivigFlags & movePress || mvOccupied->ShuntMode ) //{(BrakePress<=0.01*MaxBrakePress)} - { - OK = IncSpeedEIM(); - if( mvControlling->SpeedCtrl && mvControlling->Mains ) { - // cruise control - auto const couplinginprogress{ true == TestFlag(iDrivigFlags, moveConnect) && true == TestFlag(OrderCurrentGet(), Connect) }; - auto const SpeedCntrlVel{ ( - ActualProximityDist > std::max(50.0, fMaxProximityDist) || couplinginprogress ? - VelDesired : - min_speed( VelDesired, VelNext ) ) }; - if( SpeedCntrlVel >= mvControlling->SpeedCtrlUnit.MinVelocity - && SpeedCntrlVel - mvControlling->SpeedCtrlUnit.MinVelocity == quantize(SpeedCntrlVel - mvControlling->SpeedCtrlUnit.MinVelocity, mvControlling->SpeedCtrlUnit.VelocityStep) ) { - SpeedCntrl( SpeedCntrlVel ); - } - else if( SpeedCntrlVel > 0.1 ) { - SpeedCntrl( 0.0 ); - mvControlling->DecScndCtrl( 2 ); - } - } - } - } + if( !IsAnyMotorOverloadRelayOpen && (Ready || iDrivigFlags & movePress || mvOccupied->ShuntMode) ) { + //{(BrakePress<=0.01*MaxBrakePress)} + OK = IncSpeedEIM(); + if (mvControlling->SpeedCtrl && mvControlling->Mains) + { + // cruise control + auto const couplinginprogress{true == TestFlag(iDrivigFlags, moveConnect) && true == TestFlag(OrderCurrentGet(), Connect)}; + auto const SpeedCntrlVel{(ActualProximityDist > std::max(50.0, fMaxProximityDist) || couplinginprogress ? VelDesired : min_speed(VelDesired, VelNext))}; + if (SpeedCntrlVel >= mvControlling->SpeedCtrlUnit.MinVelocity && + SpeedCntrlVel - mvControlling->SpeedCtrlUnit.MinVelocity == quantize(SpeedCntrlVel - mvControlling->SpeedCtrlUnit.MinVelocity, mvControlling->SpeedCtrlUnit.VelocityStep)) + { + SpeedCntrl(SpeedCntrlVel); + } + else if (SpeedCntrlVel > 0.1) + { + SpeedCntrl(0.0); + mvControlling->DecScndCtrl(2); + } + } + } break; case TEngineType::WheelsDriven: if (!mvControlling->CabActive) @@ -3633,10 +3569,10 @@ bool TController::DecSpeed(bool force) { case TEngineType::None: // McZapkie-041003: wagon sterowniczy iDrivigFlags &= ~moveIncSpeed; // usunięcie flagi jazdy - if (force) // przy aktywacji kabiny jest potrzeba natychmiastowego wyzerowania - if (mvControlling->MainCtrlPosNo > 0) // McZapkie-041003: wagon sterowniczy, np. EZT - mvControlling->DecMainCtrl( std::min( mvControlling->MainCtrlPowerPos(), 2 ) ); -// mvControlling->AutoRelayCheck(); // sprawdzenie logiki sterowania + if (force && mvControlling->MainCtrlPosNo > 0) // przy aktywacji kabiny jest potrzeba natychmiastowego wyzerowania + // McZapkie-041003: wagon sterowniczy, np. EZT + mvControlling->DecMainCtrl(std::min(mvControlling->MainCtrlPowerPos(), 2)); + // mvControlling->AutoRelayCheck(); // sprawdzenie logiki sterowania return false; case TEngineType::ElectricSeriesMotor: OK = mvControlling->DecScndCtrl(2); // najpierw bocznik na zero @@ -3927,17 +3863,15 @@ void TController::SpeedSet() { } } // jak prędkość mniejsza niż minimalna na danym biegu, wrzucić niższy - else if( velocity < motorparams.fi ) { + else if( velocity < motorparams.fi && mvControlling->ScndCtrlPos > 1 ) { // ... but ensure we don't switch all way down to 0 - if( mvControlling->ScndCtrlPos > 1 ) { - mvControlling->DecMainCtrl( 2 ); - while( mvControlling->ScndCtrlPos > 1 // repeat the entry check as we're working in a loop - && mvControlling->DecScndCtrl(1) - && mvControlling->MotorParam[mvControlling->ScndCtrlPos].mIsat == 0.0 ) { // jeśli bieg jałowy to kolejny - ; - } - } - } + mvControlling->DecMainCtrl(2); + while (mvControlling->ScndCtrlPos > 1 // repeat the entry check as we're working in a loop + && mvControlling->DecScndCtrl(1) && mvControlling->MotorParam[mvControlling->ScndCtrlPos].mIsat == 0.0) + { // jeśli bieg jałowy to kolejny + ; + } + } } break; } @@ -4149,37 +4083,40 @@ void TController::SetTimeControllers() } } // 5.5 universal control for diesel electric vehicles - if (mvControlling->EngineType == TEngineType::DieselElectric && mvControlling->EIMCtrlType == 3) + if (mvControlling->EngineType == TEngineType::DieselElectric && mvControlling->EIMCtrlType == 3 && AccDesired >= 0.0 && mvControlling->StLinFlag) { // NOTE: partial implementation for speedinc/dec // TBD, TODO: implement fully? - if( AccDesired >= 0.0 - && mvControlling->StLinFlag ) { + auto const PosInc{mvControlling->MainCtrlPosNo}; + auto PosKeep{0}; + auto PosDec{0}; + for (int i = PosInc; i >= 0; --i) + { + if (mvControlling->UniCtrlList[i].SetCtrlVal <= 0) + { + if (mvControlling->UniCtrlList[i].SpeedDown == 0.0) + { + PosKeep = i; + } + if (mvControlling->UniCtrlList[i].SpeedDown > 0.01) + { + PosDec = i; + break; + } + } + } - auto const PosInc { mvControlling->MainCtrlPosNo }; - auto PosKeep { 0 }; - auto PosDec{ 0 }; - for( int i = PosInc; i >= 0; --i ) { - if( mvControlling->UniCtrlList[ i ].SetCtrlVal <= 0 ) { - if( mvControlling->UniCtrlList[ i ].SpeedDown == 0.0 ) { - PosKeep = i; - } - if( mvControlling->UniCtrlList[ i ].SpeedDown > 0.01 ) { - PosDec = i; - break; - } - } - } + auto const DesiredPos{(AccDesired > AbsAccS + 0.05 ? PosInc : AccDesired < AbsAccS - 0.05 ? PosDec : PosKeep)}; - auto const DesiredPos { ( - AccDesired > AbsAccS + 0.05 ? PosInc : - AccDesired < AbsAccS - 0.05 ? PosDec : - PosKeep ) }; - - while( mvControlling->MainCtrlPos > DesiredPos && mvControlling->DecMainCtrl( 1 ) ) { ; } - while( mvControlling->MainCtrlPos < DesiredPos && mvControlling->IncMainCtrl( 1 ) ) { ; } - } - } + while (mvControlling->MainCtrlPos > DesiredPos && mvControlling->DecMainCtrl(1)) + { + ; + } + while (mvControlling->MainCtrlPos < DesiredPos && mvControlling->IncMainCtrl(1)) + { + ; + } + } //6. UniversalBrakeButtons //6.1. Checking flags for Over pressure if (std::abs(BrakeCtrlPosition - gbh_FS)<0.5) { @@ -4228,12 +4165,9 @@ void TController::CheckTimeControllers() } } //4. Check Speed Control System - if (mvOccupied->EngineType == TEngineType::ElectricInductionMotor && mvOccupied->ScndCtrlPosNo>1 && mvOccupied->SpeedCtrlTypeTime) + if (mvOccupied->EngineType == TEngineType::ElectricInductionMotor && mvOccupied->ScndCtrlPosNo > 1 && mvOccupied->SpeedCtrlTypeTime && mvOccupied->ScndCtrlPosNo == 4) { - if (mvOccupied->ScndCtrlPosNo == 4) - { - mvOccupied->ScndCtrlPos = 2; - } + mvOccupied->ScndCtrlPos = 2; } //5. Check Main Controller in Dizels @@ -4315,15 +4249,12 @@ void TController::Doors( bool const Open, int const Side ) { return; } - if( true == doors_open() ) { + if( true == doors_open() && (pVehicle->MoverParameters->Doors.close_control == control_t::conductor || pVehicle->MoverParameters->Doors.close_control == control_t::driver || + pVehicle->MoverParameters->Doors.close_control == control_t::mixed) ) { // consist-wide remote signals to close doors - if( pVehicle->MoverParameters->Doors.close_control == control_t::conductor - || pVehicle->MoverParameters->Doors.close_control == control_t::driver - || pVehicle->MoverParameters->Doors.close_control == control_t::mixed ) { - cue_action( driver_hint::doorrightclose ); - cue_action( driver_hint::doorleftclose ); - } - } + cue_action(driver_hint::doorrightclose); + cue_action(driver_hint::doorleftclose); + } if( true == doors_permit_active() ) { cue_action( driver_hint::doorrightpermitoff ); cue_action( driver_hint::doorleftpermitoff ); @@ -5058,15 +4989,13 @@ void TController::JumpToNextOrder( bool const Ignoremergedchangedirection ) if (OrderList[OrderPos] != Wait_for_orders) { if( (OrderList[OrderPos] & Change_direction) != 0 // jeśli zmiana kierunku - && OrderList[OrderPos] != Change_direction ) { // ale nałożona na coś + && OrderList[OrderPos] != Change_direction && false == Ignoremergedchangedirection ) { // ale nałożona na coś - if( false == Ignoremergedchangedirection ) { - // usunięcie zmiany kierunku z innej komendy - OrderList[ OrderPos ] = TOrders( OrderList[ OrderPos ] & ~Change_direction ); - OrderCheck(); - return; - } - } + // usunięcie zmiany kierunku z innej komendy + OrderList[OrderPos] = TOrders(OrderList[OrderPos] & ~Change_direction); + OrderCheck(); + return; + } if (OrderPos < maxorders - 1) ++OrderPos; else @@ -5142,9 +5071,10 @@ void TController::OrderNext(TOrders NewOrder) void TController::OrderPush(TOrders NewOrder) { // zapisanie na stosie kolejnego rozkazu do wykonania - if (OrderPos == OrderTop) // jeśli miałby być zapis na aktalnej pozycji - if (OrderList[OrderPos] < Shunt) // ale nie jedzie - ++OrderTop; // niektóre operacje muszą zostać najpierw dokończone => zapis na kolejnej + if (OrderPos == OrderTop && OrderList[OrderPos] < Shunt) // jeśli miałby być zapis na aktalnej pozycji + // ale nie jedzie + ++OrderTop; + // niektóre operacje muszą zostać najpierw dokończone => zapis na kolejnej if (OrderList[OrderTop] != NewOrder) // jeśli jest to samo, to nie dodajemy OrderList[OrderTop++] = NewOrder; // dodanie rozkazu na stos // if (OrderTop" : " " ) + (std::to_string(b) + ": " + Order2Str(OrderList[b]))); - if (b) // z wyjątkiem pierwszej pozycji - if (OrderList[b] == Wait_for_orders) // jeśli końcowa komenda - break; // dalej nie trzeba + if (b && OrderList[b] == Wait_for_orders) // z wyjątkiem pierwszej pozycji + // jeśli końcowa komenda + break; + // dalej nie trzeba } }; @@ -5523,66 +5454,64 @@ TCommandType TController::BackwardScan( double const Range ) } } // reakcja AI w trybie manewrowym dodatkowo na sygnały manewrowe - if (OrderCurrentGet() ? OrderCurrentGet() & (Shunt | Loose_shunt | Connect) : - true) // w Wait_for_orders też widzi tarcze + if ((OrderCurrentGet() ? OrderCurrentGet() & (Shunt | Loose_shunt | Connect) : true) && (move || e->input_command() == TCommandType::cm_ShuntVelocity)) // w Wait_for_orders też widzi tarcze { - if (move || e->input_command() == TCommandType::cm_ShuntVelocity) - { // jeśli powyżej było SetVelocity 0 0, to dociągamy pod S1 -/* - if ((scandist > fMinProximityDist) && - ((mvOccupied->Vel > EU07_AI_NOMOVEMENT) || (scanvel == 0.0))) - { - // jeśli tarcza jest daleko, to: - //- jesli pojazd jedzie, to informujemy o zmianie prędkości - //- jeśli stoi, to z własnej inicjatywy może podjechać pod zamkniętą tarczę - if (mvOccupied->Vel > EU07_AI_NOMOVEMENT) // tylko jeśli jedzie - { // Mechanik->PutCommand("SetProximityVelocity",scandist,scanvel,sl); -#if LOGBACKSCAN - // WriteLog(edir+"SetProximityVelocity "+AnsiString(scandist)+" - // "+AnsiString(scanvel)); - WriteLog(edir); -#endif - // SetProximityVelocity(scandist,scanvel,&sl); - // jeśli jedzie na W5 albo koniec toru, to można zmienić kierunek - return (iDrivigFlags & moveTrackEnd) ? TCommandType::cm_ChangeDirection : - TCommandType::cm_Unknown; - // TBD: request direction change also if VelNext in current direction is 0, - // while signal behind requests movement? - } - } - else - { - // ustawiamy prędkość tylko wtedy, gdy ma ruszyć, albo stanąć albo ma stać pod - // tarczą stop trzeba powtarzać, bo inaczej zatrąbi i pojedzie sam if - // ((MoverParameters->Vel==0.0)||(scanvel==0.0)) //jeśli jedzie lub ma stanąć/stać - { // nie dostanie komendy jeśli jedzie i ma jechać - // PutCommand("ShuntVelocity",scanvel,e->Params[9].asMemCell->Value2(),&sl,stopSem); -#if LOGBACKSCAN - WriteLog(edir + " - [ShuntVelocity] [" + to_string(scanvel, 2) + "] [" + - to_string(e->input_value(2), 2) + "]"); -#endif - return (scanvel > 0 ? TCommandType::cm_ShuntVelocity : - TCommandType::cm_Unknown); - } - } - // NOTE: dead code due to returns in branching above - if ((scanvel != 0.0) && (scandist < 100.0)) - { - // jeśli Tm w odległości do 100m podaje zezwolenie na jazdę, to od razu ją - // ignorujemy, aby móc szukać kolejnej eSignSkip=e; //wtedy uznajemy ignorowaną przy - // poszukiwaniu nowej -#if LOGBACKSCAN - WriteLog(edir + " - will be ignored due to Ms2"); -#endif - return (scanvel > 0 ? TCommandType::cm_ShuntVelocity : TCommandType::cm_Unknown); - } -*/ - return VelNext > 0 ? + // jeśli powyżej było SetVelocity 0 0, to dociągamy pod S1 + /* + if ((scandist > fMinProximityDist) && + ((mvOccupied->Vel > EU07_AI_NOMOVEMENT) || (scanvel == 0.0))) + { + // jeśli tarcza jest daleko, to: + //- jesli pojazd jedzie, to informujemy o zmianie prędkości + //- jeśli stoi, to z własnej inicjatywy może podjechać pod zamkniętą tarczę + if (mvOccupied->Vel > EU07_AI_NOMOVEMENT) // tylko jeśli jedzie + { // Mechanik->PutCommand("SetProximityVelocity",scandist,scanvel,sl); + #if LOGBACKSCAN + // WriteLog(edir+"SetProximityVelocity "+AnsiString(scandist)+" + // "+AnsiString(scanvel)); + WriteLog(edir); + #endif + // SetProximityVelocity(scandist,scanvel,&sl); + // jeśli jedzie na W5 albo koniec toru, to można zmienić kierunek + return (iDrivigFlags & moveTrackEnd) ? TCommandType::cm_ChangeDirection : + TCommandType::cm_Unknown; + // TBD: request direction change also if VelNext in current direction is 0, + // while signal behind requests movement? + } + } + else + { + // ustawiamy prędkość tylko wtedy, gdy ma ruszyć, albo stanąć albo ma stać pod + // tarczą stop trzeba powtarzać, bo inaczej zatrąbi i pojedzie sam if + // ((MoverParameters->Vel==0.0)||(scanvel==0.0)) //jeśli jedzie lub ma stanąć/stać + { // nie dostanie komendy jeśli jedzie i ma jechać + // PutCommand("ShuntVelocity",scanvel,e->Params[9].asMemCell->Value2(),&sl,stopSem); + #if LOGBACKSCAN + WriteLog(edir + " - [ShuntVelocity] [" + to_string(scanvel, 2) + "] [" + + to_string(e->input_value(2), 2) + "]"); + #endif + return (scanvel > 0 ? TCommandType::cm_ShuntVelocity : + TCommandType::cm_Unknown); + } + } + // NOTE: dead code due to returns in branching above + if ((scanvel != 0.0) && (scandist < 100.0)) + { + // jeśli Tm w odległości do 100m podaje zezwolenie na jazdę, to od razu ją + // ignorujemy, aby móc szukać kolejnej eSignSkip=e; //wtedy uznajemy ignorowaną przy + // poszukiwaniu nowej + #if LOGBACKSCAN + WriteLog(edir + " - will be ignored due to Ms2"); + #endif + return (scanvel > 0 ? TCommandType::cm_ShuntVelocity : TCommandType::cm_Unknown); + } + */ + return VelNext > 0 ? TCommandType::cm_Unknown : // no need to bother if we can continue in current direction scanvel == 0 ? TCommandType::cm_Unknown : // no need to bother with a stop signal TCommandType::cm_ShuntVelocity; // otherwise report a relevant shunt signal behind - } // if (move?... + // if (move?... } // if (OrderCurrentGet()==Shunt) return e->m_passive && e->is_command() ? TCommandType::cm_Command : TCommandType::cm_Unknown; @@ -5978,25 +5907,19 @@ TController::determine_consist_state() { Ready = false; } // Ra: odluźnianie przeładowanych lokomotyw, ciągniętych na zimno - prowizorka... - if( bp >= 0.4 ) { // wg UIC określone sztywno na 0.04 - if( AIControllFlag || (Global.AITrainman && mvOccupied->Vel < EU07_AI_NOMOVEMENT && !is_emu() && !is_dmu())) { - if( BrakeCtrlPosition == gbh_RP // jest pozycja jazdy - && false == TestFlag(vehicle->Hamulec->GetBrakeStatus(), b_dmg) // brake isn't broken - && vehicle->PipePress - mvOccupied->Handle->GetRP() > -0.1 // jeśli ciśnienie jak dla jazdy - && vehicle->Hamulec->GetCRP() > vehicle->PipePress + 0.12 ) { // za dużo w zbiorniku - // indywidualne luzowanko - vehicle->BrakeReleaser( 1 ); - } - } - } - if (bp < 0.1) { - if ( AIControllFlag || Global.AITrainman ) { - if (false == TestFlag(vehicle->Hamulec->GetBrakeStatus(), b_dmg) // brake isn't broken - && vehicle->Hamulec->GetCRP() < vehicle->PipePress - 0.1 ) { // już nie jest za dużo w zbiorniku - // koniec indywidualnego luzowanka - vehicle->BrakeReleaser( 0 ); - } - } + if( bp >= 0.4 && (AIControllFlag || Global.AITrainman && mvOccupied->Vel < EU07_AI_NOMOVEMENT && !is_emu() && !is_dmu()) && BrakeCtrlPosition == gbh_RP // jest pozycja jazdy + && false == TestFlag(vehicle->Hamulec->GetBrakeStatus(), b_dmg) // brake isn't broken + && vehicle->PipePress - mvOccupied->Handle->GetRP() > -0.1 // jeśli ciśnienie jak dla jazdy + && vehicle->Hamulec->GetCRP() > vehicle->PipePress + 0.12 ) { // wg UIC określone sztywno na 0.04 + // za dużo w zbiorniku + // indywidualne luzowanko + vehicle->BrakeReleaser(1); + } + if (bp < 0.1 && (AIControllFlag || Global.AITrainman) && false == TestFlag(vehicle->Hamulec->GetBrakeStatus(), b_dmg) // brake isn't broken + && vehicle->Hamulec->GetCRP() < vehicle->PipePress - 0.1) { + // już nie jest za dużo w zbiorniku + // koniec indywidualnego luzowanka + vehicle->BrakeReleaser(0); } } fReady = std::max( bp, fReady ); // szukanie najbardziej zahamowanego @@ -6148,13 +6071,11 @@ TController::control_pantographs() { // uśrednione napięcie sieci: przy spadku poniżej wartości minimalnej opóźnić rozruch o losowy czas fVoltage = 0.5 * (fVoltage + std::max( mvControlling->GetTrainsetHighVoltage(), mvControlling->PantographVoltage ) ); - if( fVoltage < mvControlling->EnginePowerSource.CollectorParameters.MinV ) { + if( fVoltage < mvControlling->EnginePowerSource.CollectorParameters.MinV && fActionTime >= PrepareTime ) { // gdy rozłączenie WS z powodu niskiego napięcia - if( fActionTime >= PrepareTime ) { - // jeśli czas oczekiwania nie został ustawiony, losowy czas oczekiwania przed ponownym załączeniem jazdy - fActionTime = -2.0 - Random( 10 ); - } - } + // jeśli czas oczekiwania nie został ustawiony, losowy czas oczekiwania przed ponownym załączeniem jazdy + fActionTime = -2.0 - Random(10); + } // raise/lower pantographs as needed auto const useregularpantographlayout { @@ -6203,24 +6124,23 @@ TController::control_pantographs() { } } - if( mvOccupied->Vel > 5 ) { + if( mvOccupied->Vel > 5 && mvPantographUnit->EnginePowerSource.CollectorParameters.CollectorsNo > 1 ) { // opuszczenie przedniego po rozpędzeniu się o ile jest więcej niż jeden - if( mvPantographUnit->EnginePowerSource.CollectorParameters.CollectorsNo > 1 ) { - if( iDirection >= 0 && useregularpantographlayout ) // jak jedzie w kierunku sprzęgu 0 - { // poczekać na podniesienie tylnego - if( mvPantographUnit->PantFrontVolt != 0.0 - && mvPantographUnit->PantRearVolt != 0.0 ) { // czy jest napięcie zasilające na tylnym? - cue_action( driver_hint::frontpantographvalveoff ); // opuszcza od sprzęgu 0 - } - } - else { // poczekać na podniesienie przedniego - if( mvPantographUnit->PantRearVolt != 0.0 - && mvPantographUnit->PantFrontVolt != 0.0 ) { // czy jest napięcie zasilające na przednim? - cue_action( driver_hint::rearpantographvalveoff ); // opuszcza od sprzęgu 1 - } - } - } - } + if (iDirection >= 0 && useregularpantographlayout) // jak jedzie w kierunku sprzęgu 0 + { // poczekać na podniesienie tylnego + if (mvPantographUnit->PantFrontVolt != 0.0 && mvPantographUnit->PantRearVolt != 0.0) + { // czy jest napięcie zasilające na tylnym? + cue_action(driver_hint::frontpantographvalveoff); // opuszcza od sprzęgu 0 + } + } + else + { // poczekać na podniesienie przedniego + if (mvPantographUnit->PantRearVolt != 0.0 && mvPantographUnit->PantFrontVolt != 0.0) + { // czy jest napięcie zasilające na przednim? + cue_action(driver_hint::rearpantographvalveoff); // opuszcza od sprzęgu 1 + } + } + } } else { @@ -6237,26 +6157,29 @@ TController::control_pantographs() { } } else { - if( mvOccupied->AIHintPantUpIfIdle - && IdleTime > 45.0 - // NOTE: abs(stoptime) covers either at least 15 sec remaining for a scheduled stop, or 15+ secs spent at a basic stop - && std::abs(fStopTime) > 15.0 ) { - // spending a longer at a stop, raise also front pantograph - if( mvPantographUnit->EnginePowerSource.CollectorParameters.CollectorsNo > 1 ) { - if( iDirection >= 0 && useregularpantographlayout ) { - // jak jedzie w kierunku sprzęgu 0 - if( mvPantographUnit->PantFrontVolt == 0.0 ) { - cue_action( driver_hint::frontpantographvalveon, 5 ); // discard the hint if speed exceeds 5 km/h - } - } - else { - if( mvPantographUnit->PantRearVolt == 0.0 ) { - cue_action( driver_hint::rearpantographvalveon, 5 ); // discard the hint if speed exceeds 5 km/h - } - } - } - } - } + if( !mvOccupied->AIHintPantUpIfIdle || IdleTime <= 45.0 || std::abs(fStopTime) <= 15.0 || mvPantographUnit->EnginePowerSource.CollectorParameters.CollectorsNo <= 1 ) + { + } + else + { + // spending a longer at a stop, raise also front pantograph + if (iDirection >= 0 && useregularpantographlayout) + { + // jak jedzie w kierunku sprzęgu 0 + if (mvPantographUnit->PantFrontVolt == 0.0) + { + cue_action(driver_hint::frontpantographvalveon, 5); // discard the hint if speed exceeds 5 km/h + } + } + else + { + if (mvPantographUnit->PantRearVolt == 0.0) + { + cue_action(driver_hint::rearpantographvalveon, 5); // discard the hint if speed exceeds 5 km/h + } + } + } + } } void @@ -6465,25 +6388,21 @@ TController::control_doors() { // HACK: crude way to deal with automatic door opening on W4 preventing further ride // for human-controlled vehicles with no door control and dynamic brake auto-activating with door open // TODO: check if this situation still happens and the hack is still needed - if( false == AIControllFlag ) { + if( false == AIControllFlag && (mvControlling->EngineType == TEngineType::DieselEngine ? mvControlling->RList[mvControlling->MainCtrlPos].Mn > 0 : mvControlling->MainCtrlPowerPos() > 0) ) { // for diesel engines react when engine is put past idle revolutions // for others straightforward master controller check - if( mvControlling->EngineType == TEngineType::DieselEngine ? mvControlling->RList[mvControlling->MainCtrlPos].Mn > 0 : mvControlling->MainCtrlPowerPos() > 0 ) { - Doors( false ); - return; - } - } + Doors(false); + return; + } } void TController::UpdateCommand() { - if( false == mvOccupied->CommandIn.Command.empty() ) { - if( false == mvOccupied->RunInternalCommand() ) { - // rozpoznaj komende bo lokomotywa jej nie rozpoznaje - RecognizeCommand(); // samo czyta komendę wstawioną do pojazdu? - } - } + if( false == mvOccupied->CommandIn.Command.empty() && false == mvOccupied->RunInternalCommand() ) { + // rozpoznaj komende bo lokomotywa jej nie rozpoznaje + RecognizeCommand(); // samo czyta komendę wstawioną do pojazdu? + } } void @@ -6751,12 +6670,10 @@ TController::check_departure() { } } } - else if( VelSignal == 0.0 && VelNext > 0.0 && mvOccupied->Vel < 1.0 ) { - if( iCoupler > 0 || (iDrivigFlags & moveStopHere) == 0 ) { - // Ra: tu jest coś nie tak, bo bez tego warunku ruszało w manewrowym !!!! - SetVelocity( VelNext, VelNext, stopSem ); // omijanie SBL - } - } + else if( VelSignal == 0.0 && VelNext > 0.0 && mvOccupied->Vel < 1.0 && (iCoupler > 0 || (iDrivigFlags & moveStopHere) == 0) ) { + // Ra: tu jest coś nie tak, bo bez tego warunku ruszało w manewrowym !!!! + SetVelocity(VelNext, VelNext, stopSem); // omijanie SBL + } } // verify progress of load exchange @@ -6802,12 +6719,11 @@ TController::UpdateChangeDirection() { // TODO: rework into driver mode independent routine if( false == TestFlag( OrderCurrentGet(), Change_direction ) ) { return; } - if( true == AIControllFlag ) { + if( true == AIControllFlag && mvOccupied->Vel < EU07_AI_NOMOVEMENT ) { // sprobuj zmienic kierunek (może być zmieszane z jeszcze jakąś komendą) - if( mvOccupied->Vel < EU07_AI_NOMOVEMENT ) { - // jeśli się zatrzymał, to zmieniamy kierunek jazdy, a nawet kabinę/człon - Activation(); // ustawienie zadanego wcześniej kierunku i ewentualne przemieszczenie AI - } // Change_direction (tylko dla AI) + // jeśli się zatrzymał, to zmieniamy kierunek jazdy, a nawet kabinę/człon + Activation(); // ustawienie zadanego wcześniej kierunku i ewentualne przemieszczenie AI + // Change_direction (tylko dla AI) } // shared part of the routine, implement if direction matches what was requested if( mvOccupied->Vel < EU07_AI_NOMOVEMENT @@ -6818,15 +6734,13 @@ TController::UpdateChangeDirection() { VelSignalNext = -1; PrepareEngine(); JumpToNextOrder(); // następnie robimy, co jest do zrobienia (Shunt albo Obey_train) - if( OrderCurrentGet() & ( Shunt | Loose_shunt | Connect ) ) { + if( OrderCurrentGet() & (Shunt | Loose_shunt | Connect) && false == TestFlag(iDrivigFlags, moveStopHere) ) { // jeśli dalej mamy manewry - if( false == TestFlag( iDrivigFlags, moveStopHere ) ) { - // o ile nie ma stać w miejscu, - // jechać od razu w przeciwną stronę i nie trąbić z tego tytułu: - iDrivigFlags &= ~moveStartHorn; - SetVelocity( fShuntVelocity, fShuntVelocity ); - } - } + // o ile nie ma stać w miejscu, + // jechać od razu w przeciwną stronę i nie trąbić z tego tytułu: + iDrivigFlags &= ~moveStartHorn; + SetVelocity(fShuntVelocity, fShuntVelocity); + } } } @@ -6906,29 +6820,25 @@ TController::UpdateConnect() { auto *vehicleparameters { vehicle->MoverParameters }; if( vehicleparameters->Couplers[ couplingend ].CouplingFlag != iCoupler ) { auto const &neighbour { vehicleparameters->Neighbours[ couplingend ] }; - if( neighbour.vehicle != nullptr ) { - if( neighbour.distance < 10 ) { - // check whether we need to attach coupler adapter - auto coupleriscompatible { true }; - auto const &coupler { vehicleparameters->Couplers[ couplingend ] }; - if( coupler.type() != TCouplerType::Automatic ) { - auto const &othercoupler = neighbour.vehicle->MoverParameters->Couplers[ ( neighbour.vehicle_end != 2 ? neighbour.vehicle_end : coupler.ConnectedNr ) ]; - if( othercoupler.type() == TCouplerType::Automatic ) { - coupleriscompatible = false; - cue_action( driver_hint::couplingadapterattach, couplingend ); - } - } - // próba podczepienia - if( AIControllFlag || Global.AITrainman ) { - if( coupleriscompatible && neighbour.distance < 2 ) { - vehicleparameters->Attach( - couplingend, neighbour.vehicle_end, - neighbour.vehicle->MoverParameters, - iCoupler ); - } - } - } - } + if( neighbour.vehicle != nullptr && neighbour.distance < 10 ) { + // check whether we need to attach coupler adapter + auto coupleriscompatible{true}; + auto const &coupler{vehicleparameters->Couplers[couplingend]}; + if (coupler.type() != TCouplerType::Automatic) + { + auto const &othercoupler = neighbour.vehicle->MoverParameters->Couplers[(neighbour.vehicle_end != 2 ? neighbour.vehicle_end : coupler.ConnectedNr)]; + if (othercoupler.type() == TCouplerType::Automatic) + { + coupleriscompatible = false; + cue_action(driver_hint::couplingadapterattach, couplingend); + } + } + // próba podczepienia + if ((AIControllFlag || Global.AITrainman) && coupleriscompatible && neighbour.distance < 2) + { + vehicleparameters->Attach(couplingend, neighbour.vehicle_end, neighbour.vehicle->MoverParameters, iCoupler); + } + } } // NOTE: no else as the preceeding block can potentially establish expected connection if( vehicleparameters->Couplers[ couplingend ].CouplingFlag == iCoupler ) { @@ -6943,18 +6853,12 @@ TController::UpdateConnect() { } // moveConnect else { // początek podczepiania, z wyłączeniem sprawdzania fTrackBlock - if( Obstacle.distance <= 20.0 ) { - if( !iCouplingVehicle ) { - // write down which vehicle should be coupled with the target consist, - // so we don't lose track of it if the user does something unexpected - iCouplingVehicle = { - pVehicles[ end::front ], - ( pVehicles[ end::front ]->DirectionGet() > 0 ? - end::front : - end::rear ) }; - iDrivigFlags |= moveConnect; - } - } + if( Obstacle.distance <= 20.0 && !iCouplingVehicle ) { + // write down which vehicle should be coupled with the target consist, + // so we don't lose track of it if the user does something unexpected + iCouplingVehicle = {pVehicles[end::front], (pVehicles[end::front]->DirectionGet() > 0 ? end::front : end::rear)}; + iDrivigFlags |= moveConnect; + } } } @@ -7009,13 +6913,11 @@ TController::UpdateDisconnect() { if( iVehicleCount >= 0 ) { // early test for human drivers, who might disregard the proper procedure, or perform it before they receive the order to - if( false == AIControllFlag ) { + if( false == AIControllFlag && unit_count(iVehicleCount + 2) <= iVehicleCount + 1 ) { // iVehicleCount = 0 means the consist should be reduced to (1) leading unit, thus we increase our test values by 1 - if( unit_count( iVehicleCount + 2 ) <= iVehicleCount + 1 ) { - iVehicleCount = -2; // odczepiono, co było do odczepienia - return; // we'll wrap up the procedure on the next update beat - } - } + iVehicleCount = -2; // odczepiono, co było do odczepienia + return; // we'll wrap up the procedure on the next update beat + } // regular uncoupling procedure, performed by ai and naively expected from the human drivers // 3rd stage: change direction, compress buffers and uncouple if( iDirection != iDirectionOrder ) { @@ -7135,11 +7037,10 @@ TController::handle_engine() { OrderNext( Prepare_engine ); } // basic engine preparation - if( OrderCurrentGet() == Prepare_engine ) { - if( PrepareEngine() ) { // gotowy do drogi? - JumpToNextOrder(); - } - } + if( OrderCurrentGet() == Prepare_engine && PrepareEngine() ) { + // gotowy do drogi? + JumpToNextOrder(); + } // engine state can potentially deteriorate in one of usual driving modes if( OrderCurrentGet() & (Change_direction | Connect | Disconnect | Shunt | Loose_shunt | Obey_train | Bank) && false == iEngineActive ) { @@ -7454,16 +7355,12 @@ TController::adjust_desired_speed_for_limits() { return; // speed limit can't get any lower } - if( ( OrderCurrentGet() & ( Shunt | Loose_shunt | Obey_train | Bank ) ) != 0 ) { + if( (OrderCurrentGet() & (Shunt | Loose_shunt | Obey_train | Bank)) != 0 && (iDrivigFlags & moveStopHere) != 0 && mvOccupied->Vel < 0.01 && VelSignalNext == 0.0 ) { // w Connect nie, bo moveStopHere odnosi się do stanu po połączeniu - if( (iDrivigFlags & moveStopHere) != 0 - && mvOccupied->Vel < 0.01 - && VelSignalNext == 0.0 ) { - // jeśli ma czekać na wolną drogę, stoi a wyjazdu nie ma, to ma stać - VelDesired = 0.0; - return; // speed limit can't get any lower - } - } + // jeśli ma czekać na wolną drogę, stoi a wyjazdu nie ma, to ma stać + VelDesired = 0.0; + return; // speed limit can't get any lower + } if( OrderCurrentGet() == Wait_for_orders ) { // wait means sit and wait @@ -7719,18 +7616,11 @@ TController::adjust_desired_speed_for_current_speed() { } // NOTE: as a stop-gap measure the routine is limited to trains only while car calculations seem off - if( is_train() ) { - if( vel < VelDesired ) { - // don't adjust acceleration when going above current target speed - if( -AccDesired * BrakeAccFactor() < ( - false == Ready || VelNext > vel - 40.0 ? - fBrake_a0[ 0 ] * 0.8 : - -fAccThreshold ) - / ( 1.2 * braking_distance_multiplier( VelNext ) ) ) { - AccDesired = std::max( EU07_AI_NOACCELERATION, AccDesired ); - } - } - } + if( is_train() && vel < VelDesired && + -AccDesired * BrakeAccFactor() < (false == Ready || VelNext > vel - 40.0 ? fBrake_a0[0] * 0.8 : -fAccThreshold) / (1.2 * braking_distance_multiplier(VelNext)) ) { + // don't adjust acceleration when going above current target speed + AccDesired = std::max(EU07_AI_NOACCELERATION, AccDesired); + } } // hamowanie kontrolne @@ -7876,15 +7766,11 @@ void TController::control_relays() { void TController::control_motor_connectors() { // ensure motor connectors are active - if( mvControlling->EngineType == TEngineType::ElectricSeriesMotor - || mvControlling->EngineType == TEngineType::DieselElectric - || is_emu() ) { - if( Need_TryAgain ) { - // true, jeśli druga pozycja w elektryku nie załapała - cue_action( driver_hint::mastercontrollersetzerospeed ); - Need_TryAgain = false; - } - } + if( (mvControlling->EngineType == TEngineType::ElectricSeriesMotor || mvControlling->EngineType == TEngineType::DieselElectric || is_emu()) && Need_TryAgain ) { + // true, jeśli druga pozycja w elektryku nie załapała + cue_action(driver_hint::mastercontrollersetzerospeed); + Need_TryAgain = false; + } } void TController::control_tractive_force() { @@ -7892,23 +7778,20 @@ void TController::control_tractive_force() { auto const velocity { DirectionalVel() }; // jeśli przyspieszenie pojazdu jest mniejsze niż żądane oraz... if( AccDesired > EU07_AI_NOACCELERATION // don't add power if not asked for actual speed-up - && (AbsAccS < AccDesired /*- std::min( 0.05, 0.01 * iDriverFailCount )*/ || (mvOccupied->SpeedCtrlUnit.IsActive && velocity < mvOccupied->SpeedCtrlUnit.FullPowerVelocity)) - && false == TestFlag(iDrivigFlags, movePress) ) { + && (AbsAccS < AccDesired /*- std::min( 0.05, 0.01 * iDriverFailCount )*/ || mvOccupied->SpeedCtrlUnit.IsActive && velocity < mvOccupied->SpeedCtrlUnit.FullPowerVelocity) && + false == TestFlag(iDrivigFlags, movePress) && + velocity < (VelDesired == 1.0 ? // work around for trains getting stuck on tracks with speed limit = 1 + VelDesired : + VelDesired - fVelMinus) && + (ActualProximityDist > (is_car() ? fMinProximityDist : // cars are allowed to move within min proximity distance + fMaxProximityDist) ? // other vehicle types keep wider margin + true : + velocity + 1.0 < VelNext) ) { // ...jeśli prędkość w kierunku czoła jest mniejsza od dozwolonej o margines... - if( velocity < ( - VelDesired == 1.0 ? // work around for trains getting stuck on tracks with speed limit = 1 - VelDesired : - VelDesired - fVelMinus ) ) { - // within proximity margin don't exceed next scheduled speed, outside of the margin accelerate as you like - if( ActualProximityDist > (is_car() ? fMinProximityDist : // cars are allowed to move within min proximity distance - fMaxProximityDist) ? // other vehicle types keep wider margin - true : - velocity + 1.0 < VelNext ) { - // ...to można przyspieszyć - increase_tractive_force(); - } - } - } + // within proximity margin don't exceed next scheduled speed, outside of the margin accelerate as you like + // ...to można przyspieszyć + increase_tractive_force(); + } // zmniejszanie predkosci // margines dla prędkości jest doliczany tylko jeśli oczekiwana prędkość jest większa od 5km/h if( false == TestFlag( iDrivigFlags, movePress ) ) { @@ -7962,12 +7845,10 @@ void TController::control_braking_force() { auto const velocity { DirectionalVel() }; // jeśli przyspieszamy, to nie hamujemy - if( AccDesired > 0.0 ) { - if( OrderCurrentGet() != Disconnect // przy odłączaniu nie zwalniamy tu hamulca - && false == TestFlag(iDrivigFlags, movePress) ) { - cue_action( driver_hint::brakingforcesetzero ); - } - } + if( AccDesired > 0.0 && OrderCurrentGet() != Disconnect // przy odłączaniu nie zwalniamy tu hamulca + && false == TestFlag(iDrivigFlags, movePress) ) { + cue_action(driver_hint::brakingforcesetzero); + } if( is_emu() && !ForcePNBrake ) { // właściwie, to warunek powinien być na działający EP @@ -7989,11 +7870,10 @@ void TController::control_braking_force() { if( AbsAccS < AccDesired - 0.05 ) { // jeśli opóźnienie większe od wymaganego (z histerezą) luzowanie, gdy za dużo // TBD: check if the condition isn't redundant with the DecBrake() code - if( BrakeCtrlPosition >= 0 ) { - if( VelDesired > 0.0 ) { // sanity check to prevent unintended brake release on sharp slopes - cue_action( driver_hint::brakingforcedecrease ); - } - } + if( BrakeCtrlPosition >= 0 && VelDesired > 0.0 ) { + // sanity check to prevent unintended brake release on sharp slopes + cue_action(driver_hint::brakingforcedecrease); + } } else { cue_action( driver_hint::brakingforcelap ); @@ -8002,50 +7882,30 @@ void TController::control_braking_force() { } // type & dt_ezt else { // a stara wersja w miarę dobrze działa na składy wagonowe - if( ( AccDesired < fAccGravity - 0.1 && AbsAccS > AccDesired + fBrake_a1[0] ) // regular braking - || ( fAccGravity < -0.05 && velocity < -0.1 ) ) { // also brake if uphill and slipping back + if( (AccDesired < fAccGravity - 0.1 && AbsAccS > AccDesired + fBrake_a1[0] // regular braking + || fAccGravity < -0.05 && velocity < -0.1) && + (fBrakeTime < 0.0 || AccDesired < fAccGravity - 0.5 || BrakeCtrlPosition <= 0) ) { // also brake if uphill and slipping back // u góry ustawia się hamowanie na fAccThreshold - if( fBrakeTime < 0.0 - || AccDesired < fAccGravity - 0.5 - || BrakeCtrlPosition <= 0 ) { - // jeśli upłynął czas reakcji hamulca, chyba że nagłe albo luzował - // TODO: check whether brake delay variable still has any purpose - cue_action( - driver_hint::brakingforceincrease, - // Ra: ten czas należy zmniejszyć, jeśli czas dojazdu do zatrzymania jest mniejszy - ( 3.0 - + 0.5 * ( ( - mvOccupied->BrakeDelayFlag > bdelay_G ? - mvOccupied->BrakeDelay[ 1 ] : - mvOccupied->BrakeDelay[ 3 ] ) - - 3.0 ) ) - * 0.5 ); // Ra: tymczasowo, bo przeżyna S1 - } - } - if ( AccDesired < fAccGravity - 0.05 - && AccDesired - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 ) { + // jeśli upłynął czas reakcji hamulca, chyba że nagłe albo luzował + // TODO: check whether brake delay variable still has any purpose + cue_action(driver_hint::brakingforceincrease, + // Ra: ten czas należy zmniejszyć, jeśli czas dojazdu do zatrzymania jest mniejszy + (3.0 + 0.5 * ((mvOccupied->BrakeDelayFlag > bdelay_G ? mvOccupied->BrakeDelay[1] : mvOccupied->BrakeDelay[3]) - 3.0)) * 0.5); // Ra: tymczasowo, bo przeżyna S1 + } + if ( AccDesired < fAccGravity - 0.05 && AccDesired - fBrake_a1[0] * 0.51 - AbsAccS > 0.05 && OrderCurrentGet() != Disconnect && VelDesired > 0.0 ) { // jak hamuje, to nie tykaj kranu za często // yB: luzuje hamulec dopiero przy różnicy opóźnień rzędu 0.2 - if( OrderCurrentGet() != Disconnect ) { // przy odłączaniu nie zwalniamy tu hamulca - if( VelDesired > 0.0 ) { // sanity check to prevent unintended brake release on sharp slopes - // TODO: check whether brake delay variable still has any purpose - cue_action( - driver_hint::brakingforcedecrease, - (mvOccupied->BrakeDelayFlag > bdelay_G ? mvOccupied->BrakeDelay[0] : mvOccupied->BrakeDelay[2]) / 3.0 - * 0.5 ); // Ra: tymczasowo, bo przeżyna S1 - } - } - } + // przy odłączaniu nie zwalniamy tu hamulca + // sanity check to prevent unintended brake release on sharp slopes + // TODO: check whether brake delay variable still has any purpose + cue_action(driver_hint::brakingforcedecrease, + (mvOccupied->BrakeDelayFlag > bdelay_G ? mvOccupied->BrakeDelay[0] : mvOccupied->BrakeDelay[2]) / 3.0 * 0.5); // Ra: tymczasowo, bo przeżyna S1 + } // stop-gap measure to ensure cars actually brake to stop even when above calculactions go awry // instead of releasing the brakes and creeping into obstacle at 1-2 km/h - if( is_car() ) { - if( VelDesired == 0.0 - && velocity > VelDesired - && ActualProximityDist <= fMinProximityDist - && mvOccupied->LocalBrakePosA < 0.01 ) { - cue_action( driver_hint::brakingforceincrease ); - } - } + if( is_car() && VelDesired == 0.0 && velocity > VelDesired && ActualProximityDist <= fMinProximityDist && mvOccupied->LocalBrakePosA < 0.01 ) { + cue_action(driver_hint::brakingforceincrease); + } } // odhamowywanie składu po zatrzymaniu i zabezpieczanie lokomotywy @@ -8158,27 +8018,17 @@ void TController::control_main_pipe() { || mvOccupied->BrakeHandle == TBrakeHandle::MHZ_K8P || mvOccupied->BrakeHandle == TBrakeHandle::M394 ) { - if( iDrivigFlags & moveOerlikons - || true == IsCargoTrain ) { + if( (iDrivigFlags & moveOerlikons || true == IsCargoTrain) && BrakeCtrlPosition == gbh_RP && AbsAccS < 0.03 && AccDesired > -0.03 && VelDesired - mvOccupied->Vel > 2.0 && fReady > 0.35 && + mvOccupied->EqvtPipePress < 4.5 // don't charge with a risk of overcharging the main pipe + && mvOccupied->Compressor >= 7.5 // don't charge without sufficient pressure in the tank + && BrakeChargingCooldown >= 0.0 // don't charge while cooldown is active + && (ActualProximityDist > 100.0 // don't charge if we're about to be braking soon + || min_speed(mvOccupied->Vel, VelNext) >= mvOccupied->Vel) ) { - if( BrakeCtrlPosition == gbh_RP - && AbsAccS < 0.03 - && AccDesired > -0.03 - && VelDesired - mvOccupied->Vel > 2.0 ) { - - if( fReady > 0.35 - && mvOccupied->EqvtPipePress < 4.5 // don't charge with a risk of overcharging the main pipe - && mvOccupied->Compressor >= 7.5 // don't charge without sufficient pressure in the tank - && BrakeChargingCooldown >= 0.0 // don't charge while cooldown is active - && ( ActualProximityDist > 100.0 // don't charge if we're about to be braking soon - || min_speed(mvOccupied->Vel, VelNext) >= mvOccupied->Vel ) ) { - - BrakeLevelSet( gbh_FS ); - // don't charge the brakes too often, or we risk overcharging - BrakeChargingCooldown = -1 * std::clamp( iVehicles * 3, 30, 90 ); - } - } - } + BrakeLevelSet(gbh_FS); + // don't charge the brakes too often, or we risk overcharging + BrakeChargingCooldown = -1 * std::clamp(iVehicles * 3, 30, 90); + } if( mvOccupied->Compressor < 5.0 || ( BrakeCtrlPosition < gbh_RP @@ -8208,11 +8058,10 @@ 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 - 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 - } - } + if( (OrderCurrentGet() & ~(Shunt | Loose_shunt | Obey_train | Bank)) == 0 && std::fabs(VelSignal) >= 1.0 ) { // jedzie w dowolnym trybie albo Wait_for_orders + // 0.1 nie wysyła się do samochodow, bo potem nie ruszą + PutCommand("SetVelocity", VelSignal, VelNext, nullptr); // komenda robi dodatkowe operacje + } break; } case TCommandType::cm_ShuntVelocity: { // od wersji 357 Tm nie budzi wyłączonej lokomotywy @@ -8225,17 +8074,15 @@ 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 ) { + if( (OrderCurrentGet() & ~(Shunt | Loose_shunt | Obey_train | Bank)) == 0 && mvOccupied->Vel < 0.1 ) { // jedzie w dowolnym trybie albo Wait_for_orders - if( mvOccupied->Vel < 0.1 ) { - // dopiero jak stanie -/* - PutCommand( eSignNext->text(), eSignNext->value( 1 ), eSignNext->value( 2 ), nullptr ); - eSignNext->StopCommandSent(); // się wykonało już -*/ // replacement of the above - eSignNext->send_command( *this ); - } - } + // dopiero jak stanie + /* + PutCommand( eSignNext->text(), eSignNext->value( 1 ), eSignNext->value( 2 ), nullptr ); + eSignNext->StopCommandSent(); // się wykonało już + */ // replacement of the above + eSignNext->send_command(*this); + } break; } default: { From 76a39af8c229d39f8d89122b39d1c41670bb4c4a Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 1 Oct 2026 20:21:01 +0200 Subject: [PATCH 38/38] fix: bound MotorParam and MPTRelay indices (#196) --- McZapkie/MOVER.h | 7 +++- McZapkie/Mover.cpp | 83 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index a6658494..e7bd1d23 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -1086,7 +1086,7 @@ struct TMPTRelay double Iup = 0.0; double Idown = 0.0; }; -typedef TMPTRelay TMPTRelayTable[8]; +typedef TMPTRelay TMPTRelayTable[MotorParametersArraySize + 1]; // indexed like MotorParam struct TMotorParameters { @@ -1729,6 +1729,11 @@ class TMoverParameters TMotorParameters MotorParam[MotorParametersArraySize + 1]; /*rozne parametry silnika przy bocznikowaniach*/ /*dla lokomotywy spalinowej z przekladnia mechaniczna: przelozenia biegow*/ + // bounds-safe read access to MotorParam + TMotorParameters const &motor_param(int const Position) const + { + return MotorParam[std::clamp(Position, 0, MotorParametersArraySize)]; + } TTransmision Transmision; // record {liczba zebow przekladni} // NToothM, NToothW : byte; diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 61bdc17a..82dd245f 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -355,7 +355,7 @@ double TMoverParameters::Current(double n, double U) if (DynamicBrakeFlag && !FuseFlag && DynamicBrakeType == dbrake_automatic && Power110vIsAvailable && Mains) // hamowanie EP09 //TUHEX { // TODO: zrobic bardziej uniwersalne nie tylko dla EP09 - MotorCurrent = -std::max(MotorParam[0].fi * (Vadd / (Vadd + MotorParam[0].Isat) - MotorParam[0].fi0), 0.) * n * 2.0 / DynamicBrakeRes; + MotorCurrent = -std::max(motor_param(0).fi * (Vadd / (Vadd + motor_param(0).Isat) - motor_param(0).fi0), 0.) * n * 2.0 / DynamicBrakeRes; } else if (RList[MainCtrlActualPos].Bn == 0 || false == StLinFlag) { @@ -390,7 +390,7 @@ double TMoverParameters::Current(double n, double U) if (DynamicBrakeType == dbrake_switch && TrainType == dt_ET42) { // z Megapacka Rz = WindingRes + R; - MotorCurrent = -MotorParam[SP].fi * n / Rz; //{hamowanie silnikiem na oporach rozruchowych} + MotorCurrent = -motor_param(SP).fi * n / Rz; //{hamowanie silnikiem na oporach rozruchowych} } } else @@ -398,13 +398,13 @@ double TMoverParameters::Current(double n, double U) } else { - U1 = U + Mn * n * MotorParam[SP].fi0 * MotorParam[SP].fi; + U1 = U + Mn * n * motor_param(SP).fi0 * motor_param(SP).fi; // writepaslog("U1 ", FloatToStr(U1)); // writepaslog("Isat ", FloatToStr(MotorParam[SP].Isat)); // writepaslog("fi ", FloatToStr(MotorParam[SP].fi)); - Isf = Sign(U1) * MotorParam[SP].Isat; + Isf = Sign(U1) * motor_param(SP).Isat; // writepaslog("Isf ", FloatToStr(Isf)); - Delta = square(Isf * Rz + Mn * MotorParam[SP].fi * n - U1) + 4.0 * U1 * Isf * Rz; // 105 * 1.67 + Mn * 140.9 * 20.532 - U1 + Delta = square(Isf * Rz + Mn * motor_param(SP).fi * n - U1) + 4.0 * U1 * Isf * Rz; // 105 * 1.67 + Mn * 140.9 * 20.532 - U1 // DeltaQ = Isf * Rz + Mn * MotorParam[SP].fi * n - U1 + 4 * U1 * Isf * Rz; // writepaslog("Delta ", FloatToStr(Delta)); // writepaslog("DeltaQ ", FloatToStr(DeltaQ)); @@ -412,9 +412,9 @@ double TMoverParameters::Current(double n, double U) if (Mains) { if (U > 0) - MotorCurrent = (U1 - Isf * Rz - Mn * MotorParam[SP].fi * n + std::sqrt(Delta)) / (2.0 * Rz); + MotorCurrent = (U1 - Isf * Rz - Mn * motor_param(SP).fi * n + std::sqrt(Delta)) / (2.0 * Rz); else - MotorCurrent = (U1 - Isf * Rz - Mn * MotorParam[SP].fi * n - std::sqrt(Delta)) / (2.0 * Rz); + MotorCurrent = (U1 - Isf * Rz - Mn * motor_param(SP).fi * n - std::sqrt(Delta)) / (2.0 * Rz); } else MotorCurrent = 0; @@ -5534,9 +5534,9 @@ double TMoverParameters::TractionForce(double dt) case TEngineType::DieselEngine: { if (ShuntMode) // dodatkowa przekładnia np. dla 2Ls150 - dtrans = AnPos * Transmision.Ratio * MotorParam[ScndCtrlActualPos].mIsat; + dtrans = AnPos * Transmision.Ratio * motor_param(ScndCtrlActualPos).mIsat; else - dtrans = Transmision.Ratio * MotorParam[ScndCtrlActualPos].mIsat; + dtrans = Transmision.Ratio * motor_param(ScndCtrlActualPos).mIsat; dmoment = dizel_Momentum(dizel_fill, dtrans * nrot * DirActive, dt); // oblicza tez enrot break; @@ -5981,10 +5981,10 @@ double TMoverParameters::TractionForce(double dt) Mm = Mw / Transmision.Ratio; // moment silnika trakcyjnego // with MotorParam[ScndCtrlPos] do - if (abs(Mm) > MotorParam[ScndCtrlPos].fi) - Im = NPoweredAxles * abs(abs(Mm) / MotorParam[ScndCtrlPos].mfi + MotorParam[ScndCtrlPos].mIsat); + if (abs(Mm) > motor_param(ScndCtrlPos).fi) + Im = NPoweredAxles * abs(abs(Mm) / motor_param(ScndCtrlPos).mfi + motor_param(ScndCtrlPos).mIsat); else - Im = NPoweredAxles * sqrt(abs(Mm * MotorParam[ScndCtrlPos].Isat)); + Im = NPoweredAxles * sqrt(abs(Mm * motor_param(ScndCtrlPos).Isat)); if (ShuntMode) { @@ -6762,7 +6762,7 @@ double TMoverParameters::Momentum(double I) SP = RList[MainCtrlActualPos].ScndAct; // Momentum:=mfi*I*(1-1.0/(Abs(I)/mIsat+1)); - return MotorParam[SP].mfi * I * (abs(I) / (abs(I) + MotorParam[SP].mIsat) - MotorParam[SP].mfi0); + return motor_param(SP).mfi * I * (abs(I) / (abs(I) + motor_param(SP).mIsat) - motor_param(SP).mfi0); } // ************************************************************************************************* @@ -6773,7 +6773,7 @@ double TMoverParameters::MomentumF(double I, double Iw, int SCP) { // umozliwia dokladne sterowanie wzbudzeniem - return MotorParam[SCP].mfi * I * std::max(abs(Iw) / (abs(Iw) + MotorParam[SCP].mIsat) - MotorParam[SCP].mfi0, 0.); + return motor_param(SCP).mfi * I * std::max(abs(Iw) / (abs(Iw) + motor_param(SCP).mIsat) - motor_param(SCP).mfi0, 0.); } // ************************************************************************************************* @@ -6906,7 +6906,7 @@ bool TMoverParameters::AutoRelayCheck(void) } // sprawdzenie wszystkich warunkow (AutoRelayFlag, AutoSwitch, Im VelUp) { @@ -7831,7 +7831,7 @@ bool TMoverParameters::dizel_AutoGearCheck(void) } else dizel_EngageSwitch(0.0); - if (!(MotorParam[ScndCtrlActualPos].mIsat > 0)) + if (!(motor_param(ScndCtrlActualPos).mIsat > 0)) dizel_EngageSwitch(0.0); // wylacz sprzeglo na pozycjach neutralnych if (!AutoRelayFlag) ScndCtrlActualPos = ScndCtrlPos; @@ -7953,7 +7953,7 @@ double TMoverParameters::dizel_fillcheck(int mcp, double dt) // napelnienie zalezne od MainCtrlPos if (EIMCtrlType > 0) { - realfill = std::max(0.0, std::min(eimic_real, 1 - MotorParam[ScndCtrlActualPos].Isat)); + realfill = std::max(0.0, std::min(eimic_real, 1 - motor_param(ScndCtrlActualPos).Isat)); if (eimic_real > 0.005 && !hydro_TC_Lockup) { dizel_nreg_min = std::min(dizel_nreg_min + 2.5 * dt, dizel_nmin_hdrive + eimic_real * dizel_nmin_hdrive_factor); @@ -8041,7 +8041,7 @@ double TMoverParameters::dizel_Momentum(double dizel_fill, double n, double dt) double Moment = 0, enMoment = 0, gearMoment = 0, eps = 0, newn = 0, friction = 0, neps = 0; double TorqueH = 0, TorqueL = 0, TorqueC = 0; n = n * CabActive; - if (MotorParam[ScndCtrlActualPos].mIsat < 0.001 || DirActive == 0) + if (motor_param(ScndCtrlActualPos).mIsat < 0.001 || DirActive == 0) n = enrot; friction = dizel_engagefriction; hydro_TC_nIn = enrot; // wal wejsciowy przetwornika momentu @@ -9076,6 +9076,17 @@ int s2NNW(std::string s) // Q: 20160717 // ************************************************************************************************* // parsowanie Motor Param Table +// checks a motor parameter table position read from the .fiz file against the table size +static bool is_valid_mpt_index(int const Index) +{ + if (Index >= 0 && Index <= MotorParametersArraySize) + { + return true; + } + WriteLog("Read MPT: position " + std::to_string(Index) + " out of range 0-" + std::to_string(MotorParametersArraySize) + " in line " + std::to_string(LISTLINE)); + return false; +} + bool TMoverParameters::readMPT0(std::string const &line) { @@ -9088,6 +9099,10 @@ bool TMoverParameters::readMPT0(std::string const &line) } int idx = 0; // numer pozycji parser >> idx; + if (false == is_valid_mpt_index(idx)) + { + return false; + } switch (EngineType) { case TEngineType::DieselEngine: @@ -9152,6 +9167,10 @@ bool TMoverParameters::readMPTElectricSeries(std::string const &line) } int idx = 0; // numer pozycji parser >> idx; + if (false == is_valid_mpt_index(idx)) + { + return false; + } parser >> MotorParam[idx].mfi >> MotorParam[idx].mIsat >> MotorParam[idx].fi >> MotorParam[idx].Isat; if (true == parser.getTokens(1, false)) { @@ -9177,6 +9196,10 @@ bool TMoverParameters::readMPTDieselElectric(std::string const &line) } int idx = 0; // numer pozycji parser >> idx; + if (false == is_valid_mpt_index(idx)) + { + return false; + } parser >> MotorParam[idx].mfi >> MotorParam[idx].mIsat >> MotorParam[idx].fi >> MotorParam[idx].Isat >> MPTRelay[idx].Iup >> MPTRelay[idx].Idown; return true; @@ -9193,6 +9216,10 @@ bool TMoverParameters::readMPTDieselEngine(std::string const &line) } int idx = 0; // numer pozycji parser >> idx; + if (false == is_valid_mpt_index(idx)) + { + return false; + } parser >> MotorParam[idx].mIsat >> MotorParam[idx].fi >> MotorParam[idx].mfi; if (true == parser.getTokens(1, false)) { @@ -11737,6 +11764,14 @@ bool TMoverParameters::CheckLocomotiveParameters(bool ReadyFlag, int Dir) CompressorPower = 3; } + // for these engine types secondary controller positions index MotorParam table + if ((EngineType == TEngineType::ElectricSeriesMotor || EngineType == TEngineType::DieselEngine || EngineType == TEngineType::DieselElectric) && + ScndCtrlPosNo > MotorParametersArraySize) + { + WriteLog("SCPN=" + std::to_string(ScndCtrlPosNo) + " exceeds motor parameter table size, limited to " + std::to_string(MotorParametersArraySize)); + ScndCtrlPosNo = MotorParametersArraySize; + } + // WriteLog("aa = " + AxleArangement + " " + std::string( Pos("o", AxleArangement)) ); if (contains(AxleArangement, "o") && EngineType == TEngineType::ElectricSeriesMotor)