Generic joystick / HOTAS / pedals support: DirectInput 8 layer + capture wizard
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8): enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling with range normalization, reacquire-on-loss, 3s hot-plug re-enum. XInput devices are excluded via the documented RawInput IG_ VID/PID check (live-verified skipping a real XBOX360 pad -- no double-feed). bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/ deadzone; hats as 4-direction buttons with diagonal chords). PadRIO runs them through the existing channel/event machinery -- per-binding edge arrays release automatically on detach (the #24 rule), and a direct throttle axis maps full lever travel onto the 0..1 channel while the gait detent still snaps a lever parked in the walk/run dead band. BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move each control when prompted (stick/twist/throttle/fire), invert derived from the move direction, hat look cluster auto-added, output written between markers in bindings.txt with the rest of the file preserved. Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched. Verified: grammar accept/reject per line, XInput-exclusion live, 30s in-mission no-device polling clean. Awaiting a tester with real stick hardware for axis verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
86e387b6c7
commit
c2ee7299b2
@@ -11,6 +11,7 @@
|
||||
#include "l4padpanel.h"
|
||||
#include "l4glasswin.h"
|
||||
#include "l4ctrl.h"
|
||||
#include "l4joy.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <xinput.h>
|
||||
@@ -127,6 +128,8 @@ PadRIO::PadRIO():
|
||||
previousPadButtons(0)
|
||||
{
|
||||
memset(previousKeyHeld, 0, sizeof(previousKeyHeld));
|
||||
memset(previousJoyButtonHeld, 0, sizeof(previousJoyButtonHeld));
|
||||
memset(previousJoyHatHeld, 0, sizeof(previousJoyHatHeld));
|
||||
memset(channelValue, 0, sizeof(channelValue));
|
||||
memset(lampState, 0, sizeof(lampState));
|
||||
|
||||
@@ -672,6 +675,131 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------
|
||||
// Generic joysticks (DirectInput -- L4JOY): resolve each joydev slot
|
||||
// to an attached device (name-substring match, else attached device
|
||||
// #slot), then run the joy bindings through the same channel/event
|
||||
// machinery the pad uses. Zero DirectInput cost when the profile has
|
||||
// no joy rows. A detached device reads as all-released, so held
|
||||
// buttons let go automatically (the issue-#24 rule).
|
||||
//-----------------------------------------------------------------
|
||||
//
|
||||
if (bindings.joyAxisBindingCount > 0
|
||||
|| bindings.joyButtonBindingCount > 0
|
||||
|| bindings.joyHatBindingCount > 0)
|
||||
{
|
||||
BTJoyPoll();
|
||||
|
||||
const BTJoyDeviceState *slotState[PadBindingProfile::JoyDeviceSlots];
|
||||
for (int s = 0; s < PadBindingProfile::JoyDeviceSlots; ++s)
|
||||
{
|
||||
slotState[s] = (bindings.joyDeviceMatch[s][0] != '\0')
|
||||
? BTJoyDevice(BTJoyFindDevice(bindings.joyDeviceMatch[s]))
|
||||
: BTJoyDevice(s);
|
||||
}
|
||||
|
||||
for (int b = 0; b < bindings.joyButtonBindingCount; ++b)
|
||||
{
|
||||
const PadBindingProfile::JoyButtonBinding &binding =
|
||||
bindings.joyButtonBindings[b];
|
||||
const BTJoyDeviceState *state = slotState[binding.device];
|
||||
int held = state != 0
|
||||
&& (state->buttons & (1u << binding.button)) != 0;
|
||||
int was_held = previousJoyButtonHeld[b];
|
||||
previousJoyButtonHeld[b] = (unsigned char)held;
|
||||
if (held == was_held)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (binding.action.kind == PadBindingProfile::ActionButton)
|
||||
{
|
||||
EmitButton(binding.action.address, held);
|
||||
}
|
||||
else if (binding.action.kind == PadBindingProfile::ActionKeypad
|
||||
&& held)
|
||||
{
|
||||
EmitKeypad(binding.action.address, binding.action.key);
|
||||
}
|
||||
}
|
||||
|
||||
for (int hb = 0; hb < bindings.joyHatBindingCount; ++hb)
|
||||
{
|
||||
const PadBindingProfile::JoyHatBinding &binding =
|
||||
bindings.joyHatBindings[hb];
|
||||
const BTJoyDeviceState *state = slotState[binding.device];
|
||||
int held = 0;
|
||||
if (state != 0 && state->hat[binding.hat] >= 0)
|
||||
{
|
||||
//
|
||||
// Held when the POV is within 60 degrees of the bound
|
||||
// direction -- a 45-degree diagonal drives BOTH adjacent
|
||||
// directions (the d-pad chord model).
|
||||
//
|
||||
int diff = state->hat[binding.hat] - binding.direction * 9000;
|
||||
if (diff < 0) diff = -diff;
|
||||
if (diff > 18000) diff = 36000 - diff;
|
||||
held = (diff <= 6000);
|
||||
}
|
||||
int was_held = previousJoyHatHeld[hb];
|
||||
previousJoyHatHeld[hb] = (unsigned char)held;
|
||||
if (held != was_held)
|
||||
{
|
||||
EmitButton(binding.action.address, held);
|
||||
}
|
||||
}
|
||||
|
||||
for (int a = 0; a < bindings.joyAxisBindingCount; ++a)
|
||||
{
|
||||
const PadBindingProfile::JoyAxisBinding &binding =
|
||||
bindings.joyAxisBindings[a];
|
||||
const BTJoyDeviceState *state = slotState[binding.device];
|
||||
if (state == 0)
|
||||
{
|
||||
continue; // detached: leave the channel alone
|
||||
}
|
||||
float raw = state->axis[binding.axis];
|
||||
if (binding.deadzone > 0.0f)
|
||||
{
|
||||
float magnitude = raw < 0.0f ? -raw : raw;
|
||||
raw = (magnitude <= binding.deadzone)
|
||||
? 0.0f
|
||||
: (raw < 0.0f ? -1.0f : 1.0f)
|
||||
* (magnitude - binding.deadzone)
|
||||
/ (1.0f - binding.deadzone);
|
||||
}
|
||||
if (binding.invert)
|
||||
{
|
||||
raw = -raw;
|
||||
}
|
||||
if (binding.slew)
|
||||
{
|
||||
if (raw != 0.0f)
|
||||
{
|
||||
slewHeld[binding.channel] = 1;
|
||||
}
|
||||
channelValue[binding.channel] += raw * binding.slewRate * dt;
|
||||
}
|
||||
else if (binding.channel == PadBindingProfile::ChannelThrottle)
|
||||
{
|
||||
//
|
||||
// A physical lever OWNS the 0..1 throttle: full axis travel
|
||||
// maps [-1,1] -> [0,1] and writes EVERY frame (the lever's
|
||||
// rest position is wherever it sits, not "centered").
|
||||
// slewHeld stays clear so the GAIT DETENT below still snaps
|
||||
// a lever PARKED in the walk/run dead band to the nearer
|
||||
// edge -- the published value never hunts even though the
|
||||
// physical lever has no notches.
|
||||
//
|
||||
channelValue[binding.channel] = (raw + 1.0f) * 0.5f;
|
||||
}
|
||||
else if (raw != 0.0f)
|
||||
{
|
||||
channelValue[binding.channel] = raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------
|
||||
// GAIT DETENT (keyboard/pad accommodation, ported from the pod-build
|
||||
|
||||
Reference in New Issue
Block a user