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 : }
|