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
@@ -72,7 +72,7 @@ precise than anything you can infer.
|
||||
| First-person cockpit canopy (*_cop) + the eyepoint camera | `context/cockpit-view.md` |
|
||||
| Cockpit gauges / MFD HUD | `context/gauges-hud.md` |
|
||||
| Multiplayer, replication, netcode | `context/multiplayer.md` |
|
||||
| Pod hardware, monitors, RIO, MFD surfaces | `context/pod-hardware.md` |
|
||||
| Pod hardware, monitors, RIO, MFD surfaces, input remap (CONTROLS.MAP/XInput) | `context/pod-hardware.md` |
|
||||
| Content archives, maps, where data lives | `context/content-archives.md` |
|
||||
| Offsets, ClassIDs, addresses, resource types, env vars | `context/decomp-reference.md` |
|
||||
| What we don't know / deferred systems | `context/open-questions.md` |
|
||||
|
||||
@@ -238,6 +238,7 @@ target_compile_options(munga_engine PRIVATE ${BT_OPTS})
|
||||
add_library(bt410_l4 STATIC
|
||||
"game/reconstructed/ammobin.cpp"
|
||||
"game/reconstructed/btdirect.cpp"
|
||||
"game/reconstructed/btinput.cpp"
|
||||
"game/reconstructed/btl4app.cpp"
|
||||
"game/reconstructed/btl4galm.cpp"
|
||||
"game/reconstructed/btl4gau2.cpp"
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# ==========================================================================
|
||||
# BT411 CONTROLS.MAP -- desktop input bindings (keyboard + XInput gamepad)
|
||||
# ==========================================================================
|
||||
# One binding per line, '#' starts a comment, keywords are case-insensitive.
|
||||
# Loaded at game start; DELETE THIS FILE to restore these exact defaults
|
||||
# (the same profile is compiled in). Alternate profile: CONTROLS_NUMPAD.MAP
|
||||
# (copy it over this file to use it).
|
||||
#
|
||||
# key <name> button <addr> [toggle]
|
||||
# key <name> axis <axis> deflect <n>
|
||||
# key <name> axis <axis> rate <n-per-second>
|
||||
# key <name> keypad pilot|external <0-15>
|
||||
# key <name> pckey <char>
|
||||
# key <name> action <name>
|
||||
# pad <button> button <addr> | action <name> | pckey <char> | keypad ...
|
||||
# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]
|
||||
#
|
||||
# <addr> RIO button address 0x00-0x47 (hex or decimal):
|
||||
# 0x00-0x0F aux lower right/left banks 0x10-0x1B secondary panel
|
||||
# 0x20-0x37 aux upper center/left/right (presets + target hotbox)
|
||||
# 0x3D Panic 0x3F throttle-head (REVERSE THRUST)
|
||||
# 0x40 Main trigger 0x41-0x44 hat 0x45 Pinky 0x46 Middle 0x47 Upper
|
||||
# <axis> Throttle | LeftPedal | RightPedal | Pedals (signed, + = right)
|
||||
# | JoystickX (torso twist) | JoystickY
|
||||
# <name> A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9, Up, Down, Left,
|
||||
# Right, Space, Enter, Shift, Ctrl, Alt, PageUp, PageDown, Home,
|
||||
# End, Insert, Delete, OemMinus, Oemplus, Oemcomma, OemPeriod,
|
||||
# OemOpenBrackets, OemCloseBrackets, ...
|
||||
# <button> A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back
|
||||
# LeftShoulder RightShoulder LeftThumb RightThumb
|
||||
# LeftTrigger RightTrigger (as digital, >= 50% pressed)
|
||||
# <src> LeftStickX LeftStickY RightStickX RightStickY
|
||||
# LeftTrigger RightTrigger
|
||||
# action ViewToggle LookBehind AllStop ModeCycle Valve ConfigHold
|
||||
# Generator1-4 Reconnect
|
||||
# pckey sends an authentic 1995 PC-keyboard hotkey (the in-cockpit
|
||||
# dispatcher): '+'/'-' = target zoom, 'w'..'o' = pilot select,
|
||||
# 'a','b','c','d','f','g','s','v','x','z' = display presets, ...
|
||||
#
|
||||
# 'deflect' holds the axis there while the key is down and springs back on
|
||||
# release; 'rate' walks the axis by <n> per second and the position sticks
|
||||
# (the throttle lever). A key bound here is REMOVED from the legacy typed-
|
||||
# hotkey channel (no double dispatch); unbound keys keep their authentic
|
||||
# 1995 meaning (see pckey above).
|
||||
#
|
||||
# ---- WASD CLASSIC (the shipped default) ----------------------------------
|
||||
|
||||
# Driving: W/S sweep the throttle lever (sticks, detent at zero); A/D are
|
||||
# the turn pedals (spring back); Q/E twist the torso; X = all stop.
|
||||
key W axis Throttle rate 0.7
|
||||
key S axis Throttle rate -0.7
|
||||
key Up axis Throttle rate 0.7
|
||||
key Down axis Throttle rate -0.7
|
||||
key A axis LeftPedal deflect 1
|
||||
key D axis RightPedal deflect 1
|
||||
key Left axis LeftPedal deflect 1
|
||||
key Right axis RightPedal deflect 1
|
||||
key Q axis JoystickX deflect -1
|
||||
key E axis JoystickX deflect 1
|
||||
key X action AllStop
|
||||
|
||||
# Fire: the pod joystick's four buttons.
|
||||
key D1 button 0x40 # Main trigger (lasers group)
|
||||
key Space button 0x40
|
||||
key D2 button 0x46 # Middle thumb (PPCs group)
|
||||
key D3 button 0x47 # Upper thumb (missiles group)
|
||||
key Ctrl button 0x47
|
||||
key D4 button 0x45 # Pinky (4th group)
|
||||
|
||||
# Systems.
|
||||
key G action ConfigHold # hold + fire keys = regroup the weapon
|
||||
key C action Valve # coolant valve cycle
|
||||
key M action ModeCycle # Basic -> Standard -> Veteran controls
|
||||
key V action ViewToggle # cockpit <-> chase camera (dev)
|
||||
key B action LookBehind # HOLD = the pod's rear-view button
|
||||
key F5 action Generator1
|
||||
key F6 action Generator2
|
||||
key F7 action Generator3
|
||||
key F8 action Generator4
|
||||
key F9 action Reconnect
|
||||
|
||||
# ---- Xbox controller -----------------------------------------------------
|
||||
# Left stick steers (the pedals), right stick twists the torso and pushes
|
||||
# the throttle lever; triggers fire.
|
||||
padaxis LeftStickX axis Pedals deadzone 0.24
|
||||
padaxis RightStickX axis JoystickX deadzone 0.24
|
||||
padaxis RightStickY axis Throttle rate 0.75 deadzone 0.24
|
||||
pad RightTrigger button 0x40 # Main
|
||||
pad LeftTrigger button 0x47 # Upper (missiles)
|
||||
pad RightShoulder button 0x46 # Middle (PPCs)
|
||||
pad LeftShoulder button 0x45 # Pinky
|
||||
pad DPadUp button 0x42 # hat up
|
||||
pad DPadDown button 0x41 # hat back
|
||||
pad DPadLeft button 0x44 # hat left
|
||||
pad DPadRight button 0x43 # hat right
|
||||
pad B action AllStop
|
||||
pad Y action ViewToggle
|
||||
pad X action LookBehind
|
||||
pad Start action ModeCycle
|
||||
pad Back action Valve
|
||||
@@ -0,0 +1,99 @@
|
||||
# ==========================================================================
|
||||
# BT411 CONTROLS_NUMPAD.MAP -- the community-suggested numpad layout
|
||||
# (corrected). To use it: copy this file over CONTROLS.MAP.
|
||||
# Grammar + address reference: see CONTROLS.MAP.
|
||||
# ==========================================================================
|
||||
# Corrections vs. the original suggestion:
|
||||
# - keypads are NOT button addresses 0x50-0x6F (that space doesn't exist);
|
||||
# use `keypad pilot|external <0-15>` when those panels get mapped
|
||||
# - there is no clickable on-screen cockpit -- every function you want
|
||||
# must be bound here
|
||||
# - keyboard fire buttons added (the suggestion only had Main; Pinky/
|
||||
# Middle/Upper were pad-only, stranding weapons regrouped onto them)
|
||||
# - Ctrl throttle-down kept, but missiles moved off Ctrl to NumPad3
|
||||
# - 0x36/0x37 are target-hotbox buttons (pilot select), not "config 1/2"
|
||||
# - port actions (view, all-stop, mode cycle, valve, generators) bound
|
||||
#
|
||||
# ---- Driving: number pad ------------------------------------------------
|
||||
key NumPad8 axis Throttle rate 0.75 # lever up
|
||||
key NumPad2 axis Throttle rate -0.75 # lever down
|
||||
key NumPad4 axis LeftPedal deflect 1 # turn left
|
||||
key NumPad6 axis RightPedal deflect 1 # turn right
|
||||
key NumPad7 axis JoystickX deflect -1 # torso twist left
|
||||
key NumPad9 axis JoystickX deflect 1 # torso twist right
|
||||
key NumPad5 action AllStop
|
||||
key Shift axis Throttle rate 0.75 # throttle up (alt)
|
||||
key Ctrl axis Throttle rate -0.75 # throttle down (alt)
|
||||
key Alt button 0x3F # throttle-head = REVERSE THRUST
|
||||
|
||||
# ---- Fire: numpad cluster + space ---------------------------------------
|
||||
key Space button 0x40 # Main trigger
|
||||
key NumPad0 button 0x40
|
||||
key NumPad1 button 0x45 # Pinky
|
||||
key NumPad3 button 0x47 # Upper (missiles)
|
||||
key Decimal button 0x46 # Middle (PPCs)
|
||||
|
||||
# ---- Joystick hat on the arrows -----------------------------------------
|
||||
key Up button 0x42 # Hat Up
|
||||
key Down button 0x41 # Hat Back
|
||||
key Left button 0x44 # Hat Left
|
||||
key Right button 0x43 # Hat Right
|
||||
|
||||
# ---- Systems ------------------------------------------------------------
|
||||
key G action ConfigHold
|
||||
key Enter action ModeCycle
|
||||
key V action ViewToggle
|
||||
key B action LookBehind
|
||||
key F5 action Generator1
|
||||
key F6 action Generator2
|
||||
key F7 action Generator3
|
||||
key F8 action Generator4
|
||||
key F9 action Reconnect
|
||||
key F10 action Valve
|
||||
|
||||
# ---- Upper MFD banks: number row = the top aux row, QWERTY row = the row
|
||||
# ---- under it, left to right across the Left / Center / Right panels.
|
||||
# ---- (These buttons drive the preset/eng-data selection; the letters
|
||||
# ---- lose their 1995 typed-hotkey meaning once bound here.)
|
||||
key D1 button 0x2F
|
||||
key D2 button 0x2E
|
||||
key D3 button 0x2D
|
||||
key D4 button 0x2C
|
||||
key D5 button 0x27
|
||||
key D6 button 0x26
|
||||
key D7 button 0x25
|
||||
key D8 button 0x24
|
||||
key D9 button 0x37 # hotbox (target select)
|
||||
key D0 button 0x36 # hotbox
|
||||
key OemMinus button 0x35 # hotbox
|
||||
key Oemplus button 0x34 # hotbox
|
||||
key Q button 0x2B
|
||||
key W button 0x2A
|
||||
key E button 0x29
|
||||
key R button 0x28
|
||||
key T button 0x23
|
||||
key Y button 0x22
|
||||
key U button 0x21
|
||||
key I button 0x20
|
||||
key O button 0x33 # hotbox
|
||||
key P button 0x32 # hotbox
|
||||
key OemOpenBrackets button 0x31 # hotbox
|
||||
key OemCloseBrackets button 0x30 # hotbox
|
||||
|
||||
# ---- Xbox controller (same as the default profile) ----------------------
|
||||
padaxis LeftStickX axis Pedals deadzone 0.24
|
||||
padaxis RightStickX axis JoystickX deadzone 0.24
|
||||
padaxis RightStickY axis Throttle rate 0.75 deadzone 0.24
|
||||
pad RightTrigger button 0x40
|
||||
pad LeftTrigger button 0x47
|
||||
pad RightShoulder button 0x46
|
||||
pad LeftShoulder button 0x45
|
||||
pad DPadUp button 0x42
|
||||
pad DPadDown button 0x41
|
||||
pad DPadLeft button 0x44
|
||||
pad DPadRight button 0x43
|
||||
pad B action AllStop
|
||||
pad Y action ViewToggle
|
||||
pad X action LookBehind
|
||||
pad Start action ModeCycle
|
||||
pad Back action Valve
|
||||
@@ -267,6 +267,7 @@ default-ON (`'0'` disables).
|
||||
| `BT_RELAY=<host:port\|auto>` | **D1 relay mode** — dial the relay/operator console instead of the P2P mesh; `auto` = LAN UDP-broadcast discovery on udp/15999 ([[multiplayer]] D1) |
|
||||
| `BT_SELF=<[pilots] tag>` | claim a specific roster seat in relay mode; absent/`auto` = the relay assigns one (SEAT_REQUEST) |
|
||||
| `BT_RELAY_TCP_ONLY=1` | disable the relay UDP channel (all unreliable traffic rides the TCP relay conn) |
|
||||
| `BT_INPUT_LOG` | binding-engine trace: CONTROLS.MAP load, button/keypad emissions, pad connect ([[pod-hardware]] §Desktop input remap) |
|
||||
|
||||
Full render/locomotion gates (BT_RAMP, BT_MATPRI, BT_CULL, BT_SHADOW_*, BT_LODSEL, BT_ADDLOD,
|
||||
BT_PUNCH, …) are catalogued in [[rendering]]. Warp visuals: [[translocation-warp]].
|
||||
|
||||
@@ -25,6 +25,38 @@ to keyboard/gamepad on a dev box; real wiring is a pod bring-up task (Phase 8).
|
||||
model — `LBE4ControlsManager` groups are fed by all devices (RIO on the pod, DirectInput on dev);
|
||||
the `MechControlsMapper` interprets them ([[locomotion]]). [T2]
|
||||
|
||||
### The button space + lamps [T0, L4CTRL.h enum]
|
||||
buttonGroup addresses 0x00-0x47: `AuxLowerRight/Left 1-8` (0x00-0x0F), `Secondary1-12` (0x10-0x1B),
|
||||
`AuxUpperCenter/Left/Right 1-8` (0x20-0x37 — the preset/eng "display eng data" banks; **0x30-0x37 =
|
||||
the target HOTBOX**, pilot select), icom/door 0x39-0x3C, `Panic` 0x3D (the config-mode lamp),
|
||||
`Throttle1` 0x3F (throttle-head = REVERSE THRUST), joystick cluster 0x40-0x47 (Main trigger 0x40,
|
||||
hat 0x41-0x44, Pinky/ThumbLow/ThumbHigh 0x45-0x47 — the four MAPPABLE fire buttons). "Lamp buttons"
|
||||
are literal: physical illuminated pushbuttons in panels around the screens; the RIO `LampRequest`
|
||||
protocol lights them (`MakeLinkedLamp`), reports burned-out bulbs (`RIODeadLamps` failure page,
|
||||
per-lamp names). A lit button = active. Keypads are NOT buttons: `keyboardGroup[KeyboardPilot/
|
||||
KeyboardExternal]` carry key VALUES ('0'-'9','A'-'F', L4CTRL.cpp:2526). RIO event convention:
|
||||
press = `buttonGroup[a].Update(a+1, modeMask)` (mask saved), release = `Update(-a-1, savedMask)`
|
||||
(L4CTRL.cpp:2470-2520).
|
||||
|
||||
### Desktop input remap — CONTROLS.MAP + XInput (2026-07-18) [T2 live]
|
||||
`game/reconstructed/btinput.cpp` + `content/CONTROLS.MAP` (WASD-classic default, compiled-in twin;
|
||||
`content/CONTROLS_NUMPAD.MAP` = the corrected community numpad profile). Grammar: `key <name>
|
||||
button <addr>|axis <axis> deflect|rate <n>|keypad pilot|external <0-15>|pckey <char>|action
|
||||
<name>`; `pad <button>` same targets; `padaxis <src> axis ... [invert] [deadzone d] [rate n]`.
|
||||
Axes: Throttle (lever, rate-walks + sticks), LeftPedal/RightPedal/Pedals (turn, springs),
|
||||
JoystickX (torso twist). Emissions mirror the RIO conventions above exactly; the four fire
|
||||
buttons (0x40/45/46/47) publish as levels to the bring-up fire channels instead of buttonGroup
|
||||
(avoids double-fire once the streamed ChooseButton mappings are exercised). XInput loads
|
||||
dynamically (xinput1_4 → 9_1_0; disconnected-pad probing rate-limited to 1 Hz). **Keys claimed by
|
||||
a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP feed** (`BTInputSuppressKey`, hook at
|
||||
L4CTRL.cpp keyboard read) — this ended the historic DOUBLE-DISPATCH: 'w' drove AND selected pilot
|
||||
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups (uppercase) fed the developer
|
||||
fake-event dispatcher. Unbound keys keep their authentic 1995 dispatcher meaning (reachable from
|
||||
any key via `pckey`). The old dual-use 'V' (view toggle + look-behind) is split: V = ViewToggle,
|
||||
B = LookBehind. Verified live: bindings load, W/NumPad8 drive (spd rises), X all-stop, aux-button
|
||||
emission ([input] 0x2f PRESS), suppression both directions ('r' delivered / 'w' swallowed),
|
||||
full-keyspace WM_CHAR+WM_KEYUP fuzz survived. Diag: `BT_INPUT_LOG`.
|
||||
|
||||
### The pod throttle is ANALOG-CONTINUOUS, not notched (verified end-to-end) [T1]
|
||||
The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity question):
|
||||
1. **Hardware → RIO:** serial `AnalogReply` packet → `Ranger("Throttle", 0, 800, .05)` — raw counts
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
#include "..\munga\appmgr.h"
|
||||
#include "dxutils.h"
|
||||
|
||||
//
|
||||
// BT port (input remap): keys claimed by a CONTROLS.MAP binding are consumed
|
||||
// by the binding engine (game/reconstructed/btinput.cpp) and must NOT also
|
||||
// reach the key-command channel -- that was the historic double-dispatch
|
||||
// ('w' drove AND selected pilot 0; F5's key-up value 0x74 aliased to the
|
||||
// 't' hotkey). The symbol resolves from the game lib at the btl4 exe link.
|
||||
//
|
||||
extern "C" int BTInputSuppressKey(unsigned int key_value, int is_char);
|
||||
|
||||
// #define LOCAL_TEST
|
||||
|
||||
#if defined(LOCAL_TEST)
|
||||
@@ -1527,6 +1536,11 @@ void
|
||||
{
|
||||
// swallowed: navigation key, not a character command
|
||||
}
|
||||
else if (BTInputSuppressKey((unsigned int)keyValue,
|
||||
msg.message == WM_CHAR))
|
||||
{
|
||||
// swallowed: remapped by CONTROLS.MAP
|
||||
}
|
||||
else
|
||||
{
|
||||
keyboardGroup[KeyboardPC].ForceUpdate(&keyValue, mode_mask);
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
//===========================================================================//
|
||||
// File: btinput.cpp //
|
||||
// Project: BattleTech Brick: BattleTech LBE Application //
|
||||
// Contents: The desktop input binding engine (CONTROLS.MAP + XInput). //
|
||||
//---------------------------------------------------------------------------//
|
||||
// PORT ADDITION (2026-07-18, input-remap task): the pod reads its stick / //
|
||||
// throttle / pedals / lamp buttons / keypads through the RIO serial board //
|
||||
// (L4RIO -> LBE4ControlsManager). On a desktop none of that hardware //
|
||||
// exists; this engine maps PC keys and an XInput gamepad onto the same //
|
||||
// channels through a user-editable text file. See btinput.hpp for the //
|
||||
// architecture and content/CONTROLS.MAP for the grammar + active profile. //
|
||||
// //
|
||||
// AUTHENTIC EMISSION CONVENTIONS (LBE4ControlsManager::ProcessRIOEvent, //
|
||||
// L4CTRL.cpp:2470-2520 -- mirrored exactly): //
|
||||
// button press: buttonGroup[a].Update(value = a+1, currentModeMask) //
|
||||
// button release: buttonGroup[a].Update(value = -a-1, mask saved at press)//
|
||||
// keypad key: keyboardGroup[unit].ForceUpdate('0'-'9'/'A'-'F', mask) //
|
||||
// //
|
||||
// The four pod JOYSTICK fire buttons (0x40 trigger / 0x45 pinky / 0x46 //
|
||||
// thumb-low / 0x47 thumb-high) do NOT go through buttonGroup here: the //
|
||||
// bring-up fire dispatcher (mech4.cpp weapon groups) consumes them as //
|
||||
// levels, and double-driving both channels would double-fire once the //
|
||||
// streamed ChooseButton mappings are exercised. They publish as //
|
||||
// gBTInput.fire* levels instead. //
|
||||
//===========================================================================//
|
||||
|
||||
#include <bt.hpp>
|
||||
#include <btl4app.hpp> // application (the BTL4Application global)
|
||||
#include <l4ctrl.hpp> // LBE4ControlsManager
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "btinput.hpp"
|
||||
|
||||
BTInputState gBTInput = {0};
|
||||
|
||||
//=============================================================================
|
||||
// Binding model
|
||||
//=============================================================================
|
||||
|
||||
enum BTAxisID
|
||||
{
|
||||
BTAxisThrottle = 0,
|
||||
BTAxisLeftPedal,
|
||||
BTAxisRightPedal,
|
||||
BTAxisPedals, // signed convenience alias: + = right pedal
|
||||
BTAxisJoystickX, // the pod stick X = torso twist (Standard mode)
|
||||
BTAxisJoystickY, // parsed + tracked; no consumer yet
|
||||
BTAxisCount
|
||||
};
|
||||
|
||||
enum BTActionID
|
||||
{
|
||||
BTActNone = 0,
|
||||
BTActViewToggle,
|
||||
BTActLookBehind,
|
||||
BTActAllStop,
|
||||
BTActModeCycle,
|
||||
BTActValve,
|
||||
BTActConfigHold,
|
||||
BTActGenerator1,
|
||||
BTActGenerator2,
|
||||
BTActGenerator3,
|
||||
BTActGenerator4,
|
||||
BTActReconnect,
|
||||
BTActCount
|
||||
};
|
||||
|
||||
enum BTTargetKind
|
||||
{
|
||||
BTTargetButton = 0, // RIO buttonGroup address (or fire-level for 0x40..0x47)
|
||||
BTTargetAxisDeflect, // hold the axis at <scale> while down, spring back
|
||||
BTTargetAxisRate, // walk the axis at <scale>/second while down
|
||||
BTTargetKeypad, // keyboardGroup[unit] key value
|
||||
BTTargetPCKey, // keyboardGroup[KeyboardPC] character (authentic hotkeys)
|
||||
BTTargetAction // port-side dev control
|
||||
};
|
||||
|
||||
enum BTSourceKind
|
||||
{
|
||||
BTSourceKey = 0, // PC key (GetAsyncKeyState VK)
|
||||
BTSourcePadButton, // XInput digital button (or trigger past 50%)
|
||||
BTSourcePadAxis // XInput analog axis
|
||||
};
|
||||
|
||||
struct BTBinding
|
||||
{
|
||||
int sourceKind;
|
||||
int sourceCode; // VK / XINPUT button bit index / pad-axis id
|
||||
int targetKind;
|
||||
int targetCode; // button addr / axis id / keypad unit / char / action
|
||||
int keypadValue; // BTTargetKeypad only
|
||||
float scale; // deflect value or rate-per-second
|
||||
float deadzone; // pad axes
|
||||
int invert; // pad axes
|
||||
int toggle; // buttons: press latches on/off
|
||||
// runtime
|
||||
int wasDown; // raw source state (toggle edge detection)
|
||||
int lastEffective; // last emitted/latched state (edge -> emission)
|
||||
int latched;
|
||||
int pressMask; // mode mask captured at press (release uses it)
|
||||
};
|
||||
|
||||
#define BTINPUT_MAX_BINDINGS 192
|
||||
static BTBinding sBindings[BTINPUT_MAX_BINDINGS];
|
||||
static int sBindingCount = 0;
|
||||
static int sLoaded = 0;
|
||||
|
||||
// characters/VKs claimed by key bindings (legacy keyboard-feed suppression)
|
||||
static unsigned char sSuppressChar[256]; // WM_CHAR values
|
||||
static unsigned char sSuppressKeyUp[256]; // WM_KEYUP/-SYSKEYUP values (VK aliases)
|
||||
|
||||
static int sInputLog = -1;
|
||||
#define BTINPUT_LOG(x) \
|
||||
do { if (sInputLog < 0) sInputLog = getenv("BT_INPUT_LOG") ? 1 : 0; \
|
||||
if (sInputLog) { DEBUG_STREAM << "[input] " << x << "\n" << std::flush; } } while (0)
|
||||
|
||||
//=============================================================================
|
||||
// Name tables
|
||||
//=============================================================================
|
||||
|
||||
struct BTName { const char *name; int code; };
|
||||
|
||||
static const BTName sKeyNames[] =
|
||||
{
|
||||
{"Space", ' '}, {"Enter", 0x0D}, {"Tab", 0x09}, {"Escape", 0x1B},
|
||||
{"Shift", 0x10}, {"Ctrl", 0x11}, {"Alt", 0x12},
|
||||
{"LShift", 0xA0}, {"RShift", 0xA1}, {"LCtrl", 0xA2}, {"RCtrl", 0xA3},
|
||||
{"Up", 0x26}, {"Down", 0x28}, {"Left", 0x25}, {"Right", 0x27},
|
||||
{"PageUp", 0x21}, {"PageDown", 0x22}, {"End", 0x23}, {"Home", 0x24},
|
||||
{"Insert", 0x2D}, {"Delete", 0x2E}, {"Back", 0x08},
|
||||
{"NumPad0", 0x60}, {"NumPad1", 0x61}, {"NumPad2", 0x62},
|
||||
{"NumPad3", 0x63}, {"NumPad4", 0x64}, {"NumPad5", 0x65},
|
||||
{"NumPad6", 0x66}, {"NumPad7", 0x67}, {"NumPad8", 0x68},
|
||||
{"NumPad9", 0x69}, {"Multiply", 0x6A}, {"Add", 0x6B},
|
||||
{"Subtract", 0x6D}, {"Decimal", 0x6E}, {"Divide", 0x6F},
|
||||
{"OemMinus", 0xBD}, {"Oemplus", 0xBB}, {"Oemcomma", 0xBC},
|
||||
{"OemPeriod", 0xBE}, {"OemQuestion", 0xBF}, {"OemTilde", 0xC0},
|
||||
{"OemOpenBrackets", 0xDB}, {"OemPipe", 0xDC}, {"OemCloseBrackets", 0xDD},
|
||||
{"OemSemicolon", 0xBA}, {"OemQuotes", 0xDE},
|
||||
{0, 0}
|
||||
};
|
||||
|
||||
// XInput wButtons bit positions (XINPUT_GAMEPAD_* values are single bits)
|
||||
static const BTName sPadButtonNames[] =
|
||||
{
|
||||
{"DPadUp", 0x0001}, {"DPadDown", 0x0002},
|
||||
{"DPadLeft", 0x0004}, {"DPadRight", 0x0008},
|
||||
{"Start", 0x0010}, {"Back", 0x0020},
|
||||
{"LeftThumb", 0x0040}, {"RightThumb", 0x0080},
|
||||
{"LeftShoulder", 0x0100}, {"RightShoulder", 0x0200},
|
||||
{"A", 0x1000}, {"B", 0x2000}, {"X", 0x4000}, {"Y", 0x8000},
|
||||
// analog triggers usable as digital buttons (>= 50% = pressed);
|
||||
// out-of-band codes above the 16-bit button mask
|
||||
{"LeftTrigger", 0x10000}, {"RightTrigger", 0x20000},
|
||||
{0, 0}
|
||||
};
|
||||
|
||||
enum { BTPadLX = 0, BTPadLY, BTPadRX, BTPadRY, BTPadLT, BTPadRT, BTPadAxisCount };
|
||||
static const BTName sPadAxisNames[] =
|
||||
{
|
||||
{"LeftStickX", BTPadLX}, {"LeftStickY", BTPadLY},
|
||||
{"RightStickX", BTPadRX}, {"RightStickY", BTPadRY},
|
||||
{"LeftTrigger", BTPadLT}, {"RightTrigger", BTPadRT},
|
||||
{0, 0}
|
||||
};
|
||||
|
||||
static const BTName sAxisNames[] =
|
||||
{
|
||||
{"Throttle", BTAxisThrottle},
|
||||
{"LeftPedal", BTAxisLeftPedal}, {"RightPedal", BTAxisRightPedal},
|
||||
{"Pedals", BTAxisPedals},
|
||||
{"JoystickX", BTAxisJoystickX}, {"JoystickY", BTAxisJoystickY},
|
||||
{0, 0}
|
||||
};
|
||||
|
||||
static const BTName sActionNames[] =
|
||||
{
|
||||
{"ViewToggle", BTActViewToggle}, {"LookBehind", BTActLookBehind},
|
||||
{"AllStop", BTActAllStop}, {"ModeCycle", BTActModeCycle},
|
||||
{"Valve", BTActValve}, {"ConfigHold", BTActConfigHold},
|
||||
{"Generator1", BTActGenerator1}, {"Generator2", BTActGenerator2},
|
||||
{"Generator3", BTActGenerator3}, {"Generator4", BTActGenerator4},
|
||||
{"Reconnect", BTActReconnect},
|
||||
{0, 0}
|
||||
};
|
||||
|
||||
static int
|
||||
LookupName(const BTName *table, const char *token)
|
||||
{
|
||||
for (int i = 0; table[i].name != 0; ++i)
|
||||
{
|
||||
if (_stricmp(table[i].name, token) == 0)
|
||||
{
|
||||
return table[i].code;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
ParseKeyName(const char *token)
|
||||
{
|
||||
// single letter / digit-row Dn / function keys / named specials
|
||||
if (token[1] == '\0' && isalpha((unsigned char)token[0]))
|
||||
{
|
||||
return toupper((unsigned char)token[0]); // VK == 'A'..'Z'
|
||||
}
|
||||
if ((token[0] == 'D' || token[0] == 'd')
|
||||
&& isdigit((unsigned char)token[1]) && token[2] == '\0')
|
||||
{
|
||||
return '0' + (token[1] - '0'); // VK == '0'..'9'
|
||||
}
|
||||
if ((token[0] == 'F' || token[0] == 'f') && isdigit((unsigned char)token[1]))
|
||||
{
|
||||
int n = atoi(token + 1);
|
||||
if (n >= 1 && n <= 12)
|
||||
{
|
||||
return 0x70 + n - 1; // VK_F1..VK_F12
|
||||
}
|
||||
}
|
||||
return LookupName(sKeyNames, token);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// The compiled-in default profile: WASD CLASSIC -- byte-identical to
|
||||
// content/CONTROLS.MAP as shipped. Deleting the file restores exactly this.
|
||||
//=============================================================================
|
||||
|
||||
static const char *sDefaultProfile =
|
||||
"key W axis Throttle rate 0.7\n"
|
||||
"key S axis Throttle rate -0.7\n"
|
||||
"key Up axis Throttle rate 0.7\n"
|
||||
"key Down axis Throttle rate -0.7\n"
|
||||
"key A axis LeftPedal deflect 1\n"
|
||||
"key D axis RightPedal deflect 1\n"
|
||||
"key Left axis LeftPedal deflect 1\n"
|
||||
"key Right axis RightPedal deflect 1\n"
|
||||
"key Q axis JoystickX deflect -1\n"
|
||||
"key E axis JoystickX deflect 1\n"
|
||||
"key X action AllStop\n"
|
||||
"key D1 button 0x40\n"
|
||||
"key Space button 0x40\n"
|
||||
"key D2 button 0x46\n"
|
||||
"key D3 button 0x47\n"
|
||||
"key Ctrl button 0x47\n"
|
||||
"key D4 button 0x45\n"
|
||||
"key G action ConfigHold\n"
|
||||
"key C action Valve\n"
|
||||
"key M action ModeCycle\n"
|
||||
"key V action ViewToggle\n"
|
||||
"key B action LookBehind\n"
|
||||
"key F5 action Generator1\n"
|
||||
"key F6 action Generator2\n"
|
||||
"key F7 action Generator3\n"
|
||||
"key F8 action Generator4\n"
|
||||
"key F9 action Reconnect\n"
|
||||
"padaxis LeftStickX axis Pedals deadzone 0.24\n"
|
||||
"padaxis RightStickX axis JoystickX deadzone 0.24\n"
|
||||
"padaxis RightStickY axis Throttle rate 0.75 deadzone 0.24\n"
|
||||
"pad RightTrigger button 0x40\n"
|
||||
"pad LeftTrigger button 0x47\n"
|
||||
"pad RightShoulder button 0x46\n"
|
||||
"pad LeftShoulder button 0x45\n"
|
||||
"pad DPadUp button 0x42\n"
|
||||
"pad DPadDown button 0x41\n"
|
||||
"pad DPadLeft button 0x44\n"
|
||||
"pad DPadRight button 0x43\n"
|
||||
"pad B action AllStop\n"
|
||||
"pad Y action ViewToggle\n"
|
||||
"pad X action LookBehind\n"
|
||||
"pad Start action ModeCycle\n"
|
||||
"pad Back action Valve\n";
|
||||
|
||||
//=============================================================================
|
||||
// Parser
|
||||
//=============================================================================
|
||||
|
||||
static void
|
||||
RegisterKeySuppression(int vk)
|
||||
{
|
||||
// WM_KEYUP/-SYSKEYUP deliver the raw VK as the key value (the engine
|
||||
// keyboard feed, L4CTRL.cpp:1506) -- suppress it; this is what aliased
|
||||
// F5's key-up (0x74) to the 't' pilot-select hotkey, and letter key-ups
|
||||
// (uppercase) to developer fake events.
|
||||
if (vk >= 0 && vk < 256)
|
||||
{
|
||||
sSuppressKeyUp[vk] = 1;
|
||||
}
|
||||
// WM_CHAR delivers typed characters: suppress both cases of a letter,
|
||||
// the digit itself, space, and the base punctuation of the Oem keys.
|
||||
if (vk >= 'A' && vk <= 'Z')
|
||||
{
|
||||
sSuppressChar[tolower(vk)] = 1;
|
||||
sSuppressChar[vk] = 1;
|
||||
}
|
||||
else if (vk >= '0' && vk <= '9')
|
||||
{
|
||||
sSuppressChar[vk] = 1;
|
||||
}
|
||||
else if (vk == ' ')
|
||||
{
|
||||
sSuppressChar[' '] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
static const struct { int vk; char ch; } oem[] =
|
||||
{
|
||||
{0xBD, '-'}, {0xBB, '='}, {0xBC, ','}, {0xBE, '.'},
|
||||
{0xBF, '/'}, {0xC0, '`'}, {0xDB, '['}, {0xDC, '\\'},
|
||||
{0xDD, ']'}, {0xBA, ';'}, {0xDE, '\''}, {0x0D, '\r'},
|
||||
{0x09, '\t'}, {0x08, '\b'},
|
||||
};
|
||||
for (int i = 0; i < (int)(sizeof(oem) / sizeof(oem[0])); ++i)
|
||||
{
|
||||
if (oem[i].vk == vk)
|
||||
{
|
||||
sSuppressChar[(unsigned char)oem[i].ch] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
ParseLine(char *line, int line_number)
|
||||
{
|
||||
// strip comments + tokenize
|
||||
char *hash = strchr(line, '#');
|
||||
if (hash != 0)
|
||||
{
|
||||
*hash = '\0';
|
||||
}
|
||||
char *tokens[8];
|
||||
int token_count = 0;
|
||||
for (char *t = strtok(line, " \t\r\n");
|
||||
t != 0 && token_count < 8;
|
||||
t = strtok(0, " \t\r\n"))
|
||||
{
|
||||
tokens[token_count++] = t;
|
||||
}
|
||||
if (token_count == 0)
|
||||
{
|
||||
return 1; // blank / comment
|
||||
}
|
||||
if (token_count < 3 || sBindingCount >= BTINPUT_MAX_BINDINGS)
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": malformed (ignored)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
BTBinding b;
|
||||
memset(&b, 0, sizeof(b));
|
||||
|
||||
// ---- source ----
|
||||
int arg = 2; // first token after <verb> <source>
|
||||
if (_stricmp(tokens[0], "key") == 0)
|
||||
{
|
||||
b.sourceKind = BTSourceKey;
|
||||
b.sourceCode = ParseKeyName(tokens[1]);
|
||||
}
|
||||
else if (_stricmp(tokens[0], "pad") == 0)
|
||||
{
|
||||
b.sourceKind = BTSourcePadButton;
|
||||
b.sourceCode = LookupName(sPadButtonNames, tokens[1]);
|
||||
}
|
||||
else if (_stricmp(tokens[0], "padaxis") == 0)
|
||||
{
|
||||
b.sourceKind = BTSourcePadAxis;
|
||||
b.sourceCode = LookupName(sPadAxisNames, tokens[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown verb '"
|
||||
<< tokens[0] << "'");
|
||||
return 0;
|
||||
}
|
||||
if (b.sourceCode < 0)
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown source '"
|
||||
<< tokens[1] << "'");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- target ----
|
||||
if (_stricmp(tokens[arg], "button") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
b.targetKind = BTTargetButton;
|
||||
b.targetCode = (int)strtol(tokens[arg + 1], 0, 0);
|
||||
if (b.targetCode < 0 || b.targetCode > 0x47)
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number
|
||||
<< ": button address out of range (0x00-0x47)");
|
||||
return 0;
|
||||
}
|
||||
arg += 2;
|
||||
if (arg < token_count && _stricmp(tokens[arg], "toggle") == 0)
|
||||
{
|
||||
b.toggle = 1;
|
||||
++arg;
|
||||
}
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "axis") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
int axis = LookupName(sAxisNames, tokens[arg + 1]);
|
||||
if (axis < 0)
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown axis '"
|
||||
<< tokens[arg + 1] << "'");
|
||||
return 0;
|
||||
}
|
||||
b.targetCode = axis;
|
||||
arg += 2;
|
||||
b.targetKind = BTTargetAxisDeflect;
|
||||
b.scale = 1.0f;
|
||||
b.deadzone = 0.0f;
|
||||
while (arg < token_count)
|
||||
{
|
||||
if (_stricmp(tokens[arg], "deflect") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
b.targetKind = BTTargetAxisDeflect;
|
||||
b.scale = (float)atof(tokens[arg + 1]);
|
||||
arg += 2;
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "rate") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
b.targetKind = BTTargetAxisRate;
|
||||
b.scale = (float)atof(tokens[arg + 1]);
|
||||
arg += 2;
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "deadzone") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
b.deadzone = (float)atof(tokens[arg + 1]);
|
||||
arg += 2;
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "invert") == 0)
|
||||
{
|
||||
b.invert = 1;
|
||||
++arg;
|
||||
}
|
||||
else
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number
|
||||
<< ": unknown axis option '" << tokens[arg] << "'");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "keypad") == 0 && arg + 2 < token_count)
|
||||
{
|
||||
b.targetKind = BTTargetKeypad;
|
||||
if (_stricmp(tokens[arg + 1], "pilot") == 0)
|
||||
{
|
||||
b.targetCode = LBE4ControlsManager::KeyboardPilot;
|
||||
}
|
||||
else if (_stricmp(tokens[arg + 1], "external") == 0)
|
||||
{
|
||||
b.targetCode = LBE4ControlsManager::KeyboardExternal;
|
||||
}
|
||||
else
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number
|
||||
<< ": keypad unit must be pilot|external");
|
||||
return 0;
|
||||
}
|
||||
// the RIO converts keypad units 0-15 to '0'-'9','A'-'F'
|
||||
// (L4CTRL.cpp:2526); accept the same range
|
||||
int v = (int)strtol(tokens[arg + 2], 0, 0);
|
||||
if (v < 0 || v > 15)
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number
|
||||
<< ": keypad value must be 0-15");
|
||||
return 0;
|
||||
}
|
||||
b.keypadValue = (v <= 9) ? ('0' + v) : ('A' + v - 10);
|
||||
arg += 3;
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "pckey") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
// send an authentic PC-keyboard hotkey character (the 1995
|
||||
// KeypressMessageHandler dispatcher: '+'/'-' zoom, presets, ...)
|
||||
b.targetKind = BTTargetPCKey;
|
||||
b.targetCode = (tokens[arg + 1][1] == '\0')
|
||||
? (unsigned char)tokens[arg + 1][0]
|
||||
: (int)strtol(tokens[arg + 1], 0, 0);
|
||||
arg += 2;
|
||||
}
|
||||
else if (_stricmp(tokens[arg], "action") == 0 && arg + 1 < token_count)
|
||||
{
|
||||
b.targetKind = BTTargetAction;
|
||||
b.targetCode = LookupName(sActionNames, tokens[arg + 1]);
|
||||
if (b.targetCode < 0)
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown action '"
|
||||
<< tokens[arg + 1] << "'");
|
||||
return 0;
|
||||
}
|
||||
arg += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
BTINPUT_LOG("CONTROLS.MAP line " << line_number << ": unknown target '"
|
||||
<< tokens[arg] << "'");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (b.sourceKind == BTSourceKey)
|
||||
{
|
||||
RegisterKeySuppression(b.sourceCode);
|
||||
}
|
||||
sBindings[sBindingCount++] = b;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
LoadBindings()
|
||||
{
|
||||
sLoaded = 1;
|
||||
sBindingCount = 0;
|
||||
memset(sSuppressChar, 0, sizeof(sSuppressChar));
|
||||
memset(sSuppressKeyUp, 0, sizeof(sSuppressKeyUp));
|
||||
|
||||
char *text = 0;
|
||||
FILE *f = fopen("CONTROLS.MAP", "rb"); // cwd == content/
|
||||
if (f != 0)
|
||||
{
|
||||
fseek(f, 0, SEEK_END);
|
||||
long len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (len > 0 && len < 256 * 1024)
|
||||
{
|
||||
text = (char *)malloc((size_t)len + 1);
|
||||
if (text != 0)
|
||||
{
|
||||
fread(text, 1, (size_t)len, f);
|
||||
text[len] = '\0';
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
const char *source = text != 0 ? text : sDefaultProfile;
|
||||
char *work = _strdup(source);
|
||||
int line_number = 0;
|
||||
// strtok is used inside ParseLine, so split lines manually first
|
||||
for (char *line = work, *next = 0; line != 0; line = next)
|
||||
{
|
||||
next = strchr(line, '\n');
|
||||
if (next != 0)
|
||||
{
|
||||
*next++ = '\0';
|
||||
}
|
||||
++line_number;
|
||||
ParseLine(line, line_number);
|
||||
}
|
||||
free(work);
|
||||
|
||||
BTINPUT_LOG("loaded " << sBindingCount << " binding(s) from "
|
||||
<< (text != 0 ? "CONTROLS.MAP" : "built-in WASD-classic defaults"));
|
||||
if (text != 0)
|
||||
{
|
||||
free(text);
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Device polling
|
||||
//=============================================================================
|
||||
|
||||
// -- keyboard (dynamic, same pattern as the old shim: WndProc key messages
|
||||
// are consumed by the engine keyboard reader, so poll async state) --
|
||||
typedef short (__stdcall *BTAsyncFn)(int);
|
||||
typedef void *(__stdcall *BTFgFn)(void);
|
||||
typedef unsigned long (__stdcall *BTWtpFn)(void *, unsigned long *);
|
||||
typedef unsigned long (__stdcall *BTPidFn)(void);
|
||||
static BTAsyncFn sAsync = 0;
|
||||
static BTFgFn sFg = 0;
|
||||
static BTWtpFn sWtp = 0;
|
||||
static BTPidFn sPid = 0;
|
||||
|
||||
// -- XInput (dynamic: xinput1_4.dll on Win8+, xinput9_1_0.dll everywhere) --
|
||||
struct BTXInputGamepad
|
||||
{
|
||||
unsigned short wButtons;
|
||||
unsigned char bLeftTrigger;
|
||||
unsigned char bRightTrigger;
|
||||
short sThumbLX;
|
||||
short sThumbLY;
|
||||
short sThumbRX;
|
||||
short sThumbRY;
|
||||
};
|
||||
struct BTXInputStateRaw
|
||||
{
|
||||
unsigned long dwPacketNumber;
|
||||
BTXInputGamepad Gamepad;
|
||||
};
|
||||
typedef unsigned long (__stdcall *BTXInputGetStateFn)(unsigned long, BTXInputStateRaw *);
|
||||
static BTXInputGetStateFn sXInputGetState = 0;
|
||||
static int sXInputTried = 0;
|
||||
static int sPadConnected = 0;
|
||||
static unsigned long sPadRecheckAt = 0; // GetTickCount throttle
|
||||
|
||||
static void
|
||||
InitDevices()
|
||||
{
|
||||
HMODULE u = GetModuleHandleA("user32.dll");
|
||||
HMODULE k = GetModuleHandleA("kernel32.dll");
|
||||
if (u != 0 && k != 0)
|
||||
{
|
||||
sAsync = (BTAsyncFn)GetProcAddress(u, "GetAsyncKeyState");
|
||||
sFg = (BTFgFn)GetProcAddress(u, "GetForegroundWindow");
|
||||
sWtp = (BTWtpFn)GetProcAddress(u, "GetWindowThreadProcessId");
|
||||
sPid = (BTPidFn)GetProcAddress(k, "GetCurrentProcessId");
|
||||
}
|
||||
if (!sXInputTried)
|
||||
{
|
||||
sXInputTried = 1;
|
||||
static const char *dlls[] =
|
||||
{ "xinput1_4.dll", "xinput1_3.dll", "xinput9_1_0.dll" };
|
||||
for (int i = 0; i < 3 && sXInputGetState == 0; ++i)
|
||||
{
|
||||
HMODULE x = LoadLibraryA(dlls[i]);
|
||||
if (x != 0)
|
||||
{
|
||||
sXInputGetState =
|
||||
(BTXInputGetStateFn)GetProcAddress(x, "XInputGetState");
|
||||
if (sXInputGetState != 0)
|
||||
{
|
||||
BTINPUT_LOG("XInput via " << dlls[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static float
|
||||
PadAxisValue(const BTXInputGamepad &pad, int axis)
|
||||
{
|
||||
switch (axis)
|
||||
{
|
||||
case BTPadLX: return pad.sThumbLX / 32767.0f;
|
||||
case BTPadLY: return pad.sThumbLY / 32767.0f;
|
||||
case BTPadRX: return pad.sThumbRX / 32767.0f;
|
||||
case BTPadRY: return pad.sThumbRY / 32767.0f;
|
||||
case BTPadLT: return pad.bLeftTrigger / 255.0f;
|
||||
case BTPadRT: return pad.bRightTrigger / 255.0f;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Emission (the authentic RIO event mirror)
|
||||
//=============================================================================
|
||||
|
||||
static void
|
||||
EmitButton(BTBinding &b, int down)
|
||||
{
|
||||
LBE4ControlsManager *controls =
|
||||
(LBE4ControlsManager *)application->GetControlsManager();
|
||||
if (controls == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int addr = b.targetCode;
|
||||
// the four pod joystick fire buttons feed the bring-up fire channels
|
||||
// as LEVELS (see file banner); handled in the poll aggregation.
|
||||
if (addr >= 0x40 && addr <= 0x47 && addr != 0x41 && addr != 0x42
|
||||
&& addr != 0x43 && addr != 0x44)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (down)
|
||||
{
|
||||
ControlsButton value = (ControlsButton)(addr + 1);
|
||||
b.pressMask = application->GetModeManager()->GetModeMask();
|
||||
controls->buttonGroup[addr].Update(&value, b.pressMask);
|
||||
BTINPUT_LOG("button 0x" << std::hex << addr << std::dec << " PRESS");
|
||||
}
|
||||
else
|
||||
{
|
||||
ControlsButton value = (ControlsButton)(-addr - 1);
|
||||
controls->buttonGroup[addr].Update(&value, b.pressMask);
|
||||
BTINPUT_LOG("button 0x" << std::hex << addr << std::dec << " release");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
EmitKeypad(BTBinding &b)
|
||||
{
|
||||
LBE4ControlsManager *controls =
|
||||
(LBE4ControlsManager *)application->GetControlsManager();
|
||||
if (controls == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ControlsKey key = (ControlsKey)b.keypadValue;
|
||||
controls->keyboardGroup[b.targetCode].ForceUpdate(
|
||||
&key, application->GetModeManager()->GetModeMask());
|
||||
BTINPUT_LOG("keypad unit " << b.targetCode << " key '"
|
||||
<< (char)b.keypadValue << "'");
|
||||
}
|
||||
|
||||
static void
|
||||
EmitPCKey(BTBinding &b)
|
||||
{
|
||||
LBE4ControlsManager *controls =
|
||||
(LBE4ControlsManager *)application->GetControlsManager();
|
||||
if (controls == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ControlsKey key = (ControlsKey)b.targetCode;
|
||||
controls->keyboardGroup[LBE4ControlsManager::KeyboardPC].ForceUpdate(
|
||||
&key, application->GetModeManager()->GetModeMask());
|
||||
BTINPUT_LOG("pckey 0x" << std::hex << b.targetCode << std::dec);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// The per-frame poll
|
||||
//=============================================================================
|
||||
|
||||
void
|
||||
BTInputPoll(float /*dt*/)
|
||||
{
|
||||
if (!sLoaded)
|
||||
{
|
||||
LoadBindings();
|
||||
InitDevices();
|
||||
}
|
||||
|
||||
// foreground-focus guard (BT_KEY_NOFOCUS=1 for automation harnesses)
|
||||
int focused = 0;
|
||||
if (sFg != 0 && sWtp != 0 && sPid != 0)
|
||||
{
|
||||
void *fg = sFg();
|
||||
unsigned long fg_pid = 0;
|
||||
if (fg != 0)
|
||||
{
|
||||
sWtp(fg, &fg_pid);
|
||||
}
|
||||
focused = (fg_pid == sPid());
|
||||
}
|
||||
static int sNoFocus = -1;
|
||||
if (sNoFocus < 0)
|
||||
{
|
||||
const char *nf = getenv("BT_KEY_NOFOCUS");
|
||||
sNoFocus = (nf != 0 && *nf == '1') ? 1 : 0;
|
||||
}
|
||||
if (sNoFocus)
|
||||
{
|
||||
focused = 1;
|
||||
}
|
||||
|
||||
// pad state (rate-limit reconnect probing: XInputGetState on an EMPTY
|
||||
// slot is expensive, ~ms -- probe disconnected pads at 1 Hz only)
|
||||
BTXInputGamepad pad;
|
||||
memset(&pad, 0, sizeof(pad));
|
||||
if (sXInputGetState != 0)
|
||||
{
|
||||
unsigned long now = GetTickCount();
|
||||
if (sPadConnected || now >= sPadRecheckAt)
|
||||
{
|
||||
BTXInputStateRaw raw;
|
||||
memset(&raw, 0, sizeof(raw));
|
||||
if (sXInputGetState(0, &raw) == 0) // ERROR_SUCCESS
|
||||
{
|
||||
if (!sPadConnected)
|
||||
{
|
||||
BTINPUT_LOG("gamepad CONNECTED");
|
||||
}
|
||||
sPadConnected = 1;
|
||||
pad = raw.Gamepad;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sPadConnected)
|
||||
{
|
||||
BTINPUT_LOG("gamepad disconnected");
|
||||
}
|
||||
sPadConnected = 0;
|
||||
sPadRecheckAt = now + 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- aggregate the frame ----
|
||||
BTInputState next;
|
||||
memset(&next, 0, sizeof(next));
|
||||
float axisDeflect[BTAxisCount] = {0};
|
||||
int axisActive[BTAxisCount] = {0};
|
||||
int axisAbsolute[BTAxisCount] = {0};
|
||||
float axisRate[BTAxisCount] = {0};
|
||||
|
||||
for (int i = 0; i < sBindingCount; ++i)
|
||||
{
|
||||
BTBinding &b = sBindings[i];
|
||||
|
||||
// -- source value --
|
||||
int down = 0;
|
||||
float analog = 0.0f;
|
||||
if (b.sourceKind == BTSourceKey)
|
||||
{
|
||||
down = (focused && sAsync != 0
|
||||
&& (sAsync(b.sourceCode) & 0x8000)) ? 1 : 0;
|
||||
analog = (float)down;
|
||||
}
|
||||
else if (b.sourceKind == BTSourcePadButton)
|
||||
{
|
||||
if (!sPadConnected)
|
||||
{
|
||||
down = 0;
|
||||
}
|
||||
else if (b.sourceCode == 0x10000)
|
||||
{
|
||||
down = pad.bLeftTrigger >= 128;
|
||||
}
|
||||
else if (b.sourceCode == 0x20000)
|
||||
{
|
||||
down = pad.bRightTrigger >= 128;
|
||||
}
|
||||
else
|
||||
{
|
||||
down = (pad.wButtons & b.sourceCode) ? 1 : 0;
|
||||
}
|
||||
analog = (float)down;
|
||||
}
|
||||
else // BTSourcePadAxis
|
||||
{
|
||||
analog = sPadConnected ? PadAxisValue(pad, b.sourceCode) : 0.0f;
|
||||
if (b.invert)
|
||||
{
|
||||
analog = -analog;
|
||||
}
|
||||
if (analog > -b.deadzone && analog < b.deadzone)
|
||||
{
|
||||
analog = 0.0f;
|
||||
}
|
||||
else if (b.deadzone > 0.0f && b.deadzone < 1.0f)
|
||||
{
|
||||
// rescale so the deadzone edge is 0 (no jump)
|
||||
float sign = analog < 0.0f ? -1.0f : 1.0f;
|
||||
analog = sign * ((analog * sign - b.deadzone)
|
||||
/ (1.0f - b.deadzone));
|
||||
}
|
||||
down = analog != 0.0f;
|
||||
}
|
||||
|
||||
// toggle latch: a PRESS edge flips the held state
|
||||
if (b.toggle && b.sourceKind != BTSourcePadAxis)
|
||||
{
|
||||
if (down && !b.wasDown)
|
||||
{
|
||||
b.latched = !b.latched;
|
||||
}
|
||||
b.wasDown = down;
|
||||
down = b.latched;
|
||||
analog = (float)down;
|
||||
}
|
||||
|
||||
// -- apply to target --
|
||||
switch (b.targetKind)
|
||||
{
|
||||
case BTTargetButton:
|
||||
{
|
||||
int addr = b.targetCode;
|
||||
if (down != b.lastEffective)
|
||||
{
|
||||
EmitButton(b, down);
|
||||
}
|
||||
b.lastEffective = down;
|
||||
// fire-button levels (0x40/45/46/47) aggregate every frame
|
||||
if (down)
|
||||
{
|
||||
if (addr == 0x40) next.fireTrigger = 1;
|
||||
if (addr == 0x45) next.firePinky = 1;
|
||||
if (addr == 0x46) next.fireThumbLow = 1;
|
||||
if (addr == 0x47) next.fireThumbHigh = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BTTargetAxisDeflect:
|
||||
if (down)
|
||||
{
|
||||
axisDeflect[b.targetCode] += analog * b.scale;
|
||||
axisActive[b.targetCode] = 1;
|
||||
if (b.sourceKind == BTSourcePadAxis)
|
||||
{
|
||||
axisAbsolute[b.targetCode] = 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BTTargetAxisRate:
|
||||
if (down)
|
||||
{
|
||||
axisRate[b.targetCode] += analog * b.scale;
|
||||
}
|
||||
break;
|
||||
case BTTargetKeypad:
|
||||
if (down && !b.lastEffective)
|
||||
{
|
||||
EmitKeypad(b);
|
||||
}
|
||||
b.lastEffective = down;
|
||||
break;
|
||||
case BTTargetPCKey:
|
||||
if (down && !b.lastEffective)
|
||||
{
|
||||
EmitPCKey(b);
|
||||
}
|
||||
b.lastEffective = down;
|
||||
break;
|
||||
case BTTargetAction:
|
||||
if (down)
|
||||
{
|
||||
switch (b.targetCode)
|
||||
{
|
||||
case BTActViewToggle: next.viewToggle = 1; break;
|
||||
case BTActLookBehind: next.lookBehind = 1; break;
|
||||
case BTActAllStop: next.allStop = 1; break;
|
||||
case BTActModeCycle: next.modeCycle = 1; break;
|
||||
case BTActValve: next.valve = 1; break;
|
||||
case BTActConfigHold: next.configHold = 1; break;
|
||||
case BTActGenerator1: next.genSel = 4; break;
|
||||
case BTActGenerator2: next.genSel = 5; break;
|
||||
case BTActGenerator3: next.genSel = 6; break;
|
||||
case BTActGenerator4: next.genSel = 7; break;
|
||||
case BTActReconnect: next.genSel = 8; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- fold the axis aliases + publish ----
|
||||
// Pedals (signed alias): + = right pedal (turn right)
|
||||
float turn = (axisDeflect[BTAxisRightPedal] - axisDeflect[BTAxisLeftPedal])
|
||||
+ axisDeflect[BTAxisPedals];
|
||||
int turn_active = axisActive[BTAxisRightPedal] || axisActive[BTAxisLeftPedal]
|
||||
|| axisActive[BTAxisPedals];
|
||||
int turn_abs = axisAbsolute[BTAxisPedals]
|
||||
|| axisAbsolute[BTAxisLeftPedal] || axisAbsolute[BTAxisRightPedal];
|
||||
|
||||
if (turn > 1.0f) turn = 1.0f;
|
||||
if (turn < -1.0f) turn = -1.0f;
|
||||
float twist = axisDeflect[BTAxisJoystickX];
|
||||
if (twist > 1.0f) twist = 1.0f;
|
||||
if (twist < -1.0f) twist = -1.0f;
|
||||
|
||||
next.turnTarget = turn;
|
||||
next.turnActive = turn_active;
|
||||
next.turnAbsolute = turn_abs;
|
||||
next.twistTarget = twist;
|
||||
next.twistActive = axisActive[BTAxisJoystickX];
|
||||
next.twistAbsolute = axisAbsolute[BTAxisJoystickX];
|
||||
next.leverRate = axisRate[BTAxisThrottle];
|
||||
|
||||
gBTInput = next;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Legacy keyboard-feed suppression (called from L4CTRL.cpp:1506 block)
|
||||
//=============================================================================
|
||||
|
||||
extern "C" int
|
||||
BTInputSuppressKey(unsigned int key_value, int is_char)
|
||||
{
|
||||
if (!sLoaded)
|
||||
{
|
||||
LoadBindings(); // the feed can run before the first poll
|
||||
InitDevices();
|
||||
}
|
||||
if (key_value > 255)
|
||||
{
|
||||
return 0; // ALT_BIT-tagged values etc. -- never claimed
|
||||
}
|
||||
return is_char ? sSuppressChar[key_value] : sSuppressKeyUp[key_value];
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//#############################################################################
|
||||
// btinput.hpp -- the desktop input binding engine (CONTROLS.MAP + XInput).
|
||||
//
|
||||
// The pod's controls are a fixed physical device set (stick, throttle lever,
|
||||
// pedals, ~44 lamp buttons, keypads) reaching the game through the
|
||||
// LBE4ControlsManager push model. On a desktop there is no RIO, so this
|
||||
// engine maps PC keys and an XInput gamepad onto the SAME channels:
|
||||
//
|
||||
// axes -> the virtual-controls integrators in mech4.cpp (lever / turn
|
||||
// stick / torso twist), exactly where the BT_KEY_BRIDGE writes
|
||||
// the analog values the RIO would have produced
|
||||
// buttons -> real buttonGroup[addr] ForceUpdates (aux banks, hotbox,
|
||||
// panic, throttle-head...) so the AUTHENTIC handlers fire;
|
||||
// the four joystick fire buttons (0x40/45/46/47) drive the
|
||||
// bring-up fire channels instead (see btinput.cpp note)
|
||||
// keypads -> keyboardGroup[KeyboardPilot/KeyboardExternal] key values
|
||||
// pckey -> keyboardGroup[KeyboardPC] (any authentic '+'-style hotkey)
|
||||
// actions -> the port-side dev controls (view toggle, all-stop, ...)
|
||||
//
|
||||
// Bindings load from content/CONTROLS.MAP at first poll; if the file is
|
||||
// absent the compiled-in WASD-classic profile (identical text) applies.
|
||||
// Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/-KEYUP
|
||||
// keyboard feed (BTInputSuppressKey), ending the historic double-dispatch
|
||||
// (W = drive AND pilot-select, F5 keyup aliasing to 't' = pilot 3, ...).
|
||||
//#############################################################################
|
||||
#ifndef BTINPUT_HPP
|
||||
#define BTINPUT_HPP
|
||||
|
||||
struct BTInputState
|
||||
{
|
||||
// analog demands (consumed by the mech4 virtual-controls block)
|
||||
float leverRate; // throttle lever sweep, per second (+fwd/-back)
|
||||
float turnTarget; // turn-stick deflection target [-1,1]
|
||||
int turnActive; // any turn input held (else spring-center)
|
||||
float twistTarget; // torso-twist deflection target [-1,1]
|
||||
int twistActive;
|
||||
int turnAbsolute; // pad stick overrides the integrator outright
|
||||
int twistAbsolute;
|
||||
|
||||
// the four pod joystick fire buttons (levels)
|
||||
int fireTrigger; // 0x40 Main
|
||||
int firePinky; // 0x45
|
||||
int fireThumbLow; // 0x46 Middle
|
||||
int fireThumbHigh; // 0x47 Upper
|
||||
|
||||
// port-side actions (levels; consumers keep their own edge detectors)
|
||||
int viewToggle;
|
||||
int lookBehind;
|
||||
int allStop;
|
||||
int modeCycle;
|
||||
int valve;
|
||||
int configHold;
|
||||
int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect
|
||||
};
|
||||
|
||||
extern BTInputState gBTInput;
|
||||
|
||||
// Poll everything (keys + pad), evaluate the binding table, fire button /
|
||||
// keypad edges into the controls manager. Call ONCE per sim frame from the
|
||||
// player drive block. Handles the foreground-focus guard (BT_KEY_NOFOCUS)
|
||||
// internally.
|
||||
void BTInputPoll(float dt);
|
||||
|
||||
// The legacy keyboard feed asks before dispatching a typed character /
|
||||
// key-up value: nonzero = this key is claimed by a binding, swallow it.
|
||||
extern "C" int BTInputSuppressKey(unsigned int key_value, int is_char);
|
||||
|
||||
#endif // BTINPUT_HPP
|
||||
@@ -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