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:
arcattack
2026-07-23 16:36:32 -05:00
co-authored by Claude Opus 4.8
parent 86e387b6c7
commit c2ee7299b2
12 changed files with 1531 additions and 6 deletions
+1
View File
@@ -263,6 +263,7 @@ add_library(munga_engine STATIC
target_sources(munga_engine PRIVATE
"engine/MUNGA_L4/L4PADRIO.cpp"
"engine/MUNGA_L4/L4PADBINDINGS.cpp"
"engine/MUNGA_L4/L4JOY.cpp"
"engine/MUNGA_L4/L4PADPANEL.cpp"
"engine/MUNGA_L4/L4GLASSWIN.cpp"
"engine/MUNGA_L4/L4PLASMAWIN.cpp"
+25
View File
@@ -88,6 +88,31 @@ recovery; keyboard-only run clean). Fix: on the connected->disconnected transit
`EmitButton(address, 0)` for every binding whose mask was held, THEN forget. Not reproducible
headless (needs real hardware unplug) -- logic fix, awaiting the reporter's controller.
## Generic joysticks / HOTAS / pedals -- DirectInput 8 layer (2026-07-23) [T2 rig-verified; awaiting stick hardware]
Tester ask: configure non-Xbox controllers. **`L4JOY.cpp/.h`** (all builds, lazy-init): DI8
enumeration of every non-XInput game device (up to 4), `c_dfDIJoystick2` polling with
`DIPROP_RANGE` ±32767 + reacquire-on-loss + 3 s hot-plug re-enum, normalized
`BTJoyDeviceState` {8 axes -1..1, 32 buttons, 4 POV hats}. **XInput exclusion** = the
documented wbem-free RawInput check (device path contains `IG_` → VID/PID blacklist vs
`guidProduct.Data1`) — LIVE-verified skipping a real XBOX360 pad. **Bindings**
(L4PADBINDINGS): `joydev <slot> [name-substr]` + `joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis
<channel> [invert] [slew r] [deadzone f]` + `joybutton <0-31>` + `joyhat <0-3> <dir>` — rows
attach to the last joydev slot; unnamed slot binds attached device #slot. **PadRIO poll**
runs them through the SAME channel/event machinery (per-binding prev arrays → detach releases
automatically, the #24 rule); a direct joyaxis on Throttle maps full travel [-1,1]→[0,1] and
writes every frame (a lever OWNS the channel; slewHeld stays clear so the GAIT DETENT still
snaps a lever parked in the walk/run dead band). **Wizard** `BT_JOYCONFIG=1`
(`BTJoyConfigWizard`, console UI, `joyconfig.bat`): baselines all axes (a HOTAS lever rests
anywhere), captures the dominant mover per prompt (0.45 threshold, 250 ms hold, already-
assigned axes excluded), derives invert from the move direction (throttle from FINAL
position), auto-adds the hat look cluster, and rewrites bindings.txt between
`# >>> BT_JOYCONFIG` markers (rest of the file preserved). Sign conventions: JoystickX + =
twist right, JoystickY + = aim up (wizard binds flight-style forward=down via invert), Turn +
= right. Legacy `L4DINPUT.cpp DIJoystick` (L4CONTROLS=DIJOYSTICK, 1995 `Joystick` iface)
left untouched. Verified: grammar accept/reject by line, no-device + XInput-only polling
clean 30 s in-mission, zero DI cost with no joy rows. NOT yet verified: real stick axes
(no hardware here) — needs a tester with a stick.
## Plan of record (2026-07-17)
Working branch `glass-cockpit` (from master `e2c21c4`). Steps: (1) gates+scaffolding [THIS],
+989
View File
@@ -0,0 +1,989 @@
#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4JOY -- generic-joystick reader (DirectInput 8) for the glass cockpit.
// Design + device model: L4JOY.h. Consumed by the PadRIO poll through the
// joydev/joyaxis/joybutton/joyhat bindings (L4PADBINDINGS / L4PADRIO).
//###########################################################################
#include "l4joy.h"
#define DIRECTINPUT_VERSION 0x0800
#include <windows.h>
#include <dinput.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//###########################################################################
// State
//###########################################################################
struct JoyDevice
{
IDirectInputDevice8A *device;
BTJoyDeviceState state;
//
// Per-axis calibrated range (DIPROP_RANGE is set to +-32767 at open, but
// a driver may refuse; normalization uses what the device reports).
//
LONG axisMin[BTJoyAxisCount];
LONG axisMax[BTJoyAxisCount];
};
static IDirectInput8A *sDirectInput = NULL;
static JoyDevice sDevices[BTJoyMaxDevices];
static int sDeviceCount = 0;
static int sInitialized = 0;
static unsigned long sLastProbeMilliseconds = 0;
static int
JoyLogEnabled()
{
static int s_log = -1;
if (s_log < 0)
{
const char *value = getenv("BT_JOY_LOG");
s_log = (value != NULL && *value != '0') ? 1 : 0;
}
return s_log;
}
//###########################################################################
// XInput-device exclusion.
//
// The documented wbem-free method: every XInput-capable device's RawInput
// device path carries the "IG_" marker; collect the VID/PID of each such
// path once per enumeration pass, and skip any DirectInput device whose
// guidProduct carries a matching VID/PID (DI packs VID in the low word,
// PID in the high word of guidProduct.Data1).
//###########################################################################
enum { XInputVidPidMax = 16 };
static unsigned long sXInputVidPid[XInputVidPidMax];
static int sXInputVidPidCount = 0;
static void
CollectXInputVidPids()
{
sXInputVidPidCount = 0;
UINT device_count = 0;
if (GetRawInputDeviceList(NULL, &device_count, sizeof(RAWINPUTDEVICELIST))
!= 0 || device_count == 0)
{
return;
}
RAWINPUTDEVICELIST *list = (RAWINPUTDEVICELIST *)
malloc(device_count * sizeof(RAWINPUTDEVICELIST));
if (list == NULL)
{
return;
}
device_count = GetRawInputDeviceList(list, &device_count,
sizeof(RAWINPUTDEVICELIST));
if (device_count == (UINT)-1)
{
free(list);
return;
}
for (UINT i = 0; i < device_count; ++i)
{
if (list[i].dwType != RIM_TYPEHID)
{
continue;
}
char path[256];
UINT size = sizeof(path);
if (GetRawInputDeviceInfoA(list[i].hDevice, RIDI_DEVICENAME,
path, &size) == (UINT)-1)
{
continue;
}
path[sizeof(path) - 1] = '\0';
if (strstr(path, "IG_") == NULL && strstr(path, "ig_") == NULL)
{
continue;
}
//
// Parse "...VID_045E&PID_028E..." (case varies by driver).
//
unsigned vid = 0, pid = 0;
const char *v = strstr(path, "VID_");
if (v == NULL) v = strstr(path, "vid_");
const char *p = strstr(path, "PID_");
if (p == NULL) p = strstr(path, "pid_");
if (v == NULL || p == NULL)
{
continue;
}
vid = (unsigned)strtoul(v + 4, NULL, 16);
pid = (unsigned)strtoul(p + 4, NULL, 16);
if (sXInputVidPidCount < XInputVidPidMax)
{
sXInputVidPid[sXInputVidPidCount++] =
((unsigned long)pid << 16) | vid;
}
}
free(list);
}
static int
IsXInputProduct(const GUID &guid_product)
{
//
// DirectInput packs VID low-word / PID high-word into Data1.
//
for (int i = 0; i < sXInputVidPidCount; ++i)
{
if (sXInputVidPid[i] == (unsigned long)guid_product.Data1)
{
return 1;
}
}
return 0;
}
//###########################################################################
// Device open
//###########################################################################
//
// A window handle of THIS process for SetCooperativeLevel. BACKGROUND +
// NONEXCLUSIVE polling matches the XInput model (device readable while the
// game runs; PadRIO's focus rules stay the arbiter of what acts on it).
//
static BOOL CALLBACK
FindProcessWindowCallback(HWND hwnd, LPARAM lparam)
{
DWORD process_id = 0;
GetWindowThreadProcessId(hwnd, &process_id);
if (process_id == GetCurrentProcessId())
{
*(HWND *)lparam = hwnd;
return FALSE;
}
return TRUE;
}
static HWND
FindProcessWindow()
{
HWND hwnd = NULL;
EnumWindows(FindProcessWindowCallback, (LPARAM)&hwnd);
return hwnd != NULL ? hwnd : GetDesktopWindow();
}
static BOOL CALLBACK
SetAxisRangeCallback(LPCDIDEVICEOBJECTINSTANCEA object, LPVOID context)
{
IDirectInputDevice8A *device = (IDirectInputDevice8A *)context;
DIPROPRANGE range;
range.diph.dwSize = sizeof(DIPROPRANGE);
range.diph.dwHeaderSize = sizeof(DIPROPHEADER);
range.diph.dwHow = DIPH_BYID;
range.diph.dwObj = object->dwType;
range.lMin = -32767;
range.lMax = 32767;
device->SetProperty(DIPROP_RANGE, &range.diph); // best-effort; a driver
// may refuse -- read back
return DIENUM_CONTINUE;
}
struct EnumPassContext
{
HWND hwnd;
};
static BOOL CALLBACK
EnumDevicesCallback(const DIDEVICEINSTANCEA *instance, VOID *context)
{
EnumPassContext *pass = (EnumPassContext *)context;
if (sDeviceCount >= BTJoyMaxDevices)
{
return DIENUM_STOP;
}
if (IsXInputProduct(instance->guidProduct))
{
if (JoyLogEnabled())
{
DEBUG_STREAM << "[joy] skipping XInput-class device \""
<< instance->tszProductName << "\"\n" << std::flush;
}
return DIENUM_CONTINUE;
}
IDirectInputDevice8A *device = NULL;
if (FAILED(sDirectInput->CreateDevice(instance->guidInstance,
&device, NULL)) || device == NULL)
{
return DIENUM_CONTINUE;
}
if (FAILED(device->SetDataFormat(&c_dfDIJoystick2)))
{
device->Release();
return DIENUM_CONTINUE;
}
//
// Best-effort: some environments reject a cooperative level on the
// desktop window; the default (nonexclusive) still polls.
//
device->SetCooperativeLevel(pass->hwnd,
DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
device->EnumObjects(SetAxisRangeCallback, device, DIDFT_AXIS);
device->Acquire();
JoyDevice &slot = sDevices[sDeviceCount];
memset(&slot, 0, sizeof(slot));
slot.device = device;
slot.state.attached = 1;
strncpy(slot.state.name, instance->tszProductName,
sizeof(slot.state.name) - 1);
for (int a = 0; a < BTJoyAxisCount; ++a)
{
slot.axisMin[a] = -32767;
slot.axisMax[a] = 32767;
}
for (int h = 0; h < BTJoyHatCount; ++h)
{
slot.state.hat[h] = -1;
}
++sDeviceCount;
DEBUG_STREAM << "[joy] device " << (sDeviceCount - 1) << ": \""
<< slot.state.name << "\" attached\n" << std::flush;
return DIENUM_CONTINUE;
}
static void
ReleaseAllDevices()
{
for (int i = 0; i < sDeviceCount; ++i)
{
if (sDevices[i].device != NULL)
{
sDevices[i].device->Unacquire();
sDevices[i].device->Release();
sDevices[i].device = NULL;
}
sDevices[i].state.attached = 0;
}
sDeviceCount = 0;
}
static void
Enumerate()
{
ReleaseAllDevices();
CollectXInputVidPids();
EnumPassContext pass;
pass.hwnd = FindProcessWindow();
sDirectInput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumDevicesCallback,
&pass, DIEDFL_ATTACHEDONLY);
}
//###########################################################################
// Public surface
//###########################################################################
int
BTJoyInit(void)
{
if (sInitialized)
{
return sDeviceCount;
}
sInitialized = 1;
if (FAILED(DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION,
IID_IDirectInput8A, (void **)&sDirectInput, NULL))
|| sDirectInput == NULL)
{
sDirectInput = NULL;
DEBUG_STREAM << "[joy] DirectInput8Create failed -- generic "
<< "joysticks unavailable\n" << std::flush;
return 0;
}
Enumerate();
return sDeviceCount;
}
void
BTJoyShutdown(void)
{
ReleaseAllDevices();
if (sDirectInput != NULL)
{
sDirectInput->Release();
sDirectInput = NULL;
}
sInitialized = 0;
}
static float
NormalizeAxis(LONG value, LONG range_min, LONG range_max)
{
if (range_max <= range_min)
{
return 0.0f;
}
float normalized =
((float)(value - range_min) / (float)(range_max - range_min))
* 2.0f - 1.0f;
if (normalized < -1.0f) normalized = -1.0f;
if (normalized > 1.0f) normalized = 1.0f;
return normalized;
}
void
BTJoyPoll(void)
{
if (!sInitialized)
{
BTJoyInit();
}
if (sDirectInput == NULL)
{
return;
}
//
// Hot-plug: when nothing is attached (or a device died), re-enumerate
// on the PadRIO probe cadence (~3 s) -- enumeration is too heavy for
// every frame.
//
int any_attached = 0;
for (int i = 0; i < sDeviceCount; ++i)
{
if (sDevices[i].state.attached)
{
any_attached = 1;
break;
}
}
if (!any_attached)
{
unsigned long now = timeGetTime();
if (sLastProbeMilliseconds != 0 &&
now - sLastProbeMilliseconds < 3000)
{
return;
}
sLastProbeMilliseconds = now;
Enumerate();
}
for (int i = 0; i < sDeviceCount; ++i)
{
JoyDevice &slot = sDevices[i];
if (slot.device == NULL || !slot.state.attached)
{
continue;
}
HRESULT result = slot.device->Poll();
if (FAILED(result))
{
result = slot.device->Acquire();
if (SUCCEEDED(result))
{
result = slot.device->Poll();
}
}
DIJOYSTATE2 joystate;
if (SUCCEEDED(result) || result == DI_NOEFFECT)
{
result = slot.device->GetDeviceState(sizeof(joystate), &joystate);
}
if (FAILED(result))
{
//
// Unplugged (or the driver died). Mark detached; the binding
// layer sees zeroed state and releases everything it held (the
// issue-#24 stuck-button rule), and the probe above will try to
// re-enumerate.
//
DEBUG_STREAM << "[joy] device " << i << " (\"" << slot.state.name
<< "\") lost\n" << std::flush;
slot.state.attached = 0;
memset(slot.state.axis, 0, sizeof(slot.state.axis));
slot.state.buttons = 0;
for (int h = 0; h < BTJoyHatCount; ++h)
{
slot.state.hat[h] = -1;
}
continue;
}
LONG raw[BTJoyAxisCount];
raw[0] = joystate.lX;
raw[1] = joystate.lY;
raw[2] = joystate.lZ;
raw[3] = joystate.lRx;
raw[4] = joystate.lRy;
raw[5] = joystate.lRz;
raw[6] = joystate.rglSlider[0];
raw[7] = joystate.rglSlider[1];
for (int a = 0; a < BTJoyAxisCount; ++a)
{
slot.state.axis[a] =
NormalizeAxis(raw[a], slot.axisMin[a], slot.axisMax[a]);
}
slot.state.buttons = 0;
for (int b = 0; b < BTJoyButtonCount; ++b)
{
if (joystate.rgbButtons[b] & 0x80)
{
slot.state.buttons |= (1u << b);
}
}
for (int h = 0; h < BTJoyHatCount; ++h)
{
DWORD pov = joystate.rgdwPOV[h];
//
// Centered reads as -1 (0xFFFF in LOWORD per the DI contract;
// some drivers return the full 0xFFFFFFFF).
//
slot.state.hat[h] =
(LOWORD(pov) == 0xFFFF) ? -1 : (int)pov;
}
}
}
int
BTJoyDeviceCount(void)
{
return sDeviceCount;
}
const BTJoyDeviceState *
BTJoyDevice(int index)
{
if (index < 0 || index >= sDeviceCount ||
!sDevices[index].state.attached)
{
return NULL;
}
return &sDevices[index].state;
}
int
BTJoyFindDevice(const char *name_substring)
{
if (name_substring == NULL || *name_substring == '\0')
{
return -1;
}
char want[64];
strncpy(want, name_substring, sizeof(want) - 1);
want[sizeof(want) - 1] = '\0';
_strlwr(want);
for (int i = 0; i < sDeviceCount; ++i)
{
if (!sDevices[i].state.attached)
{
continue;
}
char have[64];
strncpy(have, sDevices[i].state.name, sizeof(have) - 1);
have[sizeof(have) - 1] = '\0';
_strlwr(have);
if (strstr(have, want) != NULL)
{
return i;
}
}
return -1;
}
//###########################################################################
// BT_JOYCONFIG -- the interactive capture wizard.
//
// Console UI (AllocConsole; the game is a GUI app). Detects which device/
// axis the player moves for each pod control, derives the sign convention
// from the direction of the move, then writes the joystick section of
// bindings.txt between marker lines (replacing any previous generated
// section; the rest of the file is preserved byte-for-byte).
//###########################################################################
#include <conio.h>
#include "l4padbindings.h"
namespace
{
struct WizardCapture
{
int used;
int device;
int axis; // axis steps (-1 for buttons)
int button; // button steps (-1 for axes)
int invert;
float deadzone;
char line[128];
};
const char *joyAxisToken(int axis)
{
static const char *names[BTJoyAxisCount] =
{ "X", "Y", "Z", "RX", "RY", "RZ", "SL0", "SL1" };
return (axis >= 0 && axis < BTJoyAxisCount) ? names[axis] : "?";
}
void WizardBaseline(float baseline[BTJoyMaxDevices][BTJoyAxisCount])
{
//
// ~600 ms of samples -> the at-rest position of every axis (a HOTAS
// throttle rests wherever its lever is; never assume 0).
//
for (int pass = 0; pass < 20; ++pass)
{
BTJoyPoll();
Sleep(30);
}
for (int d = 0; d < BTJoyMaxDevices; ++d)
{
const BTJoyDeviceState *state = BTJoyDevice(d);
for (int a = 0; a < BTJoyAxisCount; ++a)
{
baseline[d][a] = state != 0 ? state->axis[a] : 0.0f;
}
}
}
//
// Wait for a decisive axis move. Returns 1 with (device, axis, delta,
// final) filled, 0 on skip (SPACE), -1 on abort (ESC) / timeout.
//
int WizardCaptureAxis(
const float baseline[BTJoyMaxDevices][BTJoyAxisCount],
const WizardCapture *taken, int taken_count,
int allow_skip,
int *out_device, int *out_axis, float *out_delta, float *out_final)
{
unsigned long deadline = timeGetTime() + 30000;
unsigned long hold_since = 0;
int cand_device = -1, cand_axis = -1;
while (timeGetTime() < deadline)
{
while (_kbhit())
{
int key = _getch();
if (key == 27)
{
return -1;
}
if (key == ' ' && allow_skip)
{
return 0;
}
}
BTJoyPoll();
int best_device = -1, best_axis = -1;
float best_mag = 0.0f, best_delta = 0.0f;
for (int d = 0; d < BTJoyMaxDevices; ++d)
{
const BTJoyDeviceState *state = BTJoyDevice(d);
if (state == 0)
{
continue;
}
for (int a = 0; a < BTJoyAxisCount; ++a)
{
int already = 0;
for (int t = 0; t < taken_count; ++t)
{
if (taken[t].used && taken[t].axis >= 0 &&
taken[t].device == d && taken[t].axis == a)
{
already = 1;
}
}
if (already)
{
continue;
}
float delta = state->axis[a] - baseline[d][a];
float magnitude = delta < 0.0f ? -delta : delta;
if (magnitude > best_mag)
{
best_mag = magnitude;
best_delta = delta;
best_device = d;
best_axis = a;
}
}
}
if (best_mag > 0.45f)
{
if (cand_device != best_device || cand_axis != best_axis)
{
cand_device = best_device;
cand_axis = best_axis;
hold_since = timeGetTime();
}
else if (timeGetTime() - hold_since > 250)
{
const BTJoyDeviceState *state = BTJoyDevice(best_device);
*out_device = best_device;
*out_axis = best_axis;
*out_delta = best_delta;
*out_final = state != 0 ? state->axis[best_axis] : best_delta;
return 1;
}
}
else
{
cand_device = cand_axis = -1;
}
Sleep(15);
}
return -1;
}
//
// Wait for a fresh button press. Returns 1 with (device, button), 0 on
// skip, -1 on abort/timeout.
//
int WizardCaptureButton(
int allow_skip, int *out_device, int *out_button)
{
unsigned baseline[BTJoyMaxDevices];
BTJoyPoll();
for (int d = 0; d < BTJoyMaxDevices; ++d)
{
const BTJoyDeviceState *state = BTJoyDevice(d);
baseline[d] = state != 0 ? state->buttons : 0;
}
unsigned long deadline = timeGetTime() + 30000;
while (timeGetTime() < deadline)
{
while (_kbhit())
{
int key = _getch();
if (key == 27)
{
return -1;
}
if (key == ' ' && allow_skip)
{
return 0;
}
}
BTJoyPoll();
for (int d = 0; d < BTJoyMaxDevices; ++d)
{
const BTJoyDeviceState *state = BTJoyDevice(d);
if (state == 0)
{
continue;
}
unsigned fresh = state->buttons & ~baseline[d];
if (fresh != 0)
{
int button = 0;
while ((fresh & 1u) == 0)
{
fresh >>= 1;
++button;
}
*out_device = d;
*out_button = button;
return 1;
}
baseline[d] &= state->buttons; // releases refresh the baseline
}
Sleep(15);
}
return -1;
}
} // namespace
int
BTJoyConfigWizard(void)
{
//
// Console (the game is a GUI subsystem app).
//
if (GetConsoleWindow() == NULL)
{
AllocConsole();
}
FILE *io;
freopen_s(&io, "CONOUT$", "w", stdout);
freopen_s(&io, "CONIN$", "r", stdin);
printf("\n=== BattleTech joystick setup (BT_JOYCONFIG) ===\n\n");
//
// Make sure a bindings.txt exists (the wizard APPENDS its section; the
// keyboard/pad sections come from the default write).
//
{
PadBindingProfile ensure_default;
ensure_default.Load();
}
if (BTJoyInit() == 0)
{
printf("No generic (non-Xbox) game devices found.\n");
printf("Plug in the stick/throttle/pedals and run joyconfig again.\n");
printf("(Xbox-class controllers already work -- no setup needed.)\n\n");
printf("Press any key to exit.\n");
_getch();
return 1;
}
printf("Detected devices:\n");
for (int d = 0; d < BTJoyDeviceCount(); ++d)
{
const BTJoyDeviceState *state = BTJoyDevice(d);
if (state != 0)
{
printf(" [%d] %s\n", d, state->name);
}
}
printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
"skip that control, ESC to abort. Keep everything else still.\n\n");
WizardCapture captures[16];
memset(captures, 0, sizeof(captures));
int capture_count = 0;
struct AxisStep
{
const char *prompt;
const char *channel;
int invert_when_positive; // the asked move should read NEGATIVE
int throttle; // full-travel lever (invert from FINAL)
int allow_skip;
};
static const AxisStep axisSteps[] =
{
{ "AIM: push the STICK fully RIGHT", "JoystickX", 0, 0, 0 },
{ "AIM: push the STICK fully FORWARD\n"
" (forward aims DOWN, flight-style; add/remove the word\n"
" invert on that line in bindings.txt to flip it later)",
"JoystickY", 1, 0, 0 },
{ "TURN: TWIST the stick / push the rudder RIGHT (SPACE if none)",
"Turn", 0, 0, 1 },
{ "THROTTLE: move the throttle lever to FULL (SPACE if none)",
"Throttle", 0, 1, 1 },
};
float baseline[BTJoyMaxDevices][BTJoyAxisCount];
for (int s = 0; s < (int)(sizeof(axisSteps)/sizeof(axisSteps[0])); ++s)
{
printf("%s ...\n", axisSteps[s].prompt);
WizardBaseline(baseline);
int device, axis;
float delta, final_value;
int got = WizardCaptureAxis(baseline, captures, capture_count,
axisSteps[s].allow_skip, &device, &axis, &delta, &final_value);
if (got < 0)
{
printf("\nAborted -- nothing written.\n");
return 1;
}
if (got == 0)
{
printf(" skipped.\n\n");
continue;
}
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = device;
capture.axis = axis;
capture.button = -1;
if (axisSteps[s].throttle)
{
capture.invert = (final_value < 0.0f);
capture.deadzone = 0.0f;
sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0",
joyAxisToken(axis), axisSteps[s].channel,
capture.invert ? " invert" : "");
}
else
{
int negative = (delta < 0.0f);
capture.invert = axisSteps[s].invert_when_positive
? !negative : negative;
capture.deadzone = 0.08f;
sprintf(capture.line, "joyaxis %s axis %s%s",
joyAxisToken(axis), axisSteps[s].channel,
capture.invert ? " invert" : "");
}
printf(" -> device %d (%s) axis %s%s\n\n", device,
BTJoyDevice(device) ? BTJoyDevice(device)->name : "?",
joyAxisToken(axis), capture.invert ? " (inverted)" : "");
Sleep(800); // let the control come back to rest
}
struct ButtonStep
{
const char *prompt;
int address;
int allow_skip;
};
static const ButtonStep buttonSteps[] =
{
{ "FIRE 1: press the TRIGGER", 0x40, 0 },
{ "FIRE 2: press your second fire button", 0x46, 1 },
{ "FIRE 3: press your third fire button", 0x47, 1 },
{ "FIRE 4: press your fourth fire button", 0x45, 1 },
{ "REVERSE: press the button for reverse thrust", 0x3F, 1 },
};
for (int s = 0; s < (int)(sizeof(buttonSteps)/sizeof(buttonSteps[0])); ++s)
{
printf("%s ... %s\n", buttonSteps[s].prompt,
buttonSteps[s].allow_skip ? "(SPACE to skip)" : "");
int device, button;
int got = WizardCaptureButton(buttonSteps[s].allow_skip,
&device, &button);
if (got < 0)
{
printf("\nAborted -- nothing written.\n");
return 1;
}
if (got == 0)
{
printf(" skipped.\n\n");
continue;
}
WizardCapture &capture = captures[capture_count++];
capture.used = 1;
capture.device = device;
capture.axis = -1;
capture.button = button;
sprintf(capture.line, "joybutton %d button 0x%X",
button, buttonSteps[s].address);
printf(" -> device %d button %d\n\n", device, button);
Sleep(400);
}
if (capture_count == 0)
{
printf("Nothing captured -- nothing written.\n");
return 1;
}
//
// Group by device -> joydev slots in order of first use. The hat on
// the primary (stick) device gets the pod look cluster automatically
// (hats are standardized; no capture needed).
//
int slot_of_device[BTJoyMaxDevices];
int slot_count = 0;
for (int d = 0; d < BTJoyMaxDevices; ++d)
{
slot_of_device[d] = -1;
}
for (int i = 0; i < capture_count; ++i)
{
if (captures[i].used && slot_of_device[captures[i].device] < 0)
{
slot_of_device[captures[i].device] = slot_count++;
}
}
//
// Rewrite bindings.txt: preserve everything outside the marker pair.
//
static const char *beginMarker =
"# >>> BT_JOYCONFIG generated -- do not edit between markers";
static const char *endMarker = "# <<< BT_JOYCONFIG end";
static char kept[32768];
kept[0] = '\0';
{
FILE *f = fopen("bindings.txt", "rt");
if (f != NULL)
{
char line[512];
int inside = 0;
size_t used = 0;
while (fgets(line, sizeof(line), f))
{
if (strstr(line, beginMarker) != NULL)
{
inside = 1;
continue;
}
if (strstr(line, endMarker) != NULL)
{
inside = 0;
continue;
}
if (!inside && used + strlen(line) < sizeof(kept) - 1)
{
strcpy(kept + used, line);
used += strlen(line);
}
}
fclose(f);
}
}
FILE *f = fopen("bindings.txt", "wt");
if (f == NULL)
{
printf("ERROR: cannot write bindings.txt (working directory?)\n");
_getch();
return 1;
}
fputs(kept, f);
if (kept[0] != '\0' && kept[strlen(kept) - 1] != '\n')
{
fputs("\n", f);
}
fprintf(f, "%s\n", beginMarker);
int hat_done = 0;
for (int d = 0; d < BTJoyMaxDevices; ++d)
{
if (slot_of_device[d] < 0)
{
continue;
}
const BTJoyDeviceState *state = BTJoyDevice(d);
fprintf(f, "joydev %d %s\n", slot_of_device[d],
state != 0 ? state->name : "");
for (int i = 0; i < capture_count; ++i)
{
if (captures[i].used && captures[i].device == d)
{
fprintf(f, "%s\n", captures[i].line);
}
}
if (!hat_done)
{
hat_done = 1;
fprintf(f, "joyhat 0 up button 0x42\n");
fprintf(f, "joyhat 0 down button 0x41\n");
fprintf(f, "joyhat 0 left button 0x44\n");
fprintf(f, "joyhat 0 right button 0x43\n");
}
}
fprintf(f, "%s\n", endMarker);
fclose(f);
printf("bindings.txt written (%d controls + hat looks).\n", capture_count);
printf("The game uses them from the next launch -- have fun.\n\n");
printf("Press any key to continue into the game.\n");
_getch();
return 0;
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
//###########################################################################
//
// L4JOY -- generic-joystick reader (DirectInput 8) for the glass cockpit.
//
// PadRIO reads XInput; this layer adds every OTHER game device Windows
// knows -- flight sticks, HOTAS throttles, twist-sticks, rudder pedals --
// through DirectInput 8 (the standard generic-HID game API). It exposes
// up to BTJoyMaxDevices attached devices as normalized state blocks; the
// PadRIO poll maps them onto the pod's control channels through the
// joydev/joyaxis/joybutton/joyhat rows of bindings.txt (L4PADBINDINGS.h)
// -- the same binding machinery the XInput pad uses.
//
// XInput-class devices are EXCLUDED (they'd double-feed through both
// APIs): a DirectInput device whose VID/PID appears in a RawInput device
// path containing the "IG_" marker is an XInput device (the documented
// wbem-free detection).
//
// This is DISTINCT from the legacy L4DINPUT.cpp DIJoystick (the 1995-era
// single-device `Joystick` engine interface, reachable only through the
// old L4CONTROLS=DIJOYSTICK profile) -- that path is left untouched.
//
// BT_JOYCONFIG=1 runs the interactive capture wizard at boot (console
// prompts; writes the joystick section of bindings.txt). BT_JOY_LOG=1
// logs device attach/detach and the resolved bindings.
//
//###########################################################################
enum
{
BTJoyMaxDevices = 4,
BTJoyAxisCount = 8, // X Y Z RX RY RZ SL0 SL1 (DIJOYSTATE2 order)
BTJoyButtonCount = 32, // buttons exposed to bindings (DI carries 128)
BTJoyHatCount = 4,
};
struct BTJoyDeviceState
{
int attached;
float axis[BTJoyAxisCount]; // normalized -1..1 (raw; deadzones
// are the binding layer's business)
unsigned buttons; // bit n = button n held
int hat[BTJoyHatCount]; // POV in centidegrees; -1 = centered
char name[64]; // product name ("T.16000M", ...)
};
//
// Lifecycle. Init is lazy-safe (Poll calls it); returns the attached
// non-XInput device count. Re-enumeration (hot-plug) happens inside Poll
// on a ~3 s cadence whenever nothing is attached or a device was lost.
//
int BTJoyInit(void);
void BTJoyShutdown(void);
void BTJoyPoll(void);
int BTJoyDeviceCount(void);
const BTJoyDeviceState *BTJoyDevice(int index); // NULL out of range/detached
//
// Case-insensitive product-name substring match -> device index, -1 none.
//
int BTJoyFindDevice(const char *name_substring);
//
// The BT_JOYCONFIG capture wizard (console UI; called from btl4main before
// the frontend). Returns 0 if it wrote a config, nonzero on abort/no-device.
//
int BTJoyConfigWizard(void);
+192 -4
View File
@@ -101,6 +101,26 @@ static const NamedValue channelNames[] =
{ "Turn", PadBindingProfile::ChannelTurn }, // issue #25 composite
};
//
// Generic-joystick axis names (the DIJOYSTATE2 layout -- L4JOY.h order).
//
static const NamedValue joyAxisNames[] =
{
{ "X", PadBindingProfile::JoyAxisX },
{ "Y", PadBindingProfile::JoyAxisY },
{ "Z", PadBindingProfile::JoyAxisZ },
{ "RX", PadBindingProfile::JoyAxisRX },
{ "RY", PadBindingProfile::JoyAxisRY },
{ "RZ", PadBindingProfile::JoyAxisRZ },
{ "SL0", PadBindingProfile::JoyAxisSL0 },
{ "SL1", PadBindingProfile::JoyAxisSL1 },
};
static const NamedValue hatDirectionNames[] =
{
{ "up", 0 }, { "right", 1 }, { "down", 2 }, { "left", 3 },
};
static int
LookupName(const NamedValue *table, int count, const char *name)
{
@@ -252,7 +272,28 @@ static const char *defaultProfileText =
"pad DPAD_UP button 0x42\n"
"pad DPAD_DOWN button 0x41\n"
"pad DPAD_LEFT button 0x44\n"
"pad DPAD_RIGHT button 0x43\n";
"pad DPAD_RIGHT button 0x43\n"
"\n"
"# --- generic joysticks (flight sticks / HOTAS / pedals; DirectInput) ---\n"
"# EASIEST: run the game once with BT_JOYCONFIG=1 (joyconfig.bat) -- an\n"
"# interactive wizard detects your controls and writes this section for you.\n"
"# Manual grammar (rows attach to the last joydev slot; slot 0 if none):\n"
"# joydev <slot 0-3> [product-name substring]\n"
"# joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis <channel> [invert] [slew <r>] [deadzone <f>]\n"
"# joybutton <0-31> button <addr>\n"
"# joyhat <0-3> <up|down|left|right> button <addr>\n"
"# Typical flight stick (twist = RZ, throttle wheel = SL0):\n"
"# joydev 0\n"
"# joyaxis X axis JoystickX\n"
"# joyaxis Y axis JoystickY invert\n"
"# joyaxis RZ axis Turn\n"
"# joyaxis SL0 axis Throttle invert deadzone 0\n"
"# joybutton 0 button 0x40\n"
"# joybutton 1 button 0x46\n"
"# joyhat 0 up button 0x42\n"
"# joyhat 0 down button 0x41\n"
"# joyhat 0 left button 0x44\n"
"# joyhat 0 right button 0x43\n";
//###########################################################################
// PadBindingProfile
@@ -261,8 +302,13 @@ static const char *defaultProfileText =
PadBindingProfile::PadBindingProfile():
keyBindingCount(0),
padButtonBindingCount(0),
padAxisBindingCount(0)
padAxisBindingCount(0),
joyAxisBindingCount(0),
joyButtonBindingCount(0),
joyHatBindingCount(0),
parseJoyDevice(0)
{
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
}
PadBindingProfile::~PadBindingProfile()
@@ -368,7 +414,97 @@ int
}
//
// key/pad rows: parse the action clause.
// joydev <slot> [name substring...] -- select the device slot for the
// joy rows that follow; optional product-name match (joined verbatim).
//
if (_stricmp(tokens[0], "joydev") == 0)
{
int slot = (int)strtol(tokens[1], NULL, 0);
if (slot < 0 || slot >= JoyDeviceSlots)
{
return -1;
}
parseJoyDevice = slot;
joyDeviceMatch[slot][0] = '\0';
for (int t = 2; t < tokenCount; ++t)
{
if (t > 2)
{
strncat(joyDeviceMatch[slot], " ",
sizeof(joyDeviceMatch[slot]) - strlen(joyDeviceMatch[slot]) - 1);
}
strncat(joyDeviceMatch[slot], tokens[t],
sizeof(joyDeviceMatch[slot]) - strlen(joyDeviceMatch[slot]) - 1);
}
return 0;
}
//
// joyaxis <AXIS> axis <channel> [invert] [slew <rate>] [deadzone <f>]
//
if (_stricmp(tokens[0], "joyaxis") == 0)
{
if (tokenCount < 4 || _stricmp(tokens[2], "axis") != 0)
{
return -1;
}
int axis = LookupName(joyAxisNames,
sizeof(joyAxisNames)/sizeof(joyAxisNames[0]), tokens[1]);
int channel = LookupName(channelNames,
sizeof(channelNames)/sizeof(channelNames[0]), tokens[3]);
if (axis < 0 || channel < 0 ||
joyAxisBindingCount >= (int)(sizeof(joyAxisBindings)/sizeof(joyAxisBindings[0])))
{
return -1;
}
JoyAxisBinding &b = joyAxisBindings[joyAxisBindingCount];
b.device = parseJoyDevice;
b.axis = axis;
b.channel = channel;
b.invert = 0;
b.slew = 0;
b.slewRate = 0.0f;
b.deadzone = 0.08f; // stick default; levers set deadzone 0
for (int t = 4; t < tokenCount; ++t)
{
if (_stricmp(tokens[t], "invert") == 0)
{
b.invert = 1;
}
else if (_stricmp(tokens[t], "slew") == 0 && t+1 < tokenCount)
{
b.slew = 1;
b.slewRate = (float)atof(tokens[++t]);
}
else if (_stricmp(tokens[t], "deadzone") == 0 && t+1 < tokenCount)
{
b.deadzone = (float)atof(tokens[++t]);
if (b.deadzone < 0.0f) b.deadzone = 0.0f;
if (b.deadzone > 0.9f) b.deadzone = 0.9f;
}
else
{
return -1;
}
}
++joyAxisBindingCount;
return 0;
}
//
// joyhat's action clause starts one token later (joyhat <n> <dir> ...).
//
if (_stricmp(tokens[0], "joyhat") == 0)
{
actionAt = 3;
if (tokenCount < 5)
{
return -1;
}
}
//
// key/pad/joybutton/joyhat rows: parse the action clause.
//
if (_stricmp(tokens[actionAt], "button") == 0)
{
@@ -486,6 +622,45 @@ int
++padButtonBindingCount;
return 0;
}
else if (_stricmp(tokens[0], "joybutton") == 0)
{
if (action.kind != ActionButton && action.kind != ActionKeypad)
{
return -1; // joystick buttons cannot drive axes; use joyaxis
}
int button = (int)strtol(tokens[1], NULL, 0);
if (button < 0 || button >= 32 ||
joyButtonBindingCount >= (int)(sizeof(joyButtonBindings)/sizeof(joyButtonBindings[0])))
{
return -1;
}
joyButtonBindings[joyButtonBindingCount].device = parseJoyDevice;
joyButtonBindings[joyButtonBindingCount].button = button;
joyButtonBindings[joyButtonBindingCount].action = action;
++joyButtonBindingCount;
return 0;
}
else if (_stricmp(tokens[0], "joyhat") == 0)
{
if (action.kind != ActionButton)
{
return -1; // hat directions map to RIO buttons only
}
int hat = (int)strtol(tokens[1], NULL, 0);
int direction = LookupName(hatDirectionNames,
sizeof(hatDirectionNames)/sizeof(hatDirectionNames[0]), tokens[2]);
if (hat < 0 || hat >= 4 || direction < 0 ||
joyHatBindingCount >= (int)(sizeof(joyHatBindings)/sizeof(joyHatBindings[0])))
{
return -1;
}
joyHatBindings[joyHatBindingCount].device = parseJoyDevice;
joyHatBindings[joyHatBindingCount].hat = hat;
joyHatBindings[joyHatBindingCount].direction = direction;
joyHatBindings[joyHatBindingCount].action = action;
++joyHatBindingCount;
return 0;
}
return -1;
}
@@ -495,6 +670,11 @@ void
keyBindingCount = 0;
padButtonBindingCount = 0;
padAxisBindingCount = 0;
joyAxisBindingCount = 0;
joyButtonBindingCount = 0;
joyHatBindingCount = 0;
parseJoyDevice = 0;
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
//
// Parse the built-in default text line by line (one code path for
@@ -540,6 +720,11 @@ void
keyBindingCount = 0;
padButtonBindingCount = 0;
padAxisBindingCount = 0;
joyAxisBindingCount = 0;
joyButtonBindingCount = 0;
joyHatBindingCount = 0;
parseJoyDevice = 0;
memset(joyDeviceMatch, 0, sizeof(joyDeviceMatch));
char line[256];
int line_number = 0;
@@ -576,7 +761,10 @@ void
}
DEBUG_STREAM << "[padrio] bindings loaded: " << keyBindingCount
<< " keys, " << padButtonBindingCount << " pad buttons, "
<< padAxisBindingCount << " pad axes"
<< padAxisBindingCount << " pad axes, "
<< joyAxisBindingCount << " joy axes, "
<< joyButtonBindingCount << " joy buttons, "
<< joyHatBindingCount << " joy hats"
<< (rejected ? " (rejected lines logged above)" : "")
<< "\n" << std::flush;
}
+66
View File
@@ -18,6 +18,11 @@
// key <KEYNAME> axis <channel> set <value>
// pad <PADBTN> button <addr>
// padaxis <PADAXIS> axis <channel> [invert] [slew <rate>]
// joydev <slot 0-3> [product-name substring...]
// joyaxis <JOYAXIS> axis <channel> [invert] [slew <rate>] [deadzone <f>]
// joybutton <n> button <addr>
// joybutton <n> keypad <unit> <key>
// joyhat <n> <up|down|left|right> button <addr>
//
// deflect = held drives the channel toward +/-1 at <rate>/s, auto-centers
// on release (the spring-centered stick model).
@@ -25,6 +30,17 @@
// lever model).
// set = press jumps the channel to <value> (all-stop detent).
//
// joy* rows (generic DirectInput joysticks -- flight sticks, HOTAS,
// pedals; L4JOY.h) attach to the most recent joydev slot (slot 0 if none
// given). A slot with a name substring binds THAT device; without one it
// binds the Nth attached non-XInput device. JOYAXIS names follow the
// DirectInput layout: X Y Z RX RY RZ SL0 SL1 (twist is usually RZ, a
// HOTAS throttle usually Z or SL0). A direct (non-slew) joyaxis on the
// Throttle channel maps the full axis travel onto the 0..1 lever (a
// physical lever OWNS the channel); other channels follow the pad model
// (outside the deadzone owns the channel). BT_JOYCONFIG=1 generates
// these rows interactively.
//
//###########################################################################
class PadBindingProfile
@@ -88,6 +104,46 @@ public:
float slewRate; // full-deflection slew speed (units/s)
};
//
// Generic-joystick bindings (DirectInput devices -- L4JOY.h). device
// is a joydev SLOT (0-3), resolved at poll time by the slot's name
// substring (or ordinal when the slot has none).
//
enum { JoyDeviceSlots = 4 };
enum JoyAxis {
JoyAxisX = 0, JoyAxisY, JoyAxisZ,
JoyAxisRX, JoyAxisRY, JoyAxisRZ,
JoyAxisSL0, JoyAxisSL1,
JoyAxisCount
};
struct JoyAxisBinding
{
int device; // joydev slot
int axis; // JoyAxis
int channel; // Channel
int invert;
int slew;
float slewRate;
float deadzone; // 0..1 fraction (rescaled past it)
};
struct JoyButtonBinding
{
int device; // joydev slot
int button; // 0-31
Action action; // ActionButton / ActionKeypad only
};
struct JoyHatBinding
{
int device; // joydev slot
int hat; // 0-3
int direction; // 0 up / 1 right / 2 down / 3 left
Action action; // ActionButton only
};
PadBindingProfile();
~PadBindingProfile();
@@ -107,7 +163,17 @@ public:
int padAxisBindingCount;
PadAxisBinding padAxisBindings[12];
int joyAxisBindingCount;
JoyAxisBinding joyAxisBindings[24];
int joyButtonBindingCount;
JoyButtonBinding joyButtonBindings[48];
int joyHatBindingCount;
JoyHatBinding joyHatBindings[16];
char joyDeviceMatch[JoyDeviceSlots][64]; // "" = ordinal slot
protected:
int parseJoyDevice; // joydev slot for subsequent joy rows
void
LoadDefaults();
void
+128
View File
@@ -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
+10
View File
@@ -109,6 +109,16 @@ protected:
previousPadButtons;
unsigned char
previousKeyHeld[192]; // per key BINDING (parallel to the profile)
//
// Generic-joystick edge state, per BINDING (parallel to the profile's
// joy tables -- the previousKeyHeld pattern). A device detach reads
// everything as released, so held buttons let go automatically (the
// issue-#24 rule).
//
unsigned char
previousJoyButtonHeld[48];
unsigned char
previousJoyHatHeld[16];
float
channelValue[PadBindingProfile::ChannelCount];
float
+11
View File
@@ -197,6 +197,17 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
? (std::ios::out | std::ios::app) : std::ios::out);
std::cout.rdbuf(logfile.rdbuf());
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
// the player moves for each pod control and write the joystick section
// of content\bindings.txt, then the boot continues into the game with
// the new bindings live (PadRIO loads the file after this point).
if (getenv("BT_JOYCONFIG") != NULL && *getenv("BT_JOYCONFIG") != '0')
{
extern int BTJoyConfigWizard(void);
BTJoyConfigWizard();
}
// MP wire-format probe (env BT_NET_PROBE=1): print the exact packet constants
// the console emulator (tools/btconsole.py) must speak, computed from OUR build
// (message IDs chain across class enums at compile time -- never hand-compute).
+4 -1
View File
@@ -53,7 +53,10 @@ CONTROLS (keyboard):
weapon panel's red PROGRAM button with the mouse and tap a fire key;
the joystick diagram on the panel shows the binding live.
An Xbox controller works out of the box. Rebind keys in
An Xbox controller works out of the box. FLIGHT STICKS / HOTAS /
pedals: run joyconfig.bat ONCE -- a wizard asks you to move each
control (stick, twist, throttle, fire buttons), writes the bindings,
and drops you into the solo menu to try them. Rebind keys in
content\bindings.txt (regenerates with defaults if deleted).
This build: {VERSION}. Private -- do not redistribute.
+34
View File
@@ -0,0 +1,34 @@
@echo off
rem BT411 -- one-time JOYSTICK / HOTAS / pedals setup.
rem A console wizard asks you to move each control (stick, twist/rudder,
rem throttle, fire buttons); it detects what you moved and writes the
rem joystick section of content\bindings.txt. When it finishes the game
rem continues into the solo menu so you can try the bindings immediately.
rem Xbox-class controllers need NO setup -- they work out of the box.
rem Re-run this any time to redo the bindings.
cd /d %~dp0
set BT_PLATFORM=glass
if not exist "build\Release\btl4.exe" goto badpath
if not exist "content\OPERATOR.EGG" goto badpath
set BT_JOYCONFIG=1
set BT_START_INSIDE=1
set BT_DEV_GAUGES=1
set BT_LOG=join.log
set BT_FE_SOLO=1
cd content
..\build\Release\btl4.exe
exit /b
:badpath
echo.
echo ==================================================
echo ERROR: this file is not in the right place.
echo.
echo It must sit in the EXTRACTED game folder, next to
echo the 'content' and 'build' folders.
echo.
echo Fix: right-click the .zip - Extract All, open the
echo extracted folder, and run this file from THERE.
echo (Do NOT run it from inside the zip.)
echo ==================================================
echo.
pause
+2 -1
View File
@@ -75,7 +75,8 @@ def main():
zpath = os.path.join(outdir, name + ".zip")
exes = ["build/Release/btl4.exe"]
bats = ["players/play_solo.bat", "players/join.bat", "players/join_lan.bat"]
bats = ["players/play_solo.bat", "players/join.bat", "players/join_lan.bat",
"players/joyconfig.bat"]
if steam_on:
bats.append("players/play_steam.bat")
for p in exes + bats: