Branch data Line data Source code
1 : : #include "DirectInputFFB.h"
2 : :
3 : : // Standard Library Headers
4 : : #include <iostream>
5 : : #include <cmath>
6 : : #include <cstdio> // For sscanf, sprintf
7 : : #include <algorithm> // For std::max, std::min
8 : :
9 : : // Platform-Specific Headers
10 : : #ifdef _WIN32
11 : : #include <windows.h>
12 : : #include <psapi.h>
13 : : #include <dinput.h>
14 : : #include <iomanip> // For std::hex
15 : : #include <string>
16 : : #endif
17 : :
18 : : // Constants
19 : : namespace {
20 : : constexpr uint32_t DIAGNOSTIC_LOG_INTERVAL_MS = 1000; // Rate limit diagnostic logging to 1 second
21 : : constexpr uint32_t RECOVERY_COOLDOWN_MS = 2000; // Wait 2 seconds between recovery attempts
22 : : }
23 : :
24 : : // Keep existing implementations
25 : 5150 : DirectInputFFB& DirectInputFFB::Get() {
26 [ + + + - : 5150 : static DirectInputFFB instance;
+ - - - ]
27 : 5150 : return instance;
28 : : }
29 : :
30 [ + - ]: 3 : DirectInputFFB::DirectInputFFB() {}
31 : :
32 : : // NEW: Helper to get foreground window title for diagnostics - REMOVED for Security/Privacy
33 : 1 : std::string DirectInputFFB::GetActiveWindowTitle() {
34 [ + - ]: 2 : return "Window Tracking Disabled";
35 : : }
36 : :
37 : : // NEW: Helper Implementations for GUID
38 : 3 : std::string DirectInputFFB::GuidToString(const GUID& guid) {
39 : : char buf[64];
40 : : #ifdef _WIN32
41 : : sprintf_s(buf, "{%08lX-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX}",
42 : : guid.Data1, guid.Data2, guid.Data3,
43 : : guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
44 : : guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
45 : : #else
46 : 3 : snprintf(buf, sizeof(buf), "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}",
47 : 3 : (unsigned int)guid.Data1, (unsigned int)guid.Data2, (unsigned int)guid.Data3,
48 : 3 : (unsigned int)guid.Data4[0], (unsigned int)guid.Data4[1], (unsigned int)guid.Data4[2], (unsigned int)guid.Data4[3],
49 : 3 : (unsigned int)guid.Data4[4], (unsigned int)guid.Data4[5], (unsigned int)guid.Data4[6], (unsigned int)guid.Data4[7]);
50 : : #endif
51 [ + - ]: 6 : return std::string(buf);
52 : : }
53 : :
54 : 11 : GUID DirectInputFFB::StringToGuid(const std::string& str) {
55 : 11 : GUID guid = { 0 };
56 [ + + ]: 11 : if (str.empty()) return guid;
57 : : unsigned long p0;
58 : : unsigned short p1, p2;
59 : : unsigned int p3, p4, p5, p6, p7, p8, p9, p10;
60 : : #ifdef _WIN32
61 : : int n = sscanf_s(str.c_str(), "{%08lX-%04hX-%04hX-%02X%02X-%02X%02X%02X%02X%02X%02X}",
62 : : &p0, &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10);
63 : : #else
64 : 8 : int n = sscanf(str.c_str(), "{%08lX-%04hX-%04hX-%02X%02X-%02X%02X%02X%02X%02X%02X}",
65 : : &p0, &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10);
66 : : #endif
67 [ + + ]: 8 : if (n == 11) {
68 : 3 : guid.Data1 = p0;
69 : 3 : guid.Data2 = (unsigned short)p1;
70 : 3 : guid.Data3 = (unsigned short)p2;
71 : 3 : guid.Data4[0] = (unsigned char)p3; guid.Data4[1] = (unsigned char)p4;
72 : 3 : guid.Data4[2] = (unsigned char)p5; guid.Data4[3] = (unsigned char)p6;
73 : 3 : guid.Data4[4] = (unsigned char)p7; guid.Data4[5] = (unsigned char)p8;
74 : 3 : guid.Data4[6] = (unsigned char)p9; guid.Data4[7] = (unsigned char)p10;
75 : : }
76 : 8 : return guid;
77 : : }
78 : :
79 : :
80 : :
81 : : #ifdef _WIN32
82 : : /**
83 : : * @brief Returns the description for a DirectInput return code.
84 : : *
85 : : * Parsed from: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ee416869(v=vs.85)#constants
86 : : *
87 : : * @param hr The HRESULT returned by a DirectInput method.
88 : : * @return const char* The description of the error or status code.
89 : : */
90 : : const char* GetDirectInputErrorString(HRESULT hr) {
91 : : // NOTE: Using a series of if-statements instead of a switch/case to avoid
92 : : // narrowing conversion errors (-Wnarrowing) in GCC/MinGW, which occurs when
93 : : // comparing HRESULT (signed long) with large unsigned hex constants.
94 : : // Success Codes
95 : : if (hr == S_OK) return "The operation completed successfully (S_OK).";
96 : : if (hr == S_FALSE) return "Operation technically succeeded but had no effect or hit a warning (S_FALSE). The device buffer overflowed and some input was lost. This value is equal to DI_BUFFEROVERFLOW, DI_NOEFFECT, DI_NOTATTACHED, DI_PROPNOEFFECT.";
97 : : if (hr == DI_DOWNLOADSKIPPED) return "The parameters of the effect were successfully updated, but the effect could not be downloaded because the associated device was not acquired in exclusive mode.";
98 : : if (hr == DI_EFFECTRESTARTED) return "The effect was stopped, the parameters were updated, and the effect was restarted.";
99 : : if (hr == DI_POLLEDDEVICE) return "The device is a polled device.. As a result, device buffering does not collect any data and event notifications is not signaled until the IDirectInputDevice8 Interface method is called.";
100 : : if (hr == DI_SETTINGSNOTSAVED) return "The action map was applied to the device, but the settings could not be saved.";
101 : : if (hr == DI_TRUNCATED) return "The parameters of the effect were successfully updated, but some of them were beyond the capabilities of the device and were truncated to the nearest supported value.";
102 : : if (hr == DI_TRUNCATEDANDRESTARTED) return "Equal to DI_EFFECTRESTARTED | DI_TRUNCATED.";
103 : : if (hr == DI_WRITEPROTECT) return "A SUCCESS code indicating that settings cannot be modified.";
104 : :
105 : : // Error Codes
106 : : if (hr == DIERR_ACQUIRED) return "The operation cannot be performed while the device is acquired.";
107 : : if (hr == DIERR_ALREADYINITIALIZED) return "This object is already initialized.";
108 : : if (hr == DIERR_BADDRIVERVER) return "The object could not be created due to an incompatible driver version or mismatched or incomplete driver components.";
109 : : if (hr == DIERR_BETADIRECTINPUTVERSION) return "The application was written for an unsupported prerelease version of DirectInput.";
110 : : if (hr == DIERR_DEVICEFULL) return "The device is full.";
111 : : if (hr == DIERR_DEVICENOTREG) return "The device or device instance is not registered with DirectInput.";
112 : : if (hr == DIERR_EFFECTPLAYING) return "The parameters were updated in memory but were not downloaded to the device because the device does not support updating an effect while it is still playing.";
113 : : if (hr == DIERR_GENERIC) return "An undetermined error occurred inside the DirectInput subsystem.";
114 : : if (hr == DIERR_HANDLEEXISTS) return "Access denied or handle already exists. Another application may have exclusive access.";
115 : : if (hr == DIERR_HASEFFECTS) return "The device cannot be reinitialized because effects are attached to it.";
116 : : if (hr == DIERR_INCOMPLETEEFFECT) return "The effect could not be downloaded because essential information is missing. For example, no axes have been associated with the effect, or no type-specific information has been supplied.";
117 : : if (hr == DIERR_INPUTLOST) return "Access to the input device has been lost. It must be reacquired.";
118 : : if (hr == DIERR_INVALIDPARAM) return "An invalid parameter was passed to the returning function, or the object was not in a state that permitted the function to be called.";
119 : : if (hr == DIERR_MAPFILEFAIL) return "An error has occurred either reading the vendor-supplied action-mapping file for the device or reading or writing the user configuration mapping file for the device.";
120 : : if (hr == DIERR_MOREDATA) return "Not all the requested information fit into the buffer.";
121 : : if (hr == DIERR_NOAGGREGATION) return "This object does not support aggregation.";
122 : : if (hr == DIERR_NOINTERFACE) return "The object does not support the specified interface.";
123 : : if (hr == DIERR_NOTACQUIRED) return "The operation cannot be performed unless the device is acquired.";
124 : : if (hr == DIERR_NOTBUFFERED) return "The device is not buffered. Set the DIPROP_BUFFERSIZE property to enable buffering.";
125 : : if (hr == DIERR_NOTDOWNLOADED) return "The effect is not downloaded.";
126 : : if (hr == DIERR_NOTEXCLUSIVEACQUIRED) return "The operation cannot be performed unless the device is acquired in DISCL_EXCLUSIVE mode.";
127 : : if (hr == DIERR_NOTFOUND) return "The requested object does not exist (DIERR_NOTFOUND).";
128 : : if (hr == DIERR_OLDDIRECTINPUTVERSION) return "The application requires a newer version of DirectInput.";
129 : : if (hr == DIERR_OUTOFMEMORY) return "The DirectInput subsystem could not allocate sufficient memory to complete the call.";
130 : : if (hr == DIERR_REPORTFULL) return "More information was requested to be sent than can be sent to the device.";
131 : : if (hr == DIERR_UNPLUGGED) return "The operation could not be completed because the device is not plugged in.";
132 : : if (hr == DIERR_UNSUPPORTED) return "The function called is not supported at this time.";
133 : : if (hr == E_HANDLE) return "The HWND parameter is not a valid top-level window that belongs to the process.";
134 : : if (hr == E_PENDING) return "Data is not yet available.";
135 : : if (hr == E_POINTER) return "An invalid pointer, usually NULL, was passed as a parameter.";
136 : :
137 : : return "Unknown DirectInput Error";
138 : : }
139 : : #endif
140 : :
141 : 1 : DirectInputFFB::~DirectInputFFB() {
142 : : try {
143 [ + - ]: 1 : Shutdown();
144 : 0 : } catch (...) {
145 : : // Ignore errors during shutdown in destructor to avoid std::terminate
146 : : (void)0;
147 : 0 : }
148 : 1 : }
149 : :
150 : 6 : bool DirectInputFFB::Initialize(HWND hwnd) {
151 : 6 : m_hwnd = hwnd;
152 : : #ifdef _WIN32
153 : : if (FAILED(DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_pDI, NULL))) {
154 : : std::cerr << "[DI] Failed to create DirectInput8 interface." << std::endl;
155 : : return false;
156 : : }
157 : : std::cout << "[DI] Initialized." << std::endl;
158 : : return true;
159 : : #else
160 : 6 : std::cout << "[DI] Mock Initialized (Non-Windows)." << std::endl;
161 : 6 : return true;
162 : : #endif
163 : : }
164 : :
165 : 7 : void DirectInputFFB::Shutdown() {
166 : 7 : ReleaseDevice(); // Reuse logic
167 : : #ifdef _WIN32
168 : : if (m_pDI) {
169 : : ((IDirectInput8*)m_pDI)->Release();
170 : : m_pDI = nullptr;
171 : : }
172 : : #endif
173 : 7 : }
174 : :
175 : : #ifdef _WIN32
176 : : BOOL CALLBACK EnumJoysticksCallback(const DIDEVICEINSTANCE* pdidInstance, VOID* pContext) {
177 : : auto* devices = (std::vector<DeviceInfo>*)pContext;
178 : : DeviceInfo info;
179 : : info.guid = pdidInstance->guidInstance;
180 : : char name[260];
181 : : WideCharToMultiByte(CP_ACP, 0, pdidInstance->tszProductName, -1, name, 260, NULL, NULL);
182 : : info.name = std::string(name);
183 : : devices->push_back(info);
184 : : return DIENUM_CONTINUE;
185 : : }
186 : : #endif
187 : :
188 : 2 : std::vector<DeviceInfo> DirectInputFFB::EnumerateDevices() {
189 : 2 : std::vector<DeviceInfo> devices;
190 : : #ifdef _WIN32
191 : : if (!m_pDI) return devices;
192 : : ((IDirectInput8*)m_pDI)->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback, &devices, DIEDFL_ATTACHEDONLY | DIEDFL_FORCEFEEDBACK);
193 : : #else
194 [ + - ]: 2 : DeviceInfo d1; d1.name = "Simucube 2 Pro (Mock)";
195 [ + - ]: 2 : DeviceInfo d2; d2.name = "Logitech G29 (Mock)";
196 [ + - ]: 2 : devices.push_back(d1);
197 [ + - ]: 2 : devices.push_back(d2);
198 : : #endif
199 : 4 : return devices;
200 : 2 : }
201 : :
202 : 8 : void DirectInputFFB::ReleaseDevice() {
203 : : #ifdef _WIN32
204 : : if (m_pEffect) {
205 : : ((IDirectInputEffect*)m_pEffect)->Stop();
206 : : ((IDirectInputEffect*)m_pEffect)->Unload();
207 : : ((IDirectInputEffect*)m_pEffect)->Release();
208 : : m_pEffect = nullptr;
209 : : }
210 : : if (m_pDevice) {
211 : : ((IDirectInputDevice8*)m_pDevice)->Unacquire();
212 : : ((IDirectInputDevice8*)m_pDevice)->Release();
213 : : m_pDevice = nullptr;
214 : : }
215 : : #endif
216 : 8 : m_active = false;
217 : 8 : m_isExclusive = false;
218 : 8 : m_deviceName = "None";
219 : : #ifdef _WIN32
220 : : std::cout << "[DI] Device released by user." << std::endl;
221 : : #endif
222 : 8 : }
223 : :
224 : 2 : bool DirectInputFFB::SelectDevice(const GUID& guid) {
225 : : #ifdef _WIN32
226 : : if (!m_pDI) return false;
227 : :
228 : : // Cleanup old using new method
229 : : ReleaseDevice();
230 : :
231 : : std::cout << "[DI] Attempting to create device..." << std::endl;
232 : : if (FAILED(((IDirectInput8*)m_pDI)->CreateDevice(guid, (IDirectInputDevice8**)&m_pDevice, NULL))) {
233 : : std::cerr << "[DI] Failed to create device." << std::endl;
234 : : return false;
235 : : }
236 : :
237 : : std::cout << "[DI] Setting Data Format..." << std::endl;
238 : : if (FAILED(((IDirectInputDevice8*)m_pDevice)->SetDataFormat(&c_dfDIJoystick))) {
239 : : std::cerr << "[DI] Failed to set data format." << std::endl;
240 : : return false;
241 : : }
242 : :
243 : : // Reset state
244 : : m_isExclusive = false;
245 : :
246 : : // Attempt 1: Exclusive/Background (Best for FFB)
247 : : std::cout << "[DI] Attempting to set Cooperative Level (Exclusive | Background)..." << std::endl;
248 : : HRESULT hr = ((IDirectInputDevice8*)m_pDevice)->SetCooperativeLevel(m_hwnd, DISCL_EXCLUSIVE | DISCL_BACKGROUND);
249 : :
250 : : if (SUCCEEDED(hr)) {
251 : : m_isExclusive = true;
252 : : std::cout << "[DI] Cooperative Level set to EXCLUSIVE." << std::endl;
253 : : } else {
254 : : // Fallback: Non-Exclusive
255 : : std::cerr << "[DI] Exclusive mode failed (Error: " << std::hex << hr << std::dec << "). Retrying in Non-Exclusive mode..." << std::endl;
256 : : hr = ((IDirectInputDevice8*)m_pDevice)->SetCooperativeLevel(m_hwnd, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND);
257 : :
258 : : if (SUCCEEDED(hr)) {
259 : : m_isExclusive = false;
260 : : std::cout << "[DI] Cooperative Level set to NON-EXCLUSIVE." << std::endl;
261 : : }
262 : : }
263 : :
264 : : if (FAILED(hr)) {
265 : : std::cerr << "[DI] Failed to set cooperative level (Non-Exclusive failed too)." << std::endl;
266 : : return false;
267 : : }
268 : :
269 : : std::cout << "[DI] Acquiring device..." << std::endl;
270 : : if (FAILED(((IDirectInputDevice8*)m_pDevice)->Acquire())) {
271 : : std::cerr << "[DI] Failed to acquire device." << std::endl;
272 : : // Don't return false yet, might just need focus/retry
273 : : } else {
274 : : std::cout << "[DI] Device Acquired in " << (m_isExclusive ? "EXCLUSIVE" : "NON-EXCLUSIVE") << " mode." << std::endl;
275 : : }
276 : :
277 : : // Create Effect
278 : : if (CreateEffect()) {
279 : : m_active = true;
280 : : std::cout << "[DI] SUCCESS: Physical Device fully initialized and FFB Effect created." << std::endl;
281 : :
282 : : return true;
283 : : }
284 : : return false;
285 : : #else
286 : 2 : m_active = true;
287 : 2 : m_isExclusive = true; // Default to true in mock to verify UI logic
288 : 2 : m_deviceName = "Mock Device Selected";
289 : 2 : return true;
290 : : #endif
291 : : }
292 : :
293 : 0 : bool DirectInputFFB::CreateEffect() {
294 : : #ifdef _WIN32
295 : : if (!m_pDevice) return false;
296 : :
297 : : DWORD rgdwAxes[1] = { DIJOFS_X };
298 : : LONG rglDirection[1] = { 0 };
299 : : DICONSTANTFORCE cf;
300 : : cf.lMagnitude = 0;
301 : :
302 : : DIEFFECT eff;
303 : : ZeroMemory(&eff, sizeof(eff));
304 : : eff.dwSize = sizeof(DIEFFECT);
305 : : eff.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS;
306 : : eff.dwDuration = INFINITE;
307 : : eff.dwSamplePeriod = 0;
308 : : eff.dwGain = DI_FFNOMINALMAX;
309 : : eff.dwTriggerButton = DIEB_NOTRIGGER;
310 : : eff.dwTriggerRepeatInterval = 0;
311 : : eff.cAxes = 1;
312 : : eff.rgdwAxes = rgdwAxes;
313 : : eff.rglDirection = rglDirection;
314 : : eff.lpEnvelope = NULL;
315 : : eff.cbTypeSpecificParams = sizeof(DICONSTANTFORCE);
316 : : eff.lpvTypeSpecificParams = &cf;
317 : : eff.dwStartDelay = 0;
318 : :
319 : : if (FAILED(((IDirectInputDevice8*)m_pDevice)->CreateEffect(GUID_ConstantForce, &eff, (IDirectInputEffect**)&m_pEffect, NULL))) {
320 : : std::cerr << "[DI] Failed to create Constant Force effect." << std::endl;
321 : : return false;
322 : : }
323 : :
324 : : // Start immediately
325 : : ((IDirectInputEffect*)m_pEffect)->Start(1, 0);
326 : : return true;
327 : : #else
328 : 0 : return true;
329 : : #endif
330 : : }
331 : :
332 : 4767 : bool DirectInputFFB::UpdateForce(double normalizedForce) {
333 [ + + ]: 4767 : if (!m_active) return false;
334 : :
335 : : // Sanity Check: If 0.0, stop effect to prevent residual hum
336 [ + + ]: 6 : if (std::abs(normalizedForce) < 0.00001) normalizedForce = 0.0;
337 : :
338 : : // Clamp
339 : 6 : normalizedForce = (std::max)(-1.0, (std::min)(1.0, normalizedForce));
340 : :
341 : : // Scale to -10000..10000
342 : 6 : long magnitude = static_cast<long>(normalizedForce * 10000.0);
343 : :
344 : : // DirectInput Overhead Optimization: Don't call driver if value hasn't changed.
345 : : // This check prevents the expensive USB call (only talk to the driver when the force actually changes).
346 [ + + ]: 6 : if (magnitude == m_last_force) return false;
347 : 4 : m_last_force = magnitude;
348 : :
349 : : #ifdef _WIN32
350 : : if (m_pEffect) {
351 : : DICONSTANTFORCE cf;
352 : : cf.lMagnitude = magnitude;
353 : :
354 : : DIEFFECT eff;
355 : : ZeroMemory(&eff, sizeof(eff));
356 : : eff.dwSize = sizeof(DIEFFECT);
357 : : eff.cbTypeSpecificParams = sizeof(DICONSTANTFORCE);
358 : : eff.lpvTypeSpecificParams = &cf;
359 : :
360 : : // Try to update parameters
361 : : HRESULT hr = ((IDirectInputEffect*)m_pEffect)->SetParameters(&eff, DIEP_TYPESPECIFICPARAMS);
362 : :
363 : : // --- DIAGNOSTIC & RECOVERY LOGIC ---
364 : : if (FAILED(hr)) {
365 : : // 1. Identify the Error
366 : : std::string errorType = GetDirectInputErrorString(hr);
367 : :
368 : : // Append Custom Advice for Priority/Exclusive Errors
369 : : if (hr == DIERR_OTHERAPPHASPRIO || hr == DIERR_NOTEXCLUSIVEACQUIRED ) {
370 : : errorType += " [CRITICAL: Game has stolen priority! DISABLE IN-GAME FFB]";
371 : :
372 : : // Update exclusivity state to reflect reality
373 : : m_isExclusive = false;
374 : : }
375 : :
376 : : // FIX: Default to TRUE. If update failed, we must try to reconnect.
377 : : bool recoverable = true;
378 : :
379 : : // 2. Log the Context (Rate limited)
380 : : static uint32_t lastLogTime = 0;
381 : : if (GetTickCount() - lastLogTime > DIAGNOSTIC_LOG_INTERVAL_MS) {
382 : : std::cerr << "[DI ERROR] Failed to update force. Error: " << errorType
383 : : << " (0x" << std::hex << hr << std::dec << ")" << std::endl;
384 : : std::cerr << " Active Window: [" << GetActiveWindowTitle() << "]" << std::endl;
385 : : lastLogTime = GetTickCount();
386 : : }
387 : :
388 : : // 3. Attempt Recovery (with Smart Cool-down)
389 : : if (recoverable) {
390 : : // Throttle recovery attempts to prevent CPU spam when device is locked
391 : : static uint32_t lastRecoveryAttempt = 0;
392 : : uint32_t now = GetTickCount();
393 : :
394 : : // Only attempt recovery if cooldown period has elapsed
395 : : if (now - lastRecoveryAttempt > RECOVERY_COOLDOWN_MS) {
396 : : lastRecoveryAttempt = now; // Mark this attempt
397 : :
398 : : // --- DYNAMIC PROMOTION FIX ---
399 : : // If we are stuck in "Shared Mode" (0x80040205), standard Acquire()
400 : : // just re-confirms Shared Mode. We must force a mode switch.
401 : : if (hr == DIERR_NOTEXCLUSIVEACQUIRED) {
402 : : std::cout << "[DI] Attempting to promote to Exclusive Mode..." << std::endl;
403 : : ((IDirectInputDevice8*)m_pDevice)->Unacquire();
404 : : ((IDirectInputDevice8*)m_pDevice)->SetCooperativeLevel(m_hwnd, DISCL_EXCLUSIVE | DISCL_BACKGROUND);
405 : : }
406 : : // -----------------------------
407 : :
408 : : HRESULT hrAcq = ((IDirectInputDevice8*)m_pDevice)->Acquire();
409 : :
410 : : if (SUCCEEDED(hrAcq)) {
411 : : // Log recovery success (rate-limited for diagnostics)
412 : : static uint32_t lastSuccessLog = 0;
413 : : if (GetTickCount() - lastSuccessLog > 5000) { // 5 second cooldown
414 : : std::cout << "[DI RECOVERY] Device re-acquired successfully. FFB motor restarted." << std::endl;
415 : : lastSuccessLog = GetTickCount();
416 : : }
417 : :
418 : : // Update our internal state if we fixed the exclusivity
419 : : if (hr == DIERR_NOTEXCLUSIVEACQUIRED) {
420 : : m_isExclusive = true;
421 : :
422 : : // One-time notification when Dynamic Promotion first succeeds
423 : : static bool firstPromotionSuccess = false;
424 : : if (!firstPromotionSuccess) {
425 : : std::cout << "\n"
426 : : << "========================================\n"
427 : : << "[SUCCESS] Dynamic Promotion Active!\n"
428 : : << "lmuFFB has successfully recovered exclusive\n"
429 : : << "control after detecting a conflict.\n"
430 : : << "This feature will continue to protect your\n"
431 : : << "FFB experience automatically.\n"
432 : : << "========================================\n" << std::endl;
433 : : firstPromotionSuccess = true;
434 : : }
435 : : }
436 : :
437 : : // Restart the effect to ensure motor is active
438 : : ((IDirectInputEffect*)m_pEffect)->Start(1, 0);
439 : :
440 : : // Retry the update immediately
441 : : ((IDirectInputEffect*)m_pEffect)->SetParameters(&eff, DIEP_TYPESPECIFICPARAMS);
442 : : }
443 : : }
444 : : }
445 : : }
446 : : }
447 : : #endif
448 : 4 : return true;
449 : : }
|