Input remap: CONTROLS.MAP binding engine + XInput gamepad support
The pod's controls reach the game through the RIO serial board; on a
desktop that hardware is a keyboard shim of hardcoded GetAsyncKeyState
reads. New binding engine (game/reconstructed/btinput.cpp) maps PC keys
and an XInput pad onto the authentic channels through a user-editable
file (content/CONTROLS.MAP, community-suggested grammar, corrected):
key/pad -> button <addr> real buttonGroup emissions, mirroring the
RIO convention exactly (press a+1 w/ mode
mask saved, release -a-1 w/ saved mask;
L4CTRL.cpp:2470-2520) -- aux/preset banks,
hotbox, panic, reverse thrust all reachable
key/pad -> axis Throttle rate (lever), pedals/JoystickX
deflect (turn/twist) into the existing
virtual-controls integrators (feel, gait
detent, spring-centering all unchanged)
keypad pilot|external <n> keyboardGroup key values ('0'-'F')
pckey <char> any authentic 1995 typed hotkey
action <name> port dev controls (view, all-stop, ...)
Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP
feed -- ends the historic double-dispatch ('w' drove AND selected pilot
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups fed
the developer fake-event dispatcher). Unbound keys keep their authentic
meaning. The dual-use 'V' is split: V = view toggle, B = look behind.
Default profile = WASD classic (compiled-in twin; delete the file to
restore). CONTROLS_NUMPAD.MAP ships the corrected community layout
(keypads are NOT buttons 0x50-0x6F -- that space doesn't exist; no
clickable cockpit exists so everything needs a binding; keyboard fire
buttons added; 0x36/0x37 are hotbox not 'config'; missiles moved off
Ctrl). XInput loads dynamically (1_4 -> 9_1_0), disconnected-pad
probing rate-limited; pad sticks write the axes as absolute positions
(the spring is physical), triggers/buttons per the file.
Verified live: bindings load (43), W and NumPad8 drive (speedDemand 0 ->
61.5), X all-stop (spd -> 0), aux-bank emission (D1 -> [input] 0x2f
PRESS/release under the numpad profile), suppression both directions
(posted 'r' reaches [keych], posted 'w' swallowed), full-keyspace
WM_CHAR+WM_KEYUP fuzz x3 survived with sim advancing, XInput graceful
with no pad, 2-node relay session un-regressed (full ladder + UDP).
Pad-in-hand testing still needs a physical controller.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0fcd0c3106
commit
980c9cd7e5
@@ -155,6 +155,7 @@
|
||||
#include <messmgr.hpp> // SubsystemMessageManager (task #7 consolidated delivery)
|
||||
#include <mechweap.hpp> // MechWeapon::GetExplosionResourceID (per-round detonation)
|
||||
#include <mislanch.hpp> // MissileLauncher::ClassDerivations (splash-radius gate)
|
||||
#include "btinput.hpp" // the CONTROLS.MAP + XInput binding engine
|
||||
#if !defined(PLAYER_HPP)
|
||||
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
|
||||
#endif
|
||||
@@ -2527,76 +2528,45 @@ void
|
||||
const float kStickRate = 2.5f; // stick deflect per second
|
||||
const float kStickCenterRate = 5.0f; // stick auto-center per second
|
||||
|
||||
// POLL the real key state. WndProc key messages are unusable: the
|
||||
// POLL the input state. WndProc key messages are unusable: the
|
||||
// engine keyboard reader (L4CTRL.cpp:1506) GetMessage()s every
|
||||
// WM_KEYUP/WM_CHAR out of the queue for its key-command channel, so
|
||||
// only KEYDOWNs ever reached the WndProc and key state latched on
|
||||
// forever. GetAsyncKeyState is immune; the foreground guard keeps
|
||||
// background typing from driving the mech.
|
||||
// WM_KEYUP/WM_CHAR out of the queue for its key-command channel,
|
||||
// so the binding engine polls GetAsyncKeyState (+ XInput); the
|
||||
// foreground guard (BT_KEY_NOFOCUS override) lives inside it.
|
||||
{
|
||||
typedef short (__stdcall *AsyncFn)(int);
|
||||
typedef void *(__stdcall *FgFn)(void);
|
||||
typedef unsigned long (__stdcall *WtpFn)(void *, unsigned long *);
|
||||
typedef unsigned long (__stdcall *PidFn)(void);
|
||||
static AsyncFn pAsync = 0; static FgFn pFg = 0;
|
||||
static WtpFn pWtp = 0; static PidFn pPid = 0;
|
||||
if (pAsync == 0)
|
||||
{
|
||||
HMODULE u = GetModuleHandleA("user32.dll");
|
||||
HMODULE k = GetModuleHandleA("kernel32.dll");
|
||||
pAsync = (AsyncFn)GetProcAddress(u, "GetAsyncKeyState");
|
||||
pFg = (FgFn)GetProcAddress(u, "GetForegroundWindow");
|
||||
pWtp = (WtpFn)GetProcAddress(u, "GetWindowThreadProcessId");
|
||||
pPid = (PidFn)GetProcAddress(k, "GetCurrentProcessId");
|
||||
}
|
||||
int focused = 0;
|
||||
if (pFg && pWtp && pPid)
|
||||
{
|
||||
void *fg = pFg();
|
||||
unsigned long fgPid = 0;
|
||||
if (fg) pWtp(fg, &fgPid);
|
||||
focused = (fgPid == pPid());
|
||||
}
|
||||
// TEST HOOK (BT_KEY_NOFOCUS=1): accept keys without foreground focus --
|
||||
// automation harnesses (SendInput from a background shell) can't grant
|
||||
// real foreground; interactive play never needs this.
|
||||
static int sNoFocus = -1;
|
||||
if (sNoFocus < 0) { const char *nf = getenv("BT_KEY_NOFOCUS"); sNoFocus = (nf && *nf == '1') ? 1 : 0; }
|
||||
if (sNoFocus) focused = 1;
|
||||
if (pAsync)
|
||||
{
|
||||
const int dn = 0x8000;
|
||||
gBTDrive.keyFwd = focused && ((pAsync('W') | pAsync(0x26 /*VK_UP*/)) & dn) ? 1 : 0;
|
||||
gBTDrive.keyBack = focused && ((pAsync('S') | pAsync(0x28 /*VK_DOWN*/)) & dn) ? 1 : 0;
|
||||
gBTDrive.keyLeft = focused && ((pAsync('A') | pAsync(0x25 /*VK_LEFT*/)) & dn) ? 1 : 0;
|
||||
gBTDrive.keyRight = focused && ((pAsync('D') | pAsync(0x27 /*VK_RIGHT*/)) & dn) ? 1 : 0;
|
||||
// WEAPON GROUPS (task #43, KEYBOARD only per user): three fire
|
||||
// channels like three pod buttons -- 1/SPACE = lasers, 2 = PPCs,
|
||||
// 3/CTRL = missiles. (Interim; the authentic system is the
|
||||
// ConfigureMappables/ChooseButton mapper channels.)
|
||||
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
|
||||
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
|
||||
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
|
||||
// The pod's 4TH fire button (Pinky, 0x45) -- previously
|
||||
// unmapped on desktop, so any weapon the authored groups
|
||||
// put there (the Avatar's NARC etc.) was unreachable.
|
||||
gBTPinkyKey = focused && (pAsync('4') & dn) ? 1 : 0;
|
||||
// task #6: HOLD 'G' opens the config session on the selected
|
||||
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
|
||||
// held, the fire keys TOGGLE its group membership.
|
||||
gBTConfigKey = focused && (pAsync('G') & dn) ? 1 : 0;
|
||||
// task #12: F5..F8 assign the selected weapon to Generator
|
||||
// A..D; F9 toggles Manual/Auto reconnect.
|
||||
gBTGenSelKey =
|
||||
(focused && (pAsync(0x74 /*F5*/) & dn)) ? 4
|
||||
: (focused && (pAsync(0x75 /*F6*/) & dn)) ? 5
|
||||
: (focused && (pAsync(0x76 /*F7*/) & dn)) ? 6
|
||||
: (focused && (pAsync(0x77 /*F8*/) & dn)) ? 7
|
||||
: (focused && (pAsync(0x78 /*F9*/) & dn)) ? 8
|
||||
: 0;
|
||||
// task #13: 'C' cycles the coolant valve (BT_VALVE_SLOT
|
||||
// picks the condenser roster slot; default = Condenser1).
|
||||
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
|
||||
// INPUT REMAP (2026-07-18): the raw GetAsyncKeyState reads
|
||||
// moved behind the binding engine (btinput.cpp) -- keys +
|
||||
// XInput pad resolve through content/CONTROLS.MAP (built-in
|
||||
// WASD-classic profile when the file is absent). RIO lamp
|
||||
// buttons / keypads / authentic pckey hotkeys are emitted
|
||||
// into the controls manager inside the poll; the fire
|
||||
// channels, actions, and analog demands publish through
|
||||
// gBTInput. Everything below the reads (the lever/stick/
|
||||
// twist integrators, detents, edge latches) is UNCHANGED.
|
||||
BTInputPoll(dt);
|
||||
gBTDrive.keyFwd = gBTInput.leverRate > 0.0001f ? 1 : 0;
|
||||
gBTDrive.keyBack = gBTInput.leverRate < -0.0001f ? 1 : 0;
|
||||
gBTDrive.keyLeft = (gBTInput.turnActive && gBTInput.turnTarget < 0.0f) ? 1 : 0;
|
||||
gBTDrive.keyRight = (gBTInput.turnActive && gBTInput.turnTarget > 0.0f) ? 1 : 0;
|
||||
// WEAPON GROUPS (task #43): the four pod joystick fire
|
||||
// buttons -> the bring-up fire channels (0x40 Main = laser
|
||||
// group, 0x46 ThumbLow = PPCs, 0x47 ThumbHigh = missiles,
|
||||
// 0x45 Pinky = the 4th group). (Interim; the authentic
|
||||
// system is the ConfigureMappables/ChooseButton mapper.)
|
||||
gBTLaserKey = gBTInput.fireTrigger;
|
||||
gBTPPCKey = gBTInput.fireThumbLow;
|
||||
gBTMissileKey = gBTInput.fireThumbHigh;
|
||||
gBTPinkyKey = gBTInput.firePinky;
|
||||
// task #6: HOLD the ConfigHold action opens the config
|
||||
// session on the selected weapon; while held, the fire
|
||||
// keys TOGGLE its group membership.
|
||||
gBTConfigKey = gBTInput.configHold;
|
||||
// task #12: Generator1-4 actions assign the selected weapon
|
||||
// to Generator A..D; Reconnect toggles Manual/Auto.
|
||||
gBTGenSelKey = gBTInput.genSel;
|
||||
// task #13: the Valve action cycles the coolant valve.
|
||||
gBTValveKey = gBTInput.valve;
|
||||
// TORSO CONTROLS (2026-07-13): 'M' cycles the control mode
|
||||
// (Basic -> Standard -> Veteran -- the pod console button,
|
||||
// CycleControlModeMessageHandler); Q/E deflect the torso
|
||||
@@ -2604,19 +2574,23 @@ void
|
||||
// become the pedals). Ramped like the turn stick.
|
||||
{
|
||||
static int sPrevM = 0;
|
||||
const int mNow = focused && (pAsync('M') & dn) ? 1 : 0;
|
||||
const int mNow = gBTInput.modeCycle;
|
||||
if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle
|
||||
sPrevM = mNow;
|
||||
// (task #68) 'V' HELD = look behind (the pod's rear-view
|
||||
// button; releases back to the forward view). Rear-mounted
|
||||
// (task #68) LookBehind action HELD = the pod's rear-view
|
||||
// button (releases back to the forward view). Rear-mounted
|
||||
// weapons (blackhawk/owens back racks) fire only in it.
|
||||
gBTLookBehind = focused && (pAsync('V') & dn) ? 1 : 0;
|
||||
const int tw = (focused && (pAsync('E') & dn) ? 1 : 0)
|
||||
- (focused && (pAsync('Q') & dn) ? 1 : 0);
|
||||
gBTLookBehind = gBTInput.lookBehind;
|
||||
static float sTwist = 0.0f;
|
||||
if (tw != 0)
|
||||
if (gBTInput.twistAbsolute)
|
||||
{
|
||||
sTwist += tw * kStickRate * dt;
|
||||
// pad stick: the axis IS the stick position (the
|
||||
// spring is in the physical stick, not software)
|
||||
sTwist = gBTInput.twistTarget;
|
||||
}
|
||||
else if (gBTInput.twistActive)
|
||||
{
|
||||
sTwist += gBTInput.twistTarget * kStickRate * dt;
|
||||
if (sTwist > 1.0f) sTwist = 1.0f;
|
||||
if (sTwist < -1.0f) sTwist = -1.0f;
|
||||
}
|
||||
@@ -2642,7 +2616,7 @@ void
|
||||
// edge detector from the drive all-stop below -- both
|
||||
// fire on the same press.
|
||||
static int sPrevXT = 0;
|
||||
const int xtNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
||||
const int xtNow = gBTInput.allStop;
|
||||
if (xtNow && !sPrevXT)
|
||||
{
|
||||
sTwist = 0.0f;
|
||||
@@ -2655,13 +2629,15 @@ void
|
||||
// damage dispatcher + the beam-visual keepalive)
|
||||
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
|
||||
static int sPrevX = 0;
|
||||
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
||||
const int xNow = gBTInput.allStop;
|
||||
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
|
||||
sPrevX = xNow;
|
||||
// V: toggle the view between the authentic COCKPIT eyepoint
|
||||
// (the pod's only view) and the external chase camera.
|
||||
// ViewToggle action: the authentic COCKPIT eyepoint (the
|
||||
// pod's only view) <-> the external chase camera. Split
|
||||
// from LookBehind (2026-07-18): the old shim had BOTH on
|
||||
// 'V', so holding rear-view also flipped the camera.
|
||||
static int sPrevV = 0, sViewInside = 0;
|
||||
const int vNow = focused && (pAsync('V') & dn) ? 1 : 0;
|
||||
const int vNow = gBTInput.viewToggle;
|
||||
if (vNow && !sPrevV)
|
||||
{
|
||||
sViewInside = !sViewInside;
|
||||
@@ -2849,7 +2825,9 @@ void
|
||||
}
|
||||
}
|
||||
}
|
||||
float sweep = ((fwd ? 1.0f : 0.0f) - (back ? 1.0f : 0.0f)) * kLeverRate * dt;
|
||||
// the configured rate (CONTROLS.MAP `axis Throttle rate n`) IS the
|
||||
// sweep speed; kLeverRate above stays as the documented default
|
||||
float sweep = gBTInput.leverRate * dt;
|
||||
if (sweep != 0.0f && !sDetent)
|
||||
{
|
||||
const float prev = sLever;
|
||||
@@ -2866,8 +2844,13 @@ void
|
||||
if (sLever < -1.0f) sLever = -1.0f;
|
||||
}
|
||||
|
||||
const float want = (gBTDrive.keyRight ? 1.0f : 0.0f) - (gBTDrive.keyLeft ? 1.0f : 0.0f);
|
||||
if (want != 0.0f)
|
||||
const float want = gBTInput.turnActive ? gBTInput.turnTarget : 0.0f;
|
||||
if (gBTInput.turnAbsolute)
|
||||
{
|
||||
// pad stick: the axis IS the stick position (physically sprung)
|
||||
sStick = want;
|
||||
}
|
||||
else if (want != 0.0f)
|
||||
{
|
||||
sStick += want * kStickRate * dt;
|
||||
if (sStick > 1.0f) sStick = 1.0f;
|
||||
|
||||
Reference in New Issue
Block a user