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
@@ -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"
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user