Skip to content

Instantly share code, notes, and snippets.

@beardedfoo
Last active August 21, 2020 13:32
Show Gist options
  • Save beardedfoo/fcdba5b6e69e88b52c6d7f78b393ca33 to your computer and use it in GitHub Desktop.
Save beardedfoo/fcdba5b6e69e88b52c6d7f78b393ca33 to your computer and use it in GitHub Desktop.
/*
* 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