diff --git a/src/win32/i_xinput.cpp b/src/win32/i_xinput.cpp new file mode 100644 index 000000000..d10f9f843 --- /dev/null +++ b/src/win32/i_xinput.cpp @@ -0,0 +1,697 @@ +// HEADER FILES ------------------------------------------------------------ + +#define WIN32_LEAN_AND_MEAN +#define _WIN32_WINNT 0x0501 +#include +#include +#include +#include + +#define USE_WINDOWS_DWORD +#include "i_input.h" +#include "i_system.h" +#include "d_event.h" +#include "d_gui.h" +#include "c_cvars.h" +#include "c_dispatch.h" +#include "doomdef.h" +#include "doomstat.h" +#include "win32iface.h" +#include "m_menu.h" +#include "templates.h" +#include "gameconfigfile.h" +#include "cmdlib.h" +#include "v_text.h" +#include "m_argv.h" + +// MACROS ------------------------------------------------------------------ + +#ifndef NO_XINPUT + +// TYPES ------------------------------------------------------------------- + +typedef DWORD (WINAPI *XInputGetStateType)(DWORD index, XINPUT_STATE *state); +typedef DWORD (WINAPI *XInputSetStateType)(DWORD index, XINPUT_STATE *state); +typedef DWORD (WINAPI *XInputGetCapabilitiesType)(DWORD index, DWORD flags, XINPUT_CAPABILITIES *caps); +typedef void (WINAPI *XInputEnableType)(BOOL enable); + +class FXInputController : public IJoystickConfig +{ +public: + FXInputController(int index); + ~FXInputController(); + + void ProcessInput(); + void AddAxes(float axes[NUM_JOYAXIS]); + FString GetIdentifier(); + void SetDefaultConfig(); + bool IsConnected() { return Connected; } + + // IJoystickConfig interface + FString GetName(); + float GetSensitivity(); + virtual void SetSensitivity(float scale); + + int GetNumAxes(); + float GetAxisDeadZone(int axis); + EJoyAxis GetAxisMap(int axis); + const char *GetAxisName(int axis); + float GetAxisScale(int axis); + + void SetAxisDeadZone(int axis, float deadzone); + void SetAxisMap(int axis, EJoyAxis gameaxis); + void SetAxisScale(int axis, float scale); + +protected: + struct AxisInfo + { + float Value; + float DeadZone; + float Multiplier; + EJoyAxis GameAxis; + BYTE ButtonValue; + }; + struct DefaultAxisConfig + { + float DeadZone; + EJoyAxis GameAxis; + float Multiplier; + }; + enum + { + AXIS_ThumbLX, + AXIS_ThumbLY, + AXIS_ThumbRX, + AXIS_ThumbRY, + AXIS_LeftTrigger, + AXIS_RightTrigger, + NUM_AXES + }; + + int Index; + float Multiplier; + AxisInfo Axes[NUM_AXES]; + static DefaultAxisConfig DefaultAxes[NUM_AXES]; + DWORD LastPacketNumber; + WORD LastButtons; + bool Connected; + + void Attached(); + void Detached(); + + static void ProcessThumbstick(int value, AxisInfo *axis, int base); + static void ProcessTrigger(int value, AxisInfo *axis, int base); +}; + +class FXInputManager : public FJoystickCollection +{ +public: + FXInputManager(); + ~FXInputManager(); + + bool GetDevice(); + void ProcessInput(); + bool WndProcHook(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result); + void AddAxes(float axes[NUM_JOYAXIS]); + void GetDevices(TArray &sticks); + +protected: + HMODULE XInputDLL; + FXInputController *Devices[XUSER_MAX_COUNT]; +}; + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +static XInputGetStateType InputGetState; +static XInputSetStateType InputSetState; +static XInputGetCapabilitiesType InputGetCapabilities; +static XInputEnableType InputEnable; + +static const char *AxisNames[] = +{ + "Left Thumb X Axis", + "Left Thumb Y Axis", + "Right Thumb X Axis", + "Right Thumb Y Axis", + "Left Trigger", + "Right Trigger" +}; + +FXInputController::DefaultAxisConfig FXInputController::DefaultAxes[NUM_AXES] = +{ + // Dead zone, game axis, multiplier + { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, 1 }, // ThumbLX + { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, 1 }, // ThumbLY + { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, 1 }, // ThumbRX + { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, 0.75 }, // ThumbRY + { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 }, // LeftTrigger + { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 } // RightTrigger +}; + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// FXInputController - Constructor +// +//========================================================================== + +FXInputController::FXInputController(int index) +{ + Index = index; + Connected = false; + SetDefaultConfig(); +} + +//========================================================================== +// +// FXInputController - Destructor +// +//========================================================================== + +FXInputController::~FXInputController() +{ +} + +//========================================================================== +// +// FXInputController :: ProcessInput +// +//========================================================================== + +void FXInputController::ProcessInput() +{ + DWORD res; + XINPUT_STATE state; + + res = InputGetState(Index, &state); + if (res == ERROR_DEVICE_NOT_CONNECTED) + { + if (Connected) + { + Detached(); + } + return; + } + if (res != ERROR_SUCCESS) + { + return; + } + if (!Connected) + { + Attached(); + } + if (state.dwPacketNumber == LastPacketNumber) + { // Nothing has changed since last time. + return; + } + + // There is a hole in the wButtons bitmask where two buttons could fit. + // As per the XInput documentation, "bits that are set but not defined ... are reserved, + // and their state is undefined," so we clear them to make sure they're not set. + // Our keymapping uses these two slots for the triggers as buttons. + state.Gamepad.wButtons &= 0xF3FF; + + // Convert axes to floating point and cancel out deadzones. + // XInput's Y axes are reversed compared to DirectInput. + ProcessThumbstick(state.Gamepad.sThumbLX, &Axes[AXIS_ThumbLX], KEY_PAD_LTHUMB_RIGHT); + ProcessThumbstick(-state.Gamepad.sThumbLY, &Axes[AXIS_ThumbLY], KEY_PAD_LTHUMB_DOWN); + ProcessThumbstick(state.Gamepad.sThumbRX, &Axes[AXIS_ThumbRX], KEY_PAD_RTHUMB_RIGHT); + ProcessThumbstick(-state.Gamepad.sThumbRY, &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_DOWN); + ProcessTrigger(state.Gamepad.bLeftTrigger, &Axes[AXIS_LeftTrigger], KEY_PAD_LTRIGGER); + ProcessTrigger(state.Gamepad.bRightTrigger, &Axes[AXIS_RightTrigger], KEY_PAD_RTRIGGER); + + // Generate events for buttons that have changed. + Joy_GenerateButtonEvents(LastButtons, state.Gamepad.wButtons, 16, KEY_PAD_DPAD_UP); + + LastPacketNumber = state.dwPacketNumber; + LastButtons = state.Gamepad.wButtons; +} + +//========================================================================== +// +// FXInputController :: ProcessThumbstick STATIC +// +// Converts one axis of a thumb stick to floating point, cancels out the +// deadzone, and generates button up/down events for that axis. +// +//========================================================================== + +void FXInputController::ProcessThumbstick(int value, AxisInfo *axis, int base) +{ + BYTE buttonstate; + double axisval; + + axisval = (value - SHRT_MIN) * 2.0 / (SHRT_MAX - SHRT_MIN) - 1.0; + axisval = Joy_RemoveDeadZone(axisval, axis->DeadZone, &buttonstate); + Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 2, base); + axis->ButtonValue = buttonstate; + axis->Value = float(axisval); +} + +//========================================================================== +// +// FXInputController :: ProcessTrigger STATIC +// +// Much like ProcessThumbstick, except triggers only go in the positive +// direction and have less precision. +// +//========================================================================== + +void FXInputController::ProcessTrigger(int value, AxisInfo *axis, int base) +{ + BYTE buttonstate; + double axisval; + + axisval = Joy_RemoveDeadZone(value / 256.0, axis->DeadZone, &buttonstate); + Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 1, base); + axis->ButtonValue = buttonstate; + axis->Value = float(axisval); +} + +//========================================================================== +// +// FXInputController :: Attached +// +// This controller was just attached. Set all buttons and axes to 0. +// +//========================================================================== + +void FXInputController::Attached() +{ + int i; + + Connected = true; + LastPacketNumber = ~0; + LastButtons = 0; + for (i = 0; i < NUM_AXES; ++i) + { + Axes[i].Value = 0; + Axes[i].ButtonValue = 0; + } + UpdateJoystickMenu(this); +} + +//========================================================================== +// +// FXInputController :: Detached +// +// This controller was just detached. Send button ups for buttons that +// were pressed the last time we got input from it. +// +//========================================================================== + +void FXInputController::Detached() +{ + int i; + + Connected = false; + for (i = 0; i < 4; ++i) + { + ProcessThumbstick(0, &Axes[i], KEY_PAD_LTHUMB_RIGHT + i*2); + } + for (i = 0; i < 2; ++i) + { + ProcessTrigger(0, &Axes[4+i], KEY_PAD_LTRIGGER + i); + } + Joy_GenerateButtonEvents(LastButtons, 0, 16, KEY_PAD_DPAD_UP); + LastButtons = 0; + UpdateJoystickMenu(NULL); +} + +//========================================================================== +// +// FXInputController :: AddAxes +// +// Add the values of each axis to the game axes. +// +//========================================================================== + +void FXInputController::AddAxes(float axes[NUM_JOYAXIS]) +{ + float mul = Multiplier; + int i; + + if (Button_Speed.bDown) + { + mul *= 0.5f; + } + + // Add to game axes. + for (i = 0; i < NUM_AXES; ++i) + { + axes[Axes[i].GameAxis] -= float(Axes[i].Value * mul * Axes[i].Multiplier); + } +} + +//========================================================================== +// +// FXInputController :: SetDefaultConfig +// +//========================================================================== + +void FXInputController::SetDefaultConfig() +{ + Multiplier = 1; + for (int i = 0; i < NUM_AXES; ++i) + { + Axes[i].DeadZone = DefaultAxes[i].DeadZone; + Axes[i].GameAxis = DefaultAxes[i].GameAxis; + Axes[i].Multiplier = DefaultAxes[i].Multiplier; + } +} + +//========================================================================== +// +// FXInputController :: GetIdentifier +// +//========================================================================== + +FString FXInputController::GetIdentifier() +{ + char id[16]; + mysnprintf(id, countof(id), "XI:%d", Index); + return id; +} + +//========================================================================== +// +// FXInputController :: GetName +// +//========================================================================== + +FString FXInputController::GetName() +{ + FString res; + res.Format("XInput Controller #%d", Index + 1); + return res; +} + +//========================================================================== +// +// FXInputController :: GetSensitivity +// +//========================================================================== + +float FXInputController::GetSensitivity() +{ + return Multiplier; +} + +//========================================================================== +// +// FXInputController :: SetSensitivity +// +//========================================================================== + +void FXInputController::SetSensitivity(float scale) +{ + Multiplier = scale; +} + +//========================================================================== +// +// FXInputController :: GetNumAxes +// +//========================================================================== + +int FXInputController::GetNumAxes() +{ + return NUM_AXES; +} + +//========================================================================== +// +// FXInputController :: GetAxisDeadZone +// +//========================================================================== + +float FXInputController::GetAxisDeadZone(int axis) +{ + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].DeadZone; + } + return 0; +} + +//========================================================================== +// +// FXInputController :: GetAxisMap +// +//========================================================================== + +EJoyAxis FXInputController::GetAxisMap(int axis) +{ + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].GameAxis; + } + return JOYAXIS_None; +} + +//========================================================================== +// +// FXInputController :: GetAxisName +// +//========================================================================== + +const char *FXInputController::GetAxisName(int axis) +{ + if (unsigned(axis) < NUM_AXES) + { + return AxisNames[axis]; + } + return "Invalid"; +} + +//========================================================================== +// +// FXInputController :: GetAxisScale +// +//========================================================================== + +float FXInputController::GetAxisScale(int axis) +{ + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].Multiplier; + } + return 0; +} + +//========================================================================== +// +// FXInputController :: SetAxisDeadZone +// +//========================================================================== + +void FXInputController::SetAxisDeadZone(int axis, float deadzone) +{ + if (unsigned(axis) < NUM_AXES) + { + Axes[axis].DeadZone = clamp(deadzone, 0.f, 1.f); + } +} + +//========================================================================== +// +// FXInputController :: SetAxisMap +// +//========================================================================== + +void FXInputController::SetAxisMap(int axis, EJoyAxis gameaxis) +{ + if (unsigned(axis) < NUM_AXES) + { + Axes[axis].GameAxis = (unsigned(gameaxis) < NUM_JOYAXIS) ? gameaxis : JOYAXIS_None; + } +} + +//========================================================================== +// +// FXInputController :: SetAxisScale +// +//========================================================================== + +void FXInputController::SetAxisScale(int axis, float scale) +{ + if (unsigned(axis) < NUM_AXES) + { + Axes[axis].Multiplier = scale; + } +} + +//========================================================================== +// +// FXInputManager - Constructor +// +//========================================================================== + +FXInputManager::FXInputManager() +{ + XInputDLL = LoadLibrary(XINPUT_DLL); + if (XInputDLL != NULL) + { + InputGetState = (XInputGetStateType)GetProcAddress(XInputDLL, "XInputGetState"); + InputSetState = (XInputSetStateType)GetProcAddress(XInputDLL, "XInputSetState"); + InputGetCapabilities = (XInputGetCapabilitiesType)GetProcAddress(XInputDLL, "XInputGetCapabilities"); + InputEnable = (XInputEnableType)GetProcAddress(XInputDLL, "XInputEnable"); + if (InputGetState == NULL || InputSetState == NULL || InputGetCapabilities == NULL || + InputEnable == NULL) + { + FreeLibrary(XInputDLL); + XInputDLL = NULL; + } + } + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + Devices[i] = (XInputDLL != NULL) ? new FXInputController(i) : NULL; + } +} + +//========================================================================== +// +// FXInputManager - Destructor +// +//========================================================================== + +FXInputManager::~FXInputManager() +{ + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + if (Devices[i] != NULL) + { + delete Devices[i]; + } + } + if (XInputDLL != NULL) + { + FreeLibrary(XInputDLL); + } +} + +//========================================================================== +// +// FXInputManager :: GetDevice +// +//========================================================================== + +bool FXInputManager::GetDevice() +{ + return (XInputDLL != NULL); +} + +//========================================================================== +// +// FXInputManager :: ProcessInput +// +// Process input for every attached device. +// +//========================================================================== + +void FXInputManager::ProcessInput() +{ + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + Devices[i]->ProcessInput(); + } +} + +//=========================================================================== +// +// FXInputManager :: AddAxes +// +// Adds the state of all attached device axes to the passed array. +// +//=========================================================================== + +void FXInputManager::AddAxes(float axes[NUM_JOYAXIS]) +{ + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + if (Devices[i]->IsConnected()) + { + Devices[i]->AddAxes(axes); + } + } +} + +//=========================================================================== +// +// FXInputManager :: GetJoysticks +// +// Adds the IJoystick interfaces for each device we created to the sticks +// array, if they are detected as connected. +// +//=========================================================================== + +void FXInputManager::GetDevices(TArray &sticks) +{ + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + if (Devices[i]->IsConnected()) + { + sticks.Push(Devices[i]); + } + } +} + +//=========================================================================== +// +// FXInputManager :: WndProcHook +// +// Enable and disable XInput as our window is (de)activated. +// +//=========================================================================== + +bool FXInputManager::WndProcHook(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result) +{ + if (message == WM_ACTIVATE) + { + if (LOWORD(wParam) == WA_INACTIVE) + { + InputEnable(FALSE); + } + else + { + InputEnable(TRUE); + } + } + return false; +} + +//=========================================================================== +// +// I_StartupXInput +// +//=========================================================================== + +void I_StartupXInput() +{ + FJoystickCollection *joys = new FXInputManager; + if (joys->GetDevice()) + { + JoyDevices[INPUT_XInput] = joys; + } +} + +#else // NO_XINPUT + +void I_StartupXInput() +{ + JoyDevices[INPUT_XInput] = NULL; +} + +#endif +