From 551f6dbc3bb7d756d50c5e07f5109377898b26de Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 28 Sep 2026 20:49:18 +0200 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] 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 08/17] 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 09/17] 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 10/17] 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 11/17] 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 12/17] 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 13/17] 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 14/17] 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 15/17] 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) From 06fc3915f48393f5f7b77186dc9aac551cc4ee82 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Fri, 2 Oct 2026 21:19:16 +0200 Subject: [PATCH 16/17] fix(renderer): guard replacable skin lookup and light colour access (#200) --- rendering/openglrenderer.cpp | 66 +++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index dcbd1df2..bc6c9d14 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -31,6 +31,23 @@ int constexpr EU07_PICKBUFFERSIZE { 1024 }; // size of (square) textures bound w int constexpr EU07_ENVIRONMENTBUFFERSIZE { 256 }; // size of (square) environmental cube map texture int constexpr EU07_REFLECTIONFIDELITYOFFSET { 250 }; // artificial increase of range for reflection pass detail reduction +namespace { + +// returns material assigned to the submodel, resolving replacable skins. null_handle if no skin set is bound +material_handle +submodel_material( TSubModel const *Submodel ) { + + if( Submodel->m_material >= 0 ) { + return Submodel->m_material; + } + return ( + TSubModel::ReplacableSkinId != nullptr ? + TSubModel::ReplacableSkinId[ -Submodel->m_material ] : + null_handle ); +} + +} // namespace + float const EU07_OPACITYDEFAULT { 0.5f }; bool @@ -1790,8 +1807,7 @@ opengl_renderer::Material( material_handle const Material ) const { opengl_material const & opengl_renderer::Material( TSubModel const * Submodel ) const { - auto const material { Submodel->m_material >= 0 ? Submodel->m_material : Submodel->ReplacableSkinId[ -Submodel->m_material ] }; - return m_materials.material( material ); + return m_materials.material( submodel_material( Submodel ) ); } // shader methods @@ -2768,10 +2784,7 @@ opengl_renderer::Render( TSubModel *Submodel ) { } #endif // material configuration: - auto const material{ ( - Submodel->m_material < 0 ? - Submodel->ReplacableSkinId[ -Submodel->m_material ] : // zmienialne skóry - Submodel->m_material ) }; // również 0 + auto const material{ submodel_material( Submodel ) }; // zmienialne skóry, również 0 // textures... Bind_Material( material ); // ...colors and opacity... @@ -2879,7 +2892,7 @@ opengl_renderer::Render( TSubModel *Submodel ) { // material configuration: // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); + Bind_Material( submodel_material( Submodel ), Submodel ); } else { // również 0 @@ -2897,7 +2910,7 @@ 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( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); + Bind_Material( submodel_material( Submodel ), Submodel ); } else { // również 0 @@ -2971,10 +2984,10 @@ opengl_renderer::Render( TSubModel *Submodel ) { auto const unitstate = m_unitstate; switch_units( m_unitstate.diffuse, false, false ); - auto const *lightcolor { + glm::vec3 const lightcolor { Submodel->DiffuseOverride.r < 0.f ? // -1 indicates no override - glm::value_ptr( Submodel->f4Diffuse ) : - glm::value_ptr( Submodel->DiffuseOverride ) }; + glm::vec3( Submodel->f4Diffuse ) : + Submodel->DiffuseOverride }; // main draw call if( Global.Overcast > 1.f ) { @@ -2987,9 +3000,9 @@ opengl_renderer::Render( TSubModel *Submodel ) { ::glPointSize( pointsize * fogfactor ); ::glColor4f( - lightcolor[ 0 ], - lightcolor[ 1 ], - lightcolor[ 2 ], + lightcolor.r, + lightcolor.g, + lightcolor.b, Submodel->fVisible * std::min( 1.f, lightlevel ) * 0.5f ); ::glDepthMask( GL_FALSE ); m_geometry.draw( Submodel->m_geometry.handle ); @@ -2997,9 +3010,9 @@ opengl_renderer::Render( TSubModel *Submodel ) { } ::glPointSize( pointsize ); ::glColor4f( - lightcolor[ 0 ], - lightcolor[ 1 ], - lightcolor[ 2 ], + lightcolor.r, + lightcolor.g, + lightcolor.b, Submodel->fVisible * std::min( 1.f, lightlevel ) ); m_geometry.draw( Submodel->m_geometry.handle ); @@ -3845,10 +3858,7 @@ opengl_renderer::Render_Alpha( TSubModel *Submodel ) { #endif // material configuration: // textures... - auto const material { ( - Submodel->m_material < 0 ? - Submodel->ReplacableSkinId[ -Submodel->m_material ] : // zmienialne skóry - Submodel->m_material ) }; // również 0 + auto const material { submodel_material( Submodel ) }; // zmienialne skóry, również 0 // textures... Bind_Material( material ); // ...colors and opacity... @@ -3923,7 +3933,7 @@ opengl_renderer::Render_Alpha( TSubModel *Submodel ) { // material configuration: // textures... if( Submodel->m_material < 0 ) { // zmienialne skóry - Bind_Material( TSubModel::ReplacableSkinId[ -Submodel->m_material ], Submodel ); + Bind_Material( submodel_material( Submodel ), Submodel ); } else { // również 0 @@ -3986,14 +3996,14 @@ opengl_renderer::Render_Alpha( TSubModel *Submodel ) { auto const unitstate = m_unitstate; switch_units( unitstate.diffuse, false, false ); - auto const *lightcolor { + glm::vec3 const lightcolor { Submodel->DiffuseOverride.r < 0.f ? // -1 indicates no override - glm::value_ptr( Submodel->f4Diffuse ) : - glm::value_ptr( Submodel->DiffuseOverride ) }; + glm::vec3( Submodel->f4Diffuse ) : + Submodel->DiffuseOverride }; ::glColor4f( - lightcolor[ 0 ], - lightcolor[ 1 ], - lightcolor[ 2 ], + lightcolor.r, + lightcolor.g, + lightcolor.b, Submodel->fVisible * glarelevel ); // main draw call From 51521a135a8b83a63d0fa3c4c7fbad12ff203e3f Mon Sep 17 00:00:00 2001 From: pawelw Date: Sat, 3 Oct 2026 12:05:32 +0200 Subject: [PATCH 17/17] Text displays: accept characters above 127, in windows-1250 or utf-8 (#198) * Text displays: accept characters above 127, in windows-1250 or utf-8 A text display (sub-model of type "text") looks up the sub-model of each character in a 256 entry table, and indexed it with a plain char. With char being signed, any character above 127 gave a negative index: reading the text went outside the table, and so did filling the table if a character sub-model had such a name. In effect the displays could only show ASCII, and a text with national characters could crash. The lookup is now keyed by unicode code point. Both the displayed text and the names of the character sub-models are decoded as utf-8 if they are valid utf-8, and as windows-1250 otherwise, so legacy (ansi) sceneries and models work next to utf-8 ones, and the two can be mixed. A short ansi text can be valid utf-8 by accident (two upper case accented letters in a row will do); when both readings are possible the one which finds more of its characters in the model is used. The rest is unchanged: the first character of the text still isn't displayed, the characters are still drawn in the translucent pass only. A missing text pointer no longer gets dereferenced. * Text displays: prepare the characters once for each text The sequence of character sub-models was cached for the last text only, so displays of several instances drawn in turn decoded their texts again on every draw. It is now kept for each text the display shows; the texts of the instances don't change, so a draw costs a single lookup. Also the comments added in the renderers are now in English. * Text displays: move the text decoding to utilities utf8_to_utf32() and win1250_to_utf32(), with the windows-1250 table, now sit next to win1250_to_utf8(). The existing function maps only the Polish letters and passes other bytes above 127 through, so it can't serve the displays. * Text displays: address the SonarCloud findings std::array instead of the C-style arrays, the utf-8 sequence length worked out in a function of its own instead of nested conditional operators, std::byte for the bit operations, std::ranges::count_if and contains(). The character loop of the renderers moved to Render_Alpha_text(), which takes the nesting back to the limit, and the static pasText is accessed through the class. --- model/Model3d.cpp | 64 ++++++++++++++++++++++++++- model/Model3d.h | 7 ++- rendering/opengl33renderer.cpp | 39 +++++++--------- rendering/opengl33renderer.h | 1 + rendering/openglrenderer.cpp | 33 ++++++-------- rendering/openglrenderer.h | 3 ++ utilities/utilities.cpp | 81 ++++++++++++++++++++++++++++++++++ utilities/utilities.h | 4 ++ 8 files changed, 187 insertions(+), 45 deletions(-) diff --git a/model/Model3d.cpp b/model/Model3d.cpp index fba07446..e1a3a79e 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -17,6 +17,7 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others #include "utilities/Globals.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "rendering/renderer.h" #include "utilities/Timer.h" #include "simulation/simulation.h" @@ -55,8 +56,6 @@ TSubModel::~TSubModel() SafeDelete(Child); delete fMatrix; // własny transform trzeba usunąć (zawsze jeden) } - delete[] smLetter; // używany tylko roboczo dla TP_TEXT, do przyspieszenia - // wyświetlania }; void TSubModel::Name_Material(std::string const &Name) @@ -892,6 +891,67 @@ void TSubModel::InitialRotate(bool doit) Next->InitialRotate(doit); }; +// text display (TP_TEXT): returns sub-models of the characters to draw for provided text, in drawing order. +// a character is matched with the first character of a child sub-model name. both the text and the names can be +// encoded either in utf-8 or in windows-1250, so the legacy (ansi) scenery and model files work along the utf-8 ones +std::vector const &TSubModel::text_letters(std::string const &Text) +{ + // the sequence is prepared once for each text the display shows. the texts of the model instances don't change, + // so drawing a display afterwards costs a single lookup, however many instances and passes take turns + auto const prepared{m_textlayouts.find(Text)}; + if (prepared != m_textlayouts.end()) + { + return prepared->second; + } + + std::u32string characters; + if (false == m_lettersready) + { + for (auto *letter = Child; letter != nullptr; letter = letter->Next) + { + if (letter->pName.empty()) + { + continue; + } + if (false == utf8_to_utf32(letter->pName, characters)) + { + win1250_to_utf32(letter->pName, characters); + } + if (characters.front() != 0) + { + m_letters[characters.front()] = letter; + } + } + m_lettersready = true; + } + + // the text is taken for utf-8 if it's valid as such, and for windows-1250 otherwise. a short ansi text can pass + // for utf-8 by accident (two upper case accented letters in a row are enough for this to happen), + // so if both readings are possible the one which finds more of its characters in the model wins + auto const known = [this](std::u32string const &Characters) { + return std::ranges::count_if(Characters, [this](char32_t const Character) { return m_letters.contains(Character); }); + }; + std::u32string ansi; + win1250_to_utf32(Text, ansi); + if (false == utf8_to_utf32(Text, characters) || known(ansi) > known(characters)) + { + characters = ansi; + } + + auto &textletters{m_textlayouts[Text]}; + // NOTE: the first character of the text isn't displayed. the displays always worked this way, and the existing + // content relies on it + for (std::size_t idx = 1; idx < characters.size(); ++idx) + { + auto const lookup{m_letters.find(characters[idx])}; + if (lookup != m_letters.end()) + { + textletters.emplace_back(lookup->second); + } + } + return textletters; +} + void TSubModel::ChildAdd(TSubModel *SubModel) { // dodanie submodelu potemnego (uzależnionego) // Ra: zmiana kolejności, żeby kolejne móc renderować po aktualnym (było diff --git a/model/Model3d.h b/model/Model3d.h index 0e053609..53e9084f 100644 --- a/model/Model3d.h +++ b/model/Model3d.h @@ -156,7 +156,10 @@ public: // chwilowo std::uintptr_t iAnimOwner{ 0 }; // roboczy numer egzemplarza, który ustawił animację TAnimType b_aAnim{ TAnimType::at_None }; // kody animacji oddzielnie, bo zerowane std::shared_ptr mAnimMatrix; // macierz do animacji kwaternionowych - TSubModel **smLetter{ nullptr }; // wskaźnik na tablicę submdeli do generoania tekstu (docelowo zapisać do E3D) + // text display (TP_TEXT) data, filled on first use + std::unordered_map m_letters; // sub-models of the available characters, by unicode code point + bool m_lettersready { false }; // whether the character lookup was built yet + std::unordered_map> m_textlayouts; // sub-models of consecutive characters, for each text shown TSubModel *Parent{ nullptr }; // nadrzędny, np. do wymnażania macierzy int iVisible { 1 }; // roboczy stan widoczności float fVisible { 1.f }; // visibility level @@ -202,6 +205,8 @@ public: void SetTranslate(float3 vNewTransVector); void SetRotateIK1(float3 vNewAngles); TSubModel * GetFromName( std::string const &search, bool i = true ); + // text display (TP_TEXT): returns sub-models of the characters to draw for provided text, in drawing order + std::vector const & text_letters( std::string const &Text ); inline float4x4 * GetMatrix() { return fMatrix; }; inline float4x4 const * GetMatrix() const { return fMatrix; }; // returns offset vector from root diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index c07efca4..2f587033 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -4634,6 +4634,21 @@ bool opengl33_renderer::Render_Alpha(TModel3d *Model, material_data const *Mater return result; } +// characters of a text display, laid out in a row. the text can be encoded in utf-8 or in windows-1250 +void opengl33_renderer::Render_Alpha_text(TSubModel *Submodel) +{ + if (TSubModel::pasText == nullptr) + { + return; + } + for (auto *p : Submodel->text_letters(*TSubModel::pasText)) + { // translucent only, for the time being + Render_Alpha(p); + if (p->fMatrix) + ::glMultMatrixf(p->fMatrix->readArray()); // move on to the place of the next character + } +} + void opengl33_renderer::Render_Alpha(TSubModel *Submodel) { // renderowanie przezroczystych przez DL @@ -4862,29 +4877,7 @@ void opengl33_renderer::Render_Alpha(TSubModel *Submodel) { if (Submodel->eType == TP_TEXT) { // tekst renderujemy w specjalny sposób, zamiast submodeli z łańcucha Child - int i, j = (int)Submodel->pasText->size(); - TSubModel *p; - if (!Submodel->smLetter) - { // jeśli nie ma tablicy, to ją stworzyć; miejsce nieodpowiednie, ale tymczasowo może być - Submodel->smLetter = new TSubModel *[256]; // tablica wskaźników submodeli dla wyświetlania tekstu - memset(Submodel->smLetter, 0, 256 * sizeof(TSubModel *)); // wypełnianie zerami - p = Submodel->Child; - while (p) - { - Submodel->smLetter[p->pName[0]] = p; - p = p->Next; // kolejny znak - } - } - for (i = 1; i <= j; ++i) - { - p = Submodel->smLetter[(*(Submodel->pasText))[i]]; // znak do wyświetlenia - if (p) - { // na razie tylko jako przezroczyste - Render_Alpha(p); - if (p->fMatrix) - ::glMultMatrixf(p->fMatrix->readArray()); // przesuwanie widoku - } - } + Render_Alpha_text(Submodel); } else if (Submodel->iAlpha & Submodel->iFlags & 0x002F0000) Render_Alpha(Submodel->Child); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 43cd24b0..562670d9 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -310,6 +310,7 @@ class opengl33_renderer : public gfx_renderer { bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance); void Render_Alpha(TSubModel *Submodel); + void Render_Alpha_text(TSubModel *Submodel); // characters of a text display void Update_Lights(light_array &Lights); glm::vec3 pick_color(std::size_t const Index); std::size_t pick_index(glm::ivec3 const &Color); diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index bc6c9d14..b675c38f 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -3813,6 +3813,19 @@ opengl_renderer::Render_Alpha( TModel3d *Model, material_data const *Material, f return result; } +// characters of a text display, laid out in a row. the text can be encoded in utf-8 or in windows-1250 +void +opengl_renderer::Render_Alpha_text( TSubModel *Submodel ) { + + if( TSubModel::pasText == nullptr ) { return; } + + for( auto *p : Submodel->text_letters( *TSubModel::pasText ) ) { // translucent only, for the time being + Render_Alpha( p ); + if( p->fMatrix ) + ::glMultMatrixf( p->fMatrix->readArray() ); // move on to the place of the next character + } +} + void opengl_renderer::Render_Alpha( TSubModel *Submodel ) { // renderowanie przezroczystych przez DL @@ -4028,25 +4041,7 @@ opengl_renderer::Render_Alpha( TSubModel *Submodel ) { if( Submodel->Child != nullptr ) { if( Submodel->eType == TP_TEXT ) { // tekst renderujemy w specjalny sposób, zamiast submodeli z łańcucha Child - int i, j = (int)Submodel->pasText->size(); - TSubModel *p; - if( !Submodel->smLetter ) { // jeśli nie ma tablicy, to ją stworzyć; miejsce nieodpowiednie, ale tymczasowo może być - Submodel->smLetter = new TSubModel *[ 256 ]; // tablica wskaźników submodeli dla wyświetlania tekstu - std::memset(Submodel->smLetter, 0, 256 * sizeof( TSubModel * )); // wypełnianie zerami - p = Submodel->Child; - while( p ) { - Submodel->smLetter[ p->pName[ 0 ] ] = p; - p = p->Next; // kolejny znak - } - } - for( i = 1; i <= j; ++i ) { - p = Submodel->smLetter[ ( *( Submodel->pasText) )[ i ] ]; // znak do wyświetlenia - if( p ) { // na razie tylko jako przezroczyste - Render_Alpha( p ); - if( p->fMatrix ) - ::glMultMatrixf( p->fMatrix->readArray() ); // przesuwanie widoku - } - } + Render_Alpha_text( Submodel ); } else if( Submodel->iAlpha & Submodel->iFlags & 0x002F0000 ) Render_Alpha( Submodel->Child ); diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 820deb92..83dca483 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -270,6 +270,9 @@ private: Render_Alpha( TModel3d *Model, material_data const *Material, float const Squaredistance ); void Render_Alpha( TSubModel *Submodel ); + // characters of a text display + void + Render_Alpha_text( TSubModel *Submodel ); void Update_Lights( light_array &Lights ); bool diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index efaf3e6c..804ca66c 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -339,6 +339,87 @@ std::string win1250_to_utf8(const std::string &Input) return output; } +namespace +{ +// unicode code points of the upper half of the windows-1250 code page; 0 marks the unassigned bytes +std::array const win1250_upperhalf{ + 0x20AC, 0x0000, 0x201A, 0x0000, 0x201E, 0x2026, 0x2020, 0x2021, 0x0000, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, + 0x0000, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0000, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, + 0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, + 0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, + 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, + 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, + 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, + 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9}; + +// returns number of bytes of the utf-8 sequence started by provided byte, or 0 if it can't start one +std::size_t utf8_sequence_length(std::byte const Lead) +{ + if ((Lead & std::byte{0x80}) == std::byte{0x00}) + { + return 1; + } + if ((Lead & std::byte{0xE0}) == std::byte{0xC0}) + { + return 2; + } + if ((Lead & std::byte{0xF0}) == std::byte{0xE0}) + { + return 3; + } + if ((Lead & std::byte{0xF8}) == std::byte{0xF0}) + { + return 4; + } + return 0; // a continuation byte, or a lead of a sequence utf-8 doesn't allow +} +} // namespace + +bool utf8_to_utf32(std::string const &Text, std::u32string &Output) +{ + // lowest code point which needs given number of bytes; shorter values are overlong forms + static std::array const minimum{0, 0, 0x80, 0x800, 0x10000}; + + Output.clear(); + auto const size{Text.size()}; + for (std::size_t idx = 0; idx < size;) + { + auto const lead{static_cast(Text[idx])}; + auto const length{utf8_sequence_length(lead)}; + if (length == 0 || idx + length > size) + { + return false; + } + auto codepoint{std::to_integer(lead & (std::byte{0xFF} >> (length > 1 ? length + 1 : 1)))}; + for (std::size_t offset = 1; offset < length; ++offset) + { + auto const next{static_cast(Text[idx + offset])}; + if ((next & std::byte{0xC0}) != std::byte{0x80}) + { + return false; + } + codepoint = (codepoint << 6) | std::to_integer(next & std::byte{0x3F}); + } + if (codepoint < minimum[length] || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + { + return false; + } + Output += codepoint; + idx += length; + } + return true; +} + +void win1250_to_utf32(std::string const &Text, std::u32string &Output) +{ + Output.clear(); + for (auto const character : Text) + { + auto const value{std::to_integer(static_cast(character))}; + Output += (value < 0x80 ? static_cast(value) : win1250_upperhalf[value - 0x80]); + } +} + // Ra: tymczasowe rozwiązanie kwestii zagranicznych (czeskich) napisów char charsetconversiontable[] = "E?,?\"_++?%Sstzz" " ^^L$A|S^CS<--RZo±,l'uP.,as>L\"lz" diff --git a/utilities/utilities.h b/utilities/utilities.h index 6e761380..6b8111e3 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -166,6 +166,10 @@ void win1250_to_ascii(std::string &Input); std::string Bezogonkow(std::string Input, bool const Underscorestospaces = false); std::string win1250_to_utf8(const std::string &input); +// decodes utf-8 encoded text into unicode code points. returns false if the text isn't valid utf-8 +bool utf8_to_utf32(std::string const &Text, std::u32string &Output); +// decodes windows-1250 (ansi) encoded text into unicode code points +void win1250_to_utf32(std::string const &Text, std::u32string &Output); inline std::string extract_value(std::string const &Key, std::string const &Input) {