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