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