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/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 83c918e2..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; @@ -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; } @@ -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); @@ -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; } @@ -5537,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; @@ -5984,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) { @@ -6765,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); } // ************************************************************************************************* @@ -6776,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.); } // ************************************************************************************************* @@ -6909,7 +6906,7 @@ bool TMoverParameters::AutoRelayCheck(void) } // sprawdzenie wszystkich warunkow (AutoRelayFlag, AutoSwitch, Im VelUp) { @@ -7834,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; @@ -7956,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); @@ -8044,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 @@ -9079,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) { @@ -9091,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: @@ -9155,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)) { @@ -9180,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; @@ -9196,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)) { @@ -11740,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) diff --git a/application/application.cpp b/application/application.cpp index a6b88ae8..ceb5ec6c 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; @@ -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/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: 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/AnimModel.cpp b/model/AnimModel.cpp index bc2d6e84..cc54a2a2 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; @@ -478,6 +501,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; @@ -638,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 @@ -688,6 +731,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/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/model/Model3d.cpp b/model/Model3d.cpp index 38f39080..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" @@ -24,6 +25,8 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others #include "world/mtable.h" #include "scene/sn_utils.h" +#include + //--------------------------------------------------------------------------- using namespace Mtable; @@ -53,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) @@ -224,16 +225,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); } } @@ -893,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/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/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 5419bbcd..2f587033 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); @@ -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) @@ -3022,15 +3026,46 @@ 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 ); + 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 ); } + 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 }; + + // 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() ); // Pull the current pass camera/view transform once. We use the current GL // modelview matrix as the view matrix because at the point Render_Instanced @@ -3039,11 +3074,14 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector::max(); material_data const *batch_material { nullptr }; + float farthest_distancesquared { 0.f }; 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; } @@ -3076,78 +3114,103 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorMaterial(); } Instance->m_framestamp = m_framestamp; - closest_distancesquared = std::min( closest_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_modelviews.emplace_back( 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 ) }; + farthest_distancesquared = std::max( farthest_distancesquared, distance ); + 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_modelviews.empty() ) { return; } + if( m_instance_survivors.empty() ) { return; } + + // group the modelviews by lod band into a contiguous array, for the UBO upload. every distance in + // a band selects the same lod; the nearest one represents the band. + // within a band the instances are additionally ordered roughly near-to-far, in distance slots. dense + // alpha tested foliage then gets the most out of the early depth test, as the nearest instances fill + // the depth buffer first and hide the fragments of those behind them before they get shaded. + // both levels are done with a single counting sort, linear in the instance count + auto const distanceslots { 16 }; + auto const keycount { bandcount * distanceslots }; + auto const slotscale { distanceslots / std::sqrt( std::max( farthest_distancesquared, 1.f ) ) }; + m_instance_bandstarts.assign( keycount + 1, 0 ); + m_instance_banddistances.assign( bandcount, std::numeric_limits::max() ); + 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 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() ); + 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 ); + 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 ); + for( std::size_t band = 0; band < bandcount; ++band ) { - // 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 ) ) ); + std::size_t offset_idx = m_instance_bandstarts[ band * distanceslots ]; + std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ]; + while( offset_idx < band_end ) { + std::size_t const this_batch = std::min( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); - // 2b. Push identity onto the matrix stack so submodel transforms - // accumulate from identity, not from camera/instance space. - ::glPushMatrix(); - ::glLoadIdentity(); + // 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 ) ) ); - // 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; + // 2b. Push identity onto the matrix stack so submodel transforms + // accumulate from identity, not from camera/instance space. + ::glPushMatrix(); + ::glLoadIdentity(); - 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; + // 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; - Render( Model->Root ); + 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; - m_current_instance_count = 0; + Render( Model->Root ); - ::glPopMatrix(); + m_current_instance_count = 0; - // 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 ) ); + ::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; } - - offset_idx += this_batch; - ++m_renderpass.draw_stats.instanced_drawcalls; } - m_renderpass.draw_stats.instances += static_cast( 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 @@ -3730,7 +3793,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; @@ -4569,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 @@ -4630,7 +4710,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; @@ -4795,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 6fda37c3..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); @@ -376,7 +377,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 +427,21 @@ 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 + 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 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; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 1da66ba5..b675c38f 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 @@ -88,9 +105,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 +1620,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 @@ -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... @@ -2863,7 +2876,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; @@ -2877,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 @@ -2895,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 @@ -2969,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 ) { @@ -2985,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 ); @@ -2995,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 ); @@ -3798,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 @@ -3843,10 +3871,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... @@ -3921,7 +3946,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 @@ -3984,14 +4009,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 @@ -4016,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/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/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; } } } 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/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... 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/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.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/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/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/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/utilities/utilities.cpp b/utilities/utilities.cpp index 14e44897..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" @@ -426,6 +507,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..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) { @@ -220,6 +224,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 7e92c7c9..a6223689 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) @@ -449,8 +439,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 +502,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 +531,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 @@ -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 == -1) { - SemNextIndex = iLast; - } - if (Global.iWriteLogEnabled & 8) { - WriteLog("(stop signal from " - + (SemNextStopIndex != -1 ? sSpeedTable[SemNextStopIndex].GetName() : "unknown semaphor") - + ")"); - } - } - else { - if( true == newspeedpoint.IsProperSemaphor(OrderCurrentGet()) - && SemNextIndex == -1 ) { - SemNextIndex = iLast; // sprawdzamy czy pierwszy na drodze - } - if (Global.iWriteLogEnabled & 8) { - WriteLog("(forward signal for " - + (SemNextIndex != -1 ? 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; } @@ -800,13 +771,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 ] ); @@ -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 } @@ -1387,13 +1343,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 @@ -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); @@ -1461,13 +1410,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; @@ -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; @@ -1901,7 +1860,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 ); @@ -1926,17 +1885,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 @@ -1972,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"); } @@ -2093,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 @@ -2387,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 @@ -2531,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 @@ -2650,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 } @@ -2865,10 +2806,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 ) { @@ -3126,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: { @@ -3241,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); @@ -3391,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) @@ -3644,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 @@ -3938,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; } @@ -4160,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) { @@ -4239,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 @@ -4326,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 ); @@ -5069,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 @@ -5153,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 } }; @@ -5534,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; @@ -5992,25 +5910,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 @@ -6162,13 +6074,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 { @@ -6217,24 +6127,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 { @@ -6251,26 +6160,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 @@ -6479,25 +6391,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 @@ -6765,12 +6673,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 @@ -6816,12 +6722,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 @@ -6832,15 +6737,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); + } } } @@ -6920,29 +6823,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 ) { @@ -6957,18 +6856,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; + } } } @@ -7023,13 +6916,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 ) { @@ -7149,11 +7040,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 ) { @@ -7468,16 +7358,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 @@ -7733,18 +7619,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 @@ -7890,15 +7769,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() { @@ -7906,23 +7781,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 ) ) { @@ -7976,12 +7848,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 @@ -8003,11 +7873,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 ); @@ -8016,50 +7885,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 @@ -8172,27 +8021,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 @@ -8222,11 +8061,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 @@ -8239,17 +8077,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: { 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/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index e84af613..873cc4d3 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 @@ -3024,7 +3045,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 ), 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 ); 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();