Last active
August 21, 2020 13:32
-
-
Save beardedfoo/fcdba5b6e69e88b52c6d7f78b393ca33 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* The Microsoft SideWinder Force Feedback 2 joystick is a force feedback capable joystick which | |
* sometimes can have issues holding center in non-force feedback enabled games. This is an application | |
* which implements a centering workaround through the use of the DirectInput force feedback API. | |
* | |
* TL;DR: Run this application in the background while playing non-force feedback games to ensure the | |
* SideWinder FFB2 joystick stays centered and maintains proper tension. | |
*/ | |
#include <iostream> | |
// Include the DirectInput API | |
#include <dinput.h> | |
// These DirectInput libraries enable communication with FFB joysticks so are added here as linker inputs | |
#pragma comment(lib, "dinput8.lib") | |
#pragma comment(lib, "dxguid.lib") | |
// The DirectInput API is accessed through this global. It is null until | |
// the API has been initialized | |
LPDIRECTINPUT8 directInputAPI; | |
// joystick is a global object for making DirectInput API calls to the joystick. | |
// It is null until the joystick has been found with directInputAPI->EnumDevices() | |
// and intialized with directInputAPI->CreateDevice() | |
LPDIRECTINPUTDEVICE8 joystick; | |
// joystickSearchHandler is called by directInputAPI->EnumDevices() for each joystick | |
// detected in the system. This implementation searches for the MS SW FFB2 joystick | |
// and sets up the joystick variable above | |
BOOL CALLBACK | |
joystickSearchHandler(const DIDEVICEINSTANCE* discoveredDevice, VOID* context) | |
{ | |
// Store results from windows SDK function calls here | |
HRESULT hr; | |
// Announce on the console which joysticks which are found | |
std::wcout << "Found joystick: " << discoveredDevice->tszProductName << "\n"; | |
// Ignore all joysticks which are not the Microsoft SideWinder 2 FFB joystick | |
if (std::wstring(discoveredDevice->tszProductName) != L"SideWinder Force Feedback 2 Joystick") | |
{ | |
// Returning this value signals directInputAPI->EnumDevices() to continue the | |
// search for devices | |
return DIENUM_CONTINUE; | |
} | |
std::cout << "Acquiring joystick...\n"; | |
// Setup the joystick global to allow DirectInput API calls against this joystick | |
hr = directInputAPI->CreateDevice(discoveredDevice->guidInstance, &joystick, NULL); | |
// If it failed, then we can't use this joystick. (Maybe the user unplugged | |
// it while we were in the middle of enumerating it.) | |
if (FAILED(hr)) { | |
// Returning this value signals directInputAPI->EnumDevices() to continue the | |
// search for devices | |
return DIENUM_CONTINUE; | |
} | |
// SetDataFormat() is a necessary step for the following DirectInput API calls against the joystick. This | |
// seems to internally inform the DirectInput API to treat the joystick as a joystick. c_dfDIJoystick2 | |
// referenced below is a global default object within DirectInput which specifies a joystick data format | |
hr = joystick->SetDataFormat(&c_dfDIJoystick2); | |
if (FAILED(hr)) { | |
std::cout << "Failed to set joystick data format!\n"; | |
return hr; | |
} | |
std::cout << "set joystick data format\n"; | |
// Returning this value signals directInputAPI->EnumDevices() to halt the | |
// search for devices and not to call this function again | |
return DIENUM_STOP; | |
} | |
int main() | |
{ | |
// Store the result of windows SDK operations here | |
HRESULT hr; | |
// Create a DirectInput device and initialize the directInputAPI variable | |
if (FAILED(hr = DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, | |
IID_IDirectInput8, (VOID**)&directInputAPI, NULL))) { | |
return hr; | |
} | |
// Look for the first Microsoft Sidewinder Force Feedback 2 joystick we can find | |
// and setup the joystick variable referencing it. DIEDFL_ATTACHEDONLY is passed | |
// so that disconnected joysticks are ignored | |
std::cout << "Searching for SideWinder Force Feedback 2 joystick...\n"; | |
if (FAILED(hr = directInputAPI->EnumDevices(DI8DEVCLASS_GAMECTRL, joystickSearchHandler, | |
NULL, DIEDFL_ATTACHEDONLY))) { | |
return hr; | |
} | |
// The joystickSearchHandler function sets up the joystick variable when a suitable joystick has been found. | |
// If the joystick variable is still null then no suitable joystick was found. | |
if (joystick == NULL) { | |
std::cout << "Joystick not found.\n"; | |
return E_FAIL; | |
} | |
// Disable joystick auto-center. Ironically this feature should auto-center the joystick, but doesn't seem | |
// to work in every case for the Microsoft Sidewinder FFB2 joystick, hence the development of this program. | |
// Disabling the default auto-center behavior ensures there is no contention for the positioning of the joystick | |
DIPROPDWORD DIPropAutoCenter; | |
DIPropAutoCenter.diph.dwSize = sizeof(DIPropAutoCenter); | |
DIPropAutoCenter.diph.dwHeaderSize = sizeof(DIPROPHEADER); | |
DIPropAutoCenter.diph.dwObj = 0; | |
DIPropAutoCenter.diph.dwHow = DIPH_DEVICE; | |
DIPropAutoCenter.dwData = DIPROPAUTOCENTER_OFF; | |
hr = joystick->SetProperty(DIPROP_AUTOCENTER, &DIPropAutoCenter.diph); | |
if (FAILED(hr)) | |
{ | |
std::cout << "failed to enable auto-center: 0x" << std::hex << hr << "\n"; | |
return hr; | |
} | |
std::cout << "auto-center disabled\n"; | |
// DirectInput requires a window to send force feedback effects, but this isn't a GUI application. This call sets up a hidden | |
// message-only window which can be used to enable the DirectInput API force feedback functionality. | |
// See https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows | |
HWND messageOnlyWindow = CreateWindowEx(0, L"Message", NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); | |
if (messageOnlyWindow == 0) { | |
std::cout << "failed to create message-only window\n"; | |
return 1; | |
} | |
std::cout << "message-only window created\n"; | |
// Calling joystick->SetCooperativeLevel() to enable DISCL_EXCLUSIVE mode is required before using force feedback features | |
// of DirectInput. DISCL_BACKGROUND mode is enabled as well since this is a background application which can handle re-acquisition | |
// of the joystick | |
hr = joystick->SetCooperativeLevel(messageOnlyWindow, DISCL_EXCLUSIVE | DISCL_BACKGROUND); | |
if (FAILED(hr)) | |
{ | |
std::cout << "failed to set cooperative level: 0x" << std::hex << hr << "\n"; | |
return hr; | |
} | |
std::cout << "set exclusive access mode\n"; | |
// The force feedback effect we will enable to center the joystick is a 2D spring effect, which | |
// uses the motors of the joystick to hold the joystick in a given 2D position. As enough force | |
// is applied to overcome the force the joystick is then allowed to move from the specified position | |
DIEFFECT springEffectParams; | |
springEffectParams.dwSize = sizeof(DIEFFECT); | |
// DIEFF_CARTESIAN indicates that the direction should be interpreted as a cartesian coordinate | |
// DIEFF_OBJECTOFFSETS is required for the axes and trigger button values to be correctly interpreted | |
springEffectParams.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; | |
// Continously apply the spring effect while this application is running | |
springEffectParams.dwDuration = INFINITE; | |
// Disable the effect sample period feature, which we do not need, by setting a value of 0 | |
springEffectParams.dwSamplePeriod = 0; | |
// Disable the effect start delay feature, which we do not need, by setting a value of 0 | |
springEffectParams.dwStartDelay = 0; | |
// Enable maximum gain, which applies maximum force on the joystick. If a reduction of force | |
// feature were desired this would be the place to do it | |
springEffectParams.dwGain = DI_FFNOMINALMAX; | |
// Effects can optionally be applied as a reaction to the user pressing a button, but we want | |
// a constant force so we disable this feature by setting DIEB_NOTRIGGER and a repeat interval | |
// of 0 | |
springEffectParams.dwTriggerButton = DIEB_NOTRIGGER; | |
springEffectParams.dwTriggerRepeatInterval = 0; | |
// No envelope is used, which means the force is not smoothed when the effect is applied or removed | |
// from the joystick. This may cause rough movements on the joystick as the application is started, | |
// but should be okay | |
springEffectParams.lpEnvelope = nullptr; | |
// The effect is 2-dimensional, so set the axes count to 2 | |
const int axesCount = 2; | |
springEffectParams.cAxes = axesCount; | |
// Apply the effect to the X and Y axes of the joystick | |
DWORD effectAxes[axesCount] = { DIJOFS_X, DIJOFS_Y }; | |
springEffectParams.rgdwAxes = effectAxes; | |
// The effect is applied in direction 1 on the X-axis and direction 0 on the Y-axis. | |
// These values are interpreted as cartesian coordinates, but mostly just seem to work | |
// for centering the joystick, so I've set them to these values. I'm not really sure | |
// exactly how this is interpreted by DirectInput | |
LONG effectDirection[axesCount] = { 1, 0 }; | |
springEffectParams.rglDirection = effectDirection; | |
// A 2-dimensional array of spring conditions is associated with the effect, which fine-tune the spring behavior | |
DICONDITION springCondition[axesCount]; | |
springEffectParams.cbTypeSpecificParams = axesCount * sizeof(DICONDITION); | |
springEffectParams.lpvTypeSpecificParams = &springCondition; | |
// The spring conditions for the X and Y axis must now be setup, but first a detailed explanation of what these | |
// conditions mean: | |
/* | |
* From page 277 of http://read.pudn.com/downloads771/doc/3059838/Delphi_Graphics_and_Game_Programming_Exposed_with_DirectX_7.0.pdf | |
* | |
* Conditions | |
* | |
As we’ve discussed, conditions are force feedback effects that respond to the position or motion of an axis. | |
Condition effects are divided into four subtypes: spring, friction, damper, and inertia, all of which produce | |
similar but subtly different tactile feedback sensations. | |
All condition effects use a TDICondition structure for their type-specific parameters. The TDICondition | |
structure is defined as: | |
TDICondition = packed record | |
lOffset : longint; // effect offset | |
lPositiveCoefficient : longint; // positive offset coefficient | |
lNegativeCoefficient : longint; // negative offset coefficient | |
dwPositiveSaturation : DWORD; // positive offset max force output | |
dwNegativeSaturation : DWORD; // negative offset max force output | |
lDeadBand : longint; // inactive region | |
end; | |
Each condition subtype makes use of specific members of this structure, as we’ll examine below. Unlike other | |
effects, conditions can accept an array of TDICondition structures in the lpvTypeSpecificParams member of the | |
TDIEffect structure. Each TDICondition structure defines condition parameters for each axis, with each | |
TDICondition structure in the array matching up to the axis specified in the rgdwAxes member at the same | |
index. When using an array of TDICondition structures in this manner, the cbTypeSpecificParams member of | |
the TDIEffect structure must be set to SizeOf(TDICondition) multiplied by the number of members in the array. | |
We’ll see an example of this later in the chapter. | |
Spring A spring condition causes the device to exert a force on an axis when it is moved away from a central | |
location. This central location is defined by the lOffset member. A value of 0 puts this point at the center of the | |
axis. The lOffset member can be set to values ranging from 10,000 to 10,000 (or you can use the | |
DI_FFNOMINALMAX constant again), with negative values to the left and positive values to the right, relative | |
to the axis. | |
The lPositiveCoefficient indicates the strength of the force exerted on the axis when it is moved to the farthest | |
position from the defined central location. This force is increased from 0 to lPositiveCoefficient as the axis is | |
moved, giving a sensation of increased resistance as the axis is moved farther and farther away from the neutral | |
point. Again, the lPositiveCoefficient can be set to values in the range 10,000 to 10,000, with positive values | |
pushing the axis toward the central location and negative values pulling the axis away from the central location. | |
If the driver supports negative coefficients, the lNegativeCoefficient member can be set to values within this | |
range, which can be used to indicate a different amount of force to be applied when the axis is moved to the | |
negative side of the central location. If negative coefficients are not supported, the positive coefficient is used. | |
The lDeadBand member defines an area around the central location in which the axis can move before any | |
amount of force is applied. The saturation values define a similar area at the farthest distance of the axis, with | |
the negative saturation value indicating an area on the negative side of the central location. As with coefficients, | |
if negative saturation values are not supported, the positive saturation value is used. dwPositiveSaturation, | |
dwNegativeSatura- tion, and lDeadBand members can be set in the range of 0 to 10,000, indicating a percentage | |
of the range of travel in hundredths of a percent. | |
*/ | |
// Enable maximum tension on the X-axis | |
springCondition[0].dwNegativeSaturation = DI_FFNOMINALMAX; | |
springCondition[0].dwPositiveSaturation = DI_FFNOMINALMAX; | |
springCondition[0].lNegativeCoefficient = DI_FFNOMINALMAX; | |
springCondition[0].lPositiveCoefficient = DI_FFNOMINALMAX; | |
// Enable maximum tension on the Y-axis | |
springCondition[1].dwNegativeSaturation = DI_FFNOMINALMAX; | |
springCondition[1].dwPositiveSaturation = DI_FFNOMINALMAX; | |
springCondition[1].lNegativeCoefficient = DI_FFNOMINALMAX; | |
springCondition[1].lPositiveCoefficient = DI_FFNOMINALMAX; | |
// Disable the deadzone on the X and Y axis | |
springCondition[0].lDeadBand = 0; | |
springCondition[1].lDeadBand = 0; | |
// Do not offset the spring force on the X or Y axis. If a recalibration | |
// of the center point of the joystick needed to be added, I think this | |
// would be the place | |
springCondition[0].lOffset = 0; | |
springCondition[1].lOffset = 0; | |
// Use springEffectParams to initialize the springEffect variable with joystick->CreateEffect() | |
// which will create a new effect object and write it to the springEffect variable | |
// http://doc.51windows.net/Directx9_SDK/input/tuts/tut4/step4creatingeffect.htm | |
LPDIRECTINPUTEFFECT springEffect; | |
hr = joystick->CreateEffect(GUID_Spring, &springEffectParams, &springEffect, NULL); | |
if (FAILED(hr)) | |
{ | |
// Just a helpful extra troubleshooting message for a common problem | |
if (hr == DIERR_INVALIDPARAM) | |
{ | |
std::cout << "DIERR_INVALIDPARAM\n"; | |
} | |
std::cout << "CreateEffect failed: 0x" << std::hex << hr << "\n"; | |
return hr; | |
} | |
std::cout << "effect created...\n"; | |
// Loop forever applying the effect. The effect is already specified as infinite, but this is necessary | |
// anyways for some reason. After 10-15 seconds the joystick stops applying the effect unless it is continously | |
// resent to the joystick. The effect is applied once per second below, which is likely more than needed | |
while (true) | |
{ | |
// Poll the joystick for the latest information on button presses etc., which we will not use. This | |
// step is required to maintain a connection with the joystick, and even then sometimes the joystick must | |
// be re-acquired when this call fails. See below for the re-acquire handling code | |
hr = joystick->Poll(); | |
// If the Poll fails then the joystick must be re-acquired | |
if (FAILED(hr)) | |
{ | |
// Sometimes the joystick must be acquired with multiple attempts | |
// such as when DIERR_INPUTLOST is returned | |
std::cout << "reacquiring joystick...\n"; | |
hr = joystick->Acquire(); | |
while (hr == DIERR_INPUTLOST) | |
{ | |
hr = joystick->Acquire(); | |
} | |
// Ensure the joystick acquisition was finally successful | |
if (FAILED(hr)) | |
{ | |
std::cout << "failed to acquire joystick: 0x" << std::hex << hr << "\n"; | |
return hr; | |
} | |
} | |
// Start the centering effect on the joystick | |
hr = springEffect->Start(1, 0); | |
if (hr != DI_OK) | |
{ | |
std::cout << "failed to start effect: 0x" << std::hex << hr << "\n"; | |
return hr; | |
} | |
std::cout << "applying centering effect...\n"; | |
// Wait between applications of the spring effect | |
Sleep(1000); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment