LCOV - code coverage report
Current view: top level - io - GameConnector.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 90.1 % 274 247
Test Date: 2026-03-18 19:01:10 Functions: 100.0 % 18 18
Branches: 72.8 % 287 209

             Branch data     Line data    Source code
       1                 :             : #include "GameConnector.h"
       2                 :             : #include "Logger.h"
       3                 :             : #ifndef _WIN32
       4                 :             : #include "io/lmu_sm_interface/LinuxMock.h"
       5                 :             : #endif
       6                 :             : #include "io/lmu_sm_interface/SafeSharedMemoryLock.h"
       7                 :             : #include <iostream>
       8                 :             : #include <cstring>
       9                 :             : #include "StringUtils.h"
      10                 :             : 
      11                 :             : #define LEGACY_SHARED_MEMORY_NAME "$rFactor2SMMP_Telemetry$"
      12                 :             : 
      13                 :       51432 : GameConnector& GameConnector::Get() {
      14   [ +  +  +  -  :       51432 :     static GameConnector instance;
             +  -  -  - ]
      15                 :       51432 :     return instance;
      16                 :             : }
      17                 :             : 
      18                 :           1 : GameConnector::GameConnector() {}
      19                 :             : 
      20                 :           1 : GameConnector::~GameConnector() {
      21                 :           1 :     Disconnect();
      22                 :           1 : }
      23                 :             : 
      24                 :        6667 : void GameConnector::Disconnect() {
      25         [ +  - ]:        6667 :     std::lock_guard<std::recursive_mutex> lock(m_mutex);
      26                 :        6667 :     _DisconnectLocked();
      27                 :        6667 : }
      28                 :             : 
      29                 :       13359 : void GameConnector::_DisconnectLocked() {
      30                 :             : #if defined(_WIN32) || defined(HEADLESS_GUI)
      31         [ +  + ]:       13359 :     if (m_pSharedMemLayout) {
      32                 :        6674 :         UnmapViewOfFile(m_pSharedMemLayout);
      33                 :        6674 :         m_pSharedMemLayout = nullptr;
      34                 :             :     }
      35         [ +  + ]:       13359 :     if (m_hMapFile) {
      36                 :        6665 :         CloseHandle(m_hMapFile);
      37                 :        6665 :         m_hMapFile = NULL;
      38                 :             :     }
      39                 :       13359 :     m_hwndGame = NULL;
      40                 :             : #endif
      41                 :       13359 :     m_smLock.reset();
      42                 :       13359 :     m_connected = false;
      43                 :       13359 :     m_processId = 0;
      44                 :       13359 : }
      45                 :             : 
      46                 :        6671 : bool GameConnector::TryConnect() {
      47         [ +  - ]:        6671 :     std::lock_guard<std::recursive_mutex> lock(m_mutex);
      48         [ +  + ]:        6671 :     if (m_connected) return true;
      49                 :             : 
      50                 :             :     // Ensure we don't leak handles from a previous partial/failed attempt
      51                 :        6665 :     _DisconnectLocked();
      52                 :             : 
      53                 :             : #if defined(_WIN32) || defined(HEADLESS_GUI)
      54         [ +  - ]:        6665 :     m_hMapFile = OpenFileMappingA(FILE_MAP_READ, FALSE, LMU_SHARED_MEMORY_FILE);
      55                 :             :     
      56         [ -  + ]:        6665 :     if (m_hMapFile == NULL) {
      57                 :           0 :         return false;
      58                 :             :     } 
      59                 :             : 
      60         [ +  - ]:        6665 :     m_pSharedMemLayout = (SharedMemoryLayout*)MapViewOfFile(m_hMapFile, FILE_MAP_READ, 0, 0, sizeof(SharedMemoryLayout));
      61         [ -  + ]:        6665 :     if (m_pSharedMemLayout == NULL) {
      62   [ #  #  #  # ]:           0 :         Logger::Get().Log("[GameConnector] Could not map view of file.");
      63   [ #  #  #  # ]:           0 :         Logger::Get().LogWin32Error("MapViewOfFile", GetLastError());
      64                 :           0 :         _DisconnectLocked();
      65                 :           0 :         return false;
      66                 :             :     }
      67                 :             : 
      68   [ +  -  +  - ]:        6665 :     m_smLock = SafeSharedMemoryLock::MakeSafeSharedMemoryLock();
      69         [ +  + ]:        6665 :     if (!m_smLock.has_value()) {
      70   [ +  -  +  - ]:           1 :         Logger::Get().Log("[GameConnector] Failed to init LMU Shared Memory Lock");
      71                 :           1 :         _DisconnectLocked();
      72                 :           1 :         return false;
      73                 :             :     }
      74                 :             : 
      75                 :        6664 :     HWND hwnd = m_pSharedMemLayout->data.generic.appInfo.mAppWindow;
      76         [ +  + ]:        6664 :     if (hwnd) {
      77                 :           4 :         m_hwndGame = hwnd; // Store HWND for liveness check (IsWindow)
      78                 :             :         // Note: multiple threads might access shared memory, but HWND is usually stable during session.
      79                 :             :         // We use IsWindow(m_hwndGame) instead of OpenProcess to avoid AV heuristics flagging "Process Access".
      80                 :             :     }
      81                 :             : 
      82                 :        6664 :     m_connected = true;
      83                 :             : 
      84                 :             :     // Initialize Robust State Machine (#267)
      85         [ +  - ]:        6664 :     if (m_pSharedMemLayout) {
      86                 :        6664 :         m_sessionActive = (m_pSharedMemLayout->data.scoring.scoringInfo.mTrackName[0] != '\0');
      87                 :        6664 :         m_inRealtime = (m_pSharedMemLayout->data.scoring.scoringInfo.mInRealtime != 0);
      88                 :        6664 :         m_currentSessionType = m_pSharedMemLayout->data.scoring.scoringInfo.mSession;
      89                 :        6664 :         m_currentGamePhase = m_pSharedMemLayout->data.scoring.scoringInfo.mGamePhase;
      90                 :             : 
      91         [ +  + ]:        6664 :         if (m_pSharedMemLayout->data.telemetry.playerHasVehicle) {
      92                 :           6 :             uint8_t idx = m_pSharedMemLayout->data.telemetry.playerVehicleIdx;
      93         [ +  - ]:           6 :             if (idx < 104) {
      94                 :           6 :                 m_playerControl = m_pSharedMemLayout->data.scoring.vehScoringInfo[idx].mControl;
      95                 :             :             }
      96                 :             :         }
      97                 :             :     }
      98                 :             : 
      99                 :        6664 :     m_lastUpdateLocalTime = std::chrono::steady_clock::now();
     100   [ +  -  +  - ]:        6664 :     Logger::Get().Log("[GameConnector] Connected to LMU Shared Memory.");
     101                 :        6664 :     return true;
     102                 :             : #else
     103                 :             :     return false;
     104                 :             : #endif
     105                 :        6671 : }
     106                 :             : 
     107                 :           7 : bool GameConnector::CheckLegacyConflict() {
     108                 :             : #if defined(_WIN32) || defined(HEADLESS_GUI)
     109                 :           7 :     HANDLE hLegacy = OpenFileMappingA(FILE_MAP_READ, FALSE, LEGACY_SHARED_MEMORY_NAME);
     110         [ +  + ]:           7 :     if (hLegacy) {
     111                 :           2 :         Logger::Get().Log("[Warning] Legacy rFactor 2 Shared Memory Plugin detected. This may conflict with LMU 1.2 data.");
     112                 :           2 :         CloseHandle(hLegacy);
     113                 :           2 :         return true;
     114                 :             :     }
     115                 :             : #endif
     116                 :           5 :     return false;
     117                 :             : }
     118                 :             : 
     119                 :        6780 : bool GameConnector::IsConnected() const {
     120         [ +  + ]:        6780 :   if (!m_connected.load(std::memory_order_acquire)) return false;
     121                 :             : 
     122         [ +  - ]:        6512 :   std::lock_guard<std::recursive_mutex> lock(m_mutex);
     123         [ -  + ]:        6512 :   if (!m_connected.load(std::memory_order_relaxed)) return false;
     124                 :             : 
     125                 :             : #if defined(_WIN32) || defined(HEADLESS_GUI)
     126         [ +  + ]:        6512 :   if (m_hwndGame) {
     127         [ +  + ]:        6329 :     if (!IsWindow(m_hwndGame)) {
     128                 :             :       // Window is gone, game likely exited
     129                 :           1 :       const_cast<GameConnector*>(this)->_DisconnectLocked();
     130                 :           1 :       return false;
     131                 :             :     }
     132                 :             :   }
     133                 :             : #endif
     134                 :             : 
     135   [ +  -  +  -  :        6511 :   return m_connected.load(std::memory_order_relaxed) && m_pSharedMemLayout && m_smLock.has_value();
                   +  - ]
     136                 :        6512 : }
     137                 :             : 
     138                 :        5744 : bool GameConnector::CopyTelemetry(SharedMemoryObjectOut& dest) {
     139         [ +  + ]:        5744 :     if (!m_connected.load(std::memory_order_acquire)) return false;
     140                 :             : 
     141         [ +  - ]:        4398 :     std::lock_guard<std::recursive_mutex> lock(m_mutex);
     142   [ +  +  +  -  :        4398 :     if (!m_connected.load(std::memory_order_relaxed) || !m_pSharedMemLayout || !m_smLock.has_value()) return false;
             -  +  +  + ]
     143                 :             : 
     144         [ +  + ]:        4368 :     if (m_smLock->Lock(50)) {
     145                 :        4366 :         CopySharedMemoryObj(dest, m_pSharedMemLayout->data);
     146                 :             :         
     147         [ +  + ]:        4366 :         if (dest.telemetry.playerHasVehicle) {
     148                 :           3 :             uint8_t idx = dest.telemetry.playerVehicleIdx;
     149         [ +  - ]:           3 :             if (idx < 104) {
     150                 :           3 :                 double currentET = dest.telemetry.telemInfo[idx].mElapsedTime;
     151                 :           3 :                 double currentSteer = dest.telemetry.telemInfo[idx].mUnfilteredSteering;
     152                 :             : 
     153                 :             :                 // Heartbeat: Update timer if game is running (ET changes)
     154                 :             :                 // OR if user is moving the wheel (Steering changes) - Issue #184
     155   [ +  +  -  +  :           3 :                 if (currentET != m_lastElapsedTime || std::abs(currentSteer - m_lastSteering) > 0.0001) {
                   +  + ]
     156                 :           2 :                     m_lastElapsedTime = currentET;
     157                 :           2 :                     m_lastSteering = currentSteer;
     158                 :           2 :                     m_lastUpdateLocalTime = std::chrono::steady_clock::now();
     159                 :             :                 }
     160                 :             :             }
     161                 :             :         } else {
     162                 :        4363 :             m_lastUpdateLocalTime = std::chrono::steady_clock::now();
     163                 :             :         }
     164                 :             : 
     165                 :        4366 :         m_smLock->Unlock();
     166                 :             : 
     167         [ +  - ]:        4366 :         CheckTransitions(dest);
     168                 :             : 
     169                 :        4366 :         return m_inRealtime.load(std::memory_order_relaxed);
     170                 :             :     } else {
     171                 :           2 :         return false;
     172                 :             :     }
     173                 :        4398 : }
     174                 :             : 
     175                 :        3168 : bool GameConnector::IsStale(long timeoutMs) const {
     176         [ +  + ]:        3168 :     if (!m_connected.load(std::memory_order_acquire)) return true;
     177                 :             : 
     178                 :        3167 :     auto now = std::chrono::steady_clock::now();
     179   [ +  -  +  - ]:        3167 :     auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastUpdateLocalTime).count();
     180                 :        3167 :     return (diff > timeoutMs);
     181                 :             : }
     182                 :             : 
     183                 :             : // ---------------------------------------------------------------------------
     184                 :             : // Static string-lookup helpers
     185                 :             : // ---------------------------------------------------------------------------
     186                 :             : 
     187                 :          19 : /*static*/ const char* GameConnector::SmeEventName(int i) {
     188   [ +  +  +  +  :          19 :     switch (i) {
          +  +  +  +  +  
             +  +  +  - ]
     189                 :           2 :         case SME_ENTER:              return "SME_ENTER";
     190                 :           1 :         case SME_EXIT:               return "SME_EXIT";
     191                 :           2 :         case SME_STARTUP:            return "SME_STARTUP";
     192                 :           1 :         case SME_SHUTDOWN:           return "SME_SHUTDOWN";
     193                 :           2 :         case SME_LOAD:               return "SME_LOAD";
     194                 :           1 :         case SME_UNLOAD:             return "SME_UNLOAD";
     195                 :           1 :         case SME_START_SESSION:      return "SME_START_SESSION";
     196                 :           3 :         case SME_END_SESSION:        return "SME_END_SESSION";
     197                 :           2 :         case SME_ENTER_REALTIME:     return "SME_ENTER_REALTIME";
     198                 :           2 :         case SME_EXIT_REALTIME:      return "SME_EXIT_REALTIME";
     199                 :           1 :         case SME_INIT_APPLICATION:   return "SME_INIT_APPLICATION";
     200                 :           1 :         case SME_UNINIT_APPLICATION: return "SME_UNINIT_APPLICATION";
     201                 :           0 :         default:                     return "Unknown";
     202                 :             :     }
     203                 :             : }
     204                 :             : 
     205                 :          28 : /*static*/ const char* GameConnector::GamePhaseName(unsigned char phase) {
     206   [ +  -  -  -  :          28 :     switch (phase) {
          -  +  -  -  +  
                   +  - ]
     207                 :          17 :         case 0: return "Before Session";
     208                 :           0 :         case 1: return "Reconnaissance";
     209                 :           0 :         case 2: return "Grid Walk";
     210                 :           0 :         case 3: return "Formation";
     211                 :           0 :         case 4: return "Countdown";
     212                 :           8 :         case 5: return "Green Flag";
     213                 :           0 :         case 6: return "Full Course Yellow";
     214                 :           0 :         case 7: return "Stopped";
     215                 :           1 :         case 8: return "Over";
     216                 :           2 :         case 9: return "Paused";
     217                 :           0 :         default: return "Unknown";
     218                 :             :     }
     219                 :             : }
     220                 :             : 
     221                 :          26 : /*static*/ const char* GameConnector::SessionTypeName(long session) {
     222         [ +  + ]:          26 :     if (session == 0)                   return "Test Day";
     223   [ +  -  -  + ]:           7 :     if (session >= 1 && session <= 4)   return "Practice";
     224   [ +  -  +  + ]:           7 :     if (session >= 5 && session <= 8)   return "Qualifying";
     225         [ -  + ]:           4 :     if (session == 9)                   return "Warmup";
     226   [ +  -  +  - ]:           4 :     if (session >= 10 && session <= 13) return "Race";
     227                 :           0 :     return "Unknown";
     228                 :             : }
     229                 :             : 
     230                 :          13 : /*static*/ const char* GameConnector::ControlModeName(signed char control) {
     231   [ +  +  +  -  :          13 :     switch (static_cast<ControlMode>(control)) {
                   -  - ]
     232                 :           2 :         case ControlMode::NONE:   return "Nobody";
     233                 :           8 :         case ControlMode::PLAYER: return "Player";
     234                 :           3 :         case ControlMode::AI:     return "AI";
     235                 :           0 :         case ControlMode::REMOTE: return "Remote";
     236                 :           0 :         case ControlMode::REPLAY: return "Replay";
     237                 :           0 :         default:                  return "Unknown";
     238                 :             :     }
     239                 :             : }
     240                 :             : 
     241                 :           8 : /*static*/ const char* GameConnector::PitStateName(unsigned char pitState) {
     242   [ +  -  -  -  :           8 :     switch (pitState) {
                   -  - ]
     243                 :           8 :         case 0: return "None";
     244                 :           0 :         case 1: return "Request";
     245                 :           0 :         case 2: return "Entering";
     246                 :           0 :         case 3: return "Stopped";
     247                 :           0 :         case 4: return "Exiting";
     248                 :           0 :         default: return "Unknown";
     249                 :             :     }
     250                 :             : }
     251                 :             : 
     252                 :             : // ---------------------------------------------------------------------------
     253                 :             : // Phase 1: Update state machine atomics unconditionally from the SM snapshot.
     254                 :             : //
     255                 :             : // "Ground truth polling": what the shared memory says right now is the truth.
     256                 :             : // SME events (applied at the end) are a fast-path that can override the polled
     257                 :             : // values within the same tick — they handle rapid transitions before the next
     258                 :             : // poll tick arrives.
     259                 :             : // ---------------------------------------------------------------------------
     260                 :             : 
     261                 :        4426 : void GameConnector::_UpdateStateFromSnapshot(const SharedMemoryObjectOut& current) {
     262                 :        4426 :     auto& scoring = current.scoring.scoringInfo;
     263                 :        4426 :     auto  now     = std::chrono::steady_clock::now();
     264                 :             : 
     265                 :             :     // --- Ground truth (polling) ---
     266                 :             :     // Save previous inRealtime before overwriting, so we can detect transitions.
     267                 :        4426 :     bool prevInRealtime = m_inRealtime.load(std::memory_order_relaxed);
     268                 :             : 
     269                 :        4426 :     m_sessionActive      = (scoring.mTrackName[0] != '\0');
     270                 :        4426 :     m_inRealtime         = (scoring.mInRealtime != 0);
     271                 :        4426 :     m_currentGamePhase   = scoring.mGamePhase;
     272                 :        4426 :     m_currentSessionType = scoring.mSession;
     273                 :             : 
     274                 :        4426 :     bool currentInRealtime = m_inRealtime.load(std::memory_order_relaxed);
     275                 :             : 
     276                 :             :     // --- Quit-to-menu detection (#7.5) ---
     277                 :             :     // When mInRealtime goes true→false while a session is active, arm a
     278                 :             :     // pending check. If SME_ENTER fires within the deadline window, the user
     279                 :             :     // quit to the main menu (LMU doesn't fire SME_END_SESSION for this path).
     280                 :             :     // If mInRealtime goes back to true first, the user is driving again —
     281                 :             :     // cancel the check. Debug logs confirm SME_ENTER fires within 0-1 seconds
     282                 :             :     // on quit-to-menu and does NOT fire on a normal return to the garage monitor.
     283   [ +  +  +  +  :        4436 :     if (prevInRealtime && !currentInRealtime &&
             +  +  +  + ]
     284                 :          10 :         m_sessionActive.load(std::memory_order_relaxed)) {
     285                 :           3 :         m_pendingMenuCheck  = true;
     286         [ +  - ]:           3 :         m_menuCheckDeadline = now + std::chrono::seconds(3);
     287                 :             :     }
     288         [ +  + ]:        4426 :     if (currentInRealtime) {
     289                 :          12 :         m_pendingMenuCheck = false; // user clicked Drive — cancel
     290                 :             :     }
     291   [ +  +  +  -  :        4426 :     if (m_pendingMenuCheck && now > m_menuCheckDeadline) {
             -  +  -  + ]
     292                 :           0 :         m_pendingMenuCheck = false; // deadline expired — no SME_ENTER came, must be garage
     293                 :             :     }
     294                 :             : 
     295                 :             : 
     296         [ +  + ]:        4426 :     if (current.telemetry.playerHasVehicle) {
     297                 :          23 :         uint8_t idx = current.telemetry.playerVehicleIdx;
     298         [ +  - ]:          23 :         if (idx < 104) {
     299                 :          23 :             m_playerControl = current.scoring.vehScoringInfo[idx].mControl;
     300                 :             :         }
     301                 :             :     }
     302                 :             : 
     303                 :             :     // --- Fast-path: apply SME event overrides on top of polled truth (#267, #274) ---
     304                 :             :     // This provides "instantaneous" updates while polling ensures long-term consistency.
     305                 :        4426 :     auto& generic = current.generic;
     306         [ +  + ]:       75242 :     for (int i = 0; i < SME_MAX; ++i) {
     307   [ +  +  +  +  :       70816 :         if (i == SME_UPDATE_SCORING || i == SME_UPDATE_TELEMETRY || i == SME_FFB || i == SME_SET_ENVIRONMENT)
             +  +  +  + ]
     308                 :       17704 :             continue;
     309   [ +  +  +  + ]:       53112 :         if (generic.events[i] != m_prevState.eventState[i] && generic.events[i] != 0) {
     310         [ +  + ]:          19 :             if (i == SME_START_SESSION)                     m_sessionActive = true;
     311   [ +  +  +  + ]:          19 :             if (i == SME_END_SESSION || i == SME_UNLOAD) { m_sessionActive = false; m_inRealtime = false; }
     312         [ +  + ]:          19 :             if (i == SME_ENTER_REALTIME)                    m_inRealtime = true;
     313         [ +  + ]:          19 :             if (i == SME_EXIT_REALTIME)                     m_inRealtime = false;
     314                 :             : 
     315                 :             :             // Quit-to-menu detection (#7.5): SME_ENTER fires within ~1 second of
     316                 :             :             // de-realtime when the user quits to the main menu. It does NOT fire
     317                 :             :             // when the user returns to the garage monitor. (Confirmed via debug logs.)
     318   [ +  +  +  + ]:          19 :             if (i == SME_ENTER && m_pendingMenuCheck) {
     319                 :           1 :                 m_sessionActive    = false;
     320                 :           1 :                 m_pendingMenuCheck = false;
     321                 :             :             }
     322                 :             :         }
     323                 :             :     }
     324                 :        4426 : }
     325                 :             : 
     326                 :             : // ---------------------------------------------------------------------------
     327                 :             : // Phase 2: Detect changes vs m_prevState and emit log lines.
     328                 :             : //   This phase has no side-effects on the state machine atomics.
     329                 :             : // ---------------------------------------------------------------------------
     330                 :             : 
     331                 :        4426 : void GameConnector::_LogTransitions(const SharedMemoryObjectOut& current) {
     332                 :        4426 :     auto& scoring = current.scoring.scoringInfo;
     333                 :        4426 :     auto& generic = current.generic;
     334                 :             : 
     335                 :             :     // 0. Shared Memory Events (Issue #244)
     336         [ +  + ]:       75242 :     for (int i = 0; i < SME_MAX; ++i) {
     337   [ +  +  +  +  :       70816 :         if (i == SME_UPDATE_SCORING || i == SME_UPDATE_TELEMETRY || i == SME_FFB || i == SME_SET_ENVIRONMENT)
             +  +  +  + ]
     338                 :       17704 :             continue;
     339                 :             : 
     340         [ +  + ]:       53112 :         if (generic.events[i] != m_prevState.eventState[i]) {
     341         [ +  + ]:          30 :             if (generic.events[i] != 0) {
     342                 :          19 :                 bool shouldLog = true;
     343                 :             : 
     344                 :             :                 // SME_STARTUP is known to spam values (e.g. 10, 16) in menus. Apply a 5-second cooldown.
     345         [ +  + ]:          19 :                 if (i == SME_STARTUP) {
     346                 :           2 :                     auto now  = std::chrono::steady_clock::now();
     347   [ +  -  +  - ]:           2 :                     auto diff = std::chrono::duration_cast<std::chrono::seconds>(now - m_prevState.lastEventLogTime[i]).count();
     348         [ -  + ]:           2 :                     if (diff < 5) {
     349                 :           0 :                         shouldLog = false;
     350                 :             :                     } else {
     351                 :           2 :                         m_prevState.lastEventLogTime[i] = now;
     352                 :             :                     }
     353                 :             :                 }
     354                 :             : 
     355         [ +  - ]:          19 :                 if (shouldLog) {
     356                 :          19 :                     Logger::Get().LogFile("[Transition] Event: %s (%u)", SmeEventName(i), generic.events[i]);
     357                 :             :                 }
     358                 :             :             }
     359                 :          30 :             m_prevState.eventState[i] = generic.events[i];
     360                 :             :         }
     361                 :             :     }
     362                 :             : 
     363                 :             :     // 1. Options Location (UI Menu)
     364         [ +  + ]:        4426 :     if (generic.appInfo.mOptionsLocation != m_prevState.optionsLocation) {
     365                 :          27 :         const char* locStr = "Unknown";
     366   [ +  -  +  +  :          27 :         switch (generic.appInfo.mOptionsLocation) {
                      - ]
     367                 :          25 :             case 0: locStr = "Main UI"; break;
     368                 :           0 :             case 1: locStr = "Track Loading"; break;
     369                 :           1 :             case 2: locStr = "Monitor (Garage)"; break;
     370                 :           1 :             case 3: locStr = "On Track"; break;
     371                 :             :         }
     372                 :          27 :         Logger::Get().LogFile("[Transition] OptionsLocation: %d -> %d (%s)",
     373                 :          27 :             m_prevState.optionsLocation, generic.appInfo.mOptionsLocation, locStr);
     374                 :          27 :         m_prevState.optionsLocation = generic.appInfo.mOptionsLocation;
     375                 :             :     }
     376                 :             : 
     377                 :             :     // 1.1 Options Page
     378         [ -  + ]:        4426 :     if (strncmp(generic.appInfo.mOptionsPage, m_prevState.optionsPage, 31) != 0) {
     379                 :           0 :         Logger::Get().LogFile("[Transition] OptionsPage: '%s' -> '%s'", m_prevState.optionsPage, generic.appInfo.mOptionsPage);
     380                 :           0 :         StringUtils::SafeCopy(m_prevState.optionsPage, sizeof(m_prevState.optionsPage), generic.appInfo.mOptionsPage);
     381                 :             :     }
     382                 :             : 
     383                 :             :     // 2. InRealtime (Driving vs Menu)
     384                 :        4426 :     bool currentInRealtime = (scoring.mInRealtime != 0);
     385         [ +  + ]:        4426 :     if (currentInRealtime != m_prevState.inRealtime) {
     386         [ +  + ]:          28 :         Logger::Get().LogFile("[Transition] InRealtime: %s -> %s",
     387         [ +  + ]:          14 :             m_prevState.inRealtime ? "true" : "false", currentInRealtime ? "true" : "false");
     388                 :             : 
     389                 :             :         // Investigation dump (#7.4c): fires on every de-realtime event (garage return OR
     390                 :             :         // quit-to-menu). Captures signals needed to distinguish the two paths.
     391   [ +  +  +  - ]:          14 :         if (!currentInRealtime && m_prevState.inRealtime) {
     392                 :           8 :             Logger::Get().LogFile(
     393                 :             :                 "[Diag] De-realtime snapshot: track='%s' optionsLoc=%d "
     394                 :             :                 "numVehicles=%d playerHasVehicle=%s smUpdateScoring=%u",
     395                 :           4 :                 scoring.mTrackName,
     396                 :           4 :                 (int)generic.appInfo.mOptionsLocation,
     397                 :           4 :                 (int)scoring.mNumVehicles,
     398                 :           4 :                 current.telemetry.playerHasVehicle ? "true" : "false",
     399         [ +  + ]:           4 :                 (unsigned)generic.events[SME_UPDATE_SCORING]);
     400                 :             :         }
     401                 :             : 
     402                 :          14 :         m_prevState.inRealtime = currentInRealtime;
     403                 :             :     }
     404                 :             : 
     405                 :             :     // 3. Game Phase (Session state / Pause)
     406         [ +  + ]:        4426 :     if (scoring.mGamePhase != m_prevState.gamePhase) {
     407                 :          28 :         Logger::Get().LogFile("[Transition] GamePhase: %d -> %d (%s)",
     408                 :          28 :             m_prevState.gamePhase, scoring.mGamePhase, GamePhaseName(scoring.mGamePhase));
     409                 :          28 :         m_prevState.gamePhase = scoring.mGamePhase;
     410                 :             :     }
     411                 :             : 
     412                 :             :     // 4. Session Type
     413         [ +  + ]:        4426 :     if (scoring.mSession != m_prevState.session) {
     414                 :          26 :         Logger::Get().LogFile("[Transition] Session: %ld -> %ld (%s)",
     415                 :          26 :             m_prevState.session, scoring.mSession, SessionTypeName(scoring.mSession));
     416                 :          26 :         m_prevState.session = scoring.mSession;
     417                 :             :     }
     418                 :             : 
     419                 :             :     // 5. Track Name (Context Change)
     420         [ +  + ]:        4426 :     if (strcmp(scoring.mTrackName, m_prevState.trackName) != 0) {
     421                 :          12 :         Logger::Get().LogFile("[Transition] Track: '%s' -> '%s'", m_prevState.trackName, scoring.mTrackName);
     422                 :          12 :         StringUtils::SafeCopy(m_prevState.trackName, sizeof(m_prevState.trackName), scoring.mTrackName);
     423                 :             :     }
     424                 :             : 
     425                 :             :     // 6. Player Control & Pit State
     426         [ +  + ]:        4426 :     if (current.telemetry.playerHasVehicle) {
     427                 :          23 :         uint8_t idx = current.telemetry.playerVehicleIdx;
     428         [ +  - ]:          23 :         if (idx < 104) {
     429                 :          23 :             auto& vehScoring = current.scoring.vehScoringInfo[idx];
     430                 :             : 
     431         [ +  + ]:          23 :             if (vehScoring.mControl != m_prevState.control) {
     432                 :          13 :                 Logger::Get().LogFile("[Transition] Control: %d -> %d (%s)",
     433                 :          13 :                     m_prevState.control, vehScoring.mControl, ControlModeName(vehScoring.mControl));
     434                 :          13 :                 m_prevState.control = vehScoring.mControl;
     435                 :             :             }
     436                 :             : 
     437         [ +  + ]:          23 :             if (vehScoring.mPitState != m_prevState.pitState) {
     438                 :           8 :                 Logger::Get().LogFile("[Transition] PitState: %d -> %d (%s)",
     439                 :           8 :                     m_prevState.pitState, vehScoring.mPitState, PitStateName(vehScoring.mPitState));
     440                 :           8 :                 m_prevState.pitState = vehScoring.mPitState;
     441                 :             :             }
     442                 :             : 
     443         [ +  + ]:          23 :             if (strcmp(vehScoring.mVehicleName, m_prevState.vehicleName) != 0) {
     444                 :           1 :                 Logger::Get().LogFile("[Transition] Vehicle: '%s' -> '%s'", m_prevState.vehicleName, vehScoring.mVehicleName);
     445                 :           1 :                 StringUtils::SafeCopy(m_prevState.vehicleName, sizeof(m_prevState.vehicleName), vehScoring.mVehicleName);
     446                 :             :             }
     447                 :             : 
     448                 :             :             // 7. Steering Range
     449                 :          23 :             float currentRange = current.telemetry.telemInfo[idx].mPhysicalSteeringWheelRange;
     450         [ +  + ]:          23 :             if (std::abs(currentRange - m_prevState.steeringRange) > 0.001f) {
     451                 :           9 :                 Logger::Get().LogFile("[Transition] SteeringRange: %.2f -> %.2f", m_prevState.steeringRange, currentRange);
     452                 :           9 :                 m_prevState.steeringRange = currentRange;
     453                 :             :             }
     454                 :             :         }
     455                 :             :     }
     456                 :             : 
     457                 :             :     // 7. PlayerHasVehicle changes — investigation: quit-to-menu detection (#7.4a)
     458                 :        4426 :     bool currentHasVehicle = current.telemetry.playerHasVehicle;
     459         [ +  + ]:        4426 :     if (currentHasVehicle != m_prevState.playerHasVehicle) {
     460         [ +  + ]:          20 :         Logger::Get().LogFile("[Transition] PlayerHasVehicle: %s -> %s",
     461         [ +  + ]:          10 :             m_prevState.playerHasVehicle ? "true" : "false",
     462                 :             :             currentHasVehicle ? "true" : "false");
     463                 :          10 :         m_prevState.playerHasVehicle = currentHasVehicle;
     464                 :             :     }
     465                 :             : 
     466                 :             :     // 8. NumVehicles changes — investigation: quit-to-menu detection (#7.4b)
     467                 :        4426 :     int currentNumVehicles = (int)scoring.mNumVehicles;
     468         [ +  + ]:        4426 :     if (currentNumVehicles != m_prevState.numVehicles) {
     469                 :          26 :         Logger::Get().LogFile("[Transition] NumVehicles: %d -> %d",
     470                 :             :             m_prevState.numVehicles, currentNumVehicles);
     471                 :          26 :         m_prevState.numVehicles = currentNumVehicles;
     472                 :             :     }
     473                 :        4426 : }
     474                 :             : 
     475                 :             : // ---------------------------------------------------------------------------
     476                 :             : // CheckTransitions: orchestrator (lock, then run both phases in order)
     477                 :             : // ---------------------------------------------------------------------------
     478                 :             : 
     479                 :        4426 : void GameConnector::CheckTransitions(const SharedMemoryObjectOut& current) {
     480         [ +  - ]:        4426 :     std::lock_guard<std::recursive_mutex> lock(m_mutex);
     481         [ +  - ]:        4426 :     _UpdateStateFromSnapshot(current);  // Phase 1: sync state machine atomics
     482         [ +  - ]:        4426 :     _LogTransitions(current);           // Phase 2: log any changes
     483                 :        4426 : }
        

Generated by: LCOV version 2.0-1