Skip to content

Commit ec2b521

Browse files
committed
ImpulseVibration: add built-in xbox series impulse trigger support
1 parent 2348a13 commit ec2b521

File tree

6 files changed

+150
-8
lines changed

6 files changed

+150
-8
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ set(outrun2006tweaks_SOURCES
104104
"src/hooks_forcefeedback.cpp"
105105
"src/hooks_framerate.cpp"
106106
"src/hooks_graphics.cpp"
107+
"src/hooks_input.cpp"
107108
"src/hooks_misc.cpp"
108109
"src/hooks_textures.cpp"
109110
"src/Proxy.def"
@@ -159,6 +160,7 @@ target_link_libraries(outrun2006tweaks PUBLIC
159160
safetyhook
160161
version.lib
161162
xinput9_1_0.lib
163+
Hid.lib
162164
)
163165

164166
target_link_options(outrun2006tweaks PUBLIC

Outrun2006Tweaks.ini

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,30 @@ FramerateUnlockExperimental = true
2323
VSync = 1
2424

2525
[Vibration]
26-
# Xbox Series controllers should be able to use impulse trigger vibration via X1nput 1.3 (later versions don't appear to work)
27-
# https://github.com/araghon007/X1nput/releases/tag/v1.3
28-
# Just extract the X1nput.ini & 32-bit XInput1_3.dll from that ZIP to game folder, and rename XInput1_3.dll to XInput9_1_0.dll
29-
3026
# Enable/disable the xbox vibration code, or customise it
3127
# 0 = disable
3228
# 1 = enable xbox vibration code
33-
# 2 = ^ with L/R motors swapped (recommended for impulse triggers)
29+
# 2 = ^ with L/R motors swapped
3430
# 3 = ^ with L/R motors merged together
3531
VibrationMode = 0
3632

33+
# XInput device to send vibration to, default should work fine in most cases, but if you don't notice any vibration you can try increasing this here
34+
VibrationControllerId = 0
35+
3736
# VibrationStrength range is 0 to 10
3837
VibrationStrength = 10
3938

40-
# XInput device to send vibration to, default should work fine in most cases, but if you don't notice any vibration you can try increasing this here
41-
VibrationControllerId = 0
39+
# Enable/disable Xbox Series impulse trigger vibration, or customise it
40+
# (VibrationMode must be enabled above for this to work)
41+
# 0 = disable
42+
# 1 = enable impulse triggers
43+
# 2 = ^ with L/R motors swapped (recommended for impulse triggers)
44+
# 3 = ^ with L/R motors merged together
45+
ImpulseVibrationMode = 0
46+
47+
# Multipliers of the trigger vibration, the normal controller motor vibration is multiplied by these to set trigger value
48+
ImpulseVibrationLeftMultiplier = 0.25
49+
ImpulseVibrationRightMultiplier = 0.25
4250

4351
[Window]
4452
# Forces windowed mode to become borderless. (requires "DX/WINDOWED = 1" inside outrun2006.ini)

cmake.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ link-libraries = [
5252
"spdlog",
5353
"safetyhook",
5454
"version.lib",
55-
"xinput9_1_0.lib"
55+
"xinput9_1_0.lib",
56+
"Hid.lib"
5657
]
5758

5859
[target.outrun2006tweaks.properties]

src/dllmain.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ namespace Settings
8484
spdlog::info(" - VibrationMode: {}", VibrationMode);
8585
spdlog::info(" - VibrationStrength: {}", VibrationStrength);
8686
spdlog::info(" - VibrationControllerId: {}", VibrationControllerId);
87+
spdlog::info(" - ImpulseVibrationMode: {}", ImpulseVibrationMode);
88+
spdlog::info(" - ImpulseVibrationLeftMultiplier: {}", ImpulseVibrationLeftMultiplier);
89+
spdlog::info(" - ImpulseVibrationRightMultiplier: {}", ImpulseVibrationRightMultiplier);
8790

8891
spdlog::info(" - SkipIntroLogos: {}", SkipIntroLogos);
8992
spdlog::info(" - DisableCountdownTimer: {}", DisableCountdownTimer);
@@ -153,6 +156,13 @@ namespace Settings
153156
VibrationControllerId = ini.Get("Vibration", "VibrationControllerId", std::move(VibrationControllerId));
154157
VibrationControllerId = std::clamp(VibrationControllerId, 0, 4);
155158

159+
ImpulseVibrationMode = ini.Get("Vibration", "ImpulseVibrationMode", std::move(ImpulseVibrationMode));
160+
ImpulseVibrationMode = std::clamp(ImpulseVibrationMode, 0, 3);
161+
ImpulseVibrationLeftMultiplier = ini.Get("Vibration", "ImpulseVibrationLeftMultiplier", std::move(ImpulseVibrationLeftMultiplier));
162+
ImpulseVibrationLeftMultiplier = std::clamp(ImpulseVibrationLeftMultiplier, 0.0f, 1.0f);
163+
ImpulseVibrationRightMultiplier = ini.Get("Vibration", "ImpulseVibrationRightMultiplier", std::move(ImpulseVibrationRightMultiplier));
164+
ImpulseVibrationRightMultiplier = std::clamp(ImpulseVibrationRightMultiplier, 0.0f, 1.0f);
165+
156166
SkipIntroLogos = ini.Get("Misc", "SkipIntroLogos", std::move(SkipIntroLogos));
157167
DisableCountdownTimer = ini.Get("Misc", "DisableCountdownTimer", std::move(DisableCountdownTimer));
158168

src/hooks_input.cpp

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#define WIN32_LEAN_AND_MEAN
2+
#include <Windows.h>
3+
#include <shellapi.h>
4+
#include <winioctl.h>
5+
#include <hidsdi.h>
6+
7+
#include "hook_mgr.hpp"
8+
#include "plugin.hpp"
9+
#include "game_addrs.hpp"
10+
11+
// Hooks into XINPUT1_4's DeviceIoControl via undocumented DriverHook(0xBAAD0001) call
12+
// Allows us to detect SET_GAMEPAD_STATE ioctl and send the GIP HID command for trigger impulses
13+
// Hopefully will work for most series controller connection types...
14+
15+
// Defs from OpenXInput, GIP command code from X1nput
16+
constexpr DWORD IOCTL_XINPUT_BASE = 0x8000;
17+
static DWORD IOCTL_XINPUT_SET_GAMEPAD_STATE = CTL_CODE(IOCTL_XINPUT_BASE, 0x804, METHOD_BUFFERED, FILE_WRITE_ACCESS); // 0x8000A010
18+
struct InSetState_t
19+
{
20+
BYTE deviceIndex;
21+
BYTE ledState;
22+
BYTE leftMotorSpeed;
23+
BYTE rightMotorSpeed;
24+
BYTE flags;
25+
};
26+
27+
#define XUSB_SET_STATE_FLAG_VIBRATION ((BYTE)0x02)
28+
29+
class ImpulseVibration : public Hook
30+
{
31+
#define MAX_STR 255
32+
inline static wchar_t wstr[MAX_STR];
33+
34+
static BOOL WINAPI DetourDeviceIoControl(
35+
HANDLE hDevice,
36+
DWORD dwIoControlCode,
37+
LPVOID lpInBuffer,
38+
DWORD nInBufferSize,
39+
LPVOID lpOutBuffer,
40+
DWORD nOutBufferSize,
41+
LPDWORD lpBytesReturned,
42+
LPOVERLAPPED lpOverlapped
43+
)
44+
{
45+
auto ret = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped);
46+
if (dwIoControlCode != IOCTL_XINPUT_SET_GAMEPAD_STATE)
47+
return ret;
48+
49+
// Don't send GIP command to x360, may cause issues with some bad third-party ones?
50+
if (HidD_GetProductString(hDevice, wstr, MAX_STR) && wcsstr(wstr, L"360"))
51+
return ret;
52+
53+
if (!Settings::ImpulseVibrationMode)
54+
return ret; // how did we get here?
55+
56+
InSetState_t* inData = (InSetState_t*)lpInBuffer;
57+
if ((inData->flags & XUSB_SET_STATE_FLAG_VIBRATION) != 0)
58+
{
59+
float leftTriggerInput = float(inData->leftMotorSpeed);
60+
float rightTriggerInput = float(inData->rightMotorSpeed);
61+
62+
if (Settings::ImpulseVibrationMode == 2) // Swap L/R
63+
{
64+
leftTriggerInput = float(inData->rightMotorSpeed);
65+
rightTriggerInput = float(inData->leftMotorSpeed);
66+
}
67+
else if (Settings::ImpulseVibrationMode == 3) // Merge L/R by using whichever is highest
68+
{
69+
leftTriggerInput = rightTriggerInput = max(leftTriggerInput, rightTriggerInput);
70+
}
71+
72+
uint8_t buf[9] = { 0 };
73+
buf[0] = 0x03; // HID report ID (3 for bluetooth, any for USB)
74+
buf[1] = 0x0F; // Motor flag mask(?)
75+
buf[2] = uint8_t(leftTriggerInput * Settings::ImpulseVibrationLeftMultiplier); // Left trigger impulse
76+
buf[3] = uint8_t(rightTriggerInput * Settings::ImpulseVibrationRightMultiplier); // Right trigger impulse
77+
buf[4] = inData->leftMotorSpeed; // Left rumble
78+
buf[5] = inData->rightMotorSpeed; // Right rumble
79+
// "Pulse"
80+
buf[6] = 0xFF; // On time
81+
buf[7] = 0x00; // Off time
82+
buf[8] = 0xFF; // Number of repeats
83+
WriteFile(hDevice, buf, 9, lpBytesReturned, lpOverlapped);
84+
}
85+
86+
return ret;
87+
}
88+
89+
public:
90+
std::string_view description() override
91+
{
92+
return "ImpulseVibration";
93+
}
94+
95+
bool validate() override
96+
{
97+
return Settings::ImpulseVibrationMode != 0 && Settings::VibrationMode != 0;
98+
}
99+
100+
bool apply() override
101+
{
102+
auto xinput = LoadLibraryA("xinput1_4.dll");
103+
if (!xinput)
104+
return false;
105+
106+
typedef BOOL(__stdcall* DllMain_fn)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved);
107+
auto dllmain = (DllMain_fn)GetProcAddress(xinput, "DllMain");
108+
if (!dllmain)
109+
return false;
110+
111+
dllmain(nullptr, 0xBAAD0001, DetourDeviceIoControl);
112+
113+
return true;
114+
}
115+
116+
static ImpulseVibration instance;
117+
};
118+
ImpulseVibration ImpulseVibration::instance;

src/plugin.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ namespace Settings
7676
inline int VibrationMode = 0;
7777
inline int VibrationStrength = 10;
7878
inline int VibrationControllerId = 0;
79+
inline int ImpulseVibrationMode = 0;
80+
inline float ImpulseVibrationLeftMultiplier = 0.25f;
81+
inline float ImpulseVibrationRightMultiplier = 0.25f;
7982

8083
inline bool SkipIntroLogos = false;
8184
inline bool DisableCountdownTimer = false;

0 commit comments

Comments
 (0)