Applied on top of the glass-cockpit-refit merge, all from the pre-merge review: 1. README CONTROLS TABLE REWRITTEN for the new default board. It still taught "W/S throttle, A/D turn" while the defaults moved flight to the numpad and handed the letter rows to the MFD banks -- a fresh install would read controls that do not work. The new table teaches the numpad flight block, the panel-on-your-keys groups, F7 for control mode (was M), backtick for the camera (was V), the coolant valves on 1-3/QWE with the detent warning kept, and PgUp/PgDn volume. UPGRADERS ARE TOLD EXPLICITLY that their preserved bindings.txt keeps their old keys, and that deleting it opts into the board. 2. BACKTICK VIEW-TOGGLE GUARDED. The bindings-row-wins rule (KeyHasBinding) covered V and J/K/L but not backtick itself, which polled unconditionally -- and BACKTICK is a nameable, unbound key, so binding it to a button double-dispatched (button + camera). Both halves of the view poll are now guarded. 3. ENVIRON.INI REFUSES ONE-SHOTS. The loader applied ANY key=value line, and BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the wizard DELETES the process var after running, so in every relaunched child there is no real env var left to win over the file -- capture wizard at every mission start. BT_JOYCONFIG is now skipped with the reason in the template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG and friends) do not belong in a file that silently persists forever. 4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING. CONTROLS.md gains the PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never mentioned volume) and the note that the keys moved so they no longer share a key with anything in any layout; the bindings template header reserves PgUp/PgDn informally. 5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine). The C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 -- its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for'). CMake now detects an SDK older than 10.0.22000 and builds a dormant stub with the same five-function scalar interface (logs once at Start, identical behaviour otherwise) -- extending the feature's own runtime philosophy ("no Dynamic Lighting -> log once, stay dormant") to build time. Dynamic Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older build machine, and machines with a newer SDK build the real mirror unchanged. VERIFIED on the merged tree (4.11.572): * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved externals are byte-identical to every pre-merge build (20 CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40 were CSS was shorthand -- corrected here for the record). * solo boot: environ.ini template written + loaded, mode resolver announces the surround, historical bands L276/R276/T223/B336 confirmed, keylight stub logs its dormant line, BT_SHOT capture shows the under-glass button treatment and the corrected hat labels, 0 faults. * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved to volume.cfg. (Side catch: content\volume.cfg had been sitting at 0.00 from an old test -- anyone using this tree had a silent game; reset.) * full relay cycle on the merged exe: 2 pods staged, launch pair, mission, StopMission, round RESET -- all clean. * all three console suites still pass; checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1230 lines
36 KiB
C++
1230 lines
36 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
//###########################################################################
|
|
// L4PADRIO -- the hardware-less cockpit device (BT_GLASS only; this TU is
|
|
// only in the build when the gate is on -- see CMakeLists.txt).
|
|
// Design + input model: L4PADRIO.h.
|
|
//###########################################################################
|
|
|
|
#include "l4padrio.h"
|
|
#include "l4padpanel.h"
|
|
#include "l4glasswin.h"
|
|
#include "l4ctrl.h"
|
|
#include "l4joy.h"
|
|
#include "l4keylight.h" // RGB keyboard lamp mirror (scalars-only interface)
|
|
|
|
#include <windows.h>
|
|
#include <xinput.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#pragma comment(lib, "xinput9_1_0.lib")
|
|
|
|
PadRIO *PadRIO::activeInstance = NULL;
|
|
|
|
//
|
|
// Pending backtick/V view-toggle edges (set in Poll, consumed by the game's
|
|
// view-toggle block through BTPadViewToggleEdge).
|
|
//
|
|
int gBTPadViewToggleEdges = 0;
|
|
|
|
//
|
|
// REVERSE THRUST hold state (the pod's throttle-handle button, unit 0x3F --
|
|
// bound to LALT on the keyboard). Set in EmitButton, the single chokepoint all
|
|
// input sources share; consumed by MechControlsMapper::InterpretControls's
|
|
// desktop bridge, which owns `reverseThrust` (mapper attr 6 @0x124) every frame.
|
|
//
|
|
int gBTReverseHeld = 0;
|
|
|
|
//
|
|
// The desktop per-MFD preset-page cycle edges (J/K/L -> Mfd1/2/3), consumed
|
|
// by L4MechControlsMapper::InterpretControls (btl4mppr.cpp step 3b -- the
|
|
// same seam the dev-build mech4 poll feeds). Defined in mech4.cpp (always
|
|
// compiled), so the glass TU externs it -- keyboard reconciliation
|
|
// 2026-07-20: J/K/L are the CONTROLS.MAP muscle-memory keys and there is no
|
|
// single pod button that "cycles" an MFD (the pod's bank buttons are
|
|
// mode-mask-gated direct selects), so the cycle stays a port-side sender
|
|
// with the authentic SetPresetMode body.
|
|
//
|
|
extern int gBTPresetCycle[3];
|
|
|
|
int
|
|
BTPadViewToggleEdge(void)
|
|
{
|
|
if (gBTPadViewToggleEdges > 0)
|
|
{
|
|
--gBTPadViewToggleEdges;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
// XInput normalization: thumbs to -1..1 past the stock deadzone, triggers
|
|
// to 0..1 past the stock threshold.
|
|
//
|
|
//
|
|
// The lamp mirror's log sink. It cannot use DEBUG_STREAM itself: its TU
|
|
// takes no engine headers (l4keylight.h is scalars only), so it hands us
|
|
// finished lines instead.
|
|
//
|
|
//
|
|
// Does the loaded profile bind this virtual key? The built-in convenience
|
|
// keys (view toggle, preset cycles) consult this and stand down when the
|
|
// player's bindings claim the key -- see the note at their poll sites.
|
|
//
|
|
Logical
|
|
PadRIO::KeyHasBinding(int virtualKey) const
|
|
{
|
|
for (int i = 0; i < bindings.keyBindingCount; ++i)
|
|
{
|
|
if (bindings.keyBindings[i].virtualKey == virtualKey)
|
|
{
|
|
return True;
|
|
}
|
|
}
|
|
return False;
|
|
}
|
|
|
|
static void
|
|
KeyLightLog(const char *line)
|
|
{
|
|
DEBUG_STREAM << "[keylight] " << (line ? line : "") << "\n" << std::flush;
|
|
}
|
|
|
|
static float
|
|
NormalizeThumb(int value, int dead_zone)
|
|
{
|
|
float sign = (value < 0) ? -1.0f : 1.0f;
|
|
float magnitude = (float)(value < 0 ? -value : value);
|
|
if (magnitude <= (float)dead_zone)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
if (magnitude > 32767.0f)
|
|
{
|
|
magnitude = 32767.0f;
|
|
}
|
|
return sign * (magnitude - dead_zone) / (32767.0f - dead_zone);
|
|
}
|
|
|
|
static float
|
|
NormalizeTrigger(int value)
|
|
{
|
|
if (value <= XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
return (float)(value - XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
|
|
/ (float)(255 - XINPUT_GAMEPAD_TRIGGER_THRESHOLD);
|
|
}
|
|
|
|
//
|
|
// The keyboard is live only while a window of THIS process is foreground
|
|
// (the mech4.cpp focus-guard idiom) -- alt-tabbed developers must not
|
|
// drive the mech.
|
|
//
|
|
static int
|
|
ProcessHasFocus()
|
|
{
|
|
//
|
|
// BT_KEY_NOFOCUS=1: automation harnesses read keys without focus
|
|
// (the same override the btinput binding engine honors).
|
|
//
|
|
static int s_noFocus = -1;
|
|
if (s_noFocus < 0)
|
|
{
|
|
const char *value = getenv("BT_KEY_NOFOCUS");
|
|
s_noFocus = (value != 0 && *value == '1') ? 1 : 0;
|
|
}
|
|
if (s_noFocus)
|
|
{
|
|
return 1;
|
|
}
|
|
HWND foreground = GetForegroundWindow();
|
|
if (foreground == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
DWORD process_id = 0;
|
|
GetWindowThreadProcessId(foreground, &process_id);
|
|
return process_id == GetCurrentProcessId();
|
|
}
|
|
|
|
//###########################################################################
|
|
// Construction
|
|
//###########################################################################
|
|
|
|
PadRIO::PadRIO():
|
|
RIOBase(),
|
|
eventHead(0),
|
|
eventTail(0),
|
|
lastPollMilliseconds(0),
|
|
lastPadProbeMilliseconds(0),
|
|
padIndex(-1),
|
|
previousPadButtons(0)
|
|
{
|
|
memset(previousKeyHeld, 0, sizeof(previousKeyHeld));
|
|
memset(previousJoyButtonHeld, 0, sizeof(previousJoyButtonHeld));
|
|
memset(previousJoyHatHeld, 0, sizeof(previousJoyHatHeld));
|
|
memset(previousPadAxisRaw, 0, sizeof(previousPadAxisRaw));
|
|
memset(previousJoyAxisRaw, 0, sizeof(previousJoyAxisRaw));
|
|
memset(channelValue, 0, sizeof(channelValue));
|
|
memset(lampState, 0, sizeof(lampState));
|
|
|
|
bindings.Load();
|
|
BuildKeySuppression();
|
|
|
|
//
|
|
// Per-channel spring return rate = the fastest deflect rate bound to
|
|
// the channel (a channel with no deflect bindings never auto-centers).
|
|
//
|
|
for (int c = 0; c < PadBindingProfile::ChannelCount; ++c)
|
|
{
|
|
channelReturnRate[c] = 0.0f;
|
|
}
|
|
for (int k = 0; k < bindings.keyBindingCount; ++k)
|
|
{
|
|
const PadBindingProfile::Action &action = bindings.keyBindings[k].action;
|
|
if (action.kind == PadBindingProfile::ActionAxisDeflect)
|
|
{
|
|
float rate = action.rate < 0.0f ? -action.rate : action.rate;
|
|
if (rate > channelReturnRate[action.channel])
|
|
{
|
|
channelReturnRate[action.channel] = rate;
|
|
}
|
|
}
|
|
}
|
|
|
|
flipStickAxes =
|
|
(getenv("L4PADFLIP") != NULL && *getenv("L4PADFLIP") != '0');
|
|
|
|
//
|
|
// RGB KEYBOARD LAMP MIRROR (Windows Dynamic Lighting, l4keylight.h):
|
|
// every key bound to a lamp ADDRESS glows with the panel palette --
|
|
// yellow for the Secondary/Screen columns (0x10-0x1F), red for the rest,
|
|
// exactly like the on-screen buttons. Keypad binds have no lamp, so
|
|
// they are skipped; a key bound twice takes its FIRST binding.
|
|
// BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once
|
|
// and stays dormant.
|
|
//
|
|
keyLightActive = False;
|
|
{
|
|
const char *gate = getenv("BT_KEYLIGHT");
|
|
if (gate == NULL || atoi(gate) != 0)
|
|
{
|
|
static int lightKeys[192];
|
|
static int lightAddresses[192];
|
|
static unsigned char lightYellow[192];
|
|
int count = 0;
|
|
for (int k = 0; k < bindings.keyBindingCount && count < 192; ++k)
|
|
{
|
|
const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k];
|
|
if (binding.action.kind != PadBindingProfile::ActionButton)
|
|
{
|
|
continue; // axes/keypads carry no lamp
|
|
}
|
|
int address = binding.action.address;
|
|
if (address < 0 || address >= LampCount)
|
|
{
|
|
continue;
|
|
}
|
|
// A keyboard lamp array has no mouse buttons to paint.
|
|
{
|
|
extern int BTPadBindingIsMouseKey(int virtualKey);
|
|
if (BTPadBindingIsMouseKey(binding.virtualKey))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
Logical duplicate = False;
|
|
for (int j = 0; j < count; ++j)
|
|
{
|
|
if (lightKeys[j] == binding.virtualKey)
|
|
{
|
|
duplicate = True; // first binding wins
|
|
break;
|
|
}
|
|
}
|
|
if (duplicate)
|
|
{
|
|
continue;
|
|
}
|
|
lightKeys[count] = binding.virtualKey;
|
|
lightAddresses[count] = address;
|
|
lightYellow[count] = (unsigned char)
|
|
((address >= 0x10 && address <= 0x1F) ? 1 : 0);
|
|
++count;
|
|
}
|
|
if (count > 0)
|
|
{
|
|
KeyLight_SetLogger(KeyLightLog);
|
|
KeyLight_SetMap(lightKeys, lightAddresses, lightYellow, count);
|
|
KeyLight_Start();
|
|
keyLightActive = True;
|
|
DEBUG_STREAM << "[keylight] mirroring " << count
|
|
<< " bound key(s) onto RGB keyboards\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Never revision 0.0 -- some diagnostics print it; give the synthetic
|
|
// board a recognizable version.
|
|
//
|
|
MajorRevision = 9;
|
|
MinorRevision = 9;
|
|
|
|
activeInstance = this;
|
|
DEBUG_STREAM << "[padrio] PadRIO up (XInput probe pending; keyboard "
|
|
<< "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush;
|
|
|
|
//
|
|
// The on-screen cockpit buttons ride the device. BT_GLASS_PANELS (the
|
|
// glass preset default) breaks each secondary display into its own window
|
|
// with its RIO bank around it; otherwise the single combined pad panel
|
|
// (BT_PAD_PANEL=1) is used.
|
|
//
|
|
if (BTGlassPanelsActive())
|
|
{
|
|
BTGlassPanels_Create();
|
|
}
|
|
else if (getenv("BT_PAD_PANEL") != NULL && *getenv("BT_PAD_PANEL") != '0')
|
|
{
|
|
BTPadPanel_Create();
|
|
}
|
|
}
|
|
|
|
PadRIO::~PadRIO()
|
|
{
|
|
if (keyLightActive)
|
|
{
|
|
KeyLight_Stop(); // hands the LEDs back to Windows
|
|
keyLightActive = False;
|
|
}
|
|
BTGlassPanels_Destroy(); // safe no-op if the glass windows were never created
|
|
BTPadPanel_Destroy();
|
|
if (activeInstance == this)
|
|
{
|
|
activeInstance = NULL;
|
|
}
|
|
}
|
|
|
|
//###########################################################################
|
|
// Typed-channel suppression (the btinput sSuppressChar/sSuppressKeyUp
|
|
// pattern -- btinput.cpp AddSuppression -- rebuilt here because btinput
|
|
// stands down whenever a cockpit device owns the input path). A bound key
|
|
// must NOT also reach the 1995 in-cockpit keyboard dispatcher: 'w' selects
|
|
// pilot 0, 'a'/'s'/'d'/'f'/'g' flip MFD2 preset pages, letter/numpad
|
|
// KEY-UP VK values alias onto lowercase hotkeys (VK_F5==0x74=='t' = pilot
|
|
// select 3, VK_NUMPAD2==0x62=='b' = MFD3 Quad, ...). Unbound keys keep
|
|
// their authentic 1995 typed meaning.
|
|
//###########################################################################
|
|
|
|
void
|
|
PadRIO::AddKeySuppression(int virtual_key)
|
|
{
|
|
//
|
|
// WM_KEYUP delivers the raw VK; every consumer downstream compares
|
|
// typed CHARACTERS, so the VK value itself is the alias to swallow.
|
|
//
|
|
if (virtual_key >= 0 && virtual_key < 256)
|
|
{
|
|
suppressKeyUp[virtual_key] = 1;
|
|
}
|
|
//
|
|
// WM_CHAR delivers typed characters: both cases of a letter, the digit
|
|
// itself (main row AND numpad -- VK_NUMPAD0..9 type '0'..'9'), space,
|
|
// and the base punctuation of the Oem keys.
|
|
//
|
|
if (virtual_key >= 'A' && virtual_key <= 'Z')
|
|
{
|
|
suppressChar[virtual_key + ('a' - 'A')] = 1;
|
|
suppressChar[virtual_key] = 1;
|
|
}
|
|
else if (virtual_key >= '0' && virtual_key <= '9')
|
|
{
|
|
suppressChar[virtual_key] = 1;
|
|
}
|
|
else if (virtual_key >= VK_NUMPAD0 && virtual_key <= VK_NUMPAD9)
|
|
{
|
|
suppressChar['0' + (virtual_key - VK_NUMPAD0)] = 1;
|
|
}
|
|
else if (virtual_key == VK_SPACE)
|
|
{
|
|
suppressChar[' '] = 1;
|
|
suppressKeyUp[VK_SPACE] = 1;
|
|
}
|
|
else
|
|
{
|
|
static const struct { int vk; char ch; } oem[] =
|
|
{
|
|
{ VK_OEM_MINUS, '-' }, { VK_OEM_PLUS, '=' },
|
|
{ VK_OEM_COMMA, ',' }, { VK_OEM_PERIOD, '.' },
|
|
{ VK_OEM_2, '/' }, { VK_OEM_3, '`' },
|
|
{ VK_OEM_4, '[' }, { VK_OEM_5, '\\' },
|
|
{ VK_OEM_6, ']' }, { VK_OEM_1, ';' },
|
|
{ VK_OEM_7, '\'' }, { VK_RETURN, '\r' },
|
|
{ VK_TAB, '\t' }, { VK_BACK, '\b' },
|
|
};
|
|
for (int i = 0; i < (int)(sizeof(oem) / sizeof(oem[0])); ++i)
|
|
{
|
|
if (oem[i].vk == virtual_key)
|
|
{
|
|
suppressChar[(unsigned char)oem[i].ch] = 1;
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// The generic modifier VK is what WM_KEYUP reports for L/R variants.
|
|
//
|
|
if (virtual_key == VK_LSHIFT || virtual_key == VK_RSHIFT)
|
|
{
|
|
suppressKeyUp[VK_SHIFT] = 1;
|
|
}
|
|
if (virtual_key == VK_LCONTROL || virtual_key == VK_RCONTROL)
|
|
{
|
|
suppressKeyUp[VK_CONTROL] = 1;
|
|
}
|
|
}
|
|
|
|
void
|
|
PadRIO::BuildKeySuppression()
|
|
{
|
|
memset(suppressChar, 0, sizeof(suppressChar));
|
|
memset(suppressKeyUp, 0, sizeof(suppressKeyUp));
|
|
|
|
for (int k = 0; k < bindings.keyBindingCount; ++k)
|
|
{
|
|
AddKeySuppression(bindings.keyBindings[k].virtualKey);
|
|
}
|
|
|
|
//
|
|
// The hardcoded keys (Poll): backtick + V = view toggle, J/K/L = the
|
|
// per-MFD preset-page cycle.
|
|
//
|
|
AddKeySuppression(VK_OEM_3);
|
|
AddKeySuppression('V');
|
|
AddKeySuppression('J');
|
|
AddKeySuppression('K');
|
|
AddKeySuppression('L');
|
|
}
|
|
|
|
int
|
|
PadRIO::SuppressKey(unsigned int key_value, int is_char)
|
|
{
|
|
if (activeInstance == NULL || key_value > 255)
|
|
{
|
|
return 0; // no glass device / ALT_BIT-tagged value
|
|
}
|
|
return is_char
|
|
? activeInstance->suppressChar[key_value]
|
|
: activeInstance->suppressKeyUp[key_value];
|
|
}
|
|
|
|
//###########################################################################
|
|
// Event queue
|
|
//###########################################################################
|
|
|
|
void
|
|
PadRIO::PushEvent(const RIOEvent &event)
|
|
{
|
|
int next = (eventHead + 1) % EventQueueSize;
|
|
if (next == eventTail)
|
|
{
|
|
DEBUG_STREAM << "[padrio] event queue overflow -- event dropped\n"
|
|
<< std::flush;
|
|
return;
|
|
}
|
|
eventQueue[eventHead] = event;
|
|
eventHead = next;
|
|
}
|
|
|
|
void
|
|
PadRIO::EmitButton(int address, int pressed)
|
|
{
|
|
//
|
|
// REVERSE THRUST (the red button on the pod's throttle HANDLE, unit 0x3F).
|
|
// BTL4.RES's "L4" streamed record [2] maps `Button Throttle1 (0x3F)` to the
|
|
// mapper's attr 6 `ReverseThrust@0x124`, and the 1995 manual is explicit that
|
|
// you HOLD it (release = forward) -- see context/pod-hardware.md. Publish the
|
|
// hold state here, at the ONE chokepoint every source funnels through
|
|
// (keyboard bindings, gamepad, DirectInput joystick, and glass-panel clicks
|
|
// via SetScreenButton), so the desktop bridge can honour the button exactly
|
|
// like the pod's RIO board did.
|
|
//
|
|
if (address == 0x3F)
|
|
{
|
|
gBTReverseHeld = pressed ? 1 : 0;
|
|
if (getenv("BT_PAD_LOG"))
|
|
DEBUG_STREAM << "[padwrite] reverse button 0x3F "
|
|
<< (pressed ? "HELD" : "released") << "\n" << std::flush;
|
|
}
|
|
|
|
RIOEvent event;
|
|
event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent;
|
|
event.Data.Unit = address;
|
|
PushEvent(event);
|
|
}
|
|
|
|
void
|
|
PadRIO::EmitKeypad(int unit, int key)
|
|
{
|
|
RIOEvent event;
|
|
event.Type = KeyEvent;
|
|
event.Data.Keyboard.Unit = unit;
|
|
event.Data.Keyboard.Key = key;
|
|
PushEvent(event);
|
|
}
|
|
|
|
//###########################################################################
|
|
// The poll -- one pass per frame (time-gated so the manager's drain loop
|
|
// terminates; an AnalogEvent is emitted every pass to keep the manager's
|
|
// five-scalar push running, matching the serial board's analog cadence).
|
|
//###########################################################################
|
|
|
|
void
|
|
PadRIO::Poll()
|
|
{
|
|
//
|
|
// BT_BTNTEST="addr,pressPoll,releasePoll" (dev): scripted screen-button
|
|
// press through the REAL click seam (EmitButton -> RIO queue -> manager
|
|
// drain -> buttonGroup mapping) -- verifies the cockpit-click chain
|
|
// headlessly. Example: BT_BTNTEST=8,600,1200 holds button 0x8 (an MFD1
|
|
// PROGRAM element) from poll 600 to poll 1200.
|
|
//
|
|
{
|
|
static int s_btnAddr = -2, s_btnOn = 0, s_btnOff = 0, s_poll = 0, s_state = 0;
|
|
if (s_btnAddr == -2)
|
|
{
|
|
s_btnAddr = -1;
|
|
const char *e = getenv("BT_BTNTEST");
|
|
if (e != NULL)
|
|
sscanf(e, "%i,%i,%i", &s_btnAddr, &s_btnOn, &s_btnOff);
|
|
}
|
|
if (s_btnAddr >= 0)
|
|
{
|
|
++s_poll;
|
|
if (s_state == 0 && s_poll >= s_btnOn)
|
|
{
|
|
s_state = 1;
|
|
EmitButton(s_btnAddr, 1);
|
|
DEBUG_STREAM << "[btntest] PRESS 0x" << std::hex << s_btnAddr
|
|
<< std::dec << " at poll " << s_poll << "\n" << std::flush;
|
|
}
|
|
else if (s_state == 1 && s_poll >= s_btnOff)
|
|
{
|
|
s_state = 2;
|
|
EmitButton(s_btnAddr, 0);
|
|
DEBUG_STREAM << "[btntest] RELEASE 0x" << std::hex << s_btnAddr
|
|
<< std::dec << " at poll " << s_poll << "\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
unsigned long now = timeGetTime();
|
|
float dt = (lastPollMilliseconds == 0)
|
|
? 0.0f
|
|
: (float)(now - lastPollMilliseconds) * 0.001f;
|
|
if (dt > 0.1f)
|
|
{
|
|
dt = 0.1f; // resumed from a stall -- don't slam the integrators
|
|
}
|
|
lastPollMilliseconds = now;
|
|
|
|
//
|
|
// Issue #39 (field 2026-07-23): the simulation runs during load/wait,
|
|
// so a player could fire, walk and hear it all BEFORE the first frame
|
|
// presented ("I was able to input before the game screen came up").
|
|
// Until the scene is live, emit only the analog heartbeat -- keyboard,
|
|
// pad and joystick reads all stand down (edge state stays parked, so
|
|
// keys held across the boundary press cleanly at first frame). The
|
|
// scripted BT_BTNTEST harness above stays live (it injects through the
|
|
// event seam, same as the on-screen panel clicks).
|
|
//
|
|
{
|
|
extern Logical gBTSceneHasPresented;
|
|
if (!gBTSceneHasPresented)
|
|
{
|
|
RIOEvent analog;
|
|
analog.Type = AnalogEvent;
|
|
analog.Data.Unit = 0;
|
|
PushEvent(analog);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// XInput: hot-plug probe every ~3 s, then read the connected pad.
|
|
//-----------------------------------------------------------------
|
|
//
|
|
XINPUT_STATE pad_state;
|
|
int pad_connected = 0;
|
|
if (padIndex >= 0)
|
|
{
|
|
if (XInputGetState(padIndex, &pad_state) == ERROR_SUCCESS)
|
|
{
|
|
pad_connected = 1;
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "[padrio] XInput pad " << padIndex
|
|
<< " disconnected\n" << std::flush;
|
|
padIndex = -1;
|
|
// issue #24 (stuck look-view): RELEASE everything the pad was
|
|
// holding BEFORE forgetting it -- zeroing previousPadButtons
|
|
// without emitting the release edges left the game-side buttons
|
|
// pressed FOREVER (a hat held at the moment a flaky controller
|
|
// wrapper died latched the look view with no recovery; field
|
|
// report: PS3-pad-in-xbox-mode, stuck side view + pitch-down).
|
|
if (previousPadButtons != 0)
|
|
{
|
|
for (int b = 0; b < bindings.padButtonBindingCount; ++b)
|
|
{
|
|
const PadBindingProfile::PadButtonBinding &binding =
|
|
bindings.padButtonBindings[b];
|
|
if ((previousPadButtons & binding.buttonMask) != 0
|
|
&& binding.action.kind == PadBindingProfile::ActionButton)
|
|
{
|
|
EmitButton(binding.action.address, 0);
|
|
}
|
|
}
|
|
}
|
|
previousPadButtons = 0;
|
|
//
|
|
// Issue #36 (the #24 rule, axis edition): a pad that dies while
|
|
// DEFLECTED must release its direct channels too -- otherwise
|
|
// the mech keeps turning/aiming on a dead controller.
|
|
//
|
|
for (int a = 0; a < bindings.padAxisBindingCount; ++a)
|
|
{
|
|
if (previousPadAxisRaw[a] != 0.0f)
|
|
{
|
|
channelValue[bindings.padAxisBindings[a].channel] = 0.0f;
|
|
previousPadAxisRaw[a] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (padIndex < 0 && (lastPadProbeMilliseconds == 0 ||
|
|
now - lastPadProbeMilliseconds >= 3000))
|
|
{
|
|
lastPadProbeMilliseconds = now;
|
|
for (int slot = 0; slot < 4; ++slot)
|
|
{
|
|
if (XInputGetState(slot, &pad_state) == ERROR_SUCCESS)
|
|
{
|
|
padIndex = slot;
|
|
pad_connected = 1;
|
|
DEBUG_STREAM << "[padrio] XInput pad found in slot "
|
|
<< slot << "\n" << std::flush;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// Keyboard bindings: edges fire button/keypad events; held keys
|
|
// accumulate axis motion. All keys read as RELEASED without focus
|
|
// so held buttons let go when the developer alt-tabs.
|
|
//-----------------------------------------------------------------
|
|
//
|
|
int focused = ProcessHasFocus();
|
|
|
|
//
|
|
// The backtick view toggle (per Cyd: ` = 1st/3rd person in the glass
|
|
// cockpit) + V (the CONTROLS.MAP ViewToggle key -- keyboard
|
|
// reconciliation 2026-07-20). Edge-detected here (async poll,
|
|
// message-path-free) and consumed by the game's view-toggle block via
|
|
// BTPadViewToggleEdge.
|
|
//
|
|
// ⚠ A bindings.txt ROW WINS over these built-in convenience keys
|
|
// (2026-07-26). The default profile now hands the whole letter field to
|
|
// the MFD banks -- V is a Left Weapons button, J/K/L are Right Weapons --
|
|
// and polling them here as well would fire BOTH the button and the
|
|
// built-in, which is the double-dispatch the typed-channel suppression
|
|
// exists to prevent. Bind the key, and the built-in stands down.
|
|
{
|
|
static int s_backtickWas = 0;
|
|
// BOTH halves are KeyHasBinding-guarded (merge review 2026-07-26): the
|
|
// V half already was, but backtick polled unconditionally -- and
|
|
// BACKTICK is a nameable, unbound key, so a player who bound it to a
|
|
// button got a double-dispatch (button + camera toggle), the exact
|
|
// hazard the bindings-row-wins rule exists to prevent, in the one poll
|
|
// it had not been applied to.
|
|
int backtick_held = focused &&
|
|
((!KeyHasBinding(VK_OEM_3)
|
|
&& (GetAsyncKeyState(VK_OEM_3) & 0x8000) != 0) ||
|
|
(!KeyHasBinding('V') && (GetAsyncKeyState('V') & 0x8000) != 0));
|
|
if (backtick_held && !s_backtickWas)
|
|
{
|
|
extern int gBTPadViewToggleEdges;
|
|
++gBTPadViewToggleEdges;
|
|
}
|
|
s_backtickWas = backtick_held;
|
|
}
|
|
|
|
//
|
|
// J/K/L: cycle the Mfd1/Mfd2/Mfd3 preset page (the CONTROLS.MAP keys;
|
|
// the L4 mapper consumes gBTPresetCycle and runs the authentic
|
|
// SetPresetMode body -- btl4mppr.cpp CyclePresetModeNow).
|
|
//
|
|
{
|
|
static int s_presetWas[3] = { 0, 0, 0 };
|
|
static const int s_presetKey[3] = { 'J', 'K', 'L' };
|
|
for (int g = 0; g < 3; ++g)
|
|
{
|
|
int held = focused && !KeyHasBinding(s_presetKey[g]) &&
|
|
(GetAsyncKeyState(s_presetKey[g]) & 0x8000) != 0;
|
|
if (held && !s_presetWas[g])
|
|
{
|
|
gBTPresetCycle[g] = 1;
|
|
}
|
|
s_presetWas[g] = held;
|
|
}
|
|
}
|
|
|
|
float slewDelta[PadBindingProfile::ChannelCount];
|
|
int deflectHeld[PadBindingProfile::ChannelCount];
|
|
int slewHeld[PadBindingProfile::ChannelCount];
|
|
memset(slewDelta, 0, sizeof(slewDelta));
|
|
memset(deflectHeld, 0, sizeof(deflectHeld));
|
|
memset(slewHeld, 0, sizeof(slewHeld));
|
|
|
|
for (int k = 0; k < bindings.keyBindingCount; ++k)
|
|
{
|
|
const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k];
|
|
int held = focused &&
|
|
(GetAsyncKeyState(binding.virtualKey) & 0x8000) != 0;
|
|
int was_held = previousKeyHeld[k];
|
|
previousKeyHeld[k] = (unsigned char)held;
|
|
|
|
switch (binding.action.kind)
|
|
{
|
|
case PadBindingProfile::ActionButton:
|
|
if (held != was_held)
|
|
{
|
|
EmitButton(binding.action.address, held);
|
|
}
|
|
break;
|
|
|
|
case PadBindingProfile::ActionKeypad:
|
|
if (held && !was_held)
|
|
{
|
|
EmitKeypad(binding.action.address, binding.action.key);
|
|
}
|
|
break;
|
|
|
|
case PadBindingProfile::ActionAxisDeflect:
|
|
if (held)
|
|
{
|
|
deflectHeld[binding.action.channel] = 1;
|
|
channelValue[binding.action.channel] +=
|
|
binding.action.rate * dt;
|
|
}
|
|
break;
|
|
|
|
case PadBindingProfile::ActionAxisSlew:
|
|
if (held)
|
|
{
|
|
slewHeld[binding.action.channel] = 1;
|
|
slewDelta[binding.action.channel] +=
|
|
binding.action.rate * dt;
|
|
}
|
|
break;
|
|
|
|
case PadBindingProfile::ActionAxisSet:
|
|
if (held && !was_held)
|
|
{
|
|
channelValue[binding.action.channel] = binding.action.rate;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Spring return: a deflect-managed channel with no deflect key held
|
|
// re-centers at its fastest bound rate.
|
|
//
|
|
for (int c = 0; c < PadBindingProfile::ChannelCount; ++c)
|
|
{
|
|
channelValue[c] += slewDelta[c];
|
|
if (!deflectHeld[c] && channelReturnRate[c] > 0.0f)
|
|
{
|
|
float step = channelReturnRate[c] * dt;
|
|
if (channelValue[c] > step)
|
|
{
|
|
channelValue[c] -= step;
|
|
}
|
|
else if (channelValue[c] < -step)
|
|
{
|
|
channelValue[c] += step;
|
|
}
|
|
else
|
|
{
|
|
channelValue[c] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// Pad: button edges + axis writes (direct absolute past the
|
|
// deadzone; slew axes integrate).
|
|
//-----------------------------------------------------------------
|
|
//
|
|
if (pad_connected)
|
|
{
|
|
unsigned buttons = pad_state.Gamepad.wButtons;
|
|
for (int b = 0; b < bindings.padButtonBindingCount; ++b)
|
|
{
|
|
const PadBindingProfile::PadButtonBinding &binding =
|
|
bindings.padButtonBindings[b];
|
|
int held = (buttons & binding.buttonMask) != 0;
|
|
int was_held = (previousPadButtons & binding.buttonMask) != 0;
|
|
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);
|
|
}
|
|
}
|
|
previousPadButtons = buttons;
|
|
|
|
for (int a = 0; a < bindings.padAxisBindingCount; ++a)
|
|
{
|
|
const PadBindingProfile::PadAxisBinding &binding =
|
|
bindings.padAxisBindings[a];
|
|
float raw = 0.0f;
|
|
switch (binding.axis)
|
|
{
|
|
case PadBindingProfile::PadAxisLX:
|
|
raw = NormalizeThumb(pad_state.Gamepad.sThumbLX,
|
|
XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
|
|
break;
|
|
case PadBindingProfile::PadAxisLY:
|
|
raw = NormalizeThumb(pad_state.Gamepad.sThumbLY,
|
|
XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
|
|
break;
|
|
case PadBindingProfile::PadAxisRX:
|
|
raw = NormalizeThumb(pad_state.Gamepad.sThumbRX,
|
|
XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
|
|
break;
|
|
case PadBindingProfile::PadAxisRY:
|
|
raw = NormalizeThumb(pad_state.Gamepad.sThumbRY,
|
|
XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
|
|
break;
|
|
case PadBindingProfile::PadAxisLT:
|
|
raw = NormalizeTrigger(pad_state.Gamepad.bLeftTrigger);
|
|
break;
|
|
case PadBindingProfile::PadAxisRT:
|
|
raw = NormalizeTrigger(pad_state.Gamepad.bRightTrigger);
|
|
break;
|
|
}
|
|
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 (raw != 0.0f)
|
|
{
|
|
//
|
|
// Direct absolute: a deflected pad axis owns the channel;
|
|
// centered (inside the deadzone) it leaves the keyboard
|
|
// integration alone.
|
|
//
|
|
// DIAG (BT_PAD_LOG=1): who overwrites which channel?
|
|
if (getenv("BT_PAD_LOG"))
|
|
{
|
|
static int s_padWriteLog = 0;
|
|
if (s_padWriteLog < 60)
|
|
{
|
|
++s_padWriteLog;
|
|
DEBUG_STREAM << "[padwrite] axis " << binding.axis
|
|
<< " -> channel " << binding.channel
|
|
<< " value " << raw << std::endl << std::flush;
|
|
}
|
|
}
|
|
channelValue[binding.channel] = raw;
|
|
}
|
|
else if (previousPadAxisRaw[a] != 0.0f)
|
|
{
|
|
//
|
|
// RELEASE EDGE (issue #36): the axis just came back inside
|
|
// the deadzone -- write 0 ONCE so its deflection lets go
|
|
// (channels without a keyboard spring, e.g. Turn, latched
|
|
// at the last value forever). After this single write the
|
|
// centered axis leaves the channel to other inputs again.
|
|
//
|
|
channelValue[binding.channel] = 0.0f;
|
|
}
|
|
previousPadAxisRaw[a] = binding.slew ? 0.0f : raw;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// 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)
|
|
{
|
|
//
|
|
// Issue #36 (the #24 rule): a stick that detaches while
|
|
// deflected releases its direct channel once. (Throttle
|
|
// keeps its last lever position -- freezing the lever is
|
|
// safer than an all-stop on an unplug mid-fight.)
|
|
//
|
|
if (previousJoyAxisRaw[a] != 0.0f)
|
|
{
|
|
if (binding.channel != PadBindingProfile::ChannelThrottle)
|
|
{
|
|
channelValue[binding.channel] = 0.0f;
|
|
}
|
|
previousJoyAxisRaw[a] = 0.0f;
|
|
}
|
|
continue;
|
|
}
|
|
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;
|
|
}
|
|
else if (previousJoyAxisRaw[a] != 0.0f)
|
|
{
|
|
//
|
|
// RELEASE EDGE (issue #36): outside -> inside the deadzone
|
|
// writes 0 once -- see the pad-axis twin above.
|
|
//
|
|
channelValue[binding.channel] = 0.0f;
|
|
}
|
|
previousJoyAxisRaw[a] = binding.slew ? 0.0f : raw;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// GAIT DETENT (keyboard/pad accommodation, ported from the pod-build
|
|
// lever -- mech4.cpp "GAIT DETENT", 2026-07-20 glass regression): the
|
|
// gait SM has NO stable state for a demand between the walk cycle's
|
|
// cap (walkStrideLength @0x534) and the run engage speed
|
|
// (reverseSpeedMax @0x538) -- a lever PARKED in that band hunts
|
|
// walk <-> run forever, retriggering the authored EngineShiftFwd/Rev
|
|
// samples each swing. The pod's PHYSICAL throttle plausibly rested
|
|
// only at mechanical notches [T4]; reproduce the pod-build bridge's
|
|
// accommodation EXACTLY: when the throttle slew is AT REST (no slew
|
|
// key held, no pad slew axis deflected), snap the lever out of the
|
|
// dead band to the NEARER edge. Sweeping THROUGH the band while
|
|
// held stays continuous -- an authentic moving lever, firing the one
|
|
// authentic shift. The band arrives in lever units from the player
|
|
// mech via the mech4.cpp seam (hi <= lo = no band known yet).
|
|
//-----------------------------------------------------------------
|
|
//
|
|
{
|
|
extern float gBTGaitDetentLo, gBTGaitDetentHi; // mech4.cpp seam
|
|
const float band_lo = gBTGaitDetentLo;
|
|
const float band_hi = gBTGaitDetentHi;
|
|
float &lever = channelValue[PadBindingProfile::ChannelThrottle];
|
|
if (!slewHeld[PadBindingProfile::ChannelThrottle]
|
|
&& band_hi > band_lo
|
|
&& lever > band_lo && lever < band_hi)
|
|
{
|
|
const float snapped =
|
|
(lever - band_lo < (band_hi - band_lo) * 0.5f) ? band_lo : band_hi;
|
|
{ static int s_dLog = 0; if (getenv("BT_GAIT_TRACE") && s_dLog++ < 40)
|
|
DEBUG_STREAM << "[gaitdetent] pad lever " << lever
|
|
<< " in dead band [" << band_lo << "," << band_hi
|
|
<< ") -> " << snapped << "\n" << std::flush; }
|
|
lever = snapped;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// Clamp and publish the control surface. Throttle is the 0..1
|
|
// lever the mapper detents at 1.0; the rest are -1..1.
|
|
//-----------------------------------------------------------------
|
|
//
|
|
for (int c = 0; c < PadBindingProfile::ChannelCount; ++c)
|
|
{
|
|
float low = (c == PadBindingProfile::ChannelThrottle) ? 0.0f : -1.0f;
|
|
if (channelValue[c] < low) channelValue[c] = low;
|
|
if (channelValue[c] > 1.0f) channelValue[c] = 1.0f;
|
|
}
|
|
|
|
//
|
|
// PORT SIGN (user-reported inversion, closed with live sign algebra
|
|
// 2026-07-18): the mapper interprets the WIRE convention -- stick
|
|
// right = NEGATIVE JoystickX (the real RIO hardware / vRIO calibration
|
|
// convention; the keyboard bridge compensates by negating once,
|
|
// cb82d8c). Measured: wire stickX=-1 -> turnDemand=-1 = the same
|
|
// demand the user-verified D-key-RIGHT produces -- so screen-sign
|
|
// publish was inverted. X therefore publishes NEGATED; Y stays
|
|
// screen-sign. L4PADFLIP still flips both on top.
|
|
//
|
|
float stick_sign = flipStickAxes ? -1.0f : 1.0f;
|
|
Throttle = (Scalar)channelValue[PadBindingProfile::ChannelThrottle];
|
|
JoystickX = (Scalar)(stick_sign *
|
|
-channelValue[PadBindingProfile::ChannelJoystickX]);
|
|
JoystickY = (Scalar)(stick_sign *
|
|
channelValue[PadBindingProfile::ChannelJoystickY]);
|
|
LeftPedal = (Scalar)channelValue[PadBindingProfile::ChannelLeftPedal];
|
|
RightPedal = (Scalar)channelValue[PadBindingProfile::ChannelRightPedal];
|
|
// issue #25: the composite Turn channel decomposes into the pedal pair
|
|
// (signed: + = right, - = left); it ADDS to any direct pedal bindings so
|
|
// mixed rigs (stick turn + trigger pedals) compose.
|
|
{
|
|
float turn = channelValue[PadBindingProfile::ChannelTurn];
|
|
if (turn > 0.0f) RightPedal += (Scalar)turn;
|
|
else if (turn < 0.0f) LeftPedal += (Scalar)(-turn);
|
|
if (LeftPedal > 1.0f) LeftPedal = 1.0f;
|
|
if (RightPedal > 1.0f) RightPedal = 1.0f;
|
|
}
|
|
|
|
//
|
|
// The analog heartbeat: tells the manager to run the five-scalar
|
|
// push this frame (LBE4ControlsManager::Execute gates the push on
|
|
// new_RIO_values).
|
|
//
|
|
RIOEvent analog;
|
|
analog.Type = AnalogEvent;
|
|
analog.Data.Unit = 0;
|
|
PushEvent(analog);
|
|
}
|
|
|
|
//###########################################################################
|
|
// RIOBase surface
|
|
//###########################################################################
|
|
|
|
Logical
|
|
PadRIO::GetNextEvent(RIOEvent *destinationPointer)
|
|
{
|
|
Check_Pointer(destinationPointer);
|
|
|
|
if (eventTail == eventHead)
|
|
{
|
|
//
|
|
// Queue drained: poll at most once per millisecond tick so the
|
|
// manager's per-frame drain loop terminates (the poll always
|
|
// enqueues the analog heartbeat).
|
|
//
|
|
unsigned long now = timeGetTime();
|
|
if (now == lastPollMilliseconds)
|
|
{
|
|
return False;
|
|
}
|
|
Poll();
|
|
}
|
|
if (eventTail == eventHead)
|
|
{
|
|
return False;
|
|
}
|
|
*destinationPointer = eventQueue[eventTail];
|
|
eventTail = (eventTail + 1) % EventQueueSize;
|
|
return True;
|
|
}
|
|
|
|
void
|
|
PadRIO::SetLamp(int lampNumber, int state)
|
|
{
|
|
if (lampNumber >= 0 && lampNumber < LampCount)
|
|
{
|
|
lampState[lampNumber] = state;
|
|
if (keyLightActive)
|
|
{
|
|
// The mirror keeps its own copy (its paint loop runs on a private
|
|
// thread and must not reach into the device); the first 64
|
|
// addresses are the whole lamp field.
|
|
unsigned char bytes[64];
|
|
for (int i = 0; i < 64; ++i)
|
|
{
|
|
bytes[i] = (unsigned char) lampState[i];
|
|
}
|
|
KeyLight_UpdateLamps(bytes, 64);
|
|
}
|
|
}
|
|
}
|
|
|
|
//###########################################################################
|
|
// The on-screen panel entries
|
|
//###########################################################################
|
|
|
|
Logical
|
|
PadRIO::IsActive()
|
|
{
|
|
return activeInstance != NULL;
|
|
}
|
|
|
|
void
|
|
PadRIO::SetScreenButton(int unit, int pressed)
|
|
{
|
|
if (activeInstance == NULL)
|
|
{
|
|
return;
|
|
}
|
|
//
|
|
// The keypad address space (0x50-0x6F, the vRIO panel's two 4x4 hex
|
|
// keypads): press emits a keypad KeyEvent -- internal (0x50) on the
|
|
// pilot unit, external (0x60) on the operator unit; keys have no
|
|
// release event.
|
|
//
|
|
if (unit >= 0x50 && unit <= 0x6F)
|
|
{
|
|
if (pressed)
|
|
{
|
|
activeInstance->EmitKeypad((unit >= 0x60) ? 1 : 0, unit & 0x0F);
|
|
}
|
|
return;
|
|
}
|
|
if (unit < 0 || unit >= LBE4ControlsManager::ButtonCount)
|
|
{
|
|
return;
|
|
}
|
|
activeInstance->EmitButton(unit, pressed);
|
|
}
|
|
|
|
//
|
|
// Is the desktop PAD RIO the live input device? (On real pod hardware the RIO
|
|
// is the serial Ranger board and this is 0, so pod behaviour is never altered by
|
|
// the desktop-only seams that consult it.)
|
|
//
|
|
int BTPadRIOActive(void)
|
|
{
|
|
return PadRIO::HasActiveInstance();
|
|
}
|
|
|
|
int
|
|
PadRIO::GetLampState(int unit)
|
|
{
|
|
if (activeInstance == NULL || unit < 0 || unit >= LampCount)
|
|
{
|
|
return 0;
|
|
}
|
|
return activeInstance->lampState[unit];
|
|
}
|