LCOV - code coverage report
Current view: top level - src - main.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 89.2 % 213 190
Test Date: 2026-03-03 13:56:25 Functions: 100.0 % 4 4
Branches: 46.9 % 520 244

             Branch data     Line data    Source code
       1                 :             : #ifdef _WIN32
       2                 :             : #include <windows.h>
       3                 :             : #else
       4                 :             : #include <signal.h>
       5                 :             : #endif
       6                 :             : #include <iostream>
       7                 :             : #include <cmath>
       8                 :             : #include <algorithm>
       9                 :             : #include <thread>
      10                 :             : #include <chrono>
      11                 :             : 
      12                 :             : #include "FFBEngine.h"
      13                 :             : #include "GuiLayer.h"
      14                 :             : #include "Config.h"
      15                 :             : #include "DirectInputFFB.h"
      16                 :             : #include "GameConnector.h"
      17                 :             : #include "Version.h"
      18                 :             : #include "Logger.h"    // Added Logger
      19                 :             : #include "RateMonitor.h"
      20                 :             : #include "HealthMonitor.h"
      21                 :             : #include <optional>
      22                 :             : #include <atomic>
      23                 :             : #include <mutex>
      24                 :             : 
      25                 :             : // Constants
      26                 :             : 
      27                 :             : // Threading Globals
      28                 :             : #ifndef LMUFFB_UNIT_TEST
      29                 :             : std::atomic<bool> g_running(true);
      30                 :             : std::atomic<bool> g_ffb_active(true);
      31                 :             : 
      32                 :             : SharedMemoryObjectOut g_localData; // Local copy of shared memory
      33                 :             : 
      34                 :             : FFBEngine g_engine;
      35                 :             : std::recursive_mutex g_engine_mutex; // Protects settings access if GUI changes them
      36                 :             : #else
      37                 :             : extern std::atomic<bool> g_running;
      38                 :             : extern std::atomic<bool> g_ffb_active;
      39                 :             : extern SharedMemoryObjectOut g_localData;
      40                 :             : extern FFBEngine g_engine;
      41                 :             : extern std::recursive_mutex g_engine_mutex;
      42                 :             : #endif
      43                 :             : 
      44                 :             : // --- FFB Loop (High Priority 400Hz) ---
      45                 :           9 : void FFBThread() {
      46   [ +  -  +  - ]:           9 :     std::cout << "[FFB] Loop Started." << std::endl;
      47         [ +  - ]:           9 :     RateMonitor loopMonitor;
      48         [ +  - ]:           9 :     RateMonitor telemMonitor;
      49         [ +  - ]:           9 :     RateMonitor hwMonitor;
      50         [ +  - ]:           9 :     RateMonitor torqueMonitor;
      51         [ +  - ]:           9 :     RateMonitor genTorqueMonitor;
      52                 :           9 :     double lastET = -1.0;
      53                 :           9 :     double lastTorque = -9999.0;
      54                 :           9 :     float lastGenTorque = -9999.0f;
      55                 :             : 
      56                 :             :     // Extended monitors for Issue #133
      57                 :             :     struct ChannelMonitor {
      58                 :             :         RateMonitor monitor;
      59                 :             :         double lastValue = -1e18;
      60                 :       62073 :         void Update(double newValue) {
      61         [ +  + ]:       62073 :             if (newValue != lastValue) {
      62                 :          81 :                 monitor.RecordEvent();
      63                 :          81 :                 lastValue = newValue;
      64                 :             :             }
      65                 :       62073 :         }
      66                 :             :     };
      67                 :             : 
      68   [ +  -  +  -  :           9 :     ChannelMonitor mAccX, mAccY, mAccZ;
                   +  - ]
      69   [ +  -  +  -  :           9 :     ChannelMonitor mVelX, mVelY, mVelZ;
                   +  - ]
      70   [ +  -  +  -  :           9 :     ChannelMonitor mRotX, mRotY, mRotZ;
                   +  - ]
      71   [ +  -  +  -  :           9 :     ChannelMonitor mRotAccX, mRotAccY, mRotAccZ;
                   +  - ]
      72   [ +  -  +  - ]:           9 :     ChannelMonitor mUnfSteer, mFilSteer;
      73         [ +  - ]:           9 :     ChannelMonitor mRPM;
      74   [ +  -  +  -  :           9 :     ChannelMonitor mLoadFL, mLoadFR, mLoadRL, mLoadRR;
             +  -  +  - ]
      75   [ +  -  +  -  :           9 :     ChannelMonitor mLatFL, mLatFR, mLatRL, mLatRR;
             +  -  +  - ]
      76   [ +  -  +  -  :           9 :     ChannelMonitor mPosX, mPosY, mPosZ;
                   +  - ]
      77         [ +  - ]:           9 :     ChannelMonitor mDtMon;
      78                 :             : 
      79                 :             :     // Precise Timing: Target 400Hz (2500 microseconds)
      80                 :           9 :     const std::chrono::microseconds target_period(2500);
      81                 :           9 :     auto next_tick = std::chrono::steady_clock::now();
      82                 :             : 
      83         [ +  + ]:        4771 :     while (g_running) {
      84         [ +  - ]:        4762 :         loopMonitor.RecordEvent();
      85   [ +  -  +  - ]:        4762 :         next_tick += target_period;
      86                 :             : 
      87                 :        4762 :         double force = 0.0;
      88                 :        4762 :         double dt = 0.0025; // Default 400Hz
      89                 :        4762 :         bool restricted = true;
      90                 :             : 
      91   [ +  -  +  -  :        4762 :         if (g_ffb_active && GameConnector::Get().IsConnected()) {
          +  -  +  -  +  
                      - ]
      92   [ +  -  +  - ]:        4762 :             bool in_realtime = GameConnector::Get().CopyTelemetry(g_localData);
      93   [ +  -  +  - ]:        4762 :             bool is_stale = GameConnector::Get().IsStale(100);
      94                 :             : 
      95                 :             :             static bool was_in_menu = true;
      96   [ +  +  +  + ]:        4762 :             if (was_in_menu && in_realtime) {
      97   [ +  -  +  - ]:           2 :                 std::cout << "[Game] User entered driving session." << std::endl;
      98   [ +  -  +  -  :           2 :                 if (Config::m_auto_start_logging && !AsyncLogger::Get().IsLogging()) {
             +  -  +  - ]
      99                 :           2 :                     SessionInfo info;
     100         [ +  - ]:           2 :                     info.app_version = LMUFFB_VERSION;
     101         [ +  - ]:           2 :                     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     102         [ +  - ]:           2 :                     info.vehicle_name = g_engine.m_vehicle_name;
     103         [ +  - ]:           2 :                     info.track_name = g_engine.m_track_name;
     104         [ +  - ]:           2 :                     info.driver_name = "Auto";
     105                 :           2 :                     info.gain = g_engine.m_gain;
     106                 :           2 :                     info.understeer_effect = g_engine.m_understeer_effect;
     107                 :           2 :                     info.sop_effect = g_engine.m_sop_effect;
     108                 :           2 :                     info.slope_enabled = g_engine.m_slope_detection_enabled;
     109                 :           2 :                     info.slope_sensitivity = g_engine.m_slope_sensitivity;
     110                 :           2 :                     info.slope_threshold = (float)g_engine.m_slope_min_threshold;
     111                 :           2 :                     info.slope_alpha_threshold = g_engine.m_slope_alpha_threshold;
     112                 :           2 :                     info.slope_decay_rate = g_engine.m_slope_decay_rate;
     113                 :           2 :                     info.torque_passthrough = g_engine.m_torque_passthrough;
     114   [ +  -  +  - ]:           2 :                     AsyncLogger::Get().Start(info, Config::m_log_path);
     115                 :           2 :                 }
     116   [ +  +  +  + ]:        4762 :             } else if (!was_in_menu && !in_realtime) {
     117   [ +  -  +  - ]:           1 :                 std::cout << "[Game] User exited to menu (FFB Muted)." << std::endl;
     118   [ +  -  +  -  :           1 :                 if (Config::m_auto_start_logging && AsyncLogger::Get().IsLogging()) {
             -  +  -  + ]
     119         [ #  # ]:           0 :                     AsyncLogger::Get().Stop();
     120                 :             :                 }
     121                 :             :             }
     122                 :        4762 :             was_in_menu = !in_realtime;
     123                 :             :             
     124                 :        4762 :             bool should_output = false;
     125                 :             : 
     126                 :             :             // v0.7.78 FIX: Support stationary/garage soft lock (Issue #184)
     127                 :             :             // We now process FFB even if not in realtime, provided we have a player vehicle.
     128                 :             :             // This allows Soft Lock to function in the garage or when AI is driving.
     129   [ +  +  +  - ]:        4762 :             if (!is_stale && g_localData.telemetry.playerHasVehicle) {
     130                 :        2299 :                 uint8_t idx = g_localData.telemetry.playerVehicleIdx;
     131         [ +  - ]:        2299 :                 if (idx < 104) {
     132                 :        2299 :                     auto& scoring = g_localData.scoring.vehScoringInfo[idx];
     133                 :        2299 :                     TelemInfoV01* pPlayerTelemetry = &g_localData.telemetry.telemInfo[idx];
     134                 :        2299 :                     dt = pPlayerTelemetry->mDeltaTime;
     135                 :             : 
     136                 :             :                     // Track telemetry update rate
     137         [ +  + ]:        2299 :                     if (pPlayerTelemetry->mElapsedTime != lastET) {
     138         [ +  - ]:         552 :                         telemMonitor.RecordEvent();
     139                 :         552 :                         lastET = pPlayerTelemetry->mElapsedTime;
     140                 :             :                     }
     141                 :             : 
     142                 :             :                     // Track torque update rates
     143         [ +  + ]:        2299 :                     if (pPlayerTelemetry->mSteeringShaftTorque != lastTorque) {
     144         [ +  - ]:         552 :                         torqueMonitor.RecordEvent();
     145                 :         552 :                         lastTorque = pPlayerTelemetry->mSteeringShaftTorque;
     146                 :             :                     }
     147         [ +  + ]:        2299 :                     if (g_localData.generic.FFBTorque != lastGenTorque) {
     148         [ +  - ]:           3 :                         genTorqueMonitor.RecordEvent();
     149                 :           3 :                         lastGenTorque = g_localData.generic.FFBTorque;
     150                 :             :                     }
     151                 :             : 
     152                 :             :                     // Extended monitoring (Issue #133)
     153         [ +  - ]:        2299 :                     mAccX.Update(pPlayerTelemetry->mLocalAccel.x);
     154         [ +  - ]:        2299 :                     mAccY.Update(pPlayerTelemetry->mLocalAccel.y);
     155         [ +  - ]:        2299 :                     mAccZ.Update(pPlayerTelemetry->mLocalAccel.z);
     156         [ +  - ]:        2299 :                     mVelX.Update(pPlayerTelemetry->mLocalVel.x);
     157         [ +  - ]:        2299 :                     mVelY.Update(pPlayerTelemetry->mLocalVel.y);
     158         [ +  - ]:        2299 :                     mVelZ.Update(pPlayerTelemetry->mLocalVel.z);
     159         [ +  - ]:        2299 :                     mRotX.Update(pPlayerTelemetry->mLocalRot.x);
     160         [ +  - ]:        2299 :                     mRotY.Update(pPlayerTelemetry->mLocalRot.y);
     161         [ +  - ]:        2299 :                     mRotZ.Update(pPlayerTelemetry->mLocalRot.z);
     162         [ +  - ]:        2299 :                     mRotAccX.Update(pPlayerTelemetry->mLocalRotAccel.x);
     163         [ +  - ]:        2299 :                     mRotAccY.Update(pPlayerTelemetry->mLocalRotAccel.y);
     164         [ +  - ]:        2299 :                     mRotAccZ.Update(pPlayerTelemetry->mLocalRotAccel.z);
     165         [ +  - ]:        2299 :                     mUnfSteer.Update(pPlayerTelemetry->mUnfilteredSteering);
     166         [ +  - ]:        2299 :                     mFilSteer.Update(pPlayerTelemetry->mFilteredSteering);
     167         [ +  - ]:        2299 :                     mRPM.Update(pPlayerTelemetry->mEngineRPM);
     168         [ +  - ]:        2299 :                     mLoadFL.Update(pPlayerTelemetry->mWheel[0].mTireLoad);
     169         [ +  - ]:        2299 :                     mLoadFR.Update(pPlayerTelemetry->mWheel[1].mTireLoad);
     170         [ +  - ]:        2299 :                     mLoadRL.Update(pPlayerTelemetry->mWheel[2].mTireLoad);
     171         [ +  - ]:        2299 :                     mLoadRR.Update(pPlayerTelemetry->mWheel[3].mTireLoad);
     172         [ +  - ]:        2299 :                     mLatFL.Update(pPlayerTelemetry->mWheel[0].mLateralForce);
     173         [ +  - ]:        2299 :                     mLatFR.Update(pPlayerTelemetry->mWheel[1].mLateralForce);
     174         [ +  - ]:        2299 :                     mLatRL.Update(pPlayerTelemetry->mWheel[2].mLateralForce);
     175         [ +  - ]:        2299 :                     mLatRR.Update(pPlayerTelemetry->mWheel[3].mLateralForce);
     176         [ +  - ]:        2299 :                     mPosX.Update(pPlayerTelemetry->mPos.x);
     177         [ +  - ]:        2299 :                     mPosY.Update(pPlayerTelemetry->mPos.y);
     178         [ +  - ]:        2299 :                     mPosZ.Update(pPlayerTelemetry->mPos.z);
     179         [ +  - ]:        2299 :                     mDtMon.Update(pPlayerTelemetry->mDeltaTime);
     180                 :             : 
     181         [ +  - ]:        2299 :                     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     182                 :             :                     // Determine if full FFB is allowed.
     183                 :             :                     // full_allowed requires: player control, not disqualified, and in realtime.
     184   [ +  -  -  +  :        2299 :                     bool full_allowed = g_engine.IsFFBAllowed(scoring, g_localData.scoring.scoringInfo.mGamePhase) && in_realtime;
                   -  - ]
     185                 :             : 
     186                 :             :                     // v0.7.108: Explicitly zero force if not in realtime (Issue #174).
     187                 :             :                     // v0.7.118: REMOVED the explicit zeroing (Issue #184) to allow Soft Lock in menus.
     188                 :             :                     // We still call calculate_force with full_allowed=false which internally mutes
     189                 :             :                     // all effects except Soft Lock.
     190                 :             :                     // Force dt to 0.0025 to ensure consistent internal physics regardless of game jitter.
     191         [ +  - ]:        2299 :                     force = g_engine.calculate_force(pPlayerTelemetry, scoring.mVehicleClass, scoring.mVehicleName, g_localData.generic.FFBTorque, full_allowed, 0.0025);
     192                 :        2299 :                     should_output = true;
     193                 :             : 
     194                 :             :                     // If not full_allowed, use tighter slew rate limiting
     195   [ -  +  -  - ]:        2299 :                     restricted = !full_allowed || (scoring.mFinishStatus != 0);
     196                 :        2299 :                 }
     197                 :             :             }
     198                 :             :             
     199         [ +  + ]:        4762 :             if (!should_output) force = 0.0;
     200                 :             : 
     201                 :             :             // Warning for low sample rate (Issue #133)
     202   [ +  +  +  - ]:        4762 :             static auto lastWarningTime = std::chrono::steady_clock::now();
     203                 :             : 
     204                 :        4762 :             HealthStatus health;
     205                 :             :             {
     206         [ +  - ]:        4762 :                 std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     207         [ -  + ]:        4762 :                 double t_rate = (g_engine.m_torque_source == 1) ? genTorqueMonitor.GetRate() : torqueMonitor.GetRate();
     208                 :        4762 :                 health = HealthMonitor::Check(loopMonitor.GetRate(), telemMonitor.GetRate(), t_rate, g_engine.m_torque_source);
     209                 :        4762 :             }
     210                 :             : 
     211   [ +  +  -  + ]:        4762 :             if (in_realtime && !health.is_healthy) {
     212                 :           0 :                  auto now = std::chrono::steady_clock::now();
     213   [ #  #  #  #  :           0 :                  if (std::chrono::duration_cast<std::chrono::seconds>(now - lastWarningTime).count() >= 5) {
                   #  # ]
     214         [ #  # ]:           0 :                      std::string reason = "";
     215   [ #  #  #  #  :           0 :                      if (health.loop_low) reason += "Loop=" + std::to_string((int)health.loop_rate) + "Hz ";
             #  #  #  # ]
     216   [ #  #  #  #  :           0 :                      if (health.telem_low) reason += "Telemetry=" + std::to_string((int)health.telem_rate) + "Hz ";
             #  #  #  # ]
     217   [ #  #  #  #  :           0 :                      if (health.torque_low) reason += "Torque=" + std::to_string((int)health.torque_rate) + "Hz (Target " + std::to_string((int)health.expected_torque_rate) + "Hz) ";
          #  #  #  #  #  
                #  #  # ]
     218                 :             : 
     219   [ #  #  #  #  :           0 :                      std::cout << "[WARNING] Low Sample Rate detected: " << reason << std::endl;
                   #  # ]
     220   [ #  #  #  # ]:           0 :                      Logger::Get().Log("Low Sample Rate detected: %s", reason.c_str());
     221                 :           0 :                      lastWarningTime = now;
     222                 :           0 :                  }
     223                 :             :             }
     224                 :             :         }
     225                 :             : 
     226                 :             :         // Safety Layer (v0.7.49): Slew Rate Limiting and NaN protection
     227                 :             :         // v0.7.48: Always update hardware even if disconnected/inactive to ensure zeroing
     228                 :             :         {
     229         [ +  - ]:        4762 :             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     230         [ -  + ]:        4762 :             if (dt < 0.0001) dt = 0.0025;
     231                 :             : 
     232                 :             :             // Push rates to engine for GUI/Snapshot
     233                 :        4762 :             g_engine.m_ffb_rate = loopMonitor.GetRate();
     234                 :        4762 :             g_engine.m_telemetry_rate = telemMonitor.GetRate();
     235                 :        4762 :             g_engine.m_hw_rate = hwMonitor.GetRate();
     236                 :        4762 :             g_engine.m_torque_rate = torqueMonitor.GetRate();
     237                 :        4762 :             g_engine.m_gen_torque_rate = genTorqueMonitor.GetRate();
     238                 :             : 
     239         [ +  - ]:        4762 :             force = g_engine.ApplySafetySlew(force, dt, restricted);  // TODO: review for correctedness and bugs
     240                 :        4762 :         }
     241                 :             : 
     242   [ +  -  +  -  :        4762 :         if (DirectInputFFB::Get().UpdateForce(force)) {
                   -  + ]
     243         [ #  # ]:           0 :             hwMonitor.RecordEvent();
     244                 :             :         }
     245                 :             : 
     246                 :             :         // Extended Logging (Issue #133)
     247   [ +  +  +  - ]:        4762 :         static auto lastExtLogTime = std::chrono::steady_clock::now();
     248                 :        4762 :         auto now = std::chrono::steady_clock::now();
     249   [ +  -  +  -  :        4762 :         if (std::chrono::duration_cast<std::chrono::seconds>(now - lastExtLogTime).count() >= 5) {
                   +  + ]
     250                 :           2 :             lastExtLogTime = now;
     251   [ +  -  +  -  :           2 :             if (GameConnector::Get().IsConnected() && g_localData.telemetry.playerHasVehicle) {
          +  -  +  -  +  
                      - ]
     252   [ +  -  +  - ]:           2 :                 Logger::Get().Log("--- Telemetry Sample Rates (Hz) ---");
     253   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Loop: %.1f, ET: %.1f, HW: %.1f", loopMonitor.GetRate(), telemMonitor.GetRate(), hwMonitor.GetRate());
     254   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Torque: Shaft=%.1f, Generic=%.1f", torqueMonitor.GetRate(), genTorqueMonitor.GetRate());
     255   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Accel: X=%.1f, Y=%.1f, Z=%.1f", mAccX.monitor.GetRate(), mAccY.monitor.GetRate(), mAccZ.monitor.GetRate());
     256   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Vel: X=%.1f, Y=%.1f, Z=%.1f", mVelX.monitor.GetRate(), mVelY.monitor.GetRate(), mVelZ.monitor.GetRate());
     257   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Rot: X=%.1f, Y=%.1f, Z=%.1f", mRotX.monitor.GetRate(), mRotY.monitor.GetRate(), mRotZ.monitor.GetRate());
     258   [ +  -  +  - ]:           2 :                 Logger::Get().Log("RotAcc: X=%.1f, Y=%.1f, Z=%.1f", mRotAccX.monitor.GetRate(), mRotAccY.monitor.GetRate(), mRotAccZ.monitor.GetRate());
     259   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Steering: Unf=%.1f, Fil=%.1f, RPM=%.1f", mUnfSteer.monitor.GetRate(), mFilSteer.monitor.GetRate(), mRPM.monitor.GetRate());
     260   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Load: FL=%.1f, FR=%.1f, RL=%.1f, RR=%.1f", mLoadFL.monitor.GetRate(), mLoadFR.monitor.GetRate(), mLoadRL.monitor.GetRate(), mLoadRR.monitor.GetRate());
     261   [ +  -  +  - ]:           2 :                 Logger::Get().Log("LatForce: FL=%.1f, FR=%.1f, RL=%.1f, RR=%.1f", mLatFL.monitor.GetRate(), mLatFR.monitor.GetRate(), mLatRL.monitor.GetRate(), mLatRR.monitor.GetRate());
     262   [ +  -  +  - ]:           2 :                 Logger::Get().Log("Pos: X=%.1f, Y=%.1f, Z=%.1f, DeltaTime=%.1f", mPosX.monitor.GetRate(), mPosY.monitor.GetRate(), mPosZ.monitor.GetRate(), mDtMon.monitor.GetRate());
     263   [ +  -  +  - ]:           2 :                 Logger::Get().Log("-----------------------------------");
     264                 :             :             }
     265                 :             :         }
     266                 :             : 
     267                 :             :         // Precise Timing: Sleep until next tick
     268         [ +  - ]:        4762 :         std::this_thread::sleep_until(next_tick);
     269                 :             :     }
     270                 :             : 
     271   [ +  -  +  - ]:           9 :     std::cout << "[FFB] Loop Stopped." << std::endl;
     272                 :           9 : }
     273                 :             : 
     274                 :             : #ifndef _WIN32
     275                 :           2 : void handle_sigterm(int sig) {
     276                 :           2 :     g_running = false;
     277                 :           2 : }
     278                 :             : #endif
     279                 :             : 
     280                 :             : #ifdef LMUFFB_UNIT_TEST
     281                 :           4 : int lmuffb_app_main(int argc, char* argv[]) noexcept {
     282                 :             : #else
     283                 :             : int main(int argc, char* argv[]) noexcept {
     284                 :             : #endif
     285                 :             :     try {
     286                 :             : #ifdef _WIN32
     287                 :             :         timeBeginPeriod(1);
     288                 :             : #else
     289                 :           4 :         signal(SIGTERM, handle_sigterm);
     290                 :           4 :         signal(SIGINT, handle_sigterm);
     291                 :             : #endif
     292                 :             : 
     293                 :           4 :     bool headless = false;
     294         [ +  + ]:           8 :     for (int i = 1; i < argc; ++i) {
     295   [ +  -  +  -  :           8 :         if (std::string(argv[i]) == "--headless") headless = true;
                   +  + ]
     296                 :             :     }
     297                 :             : 
     298   [ +  -  +  - ]:           4 :     std::cout << "Starting lmuFFB (C++ Port)..." << std::endl;
     299                 :             :     // Initialize persistent debug logging for crash analysis
     300   [ +  -  +  -  :           8 :     Logger::Get().Init("lmuffb_debug.log");
                   +  - ]
     301   [ +  -  +  - ]:           4 :     Logger::Get().Log("Application Started. Version: %s", LMUFFB_VERSION);
     302   [ +  +  +  -  :           4 :     if (headless) Logger::Get().Log("Mode: HEADLESS");
                   +  - ]
     303   [ +  -  +  - ]:           1 :     else Logger::Get().Log("Mode: GUI");
     304                 :             : 
     305         [ +  - ]:           4 :     Preset::ApplyDefaultsToEngine(g_engine);
     306   [ +  -  +  - ]:           4 :     Config::Load(g_engine);
     307                 :             : 
     308         [ +  + ]:           4 :     if (!headless) {
     309   [ +  -  -  + ]:           1 :         if (!GuiLayer::Init()) {
     310   [ #  #  #  # ]:           0 :             std::cerr << "Failed to initialize GUI." << std::endl;
     311                 :             :         }
     312   [ +  -  +  -  :           1 :         DirectInputFFB::Get().Initialize(reinterpret_cast<HWND>(GuiLayer::GetWindowHandle()));
                   +  - ]
     313                 :             :     } else {
     314   [ +  -  +  - ]:           3 :         std::cout << "Running in HEADLESS mode." << std::endl;
     315   [ +  -  +  - ]:           3 :         DirectInputFFB::Get().Initialize(NULL);
     316                 :             :     }
     317                 :             : 
     318   [ +  -  +  -  :           4 :     if (GameConnector::Get().CheckLegacyConflict()) {
                   -  + ]
     319   [ #  #  #  # ]:           0 :         std::cout << "[Info] Legacy rF2 plugin detected (not a problem for LMU 1.2+)" << std::endl;
     320                 :             :     }
     321                 :             : 
     322   [ +  -  +  -  :           4 :     if (!GameConnector::Get().TryConnect()) {
                   -  + ]
     323   [ #  #  #  # ]:           0 :         std::cout << "Game not running or Shared Memory not ready. Waiting..." << std::endl;
     324                 :             :     }
     325                 :             : 
     326         [ +  - ]:           4 :     std::thread ffb_thread(FFBThread);
     327   [ +  -  +  - ]:           4 :     std::cout << "[GUI] Main Loop Started." << std::endl;
     328                 :             : 
     329         [ +  + ]:          30 :     while (g_running) {
     330         [ +  - ]:          26 :         GuiLayer::Render(g_engine);
     331                 :             : 
     332                 :             :         // Process background save requests from the FFB thread (v0.7.70)
     333         [ +  + ]:          26 :         if (Config::m_needs_save.exchange(false)) {
     334   [ +  -  +  - ]:           2 :             Config::Save(g_engine);
     335                 :             :         }
     336                 :             : 
     337                 :             :         // Maintain a consistent 60Hz message loop even when backgrounded
     338                 :             :         // to ensure DirectInput performance and reliability.
     339         [ +  - ]:          26 :         std::this_thread::sleep_for(std::chrono::milliseconds(16));
     340                 :             :     }
     341                 :             :     
     342   [ +  -  +  - ]:           4 :     Config::Save(g_engine);
     343         [ +  + ]:           4 :     if (!headless) {
     344   [ +  -  +  - ]:           1 :         Logger::Get().Log("Shutting down GUI...");
     345         [ +  - ]:           1 :         GuiLayer::Shutdown(g_engine);
     346                 :             :     }
     347         [ +  - ]:           4 :     if (ffb_thread.joinable()) {
     348   [ +  -  +  - ]:           4 :         Logger::Get().Log("Stopping FFB Thread...");
     349                 :           4 :         g_running = false; // Ensure loop breaks
     350         [ +  - ]:           4 :         ffb_thread.join();
     351   [ +  -  +  - ]:           4 :         Logger::Get().Log("FFB Thread Stopped.");
     352                 :             :     }
     353   [ +  -  +  - ]:           4 :     DirectInputFFB::Get().Shutdown();
     354   [ +  -  +  - ]:           4 :     Logger::Get().Log("Main Loop Ended. Clean Exit.");
     355                 :             :     
     356                 :           4 :     return 0;
     357         [ -  - ]:           4 :     } catch (const std::exception& e) {
     358                 :           0 :         fprintf(stderr, "Fatal exception: %s\n", e.what());
     359                 :             :         // Attempt to log if possible, but don't risk more throws
     360   [ -  -  -  - ]:           0 :         try { Logger::Get().Log("Fatal exception: %s", e.what()); } catch(...) { (void)0; }
     361                 :           0 :         return 1;
     362                 :           0 :     } catch (...) {
     363                 :           0 :         fprintf(stderr, "Fatal unknown exception.\n");
     364   [ -  -  -  - ]:           0 :         try { Logger::Get().Log("Fatal unknown exception."); } catch(...) { (void)0; }
     365                 :           0 :         return 1;
     366                 :           0 :     }
     367                 :             : }
        

Generated by: LCOV version 2.0-1