diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 20ba884a9..ef5bf40d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -60,6 +60,7 @@ if( WIN32 ) wsock32 winmm dinput8 + Xinput ole32 user32 gdi32 @@ -1094,6 +1095,7 @@ set (PCH_SOURCES common/engine/renderstyle.cpp common/engine/v_colortables.cpp common/engine/serializer.cpp + common/engine/m_haptics.cpp common/engine/m_joy.cpp common/engine/m_random.cpp common/objects/autosegs.cpp diff --git a/src/common/engine/m_haptics.cpp b/src/common/engine/m_haptics.cpp new file mode 100644 index 000000000..8db6c99e1 --- /dev/null +++ b/src/common/engine/m_haptics.cpp @@ -0,0 +1,381 @@ +/* +** m_haptics.cpp +** +** Haptic feedback implementation +** +**--------------------------------------------------------------------------- +** +** Copyright 2025 Marcus Minhorst +** Copyright 2025 ZDoom + GZDoom teams, and contributors +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see http://www.gnu.org/licenses/ +** +**--------------------------------------------------------------------------- +** +*/ + +// HEADER FILES ------------------------------------------------------------ + +#include + +#include "c_cvars.h" +#include "doomstat.h" +#include "m_haptics.h" +#include "name.h" +#include "printf.h" +#include "s_soundinternal.h" +#include "tarray.h" +#include "zstring.h" + +// MACROS ------------------------------------------------------------------ + +#ifndef MAX_TRY_DEPTH +#define MAX_TRY_DEPTH 8 +#endif + +// TYPES ------------------------------------------------------------------- + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +extern bool AppActive; + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +struct { + int tic = gametic; // last tic processed + bool dirty = false; // do we need to do something next tick ? + bool enabled = true; // do we need to do anything ever ? + bool active = true; // is the game currently not paused ? + struct Haptics current = {0,0,0,0,0}; // current state of the controller + TMap channels; // active rumbles (that will be mixed) +} Haptics; + +TMap RumbleDefinition = {}; +TMap RumbleMapping = {}; +TMap RumbleAlias = {}; + +// fallback names. these exist in base sndinfo +const FName HapticIntense = "INTENSE", + HapticHeavy = "HEAVY", + HapticMedium = "MEDIUM", + HapticLight = "LIGHT", + HapticSubtle = "SUBTLE"; + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +CVARD(Bool, haptics_debug, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG, "print diagnostics for haptic feedback"); + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// Joy_GetMapping +// +// Takes a sound name, and find its corresponding rumble identifier. +// +//========================================================================== + +const FName * Joy_GetMapping(const FName identifier) +{ + const FName * mapping = RumbleMapping.CheckKey(identifier); + + if (!mapping && identifier != "") + { + Printf(DMSG_WARNING, "Unknown rumble mapping '%s'\n", identifier.GetChars()); + } + + return mapping; +} + +//========================================================================== +// +// Joy_GetRumble +// +// Takes a rumble identifier, and returns its haptics definition. +// +//========================================================================== + +const struct Haptics * Joy_GetRumble(FName identifier) +{ + struct Haptics * rumble = RumbleDefinition.CheckKey(identifier); + + if (!rumble) { + Printf(DMSG_ERROR, TEXTCOLOR_RED "Rumble mapping not found! '%s'\n", identifier.GetChars()); + return nullptr; + } + + return rumble; +} + +//========================================================================== +// +// Joy_AddRumbleType +// +// Ties a rumble identifier to a haptics definition. +// +//========================================================================== + +void Joy_AddRumbleType(const FName identifier, const struct Haptics data) +{ + RumbleDefinition.Insert(identifier, data); +} + +//========================================================================== +// +// Joy_AddRumbleAlias +// +// Adds an alternative rumble identifier for a haptics definition. +// Joy_ReadyRumbleMapping must be called after adding any aliases. +// +//========================================================================== + +void Joy_AddRumbleAlias(const FName alias, const FName actual) +{ + RumbleAlias.Insert(alias, actual); +} + +//========================================================================== +// +// Joy_MapRumbleType +// +// Ties a sound to a rumble identifier. +// +//========================================================================== + +void Joy_MapRumbleType(const FName sound, const FName identifier) +{ + RumbleMapping.Insert(sound, identifier); +} + +//========================================================================== +// +// Joy_ResetRumbleMapping +// +// Resets entire rumble state +// +//========================================================================== + +void Joy_ResetRumbleMapping() +{ + RumbleMapping.Clear(); + RumbleAlias.Clear(); + RumbleDefinition.Clear(); + Haptics.channels.Clear(); + Haptics.current.high_frequency = Haptics.current.low_frequency = + Haptics.current.left_trigger = Haptics.current.right_trigger = + Haptics.current.ticks = 0; + I_Rumble(0, 0, 0, 0); +} + +//========================================================================== +// +// Joy_ReadyRumbleMapping +// +// Resets entire rumble state +// +//========================================================================== + +void Joy_ReadyRumbleMapping() +{ + TArray found; + TMapIterator it(RumbleAlias); + TMap::Pair* pair; + + while (RumbleAlias.CountUsed()) + { + while (it.NextPair(pair)) + { + auto predefined = RumbleDefinition.CheckKey(pair->Key); + if (predefined) + { + Printf(DMSG_ERROR, TEXTCOLOR_RED "Rumble alias trying to redefine mapping! '%s'\n", pair->Key.GetChars()); + continue; + } + + auto mapping = RumbleDefinition.CheckKey(pair->Value); + if (!mapping) continue; + + found.AddUnique(pair->Key); + RumbleDefinition.Insert(pair->Key, *mapping); + } + it.Reset(); + + if (found.Size() == 0) + { + FString list = "["; + while (it.NextPair(pair)) + list.AppendFormat(" '%s'->'%s'", pair->Key.GetChars(), pair->Value.GetChars()); + Printf(DMSG_ERROR, TEXTCOLOR_RED "Circular rumble alias found! (%d) %s ]\n", RumbleAlias.CountUsed(), list.GetChars()); + break; + } + + while (found.size() > 0) { + RumbleAlias.Remove(found.Last()); + found.Pop(); + } + } +} + +//========================================================================== +// +// Joy_RumbleTick +// +// Called once per gametic. Manages rumble state. +// +//========================================================================== + +void Joy_RumbleTick() { + if (!Haptics.enabled) return; + + // pause detection + if (AppActive != Haptics.active) + { + Haptics.active = AppActive; + + if (Haptics.active && Haptics.current.ticks != 0) + { + I_Rumble( + Haptics.current.high_frequency, + Haptics.current.low_frequency, + Haptics.current.left_trigger, + Haptics.current.right_trigger + ); + } + else + { + I_Rumble(0, 0, 0, 0); + } + } + + if (Haptics.tic >= gametic) return; + + Haptics.tic = gametic; + + // new value OR time elapsed + Haptics.dirty |= Haptics.current.ticks != 0 && Haptics.current.ticks < Haptics.tic; + + if (!Haptics.dirty) return; + + // init + Haptics.current.ticks = 0; + Haptics.current.high_frequency = 0; + Haptics.current.low_frequency = 0; + Haptics.current.left_trigger = 0; + Haptics.current.right_trigger = 0; + + TMapIterator it(Haptics.channels); + TMap::Pair* pair; + TArray stale; + + // remove old rumble data + while (it.NextPair(pair)) + { + if (pair->Value.ticks < Haptics.tic) + { + stale.Push(pair->Key); + } + } + for (auto key: stale) + { + Haptics.channels.Remove(key); + } + + it.Reset(); + while (it.NextPair(pair)) + { + // grab upcoming event time + Haptics.current.ticks = Haptics.current.ticks == 0 + ? pair->Value.ticks + : std::min(Haptics.current.ticks, pair->Value.ticks); + + // simple intensity mixing + Haptics.current.high_frequency += pair->Value.high_frequency; + Haptics.current.low_frequency += pair->Value.low_frequency; + Haptics.current.left_trigger += pair->Value.left_trigger; + Haptics.current.right_trigger += pair->Value.right_trigger; + } + + // should this be clamped to [0,1]? Maybe a controller api will support "hdr" haptics lol + // Haptics.current.high_frequency = std::min(std::max(0.0, Haptics.current.high_frequency), 1.0); + // Haptics.current.low_frequency = std::min(std::max(0.0, Haptics.current.low_frequency), 1.0); + // Haptics.current.left_trigger = std::min(std::max(0.0, Haptics.current.left_trigger), 1.0); + // Haptics.current.right_trigger = std::min(std::max(0.0, Haptics.current.right_trigger), 1.0); + + I_Rumble( + Haptics.current.high_frequency, + Haptics.current.low_frequency, + Haptics.current.left_trigger, + Haptics.current.right_trigger + ); + + Haptics.dirty = false; +} + +//========================================================================== +// +// Joy_Rumble +// +// Requests haptic feedback. Attenuation of >=1 results in no-op. +// +//========================================================================== + +void Joy_Rumble(const FName source, const struct Haptics data, double attenuation) +{ + if (!Haptics.enabled) return; + if (data.ticks <= 0) return; + if (attenuation >= 1) return; + + float strength = 1 - (attenuation < 0? 0: attenuation); + + if (haptics_debug) + Printf("r %s * %g\n", source.GetChars(), strength); + + // this will overwrite stuff from same source mapping (weapons/pistol not W_BULLET) + Haptics.channels.Insert(source, { + Haptics.tic + data.ticks + 1, + data.high_frequency * strength, + data.low_frequency * strength, + data.left_trigger * strength, + data.right_trigger * strength, + }); + + Haptics.dirty = true; +} + +//========================================================================== +// +// Joy_Rumble +// +// Requests haptic feedback. Attenuation of >=1 results in no-op. +// +//========================================================================== + +void Joy_Rumble(const FName identifier, double attenuation) +{ + const FName * mapping = Joy_GetMapping(identifier); + + if (mapping == nullptr) return; + + const struct Haptics * rumble = Joy_GetRumble(*mapping); + + if (rumble == nullptr) return; + + Joy_Rumble(identifier, * rumble, attenuation); +} diff --git a/src/common/engine/m_haptics.h b/src/common/engine/m_haptics.h new file mode 100644 index 000000000..cb1e6294c --- /dev/null +++ b/src/common/engine/m_haptics.h @@ -0,0 +1,34 @@ +#ifndef M_HAPTICS_H +#define M_HAPTICS_H + +#include "name.h" + +enum EHapticCompat { + HAPTCOMPAT_WARN, + HAPTCOMPAT_NONE, + HAPTCOMPAT_MATCH, + HAPTCOMPAT_ALL, + + NUM_HAPTCOMPAT +}; + +struct Haptics { + int ticks; + double high_frequency; + double low_frequency; + double left_trigger; + double right_trigger; +}; + +void I_Rumble(double high_freq, double low_freq, double left_trig, double right_trig); + +void Joy_AddRumbleType(const FName idenifier, const struct Haptics data); +void Joy_AddRumbleAlias(const FName alias, const FName actual); +void Joy_MapRumbleType(const FName sound, const FName idenifier); +void Joy_ResetRumbleMapping(); +void Joy_ReadyRumbleMapping(); +void Joy_RumbleTick(); +void Joy_Rumble(const FName source, const struct Haptics data, double attenuation = 0); +void Joy_Rumble(const FName identifier, double attenuation = 0); + +#endif diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp index bbd51e4e9..b85001ef6 100644 --- a/src/common/engine/m_joy.cpp +++ b/src/common/engine/m_joy.cpp @@ -36,7 +36,6 @@ // HEADER FILES ------------------------------------------------------------ -#include "m_joy.h" #include #include "c_cvars.h" @@ -45,6 +44,7 @@ #include "configfile.h" #include "d_eventbase.h" #include "i_interface.h" +#include "m_joy.h" #include "name.h" #include "printf.h" #include "vectors.h" @@ -70,6 +70,8 @@ extern const float JOYDEADZONE_DEFAULT = 0.1; // reduced from 0.25 extern const float JOYSENSITIVITY_DEFAULT = 1.0; +extern const float JOYHAPSTRENGTH_DEFAULT = 1.0; + extern const float JOYTHRESH_DEFAULT = 0.05; extern const float JOYTHRESH_TRIGGER = 0.05; extern const float JOYTHRESH_STICK_X = 0.65; diff --git a/src/common/engine/m_joy.h b/src/common/engine/m_joy.h index f8996e945..a17c87fde 100644 --- a/src/common/engine/m_joy.h +++ b/src/common/engine/m_joy.h @@ -29,6 +29,8 @@ extern const float JOYDEADZONE_DEFAULT; extern const float JOYSENSITIVITY_DEFAULT; +extern const float JOYHAPSTRENGTH_DEFAULT; + extern const float JOYTHRESH_DEFAULT; extern const float JOYTHRESH_TRIGGER; diff --git a/src/common/platform/posix/cocoa/i_joystick.cpp b/src/common/platform/posix/cocoa/i_joystick.cpp index 4e348fb8c..34d60d158 100644 --- a/src/common/platform/posix/cocoa/i_joystick.cpp +++ b/src/common/platform/posix/cocoa/i_joystick.cpp @@ -1,35 +1,36 @@ /* - ** i_joystick.cpp - ** - **--------------------------------------------------------------------------- - ** Copyright 2012-2015 Alexey Lysiuk - ** All rights reserved. - ** - ** Redistribution and use in source and binary forms, with or without - ** modification, are permitted provided that the following conditions - ** are met: - ** - ** 1. Redistributions of source code must retain the above copyright - ** notice, this list of conditions and the following disclaimer. - ** 2. Redistributions in binary form must reproduce the above copyright - ** notice, this list of conditions and the following disclaimer in the - ** documentation and/or other materials provided with the distribution. - ** 3. The name of the author may not be used to endorse or promote products - ** derived from this software without specific prior written permission. - ** - ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **--------------------------------------------------------------------------- - ** - */ +** i_joystick.cpp +** +**--------------------------------------------------------------------------- +** Copyright 2012-2015 Alexey Lysiuk +** Copyright 2017-2025 GZDoom Maintainers and Contributors +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ #include #include @@ -46,6 +47,7 @@ EXTERN_CVAR(Bool, joy_axespolling) +EXTERN_CVAR(Bool, use_joystick) namespace @@ -1379,3 +1381,10 @@ CUSTOM_CVAR(Bool, joy_axespolling, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR s_joystickManager->UseAxesPolling(self); } } + +void I_Rumble(double high_freq, double low_freq, double left_trig, double right_trig) +{ + if (!use_joystick) return; + + // stub +} diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp index d60c806a4..159bfe393 100644 --- a/src/common/platform/posix/sdl/i_input.cpp +++ b/src/common/platform/posix/sdl/i_input.cpp @@ -46,6 +46,7 @@ #include "i_input.h" #include "i_interface.h" #include "keydef.h" +#include "m_haptics.h" #include "m_joy.h" #include "utf8.h" #include "v_video.h" @@ -609,6 +610,7 @@ void I_StartTic () I_CheckGUICapture (); I_CheckNativeMouse (); I_GetEvent (); + Joy_RumbleTick(); } void I_ProcessJoysticks (); diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index ce5ca9f9b..63b714a06 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -55,6 +55,11 @@ static const EAxisCodes ControllerAxisCodes[][2] = { AXIS_CODE_PAD_RTRIGGER, AXIS_CODE_NULL } }; +#define HAPTICS 0b0001 +#define HAPTICS_TRIGGERS 0b0010 + +EXTERN_CVAR(Bool, use_joystick) + class SDLInputJoystick: public IJoystickConfig { public: @@ -63,6 +68,8 @@ public: InstanceID(SDL_JoystickGetDeviceInstanceID(DeviceIndex)), Multiplier(JOYSENSITIVITY_DEFAULT), Enabled(true), + Haptics(0), + HapticsStrength(JOYHAPSTRENGTH_DEFAULT), SettingsChanged(false) { if (SDL_IsGameController(DeviceIndex)) @@ -77,6 +84,7 @@ public: { NumAxes = SDL_CONTROLLER_AXIS_MAX; NumHats = 0; + Haptics = SDL_GameControllerHasRumble(Mapping) | SDL_GameControllerHasRumbleTriggers(Mapping) << 1; SetDefaultConfig(); } @@ -224,6 +232,29 @@ public: return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset; } + void Rumble(float high_freq, float low_freq, float left_trig, float right_trig) + { + uint16_t duration_ms = -1; // turn on for max time (we'll turn it off later ourselves) + + if (Haptics & HAPTICS) + { + SDL_GameControllerRumble( + Mapping, + static_cast (0xffff * clamp(high_freq*HapticsStrength, 0.f, 1.f)), + static_cast (0xffff * clamp(low_freq*HapticsStrength, 0.f, 1.f)), + duration_ms); + } + + if (Haptics & HAPTICS_TRIGGERS) + { + SDL_GameControllerRumbleTriggers( + Mapping, + static_cast (0xffff * clamp(left_trig*HapticsStrength, 0.f, 1.f)), + static_cast (0xffff * clamp(right_trig*HapticsStrength, 0.f, 1.f)), + duration_ms); + } + } + void SetDefaultConfig() { if (Axes.size() == 0) @@ -528,6 +559,8 @@ protected: TArray Axes; int NumAxes; int NumHats; + int Haptics; + float HapticsStrength; bool SettingsChanged; friend class SDLInputJoystickManager; @@ -588,6 +621,16 @@ public: } } + void Rumble(float high_freq, float low_freq, float left_trig, float right_trig) + { + for(unsigned int i = 0;i < Joysticks.Size();i++) + { + if (Joysticks[i]->Enabled) { + Joysticks[i]->Rumble(high_freq, low_freq, left_trig, right_trig); + } + } + } + void ProcessInput() const { for(unsigned int i = 0;i < Joysticks.Size();++i) @@ -615,7 +658,7 @@ static SDLInputJoystickManager *JoystickManager; void I_StartupJoysticks() { #ifndef NO_SDL_JOYSTICK - if(SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) >= 0) + if(SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER) >= 0) JoystickManager = new SDLInputJoystickManager(); #endif } @@ -649,6 +692,13 @@ void I_GetAxes(float axes[NUM_AXIS_CODES]) } } +void I_Rumble(double high_freq, double low_freq, double left_trig, double right_trig) +{ + if (!use_joystick) return; + + JoystickManager->Rumble(high_freq, low_freq, left_trig, right_trig); +} + void I_ProcessJoysticks() { if (use_joystick && JoystickManager) diff --git a/src/common/platform/win32/i_input.cpp b/src/common/platform/win32/i_input.cpp index db9fe40d5..a03e8a434 100644 --- a/src/common/platform/win32/i_input.cpp +++ b/src/common/platform/win32/i_input.cpp @@ -75,6 +75,7 @@ #include "c_buttons.h" #include "cmdlib.h" #include "i_mainwindow.h" +#include "m_haptics.h" // Compensate for w32api's lack #ifndef GET_XBUTTON_WPARAM @@ -612,6 +613,7 @@ void I_StartTic () EventHandlerResultForNativeMouse = sysCallbacks.WantNativeMouse && sysCallbacks.WantNativeMouse(); I_CheckNativeMouse (false, EventHandlerResultForNativeMouse); I_GetEvent (); + Joy_RumbleTick(); } // diff --git a/src/common/platform/win32/i_xinput.cpp b/src/common/platform/win32/i_xinput.cpp index 5e45f1cfc..9bc2c2a08 100644 --- a/src/common/platform/win32/i_xinput.cpp +++ b/src/common/platform/win32/i_xinput.cpp @@ -1,8 +1,11 @@ /* +** i_xinput.cpp ** +** Handles direct input gamepads ** **--------------------------------------------------------------------------- ** Copyright 2005-2016 Randy Heit +** Copyright 2017-2025 GZDoom Maintainers and Contributors ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without @@ -33,6 +36,7 @@ // HEADER FILES ------------------------------------------------------------ +#include "m_joy.h" #define WIN32_LEAN_AND_MEAN #include #include @@ -67,6 +71,8 @@ extern bool AppActive; +EXTERN_CVAR(Bool, use_joystick) + // TYPES ------------------------------------------------------------------- typedef DWORD (WINAPI *XInputGetStateType)(DWORD index, XINPUT_STATE *state); @@ -112,6 +118,8 @@ public: bool GetEnabled(); void SetEnabled(bool enabled); + void Rumble(float low_freq, float high_freq); + bool AllowsEnabledInBackground() { return true; } bool GetEnabledInBackground() { return EnabledInBackground; } void SetEnabledInBackground(bool enabled) { EnabledInBackground = enabled; } @@ -137,6 +145,7 @@ protected: float DigitalThreshold; EJoyCurve ResponseCurvePreset; }; + XINPUT_VIBRATION Vibration; enum { AXIS_ThumbLX, @@ -157,6 +166,8 @@ protected: bool Connected; bool Enabled; bool EnabledInBackground; + bool Haptics; + float HapticStrength; void Attached(); void Detached(); @@ -178,6 +189,8 @@ public: void GetDevices(TArray &sticks); IJoystickConfig *Rescan(); + void Rumble(float low_freq, float high_freq); + protected: HMODULE XInputDLL; FXInputController *Devices[XUSER_MAX_COUNT]; @@ -405,9 +418,27 @@ void FXInputController::Attached() Axes[i].Value = 0; Axes[i].ButtonValue = 0; } + XINPUT_CAPABILITIES capabilities; + if (XInputGetCapabilities(Index, XINPUT_FLAG_GAMEPAD, &capabilities) == ERROR_SUCCESS) + { + Haptics = capabilities.Vibration.wLeftMotorSpeed != 0 || capabilities.Vibration.wRightMotorSpeed != 0; + } UpdateJoystickMenu(this); } +//========================================================================== +// +// FXInputController :: Rumble +// +//========================================================================== + +void FXInputController::Rumble(float low_freq, float high_freq) +{ + Vibration.wLeftMotorSpeed = static_cast(USHRT_MAX*clamp(high_freq*HapticStrength, 0.f, 1.f)); + Vibration.wRightMotorSpeed = static_cast(USHRT_MAX*clamp( low_freq*HapticStrength, 0.f, 1.f)); + XInputSetState(Index, &Vibration); +} + //========================================================================== // // FXInputController :: Detached @@ -476,6 +507,7 @@ void FXInputController::AddAxes(float axes[NUM_AXIS_CODES]) void FXInputController::SetDefaultConfig() { Multiplier = JOYSENSITIVITY_DEFAULT; + HapticStrength = JOYHAPSTRENGTH_DEFAULT; for (int i = 0; i < NUM_AXES; ++i) { Axes[i].DeadZone = DefaultAxes[i].DeadZone; @@ -944,6 +976,22 @@ bool FXInputManager::WndProcHook(HWND hWnd, uint32_t message, WPARAM wParam, LPA return false; } +//=========================================================================== +// +// FXInputManager :: Rumble +// +//=========================================================================== + +void FXInputManager::Rumble(float low_freq, float high_freq) { + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + if (Devices[i] && Devices[i]->IsConnected() && Devices[i]->GetEnabled()) + { + Devices[i]->Rumble(low_freq, high_freq); + } + } +} + //=========================================================================== // // FXInputManager :: Rescan @@ -989,3 +1037,12 @@ void I_StartupXInput() } } +void I_Rumble(double high_freq, double low_freq, double _left_trig, double _right_trig) { + if (!use_joystick) return; + + FXInputManager* XInputManager = & static_cast (*JoyDevices[INPUT_XInput]); + if (XInputManager != NULL) + { + XInputManager->Rumble(high_freq, low_freq); + } +}