win_callbacks.h
#pragma once
#if PLATFORM_WINDOWS
#define JOYSTICK_DEVICE_ENABLED 1
#define STRICT
#define DIRECTINPUT_VERSION 0x0800
#define _CRT_SECURE_NO_DEPRECATE
#ifndef _WIN32_DCOM
#define _WIN32_DCOM
#endif
#include "AllowWindowsPlatformTypes.h"
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <commctrl.h>
#include <basetsd.h>
#pragma warning(push)
#pragma warning(disable:6000 28251)
#include <dinput.h>
#pragma warning(pop)
#include <dinputd.h>
#include <assert.h>
#include <oleauto.h>
#include <shellapi.h>
#include <wbemidl.h>
#include "JoystickDevice.h"
#include "JoystickUtils.h"
#include "FJoystickDevice.h"
namespace {
struct XINPUT_DEVICE_NODE {
DWORD dwVidPid;
XINPUT_DEVICE_NODE* pNext;
};
struct FJoystickManager {
XINPUT_DEVICE_NODE* pXInputDeviceList = nullptr;
LPDIRECTINPUT8 pDI = nullptr;
LPDIRECTINPUTJOYCONFIG8 pJoyConfig = nullptr;
DIJOYCONFIG PreferredJoyCfg;
bool bFilterOutXinputDevices = false;
struct {
DIJOYCONFIG* pPreferredJoyCfg;
bool bPreferredJoyCfgValid;
} enumContext;
};
struct FJoystickHandler {
FJoystickManager *Manager;
FJoystickDeviceInfo &DeviceInfo;
FORCEINLINE FJoystickHandler(FJoystickManager *InManager, FJoystickDeviceInfo &InDeviceInfo) :
Manager(InManager),
DeviceInfo(InDeviceInfo)
{}
};
/**
* Enum each PNP device using WMI and check each device ID to see if it contains "IG_" (ex. "VID_045E&PID_028E&IG_00").
* If it does, then it's an XInput device. Unfortunately this information can not be found by just using DirectInput.
* Checking against a VID/PID of 0x028E/0x045E won't find 3rd party or future XInput devices.
*
* This function stores the list of xinput devices in a linked list at pXInputDeviceList, and IsXInputDevice() searchs that linked list.
*/
FORCEINLINE HRESULT SetupForIsXInputDevice(FJoystickManager *Manager) {
IWbemServices* pIWbemServices = nullptr;
IEnumWbemClassObject* pEnumDevices = nullptr;
IWbemLocator* pIWbemLocator = nullptr;
IWbemClassObject* pDevices[20] = { 0 };
BSTR bstrDeviceID = nullptr;
BSTR bstrClassName = nullptr;
BSTR bstrNamespace = nullptr;
DWORD uReturned = 0;
bool bCleanupCOM = false;
UINT iDevice = 0;
VARIANT var;
HRESULT hr;
// CoInit if needed
bCleanupCOM = SUCCEEDED(CoInitialize(nullptr));
// Create WMI
if (FAILED(hr = CoCreateInstance(__uuidof(WbemLocator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IWbemLocator), (LPVOID*)&pIWbemLocator)) || !pIWbemLocator) {
JD_CERR("Failed to create COM instance!");
goto LCleanup;
}
// Create BSTRs for WMI
bstrNamespace = SysAllocString(L"\\\\.\\root\\cimv2"); if (!bstrNamespace) goto LCleanup;
bstrDeviceID = SysAllocString(L"DeviceID"); if (!bstrDeviceID) goto LCleanup;
bstrClassName = SysAllocString(L"Win32_PNPEntity"); if (!bstrClassName) goto LCleanup;
// Connect to WMI
if (FAILED(hr = pIWbemLocator->ConnectServer(bstrNamespace, nullptr, nullptr, 0L, 0L, nullptr, nullptr, &pIWbemServices)) || !pIWbemServices) {
JD_CERR("Failed to connect WMI server!");
goto LCleanup;
}
// Switch security level to IMPERSONATE
CoSetProxyBlanket(pIWbemServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, 0);
// Get list of Win32_PNPEntity devices
if (FAILED(hr = pIWbemServices->CreateInstanceEnum(bstrClassName, 0, nullptr, &pEnumDevices)) || !pEnumDevices) {
JD_CERR("Failed to get the list of Win32_PNPEntity devices!");
goto LCleanup;
}
// Loop over all devices
for (;;) {
// Get 20 at a time
if (FAILED(hr = pEnumDevices->Next(10000, 20, pDevices, &uReturned))) {
JD_CERR("Failed to get devices!");
goto LCleanup;
}
if (uReturned == 0)
break;
for (iDevice = 0; iDevice < uReturned; ++iDevice) {
if (!pDevices[iDevice])
continue;
// For each device, get its device ID
if (SUCCEEDED(pDevices[iDevice]->Get(bstrDeviceID, 0L, &var, nullptr, nullptr)) && var.vt == VT_BSTR && var.bstrVal) {
// Check if the device ID contains "IG_". If it does, then it's an XInput device
// Unfortunately this information can not be found by just using DirectInput
if (wcsstr(var.bstrVal, L"IG_")) {
// If it does, then get the VID/PID from var.bstrVal
DWORD dwPid = 0, dwVid = 0;
WCHAR* strVid = wcsstr(var.bstrVal, L"VID_");
if (strVid && swscanf(strVid, L"VID_%4X", &dwVid) != 1)
dwVid = 0;
WCHAR* strPid = wcsstr(var.bstrVal, L"PID_");
if (strPid && swscanf(strPid, L"PID_%4X", &dwPid) != 1)
dwPid = 0;
DWORD dwVidPid = MAKELONG(dwVid, dwPid);
// Add the VID/PID to a linked list
XINPUT_DEVICE_NODE* pNewNode = new XINPUT_DEVICE_NODE;
if (pNewNode) {
pNewNode->dwVidPid = dwVidPid;
pNewNode->pNext = Manager->pXInputDeviceList;
Manager->pXInputDeviceList = pNewNode;
}
}
}
SAFE_RELEASE(pDevices[iDevice]);
}
}
LCleanup:
if (bstrNamespace)
SysFreeString(bstrNamespace);
if (bstrDeviceID)
SysFreeString(bstrDeviceID);
if (bstrClassName)
SysFreeString(bstrClassName);
for (iDevice = 0; iDevice < 20; iDevice++)
SAFE_RELEASE(pDevices[iDevice]);
SAFE_RELEASE(pEnumDevices);
SAFE_RELEASE(pIWbemLocator);
SAFE_RELEASE(pIWbemServices);
return hr;
}
/**
* Returns true if the DirectInput device is also an XInput device.
* Call SetupForIsXInputDevice() before, and CleanupForIsXInputDevice() after
*/
FORCEINLINE bool IsXInputDevice(const GUID* pGuidProductFromDirectInput, FJoystickManager *Manager) {
// Check each xinput device to see if this device's vid/pid matches
XINPUT_DEVICE_NODE* pNode = Manager->pXInputDeviceList;
while (pNode) {
if (pNode->dwVidPid == pGuidProductFromDirectInput->Data1)
return true;
pNode = pNode->pNext;
}
return false;
}
/**
* Cleanup needed for IsXInputDevice()
*/
FORCEINLINE void CleanupForIsXInputDevice(FJoystickManager *Manager) {
// Cleanup linked list
XINPUT_DEVICE_NODE* pNode = Manager->pXInputDeviceList;
while (pNode) {
XINPUT_DEVICE_NODE* pDelete = pNode;
pNode = pNode->pNext;
delete pDelete;
}
}
/**
* Called once for each enumerated joystick.
* If we find one, create a device interface on it so we can play with it.
*/
static BOOL CALLBACK EnumJoysticksCallback(const DIDEVICEINSTANCE* pdidInstance, VOID* pContext) {
FJoystickHandler *Handler = reinterpret_cast<FJoystickHandler*>(pContext);
auto Manager = Handler->Manager;
auto &pJoystick = reinterpret_cast<LPDIRECTINPUTDEVICE8&>(Handler->DeviceInfo.DevicePtr);
if (Manager->bFilterOutXinputDevices && IsXInputDevice(&pdidInstance->guidProduct, Manager)) {
JD_COUT("This joystick is not a XInputDevice.");
return DIENUM_CONTINUE;
}
// Skip anything other than the perferred joystick device as defined by the control panel.
// Instead you could store all the enumerated joysticks and let the user pick.
if (Manager->enumContext.bPreferredJoyCfgValid && !IsEqualGUID(pdidInstance->guidInstance, Manager->enumContext.pPreferredJoyCfg->guidInstance)) {
JD_COUT("This joystick doesn\'t match the requiered configuration.");
return DIENUM_CONTINUE;
}
// Obtain an interface to the enumerated joystick.
// 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(Manager->pDI->CreateDevice(pdidInstance->guidInstance, &pJoystick, nullptr))) {
JD_COUT("Failed to create this joystick device.");
return DIENUM_CONTINUE;
}
// Stop enumeration. Note: we're just taking the first joystick we get. You could store all the enumerated joysticks and let the user pick.
JD_COUT("End of the joystick enumeration");
return DIENUM_STOP;
}
/**
* Callback function for enumerating objects (axes, buttons, POVs) on a joystick.
* This function enables user interface elements for objects that are found to exist, and scales axes min/max values.
*/
static BOOL CALLBACK EnumObjectsCallback(const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext) {
FJoystickHandler *Handler = reinterpret_cast<FJoystickHandler*>(pContext);
auto &DeviceInfo = Handler->DeviceInfo;
auto &pJoystick = reinterpret_cast<LPDIRECTINPUTDEVICE8&>(Handler->DeviceInfo.DevicePtr);
// For axes that are returned, set the DIPROP_RANGE property for the
// enumerated axis in order to scale min/max values.
if (pdidoi->dwType & DIDFT_AXIS) {
DIPROPRANGE diprg;
diprg.diph.dwSize = sizeof(DIPROPRANGE);
diprg.diph.dwHeaderSize = sizeof(DIPROPHEADER);
diprg.diph.dwHow = DIPH_BYID;
diprg.diph.dwObj = pdidoi->dwType; // Specify the enumerated axis
// Get the range for the axis
HRESULT hr = pJoystick->GetProperty(DIPROP_RANGE, &diprg.diph);
switch (hr) {
case DIERR_INVALIDPARAM:
JD_CERR("Failed to set joystick property: Invalid parameter!");
return DIENUM_STOP;
case DIERR_NOTINITIALIZED:
JD_CERR("Failed to set joystick property: Not initialized!");
return DIENUM_STOP;
case DIERR_OBJECTNOTFOUND:
JD_CERR("Failed to set joystick property: Object not found!");
return DIENUM_STOP;
case DIERR_UNSUPPORTED:
JD_CERR("Failed to set joystick property: Unknown error code (%#x)!", hr);
return DIENUM_STOP;
}
DeviceInfo.AxisInfo.MinValue = diprg.lMin;
DeviceInfo.AxisInfo.MaxValue = diprg.lMax;
}
return DIENUM_CONTINUE;
}
}
FORCEINLINE IOJoystickManagerPtr IOJoystickDevice::CreateManager() {
return new FJoystickManager();
}
FORCEINLINE void IOJoystickDevice::ReleaseManager(IOJoystickManagerPtr JoystickManager) {
FJoystickManager* Manager = reinterpret_cast<FJoystickManager*>(JoystickManager);
SAFE_RELEASE(Manager->pJoyConfig);
SAFE_RELEASE(Manager->pDI);
delete Manager;
}
FORCEINLINE bool IOJoystickDevice::OpenManager(IOJoystickManagerPtr JoystickManager) {
FJoystickManager* Manager = reinterpret_cast<FJoystickManager*>(JoystickManager);
// Register with the DirectInput subsystem and get a pointer to a IDirectInput interface we can use.
// Create a DInput object
if (FAILED(DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&Manager->pDI, nullptr))) {
JD_CERR("Failed to create DInput object!");
return false;
}
if (Manager->bFilterOutXinputDevices)
SetupForIsXInputDevice(Manager);
FMemory::Memzero(&Manager->PreferredJoyCfg, sizeof(Manager->PreferredJoyCfg));
Manager->enumContext.pPreferredJoyCfg = &Manager->PreferredJoyCfg;
Manager->enumContext.bPreferredJoyCfgValid = false;
if (FAILED(Manager->pDI->QueryInterface(IID_IDirectInputJoyConfig8, (void**)&Manager->pJoyConfig))) {
JD_CERR("Failed to query joystick interface!");
return false;
}
Manager->PreferredJoyCfg.dwSize = sizeof(Manager->PreferredJoyCfg);
return true;
}
FORCEINLINE bool IOJoystickDevice::OpenDevice(IOJoystickManagerPtr JoystickManager, FJoystickDeviceInfo &DeviceInfo) {
FJoystickManager* Manager = reinterpret_cast<FJoystickManager*>(JoystickManager);
auto &pJoystick = reinterpret_cast<LPDIRECTINPUTDEVICE8&>(DeviceInfo.DevicePtr);
if (SUCCEEDED(Manager->pJoyConfig->GetConfig(0, &Manager->PreferredJoyCfg, DIJC_GUIDINSTANCE))) // This function is expected to fail if no joystick is attached
Manager->enumContext.bPreferredJoyCfgValid = true;
// Look for a simple joystick we can use
FJoystickHandler joystickHandler = { Manager, DeviceInfo };
if (FAILED(Manager->pDI->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback, &joystickHandler, DIEDFL_ATTACHEDONLY))) {
JD_CERR("Failed to enumerate devices!");
return false;
}
if (Manager->bFilterOutXinputDevices)
CleanupForIsXInputDevice(Manager);
// Make sure we got a joystick
if (!pJoystick) {
JD_CERR("Failed to get a joystick!");
return false;
}
// Set the data format to "simple joystick" - a predefined data format
//
// A data format specifies which controls on a device we are interested in,
// and how they should be reported. This tells DInput that we will be
// passing a DIJOYSTATE structure to IDirectInputDevice::GetDeviceState().
if (FAILED(pJoystick->SetDataFormat(&c_dfDIJoystick))) {
JD_CERR("Failed to initialize joystick!");
return false;
}
// Enumerate the joystick objects. The callback function enabled user
// interface elements for objects that are found, and sets the min/max
// values property for discovered axes.
if (FAILED(pJoystick->EnumObjects(EnumObjectsCallback, &joystickHandler, DIDFT_ALL))) {
JD_CERR("Failed to enumarate joystick objects!");
return false;
}
DeviceInfo.Buttons.SetNumZeroed(MAX_NUM_CONTROLLER_BUTTONS);
return true;
}
FORCEINLINE void IOJoystickDevice::CloseDevice(IOJoystickManagerPtr JoystickManager, FJoystickDeviceInfo &DeviceInfo) {
auto &pJoystick = reinterpret_cast<LPDIRECTINPUTDEVICE8&>(DeviceInfo.DevicePtr);
// Unacquire the device one last time just in case
// the app tried to exit while the device is still acquired.
if (pJoystick)
pJoystick->Unacquire();
// Release any DirectInput objects.
SAFE_RELEASE(pJoystick);
}
FORCEINLINE bool IOJoystickDevice::GetDeviceState(IOJoystickManagerPtr JoystickManager, FJoystickDeviceInfo &DeviceInfo) {
FJoystickManager* Manager = reinterpret_cast<FJoystickManager*>(JoystickManager);
auto &pJoystick = reinterpret_cast<LPDIRECTINPUTDEVICE8&>(DeviceInfo.DevicePtr);
DIJOYSTATE js; // DInput joystick state
// Poll the device to read the current state
if (FAILED(pJoystick->Poll())) {
// DInput is telling us that the input stream has been
// interrupted. We aren't tracking any state between polls, so
// we don't have any special reset that needs to be done. We
// just re-acquire and try again.
HRESULT hr;
do {
hr = pJoystick->Acquire();
} while (hr == DIERR_INPUTLOST);
switch (hr) {
case DI_OK:
// hr may be DIERR_OTHERAPPHASPRIO or other errors. This
// may occur when the app is minimized or in the process of
// switching, so just try again later
return false;
case DIERR_INVALIDPARAM:
JD_CERR("Failed to poll the joystick: Invalid parameter!");
return false;
case DIERR_NOTINITIALIZED:
JD_CERR("Failed to poll the joystick: Not initialized!");
return false;
case DIERR_OTHERAPPHASPRIO:
JD_CERR("Failed to poll the joystick: Already aquiered by another app with a higher priority!");
return false;
default:
JD_CERR("Failed to poll the joystick: Unknown error code (%#x)!", hr);
return false;
}
}
// Get the input's device state
if (FAILED(pJoystick->GetDeviceState(sizeof(DIJOYSTATE), &js))) {
JD_CERR("Failed to get joystick device state!");
return false; // The device should have been acquired during the Poll()
}
// Update parameters
DeviceInfo.AxisInfo.xValue = js.lX;
DeviceInfo.AxisInfo.yValue = js.lY;
for (int32 i = 0; i < MAX_NUM_CONTROLLER_BUTTONS; ++i)
DeviceInfo.Buttons*.Value = js.rgbButtons*;
return true;
}
#include "HideWindowsPlatformTypes.h"
#endif