Input: PadRIO -- play without the pod (L4CONTROLS=PAD), Workstream A.1

Ported from RP412: RIOBase split out of the serial RIO (L4RIO.h),
rioPointer is RIOBase* (L4CTRL.h), PAD token -> new PadRIO() speaking
the RIO surface from an XInput pad + keyboard (L4PADRIO/L4PADBINDINGS,
vRIO bindings.txt grammar, hot-plug), KeyLight RGB mirror TU
(BT412KEYLIGHT, /std:c++17 per-file).

BT-side fixes PadRIO forced into the open:
- Both keyboard input bridges (mech4.cpp, mechmppr.cpp BT_KEY_BRIDGE)
  stand down when a RIO device exists -- they overwrote the engine
  controls push every frame. M/X conveniences stay live.
- Mapper attribute chain OFF BY ONE (latent real-pod bug): the DOS
  chain below MechControlsMapper carried two base attributes, WinTesla
  carries one, and AttributeIndexSet::Find is positional -- the .CTL
  stick mapping wrote throttlePosition. Pad slot + binary-locked enum;
  gotcha ledgered (reconstruction-gotchas #11).

Verified: PAD throttle lever ramps + sticks, stick turns with the
authentic speed-vs-turn clamp (61.5 -> 22.0 u/s), mech drives; keyboard
fallback intact (BT_FORCE_THROTTLE harness). New diags: BT_CTRLMAP_LOG,
BT_STICK_LOG. (Phase 2 of docs/BT412-ROADMAP.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-14 08:16:33 -05:00
co-authored by Claude Fable 5
parent 55e4295dc3
commit fe97746a51
17 changed files with 2050 additions and 60 deletions
+3
View File
@@ -14,3 +14,6 @@ cdb_*.txt
.vs/
*.user
Thumbs.db
# per-user input profile, written by the game on first PAD run (cwd=content)
/content/bindings.txt
+10
View File
@@ -200,6 +200,9 @@ add_library(munga_engine STATIC
"engine/MUNGA_L4/L4NET.CPP"
"engine/MUNGA_L4/L4PARTICLES.cpp"
"engine/MUNGA_L4/L4PCSPAK.cpp"
"engine/MUNGA_L4/L4PADBINDINGS.cpp"
"engine/MUNGA_L4/L4PADRIO.cpp"
"engine/MUNGA_L4/L4KEYLIGHT.cpp"
"engine/MUNGA_L4/L4PLASMA.cpp"
"engine/MUNGA_L4/L4RIO.cpp"
"engine/MUNGA_L4/L4SERIAL.cpp"
@@ -225,6 +228,13 @@ target_include_directories(munga_engine BEFORE PRIVATE
target_compile_definitions(munga_engine PRIVATE ${BT_DEFS})
target_compile_options(munga_engine PRIVATE ${BT_OPTS})
# L4KEYLIGHT (RGB keyboard lamp mirror) is C++/WinRT: it needs C++17 and
# conformance mode, and must not include engine headers (see its header
# comment). The engine's /std:c++14 is overridden per-file; MSVC takes the
# last /std on the command line.
set_source_files_properties("engine/MUNGA_L4/L4KEYLIGHT.cpp" PROPERTIES
COMPILE_OPTIONS "/std:c++17;/permissive-")
#===========================================================================#
# Game logic: reconstructed BT modules + surviving-original BT source.
# Headers come from game/original/BT{,_L4} and the game/fwd shims
+4 -20
View File
@@ -1,8 +1,8 @@
[mission]
adventure=BattleTech
map=cavern
map=grass
scenario=freeforall
time=night
time=day
weather=clear
temperature=27
length=600
@@ -156,9 +156,8 @@ x=128
y=32
width=8
[pilots]
pilot=127.0.0.1:1502
pilot=127.0.0.1:1602
[127.0.0.1:1502]
pilot=200.0.0.96
[200.0.0.96]
hostType=0
advancedDamage=1
loadzones=1
@@ -172,20 +171,6 @@ dropzone=one
vehicle=bhk1
vehicleValue=1000
color=White
[127.0.0.1:1602]
hostType=0
advancedDamage=1
loadzones=1
name=Boreas
bitmapindex=2
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=ava1
vehicleValue=1000
color=Red
[largebitmap]
bitmap=BitMap::Large::Aeolus
[BitMap::Large::Aeolus]
@@ -242,4 +227,3 @@ width=4
model=dfltrole
[Role::NoReturn]
model=noretun
+13
View File
@@ -206,6 +206,19 @@ as `AttributeIndexSet::Find` crashing in `WeaponCluster::WeaponCluster("PercentD
(mechweap.cpp now static_asserts its pad base against `PoweredSubsystem::NextAttributeID`).
[T2]
**The inverse trap — SHORT parent chain (BT412, 2026-07-14):** `Find(ID)` is positional
(`attributeIndex[ID-1]`), so when the WinTesla parent chain carries FEWER attributes than the
DOS binary's did, every child attribute resolves one slot EARLY relative to a 1995 stream's
numeric IDs — no crash, values silently land in the WRONG MEMBER. Proven live the first time
a real device drove `MechControlsMapper`: the binary's `.CTL` control-mapping stream uses
stick=3/throttle=4 (the DOS Subsystem chain had 2 base attrs), the WinTesla chain has 1
(`SimulationState`), so the stick mapping wrote `throttlePosition` and the throttle mapping
wrote `pedalsPosition`. Only the numeric-ID path (CreateStreamedMappings / `.CTL`) is
affected — gauge databinding resolves by NAME and never saw it. Fix: a named pad slot at the
front of `mechmppr.cpp AttributePointers[]` + the enum locked to the binary's values.
Diagnostic: `BT_CTRLMAP_LOG=1` dumps each streamed mapping's resolved pointer — compare against
`&mapper->member` (the mech4 `[mppr]` log prints `&stick`/`&thr`). [T2]
## 12. Frame-pacing trap — the binary assumes a LOCKED 60 fps (task #11)
The 1995 pod ran frame-locked; reconstructed per-frame logic can carry HIDDEN frame-rate
+24 -1
View File
@@ -41,7 +41,30 @@ pattern). Forked at BT411 `4e72f0c` (2026-07-14).
31-subsystem ticks); two-instance loopback MP via btconsole.py — mesh forms
(console + 1502↔1602 game ports, "All connections completed!"), mission runs both
sides, each world ticks its master + the remote replicant. [T2]
- Phases 27: not started — see the roadmap.
- **Phase 2 (PadRIO input layer) — DONE 2026-07-14.** Ported from RP412:
`L4RIO.h` (RIOBase split; serial RIO re-parented), `L4CTRL.h` (`rioPointer` is
`RIOBase*`), `L4CTRL.cpp` PAD-token hook (→ `primaryControlType=PrimaryRIO`, so the
stock `MechRIOMapper` engages — confirmed), new `L4PADRIO.*` / `L4PADBINDINGS.*` /
`L4KEYLIGHT.*` (gates renamed `BT412KEYLIGHT`; KeyLight TU builds `/std:c++17
/permissive-` per-file). Default `bindings.txt` = the shared Tesla board layout
(numpad flight, letter rows = MFD banks, F-keys = Secondary columns, joystick head
0x40-0x47 on Space/arrows/pad). `content/bindings.txt` is gitignored (written on
first PAD run).
**BT-side changes PadRIO forced:** (1) both keyboard input bridges (mech4.cpp write
of mapper attrs; mechmppr.cpp `BT_KEY_BRIDGE`) now stand down when a RIO device
exists (`bridge_controls->rioPointer != 0`) — they clobbered the engine push every
frame; M mode-cycle / X recenter conveniences stay live. (2) **the mapper attribute
chain was off by one** — see [[reconstruction-gotchas]] §11 (short-parent-chain
positional trap): the `.CTL` stick mapping wrote `throttlePosition`. Fixed with a
pad slot + binary-locked enum. This was a latent REAL-POD bug (Phase 8 would have
hit it).
Verified live: `L4CONTROLS=PAD` → throttle lever ramps (rate axis, sticky), stick
deflects (turnDemand=1 + the authentic speed-vs-turn clamp: 61.5→22.0 u/s while
turning), mech drives across the map, XInput pad detected + 3 s re-probe, bindings
parse (59 key buttons/8 axes/12 pad buttons/5 pad axes). Keyboard fallback
regression-verified headless (`BT_FORCE_THROTTLE=1` walks). New diagnostics:
`BT_CTRLMAP_LOG=1` (streamed-mapping dump), `BT_STICK_LOG=1` (RIO stick push trace).
- Phases 37: not started — see the roadmap.
## The seams (what plugs in where) [T0 unless noted]
+38
View File
@@ -1,6 +1,8 @@
#include "mungal4.h"
#pragma hdrstop
#include "l4padrio.h"
#include "l4ctrl.h"
#include "l4keybd.h"
#include "l4app.h"
@@ -830,6 +832,19 @@ LBE4ControlsManager::LBE4ControlsManager():
joystickPointer = NULL;
}
}
else if (strcmpi(temp, "PAD") == 0)
{
//------------------------------------------------------
// Cockpit-less play: PadRIO speaks the RIO control
// surface from an XInput pad + the PC keyboard, so the
// full RIO mapper/lamp/button path runs unchanged.
//------------------------------------------------------
rioPointer = new PadRIO();
Check(rioPointer);
Register_Object(rioPointer);
flags.RIOExists = 1;
primaryControlType = LBE4ControlsManager::PrimaryRIO;
}
}
while (temp[0] != '\0');
}
@@ -1469,6 +1484,16 @@ void
stick.x = rioPointer->JoystickX;
stick.y = rioPointer->JoystickY;
{
// BT412 diagnostic (BT_STICK_LOG=1): trace the RIO stick push.
static const int s_stickLog = (getenv("BT_STICK_LOG") != 0);
if (s_stickLog && (stick.x != (Scalar)0 || stick.y != (Scalar)0))
{
DEBUG_STREAM << "[stick-push] x=" << (float)stick.x
<< " y=" << (float)stick.y << "\n" << std::flush;
}
}
Check(&joystickGroup[LBE4ControlsManager::JoystickPhysical]);
joystickGroup[LBE4ControlsManager::JoystickPhysical].Update(
&stick, mode_mask
@@ -2124,6 +2149,19 @@ void
if (mapping->mappingType == ControlsMapping::DirectMapping)
{
attribute = subsystem->GetAttributePointer(mapping->attributeID);
{
// BT412 diagnostic (BT_CTRLMAP_LOG=1): dump each streamed
// mapping as it installs -- group/element/attribute/mask.
static const int s_mapLog = (getenv("BT_CTRLMAP_LOG") != 0);
if (s_mapLog)
{
DEBUG_STREAM << "[ctrlmap] direct grp=" << (int)mapping->controlsGroup
<< " elem=" << element
<< " attrID=" << (int)mapping->attributeID
<< " mask=" << (unsigned)mode_mask
<< " ptr=" << attribute << "\n" << std::flush;
}
}
switch (mapping->controlsGroup)
{
case ScalarGroup:
+2 -2
View File
@@ -542,9 +542,9 @@ public:
public:
//-------------------------------------------------------------------
// RIO data
// RIO data (serial hardware or the PadRIO pad/keyboard synthesizer)
//-------------------------------------------------------------------
RIO
RIOBase
*rioPointer;
protected:
+397
View File
@@ -0,0 +1,397 @@
//===========================================================================//
// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror.
//
// COMPILED APART FROM THE ENGINE: /std:c++17, DEFAULT struct packing,
// conformance mode (see the per-file settings in Munga_L4.vcxproj).
// No engine headers may be included here - the engine compiles /Zp1
// and its types would take a different layout in this translation
// unit. The l4keylight.h interface is scalars only for that reason.
//
// Everything WinRT runs on a private worker thread: device watcher,
// keyboard claiming, and a 100 ms paint loop that mirrors the lamp
// bytes (same flash formula as the on-screen cockpit buttons in
// L4MFDVIEW, so the board and the screen blink in step).
//===========================================================================//
#include <windows.h>
#include <atomic>
#include <mutex>
#include <thread>
#include <vector>
#include <cstring>
#include <cstdio>
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <winrt/Windows.Devices.Lights.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.UI.h>
#pragma comment(lib, "windowsapp.lib")
#include "l4keylight.h"
namespace
{
using namespace winrt;
using namespace winrt::Windows::Devices::Enumeration;
using namespace winrt::Windows::Devices::Lights;
using winrt::Windows::UI::Color;
using winrt::Windows::System::VirtualKey;
struct KeyEntry
{
int virtualKey;
int address;
bool yellow;
};
struct ClaimedArray
{
hstring id;
LampArray array{ nullptr };
bool perKey = false;
bool baseCoated = false;
};
std::mutex gLock;
std::vector<KeyEntry> gMap;
unsigned char gLamps[64] = {};
void (*gLogger)(const char *) = nullptr;
std::atomic<bool> gRunning{ false };
std::thread gWorker;
void Log(const char *line)
{
void (*logger)(const char *);
{
std::lock_guard<std::mutex> hold(gLock);
logger = gLogger;
}
if (logger != nullptr)
{
logger(line);
}
}
void Logf(const char *format, ...)
{
char line[256];
va_list args;
va_start(args, format);
_vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args);
va_end(args);
Log(line);
}
//---------------------------------------------------------------
// Lamp byte -> brightness level 0-3, animating the flash modes.
// Identical to LampLevel in L4MFDVIEW.cpp so the keyboard and the
// on-screen buttons blink together.
//---------------------------------------------------------------
int LampLevel(int lamp_state)
{
int mode = lamp_state & 0x03;
int level1 = (lamp_state >> 2) & 0x03;
int level2 = (lamp_state >> 4) & 0x03;
if (mode == 0)
{
return level1;
}
static const int half_period[4] = { 0, 500, 250, 125 };
return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
}
//---------------------------------------------------------------
// The panel palette (vRIO's KeyboardLampMirror shades): red for
// the banks, yellow for Secondary/Screen; the off shade keeps the
// bound keys faintly visible so the board reads as a button field.
//---------------------------------------------------------------
Color Shade(int level, bool yellow)
{
Color color;
color.A = 255;
if (yellow)
{
if (level >= 3) { color.R = 245; color.G = 210; color.B = 60; }
else if (level >= 1) { color.R = 140; color.G = 118; color.B = 38; }
else { color.R = 70; color.G = 60; color.B = 24; }
}
else
{
if (level >= 3) { color.R = 230; color.G = 70; color.B = 70; }
else if (level >= 1) { color.R = 120; color.G = 50; color.B = 50; }
else { color.R = 64; color.G = 40; color.B = 40; }
}
return color;
}
bool SameColor(const Color &a, const Color &b)
{
return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B;
}
//---------------------------------------------------------------
// The worker: watcher + claim + paint loop
//---------------------------------------------------------------
void Worker()
{
try
{
init_apartment();
}
catch (...)
{
// apartment already set on this thread; carry on
}
std::mutex claimedLock;
std::vector<ClaimedArray> claimed;
bool anySeen = false;
DeviceWatcher watcher{ nullptr };
try
{
watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector());
watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info)
{
try
{
LampArray array = LampArray::FromIdAsync(info.Id()).get();
if (array == nullptr ||
array.LampArrayKind() != LampArrayKind::Keyboard)
{
return; // mice / strips / cases stay untouched
}
anySeen = true;
ClaimedArray entry;
entry.id = info.Id();
entry.array = array;
entry.perKey = array.SupportsVirtualKeys();
{
std::lock_guard<std::mutex> hold(claimedLock);
claimed.push_back(entry);
}
Logf(entry.perKey
? "KeyLight: + %ls (%d LEDs, per-key)"
: "KeyLight: + %ls (%d zones - board-wide mirror)",
info.Name().c_str(), (int) array.LampCount());
}
catch (...)
{
Log("KeyLight: could not open a lamp array");
}
});
watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update)
{
std::lock_guard<std::mutex> hold(claimedLock);
for (size_t i = 0; i < claimed.size(); ++i)
{
if (claimed[i].id == update.Id())
{
claimed.erase(claimed.begin() + i);
Log("KeyLight: keyboard disconnected");
break;
}
}
});
watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &)
{
// required for the watcher to progress
});
watcher.Start();
}
catch (...)
{
Log("KeyLight: Dynamic Lighting unavailable on this system");
return;
}
//
// Paint loop: 100 ms cadence, repaint only on change
//
std::vector<Color> lastColors;
int waited = 0;
while (gRunning.load())
{
Sleep(50);
waited += 50;
if (waited < 100)
{
continue;
}
waited = 0;
std::vector<KeyEntry> map;
unsigned char lamps[64];
{
std::lock_guard<std::mutex> hold(gLock);
map = gMap;
memcpy(lamps, gLamps, sizeof(lamps));
}
std::vector<Color> colors(map.size());
std::vector<VirtualKey> keys(map.size());
int bestLevel = 0;
bool bestYellow = false;
bool changed = (lastColors.size() != map.size());
for (size_t i = 0; i < map.size(); ++i)
{
int address = map[i].address;
int level = (address >= 0 && address < 64)
? LampLevel(lamps[address]) : 0;
if (level > bestLevel)
{
bestLevel = level;
bestYellow = map[i].yellow;
}
colors[i] = Shade(level, map[i].yellow);
keys[i] = (VirtualKey) map[i].virtualKey;
if (!changed && !SameColor(colors[i], lastColors[i]))
{
changed = true;
}
}
std::lock_guard<std::mutex> hold(claimedLock);
bool freshClaim = false;
for (ClaimedArray &entry : claimed)
{
if (!entry.baseCoated)
{
freshClaim = true;
}
}
if (!changed && !freshClaim)
{
continue;
}
Color aggregate = Shade(bestLevel, bestYellow);
for (ClaimedArray &entry : claimed)
{
try
{
if (entry.perKey)
{
if (!entry.baseCoated)
{
Color black;
black.A = 255; black.R = 0; black.G = 0; black.B = 0;
entry.array.SetColor(black);
entry.baseCoated = true;
}
if (!map.empty())
{
entry.array.SetColorsForKeys(
array_view<Color const>(colors.data(), colors.data() + colors.size()),
array_view<VirtualKey const>(keys.data(), keys.data() + keys.size()));
}
}
else
{
entry.baseCoated = true;
entry.array.SetColor(aggregate);
}
}
catch (...)
{
// device wobble; the watcher handles removal
}
}
lastColors = colors;
}
//
// Releasing the arrays hands the LEDs back to Windows
//
try
{
watcher.Stop();
}
catch (...)
{
}
{
std::lock_guard<std::mutex> hold(claimedLock);
claimed.clear();
}
if (!anySeen)
{
Log("KeyLight: no Dynamic Lighting keyboard was found this session");
}
}
}
//########################################################################
// The scalar interface (safe across the packing boundary)
//########################################################################
void
KeyLight_SetLogger(void (*logger)(const char *line))
{
std::lock_guard<std::mutex> hold(gLock);
gLogger = logger;
}
void
KeyLight_SetMap(
const int *virtual_keys,
const int *addresses,
const unsigned char *yellow,
int count
)
{
std::lock_guard<std::mutex> hold(gLock);
gMap.clear();
gMap.reserve(count);
for (int i = 0; i < count; ++i)
{
KeyEntry entry;
entry.virtualKey = virtual_keys[i];
entry.address = addresses[i];
entry.yellow = (yellow[i] != 0);
gMap.push_back(entry);
}
}
void
KeyLight_UpdateLamps(const unsigned char *lamp_state, int count)
{
if (count > 64)
{
count = 64;
}
std::lock_guard<std::mutex> hold(gLock);
memcpy(gLamps, lamp_state, count);
}
void
KeyLight_Start()
{
if (gRunning.exchange(true))
{
return;
}
gWorker = std::thread(Worker);
}
void
KeyLight_Stop()
{
if (!gRunning.exchange(false))
{
return;
}
if (gWorker.joinable())
{
gWorker.join();
}
}
+53
View File
@@ -0,0 +1,53 @@
//===========================================================================//
// File: l4keylight.h //
// Project: MUNGA Brick: RGB keyboard lamp mirror //
// Contents: Windows Dynamic Lighting bridge (vRIO's KeyboardLampMirror) //
//---------------------------------------------------------------------------//
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
#pragma once
//########################################################################
// Mirrors the game-commanded RIO lamp states onto per-key RGB keyboards
// through Windows Dynamic Lighting (the WinRT LampArray API), following
// vRIO's KeyboardLampMirror: keys bound to lamp addresses glow with the
// panel palette (red for the banks, yellow for the Secondary/Screen
// columns), flash modes blink at the panel's rates, other keys are
// blacked out so the keyboard reads as the button field. Zone-lit
// keyboards mirror the strongest lamp board-wide. Dynamic Lighting
// grants LED control to the FOREGROUND app - which is the game itself,
// so no Windows settings dance is needed.
//
// The implementation is C++/WinRT on a private worker thread, compiled
// with default struct packing (the engine builds /Zp1, which would
// break the WinRT ABI) - hence this interface is scalars only. All
// functions are safe to call when Dynamic Lighting is unavailable;
// failures log once and the mirror stays dormant.
//########################################################################
// Where the mirror's status lines go (the caller owns the sink).
void
KeyLight_SetLogger(void (*logger)(const char *line));
// The key map: parallel arrays of virtual keys, their RIO lamp
// addresses (0x00-0x47), and whether each paints yellow (Secondary /
// Screen columns) instead of red.
void
KeyLight_SetMap(
const int *virtual_keys,
const int *addresses,
const unsigned char *yellow,
int count
);
// Latest game-commanded lamp bytes (indexed by RIO lamp number).
void
KeyLight_UpdateLamps(const unsigned char *lamp_state, int count);
// Claim keyboards and start painting / release the LEDs to Windows.
void
KeyLight_Start();
void
KeyLight_Stop();
+542
View File
@@ -0,0 +1,542 @@
#include "mungal4.h"
#pragma hdrstop
#include "l4padbindings.h"
#include <XInput.h>
#include <stdio.h>
//########################################################################
// Name tables: .NET Keys names (what vRIO uses) to virtual keys, pad
// names to XInput masks. Matching is case-insensitive.
//########################################################################
namespace
{
struct NameValue
{
const char *name;
int value;
};
const NameValue kKeyNames[] =
{
// letters and digits are generated in LookupKey
{ "F1", VK_F1 }, { "F2", VK_F2 }, { "F3", VK_F3 }, { "F4", VK_F4 },
{ "F5", VK_F5 }, { "F6", VK_F6 }, { "F7", VK_F7 }, { "F8", VK_F8 },
{ "F9", VK_F9 }, { "F10", VK_F10 }, { "F11", VK_F11 }, { "F12", VK_F12 },
{ "NumPad0", VK_NUMPAD0 }, { "NumPad1", VK_NUMPAD1 },
{ "NumPad2", VK_NUMPAD2 }, { "NumPad3", VK_NUMPAD3 },
{ "NumPad4", VK_NUMPAD4 }, { "NumPad5", VK_NUMPAD5 },
{ "NumPad6", VK_NUMPAD6 }, { "NumPad7", VK_NUMPAD7 },
{ "NumPad8", VK_NUMPAD8 }, { "NumPad9", VK_NUMPAD9 },
{ "Divide", VK_DIVIDE }, { "Multiply", VK_MULTIPLY },
{ "Subtract", VK_SUBTRACT }, { "Add", VK_ADD },
{ "Decimal", VK_DECIMAL },
{ "Up", VK_UP }, { "Down", VK_DOWN },
{ "Left", VK_LEFT }, { "Right", VK_RIGHT },
{ "Space", VK_SPACE }, { "Return", VK_RETURN }, { "Enter", VK_RETURN },
{ "Escape", VK_ESCAPE }, { "Tab", VK_TAB }, { "Back", VK_BACK },
{ "Home", VK_HOME }, { "End", VK_END },
{ "PageUp", VK_PRIOR }, { "PageDown", VK_NEXT },
{ "Prior", VK_PRIOR }, { "Next", VK_NEXT },
{ "Insert", VK_INSERT }, { "Delete", VK_DELETE },
{ "ShiftKey", VK_SHIFT }, { "Shift", VK_SHIFT },
{ "ControlKey", VK_CONTROL }, { "Ctrl", VK_CONTROL }, { "Control", VK_CONTROL },
{ "Menu", VK_MENU }, { "Alt", VK_MENU },
{ "OemMinus", VK_OEM_MINUS }, { "Oemplus", VK_OEM_PLUS },
{ "Oemcomma", VK_OEM_COMMA }, { "OemPeriod", VK_OEM_PERIOD },
{ "OemOpenBrackets", VK_OEM_4 }, { "OemCloseBrackets", VK_OEM_6 },
{ "OemSemicolon", VK_OEM_1 }, { "OemQuotes", VK_OEM_7 },
{ "OemQuestion", VK_OEM_2 }, { "OemPipe", VK_OEM_5 },
{ "Oemtilde", VK_OEM_3 },
};
const NameValue kPadButtonNames[] =
{
{ "A", XINPUT_GAMEPAD_A }, { "B", XINPUT_GAMEPAD_B },
{ "X", XINPUT_GAMEPAD_X }, { "Y", XINPUT_GAMEPAD_Y },
{ "DPadUp", XINPUT_GAMEPAD_DPAD_UP },
{ "DPadDown", XINPUT_GAMEPAD_DPAD_DOWN },
{ "DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT },
{ "DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT },
{ "Start", XINPUT_GAMEPAD_START }, { "Back", XINPUT_GAMEPAD_BACK },
{ "LeftShoulder", XINPUT_GAMEPAD_LEFT_SHOULDER },
{ "RightShoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER },
{ "LeftThumb", XINPUT_GAMEPAD_LEFT_THUMB },
{ "RightThumb", XINPUT_GAMEPAD_RIGHT_THUMB },
};
const NameValue kPadAxisNames[] =
{
{ "LeftStickX", BindPadLeftStickX }, { "LeftStickY", BindPadLeftStickY },
{ "RightStickX", BindPadRightStickX }, { "RightStickY", BindPadRightStickY },
{ "LeftTrigger", BindPadLeftTrigger }, { "RightTrigger", BindPadRightTrigger },
};
const NameValue kRioAxisNames[] =
{
{ "Throttle", BindAxisThrottle },
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
};
Logical NameEquals(const char *a, const char *b)
{
return _stricmp(a, b) == 0;
}
int LookupTable(const NameValue *table, int count, const char *name)
{
for (int i = 0; i < count; ++i)
{
if (NameEquals(table[i].name, name))
{
return table[i].value;
}
}
return -1;
}
int LookupKey(const char *name)
{
// single letter A-Z (VK == uppercase ASCII)
if (name[1] == '\0' &&
((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')))
{
return toupper(name[0]);
}
// digit row: D0-D9 (VK == '0'-'9')
if ((name[0] == 'D' || name[0] == 'd') &&
name[1] >= '0' && name[1] <= '9' && name[2] == '\0')
{
return name[1];
}
return LookupTable(kKeyNames, sizeof(kKeyNames) / sizeof(kKeyNames[0]), name);
}
Logical ParseAddress(const char *text, int *address)
{
int value = -1;
if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X'))
{
if (sscanf(text + 2, "%x", &value) != 1)
{
return False;
}
}
else if (sscanf(text, "%d", &value) != 1)
{
return False;
}
if ((value >= 0x00 && value <= 0x47) || (value >= 0x50 && value <= 0x6F))
{
*address = value;
return True;
}
return False;
}
Logical ParseNumber(const char *text, Scalar *value)
{
float parsed;
if (sscanf(text, "%f", &parsed) != 1)
{
return False;
}
*value = (Scalar) parsed;
return True;
}
//---------------------------------------------------------------
// Line tokenizer: whitespace separated, '#' starts a comment
//---------------------------------------------------------------
int Tokenize(char *line, char *tokens[], int max_tokens)
{
char *hash = strchr(line, '#');
if (hash != NULL)
{
*hash = '\0';
}
int count = 0;
char *cursor = line;
while (count < max_tokens)
{
while (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n')
{
++cursor;
}
if (*cursor == '\0')
{
break;
}
tokens[count++] = cursor;
while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' &&
*cursor != '\r' && *cursor != '\n')
{
++cursor;
}
if (*cursor != '\0')
{
*cursor++ = '\0';
}
}
return count;
}
//---------------------------------------------------------------
// One line of the profile grammar
//---------------------------------------------------------------
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
{
if (token_count < 4)
{
return False;
}
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "button"))
{
int key = LookupKey(tokens[1]);
int address;
if (key < 0 || !ParseAddress(tokens[3], &address) ||
profile->keyButtonCount >= PadBindingProfile::maxKeyButtons)
{
return False;
}
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
PadKeyButtonBinding *binding = &profile->keyButtons[profile->keyButtonCount++];
memset(binding, 0, sizeof(*binding));
binding->virtualKey = key;
binding->address = address;
binding->toggle = toggle;
return True;
}
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "axis"))
{
if (token_count < 6)
{
return False;
}
int key = LookupKey(tokens[1]);
int axis = LookupTable(kRioAxisNames,
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
int mode = NameEquals(tokens[4], "deflect") ? BindKeyDeflect
: NameEquals(tokens[4], "rate") ? BindKeyRate : -1;
Scalar value;
if (key < 0 || axis < 0 || mode < 0 || !ParseNumber(tokens[5], &value) ||
profile->keyAxisCount >= PadBindingProfile::maxKeyAxes)
{
return False;
}
PadKeyAxisBinding *binding = &profile->keyAxes[profile->keyAxisCount++];
binding->virtualKey = key;
binding->axis = axis;
binding->mode = mode;
binding->value = value;
return True;
}
if (NameEquals(tokens[0], "pad") && NameEquals(tokens[2], "button"))
{
int mask = LookupTable(kPadButtonNames,
sizeof(kPadButtonNames) / sizeof(kPadButtonNames[0]), tokens[1]);
int address;
if (mask < 0 || !ParseAddress(tokens[3], &address) ||
profile->padButtonCount >= PadBindingProfile::maxPadButtons)
{
return False;
}
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
PadPadButtonBinding *binding = &profile->padButtons[profile->padButtonCount++];
memset(binding, 0, sizeof(*binding));
binding->padMask = (unsigned short) mask;
binding->address = address;
binding->toggle = toggle;
return True;
}
if (NameEquals(tokens[0], "padaxis") && NameEquals(tokens[2], "axis"))
{
int source = LookupTable(kPadAxisNames,
sizeof(kPadAxisNames) / sizeof(kPadAxisNames[0]), tokens[1]);
int axis = LookupTable(kRioAxisNames,
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
if (source < 0 || axis < 0 ||
profile->padAxisCount >= PadBindingProfile::maxPadAxes)
{
return False;
}
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
memset(binding, 0, sizeof(*binding));
binding->source = source;
binding->axis = axis;
for (int i = 4; i < token_count; ++i)
{
if (NameEquals(tokens[i], "invert"))
{
binding->invert = True;
}
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
{
if (!ParseNumber(tokens[++i], &binding->deadzone))
{
return False;
}
}
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
{
if (!ParseNumber(tokens[++i], &binding->rate))
{
return False;
}
}
else
{
return False;
}
}
return True;
}
return False;
}
//---------------------------------------------------------------
// The default profile: vRIO's board-complete layout, adapted for
// the desktop game - the driving keys (WASD/QE/PgUp/PgDn) stay
// axes, so six bank buttons lose their keyboard keys (they are
// still clickable on the on-screen cockpit, and rebindable here).
//---------------------------------------------------------------
const char kDefaultProfile[] =
"# BT412 input bindings - keyboard and Xbox (XInput) controller.\n"
"#\n"
"# One binding per line, '#' starts a comment, keywords are case-\n"
"# insensitive. Loaded at game start; delete this file to restore\n"
"# the defaults.\n"
"#\n"
"# key <name> button <addr> [toggle]\n"
"# key <name> axis <axis> deflect <n>\n"
"# key <name> axis <axis> rate <n-per-second>\n"
"# pad <button> button <addr> [toggle]\n"
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
"#\n"
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
"# <button> A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back\n"
"# LeftShoulder RightShoulder LeftThumb RightThumb\n"
"# <src> LeftStickX LeftStickY RightStickX RightStickY\n"
"# LeftTrigger RightTrigger\n"
"#\n"
"# 'deflect' holds the axis there while the key is down and springs\n"
"# back on release; 'rate' walks the axis by <n> per second and the\n"
"# position sticks (the throttle). Every lamp button is also clickable\n"
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
"\n"
"# ---- Driving: number pad + modifiers ------------------------------\n"
"# The whole letter board stays free for the MFD banks; driving lives\n"
"# on the numpad with the throttle lever on the modifier keys.\n"
"key NumPad8 axis JoystickY deflect -1 # stick forward\n"
"key NumPad2 axis JoystickY deflect 1 # stick back\n"
"key NumPad4 axis JoystickX deflect 1 # stick left\n"
"key NumPad6 axis JoystickX deflect -1 # stick right\n"
"key NumPad7 axis LeftPedal deflect 1\n"
"key NumPad9 axis RightPedal deflect 1\n"
"key NumPad0 button 0x40 # Main trigger (Space works too)\n"
"key Shift axis Throttle rate 0.75 # throttle up\n"
"key Ctrl axis Throttle rate -0.75 # throttle down\n"
"key Alt button 0x3F # Throttle-head button\n"
"\n"
"# ---- Driving: Xbox controller (signs match the pod convention) ----\n"
"padaxis LeftStickX axis JoystickX invert deadzone 0.24\n"
"padaxis LeftStickY axis JoystickY invert deadzone 0.24\n"
"padaxis RightStickY axis Throttle deadzone 0.24 rate 0.75\n"
"padaxis LeftTrigger axis LeftPedal deadzone 0.12\n"
"padaxis RightTrigger axis RightPedal deadzone 0.12\n"
"\n"
"# ---- Joystick column: hat on the arrows, Main on Space ------------\n"
"key Space button 0x40 # Main trigger\n"
"key Up button 0x42 # Hat Up\n"
"key Down button 0x41 # Hat Back\n"
"key Left button 0x44 # Hat Left\n"
"key Right button 0x43 # Hat Right\n"
"\n"
"# ---- Xbox controller: buttons -------------------------------------\n"
"pad A button 0x40 # Main\n"
"pad B button 0x46 # Middle\n"
"pad X button 0x45 # Pinky\n"
"pad Y button 0x47 # Upper\n"
"pad DPadUp button 0x42 # Hat Up\n"
"pad DPadDown button 0x41 # Hat Back\n"
"pad DPadLeft button 0x44 # Hat Left\n"
"pad DPadRight button 0x43 # Hat Right\n"
"pad LeftShoulder button 0x3D # Panic\n"
"pad RightShoulder button 0x3F # Throttle-head button\n"
"pad Start button 0x37 # config 1\n"
"pad Back button 0x36 # config 2\n"
"\n"
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
"# ---- QWERTY row is the row under it, left to right across the\n"
"# ---- Left / Middle / Right MFDs.\n"
"key D1 button 0x2F\n"
"key D2 button 0x2E\n"
"key D3 button 0x2D\n"
"key D4 button 0x2C\n"
"key D5 button 0x27\n"
"key D6 button 0x26\n"
"key D7 button 0x25\n"
"key D8 button 0x24\n"
"key D9 button 0x37\n"
"key D0 button 0x36\n"
"key OemMinus button 0x35\n"
"key Oemplus button 0x34\n"
"key Q button 0x2B\n"
"key W button 0x2A\n"
"key E button 0x29\n"
"key R button 0x28\n"
"key T button 0x23\n"
"key Y button 0x22\n"
"key U button 0x21\n"
"key I button 0x20\n"
"key O button 0x33\n"
"key P button 0x32\n"
"key OemOpenBrackets button 0x31\n"
"key OemCloseBrackets button 0x30\n"
"\n"
"# ---- Lower MFD bank: home row and the row below it, two 4-key\n"
"# ---- blocks split by an unbound gap key (G / B), mirroring the\n"
"# ---- keypad gap between the Lower Left and Lower Right MFDs.\n"
"key A button 0x0F\n"
"key S button 0x0E\n"
"key D button 0x0D\n"
"key F button 0x0C\n"
"key H button 0x07\n"
"key J button 0x06\n"
"key K button 0x05\n"
"key L button 0x04\n"
"key Z button 0x0B\n"
"key X button 0x0A\n"
"key C button 0x09\n"
"key V button 0x08\n"
"key N button 0x03\n"
"key M button 0x02\n"
"key Oemcomma button 0x01\n"
"key OemPeriod button 0x00\n"
"\n"
"# ---- Secondary / Screen columns on the function keys, top to\n"
"# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).\n"
"key F1 button 0x10\n"
"key F2 button 0x11\n"
"key F3 button 0x12\n"
"key F4 button 0x13\n"
"key F5 button 0x14\n"
"key F6 button 0x15\n"
"key F7 button 0x18\n"
"key F8 button 0x19\n"
"key F9 button 0x1A\n"
"key F10 button 0x1B\n"
"key F11 button 0x1C\n"
"key F12 button 0x1D\n"
"\n"
"# The pilot keypad (0x50-0x5F) and external keypad (0x60-0x6F) are\n"
"# unbound - the game never reads them - but remain bindable here\n"
"# (they arrive as arcade RIO key events).\n";
const char kBindingsFileName[] = "bindings.txt";
}
//########################################################################
// Loader
//########################################################################
void
PadBindings_Load(PadBindingProfile *profile)
{
memset(profile, 0, sizeof(*profile));
//
// First run: write the default profile so the player has a
// self-documenting file to edit
//
FILE *file = fopen(kBindingsFileName, "rb");
if (file == NULL)
{
FILE *out = fopen(kBindingsFileName, "wb");
if (out != NULL)
{
fwrite(kDefaultProfile, 1, strlen(kDefaultProfile), out);
fclose(out);
DEBUG_STREAM << "PadBindings: wrote default " << kBindingsFileName
<< "\n" << std::flush;
}
file = fopen(kBindingsFileName, "rb");
}
//
// Parse: file if we have one, the built-in default if not
//
char *text = NULL;
long size = 0;
if (file != NULL)
{
fseek(file, 0, SEEK_END);
size = ftell(file);
fseek(file, 0, SEEK_SET);
text = new char[size + 1];
fread(text, 1, size, file);
text[size] = '\0';
fclose(file);
}
const char *source = (text != NULL) ? text : kDefaultProfile;
char line[256];
int line_number = 0;
int error_count = 0;
const char *cursor = source;
while (*cursor != '\0')
{
int length = 0;
while (cursor[length] != '\0' && cursor[length] != '\n' &&
length < (int) sizeof(line) - 1)
{
line[length] = cursor[length];
++length;
}
line[length] = '\0';
cursor += length;
if (*cursor == '\n')
{
++cursor;
}
++line_number;
char *tokens[12];
int token_count = Tokenize(line, tokens, 12);
if (token_count == 0)
{
continue;
}
if (!ParseLine(tokens, token_count, profile))
{
++error_count;
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
<< line_number << " rejected\n" << std::flush;
}
}
if (text != NULL)
{
delete[] text;
}
DEBUG_STREAM << "PadBindings: " << profile->keyButtonCount << " key buttons, "
<< profile->keyAxisCount << " key axes, "
<< profile->padButtonCount << " pad buttons, "
<< profile->padAxisCount << " pad axes"
<< (error_count ? " (with rejected lines)" : "")
<< "\n" << std::flush;
}
+116
View File
@@ -0,0 +1,116 @@
//===========================================================================//
// File: l4padbindings.h //
// Project: MUNGA Brick: PadRIO binding profiles //
// Contents: The rebindable input profile (vRIO bindings format) //
//---------------------------------------------------------------------------//
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
#pragma once
#include "..\munga\style.h"
//########################################################################
// The bindings file (vRIO's format, shared grammar):
//
// key <name> button <addr> [toggle]
// key <name> axis <axis> deflect <n> | rate <n>
// pad <button> button <addr> [toggle]
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
//
// <addr> is a RIO input address: lamp buttons 0x00-0x47, internal
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
// bindings.txt beside the exe; written there (self-documenting, with
// the full default profile) on first run. Bad lines are logged and
// skipped, good lines always win.
//########################################################################
enum PadBindRioAxis
{
BindAxisThrottle = 0,
BindAxisLeftPedal,
BindAxisRightPedal,
BindAxisJoystickY,
BindAxisJoystickX,
BindAxisCount
};
enum PadBindKeyMode
{
BindKeyDeflect = 0, // hold at value while down, spring back
BindKeyRate // walk by value/second while down, position holds
};
enum PadBindPadAxis
{
BindPadLeftStickX = 0,
BindPadLeftStickY,
BindPadRightStickX,
BindPadRightStickY,
BindPadLeftTrigger,
BindPadRightTrigger,
BindPadAxisCount
};
struct PadKeyButtonBinding
{
int virtualKey;
int address; // 0x00-0x47 button, 0x50-0x6F keypad
Logical toggle;
// runtime edge/latch state
Logical wasDown;
Logical latched;
};
struct PadKeyAxisBinding
{
int virtualKey;
int axis; // PadBindRioAxis
int mode; // PadBindKeyMode
Scalar value;
};
struct PadPadButtonBinding
{
unsigned short padMask; // XINPUT_GAMEPAD_*
int address;
Logical toggle;
Logical wasDown;
Logical latched;
};
struct PadPadAxisBinding
{
int source; // PadBindPadAxis
int axis; // PadBindRioAxis
Logical invert;
Scalar deadzone; // normalized 0..1
Scalar rate; // 0 = direct position, >0 = speed integrate
};
struct PadBindingProfile
{
enum
{
maxKeyButtons = 128,
maxKeyAxes = 32,
maxPadButtons = 32,
maxPadAxes = 16
};
PadKeyButtonBinding keyButtons[maxKeyButtons];
int keyButtonCount;
PadKeyAxisBinding keyAxes[maxKeyAxes];
int keyAxisCount;
PadPadButtonBinding padButtons[maxPadButtons];
int padButtonCount;
PadPadAxisBinding padAxes[maxPadAxes];
int padAxisCount;
};
// Load bindings.txt from the working directory into the profile,
// writing the default profile file first when absent. Errors go to
// the debug log with line numbers.
void
PadBindings_Load(PadBindingProfile *profile);
+546
View File
@@ -0,0 +1,546 @@
#include "mungal4.h"
#pragma hdrstop
#include "l4padrio.h"
#include "l4keylight.h"
#include <XInput.h>
#pragma comment(lib, "xinput9_1_0.lib")
//########################################################################
// Input helpers; the binding tables live in bindings.txt now
// (l4padbindings.cpp writes and parses the vRIO-format profile)
//########################################################################
namespace
{
Scalar StickValue(int raw, int dead_zone)
{
if (raw > -dead_zone && raw < dead_zone)
{
return (Scalar) 0;
}
Scalar value =
(raw > 0)
? (Scalar)(raw - dead_zone) / (Scalar)(32767 - dead_zone)
: (Scalar)(raw + dead_zone) / (Scalar)(32768 - dead_zone);
if (value > 1.0f) value = 1.0f;
if (value < -1.0f) value = -1.0f;
return value;
}
Scalar Clamp01(Scalar value)
{
if (value < 0.0f) return 0.0f;
if (value > 1.0f) return 1.0f;
return value;
}
Logical KeyDown(int virtual_key)
{
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
}
void KeyLightLog(const char *line)
{
DEBUG_STREAM << line << "\n" << std::flush;
}
}
//########################################################################
//############################### PadRIO #################################
//########################################################################
PadRIO *PadRIO::activeInstance = NULL;
void
PadRIO::SetScreenButton(int unit, Logical pressed)
{
if (activeInstance != NULL && unit >= 0 && unit < buttonUnits)
{
activeInstance->screenButton[unit] = pressed ? 1 : 0;
}
}
int
PadRIO::GetLampState(int unit)
{
if (activeInstance != NULL && unit >= 0 && unit < lampCount)
{
return activeInstance->lampState[unit];
}
return 0;
}
PadRIO::PadRIO()
{
Check_Pointer(this);
queueHead = 0;
queueTail = 0;
lastPollTick = GetTickCount();
lastPadCheckTick = 0;
padIndex = -1;
padReported = False;
analogRequested = False;
throttleAccum = (Scalar) 0;
sentThrottle = sentLeftPedal = sentRightPedal = (Scalar) 0;
sentJoystickX = sentJoystickY = (Scalar) 0;
memset(buttonDown, 0, sizeof(buttonDown));
memset(keypadDown, 0, sizeof(keypadDown));
memset(lampState, 0, sizeof(lampState));
memset(screenButton, 0, sizeof(screenButton));
PadBindings_Load(&profile);
//
// RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound
// to lamp addresses glow with the panel. Yellow = the Secondary
// columns (0x10-0x1F), red = everything else, like the physical
// panel. BT412KEYLIGHT=0 opts out.
//
keyLightActive = False;
const char *keylight = getenv("BT412KEYLIGHT");
if (keylight == NULL || atoi(keylight) != 0)
{
int light_keys[PadBindingProfile::maxKeyButtons];
int light_addresses[PadBindingProfile::maxKeyButtons];
unsigned char light_yellow[PadBindingProfile::maxKeyButtons];
int light_count = 0;
for (int i = 0; i < profile.keyButtonCount; ++i)
{
int address = profile.keyButtons[i].address;
if (address >= buttonUnits)
{
continue; // keypads have no lamps
}
Logical duplicate = False;
for (int j = 0; j < light_count; ++j)
{
if (light_keys[j] == profile.keyButtons[i].virtualKey)
{
duplicate = True; // first binding wins
break;
}
}
if (duplicate)
{
continue;
}
light_keys[light_count] = profile.keyButtons[i].virtualKey;
light_addresses[light_count] = address;
light_yellow[light_count] =
(address >= 0x10 && address <= 0x1F) ? 1 : 0;
++light_count;
}
if (light_count > 0)
{
KeyLight_SetLogger(&KeyLightLog);
KeyLight_SetMap(light_keys, light_addresses, light_yellow, light_count);
KeyLight_Start();
keyLightActive = True;
}
}
invertX = False;
invertY = False;
const char *flip = getenv("L4PADFLIP");
if (flip != NULL)
{
if (strchr(flip, 'X') || strchr(flip, 'x'))
{
invertX = True;
}
if (strchr(flip, 'Y') || strchr(flip, 'y'))
{
invertY = True;
}
}
// Report as a v4.2 board, like vRIO does
MajorRevision = 4;
MinorRevision = 2;
activeInstance = this;
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
}
PadRIO::~PadRIO()
{
Check_Pointer(this);
if (keyLightActive)
{
KeyLight_Stop();
keyLightActive = False;
}
if (activeInstance == this)
{
activeInstance = NULL;
}
}
Logical
PadRIO::TestInstance() const
{
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// The controls manager drains events every frame; sampling lives here so
// button latency does not depend on the analog request cadence (which is
// 15 s outside of missions).
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Logical
PadRIO::GetNextEvent(RIOEvent *destinationPointer)
{
Check_Pointer(this);
Check_Pointer(destinationPointer);
PollInputs();
if (queueTail == queueHead)
{
return False;
}
*destinationPointer = eventQueue[queueTail];
queueTail = (queueTail + 1) % queueSize;
return True;
}
void
PadRIO::RequestAnalogUpdate()
{
Check_Pointer(this);
analogRequested = True;
}
void
PadRIO::GeneralReset()
{
Check_Pointer(this);
throttleAccum = (Scalar) 0;
Throttle = (Scalar) 0;
LeftPedal = (Scalar) 0;
RightPedal = (Scalar) 0;
JoystickX = (Scalar) 0;
JoystickY = (Scalar) 0;
analogRequested = True;
memset(lampState, 0, sizeof(lampState));
if (keyLightActive)
{
KeyLight_UpdateLamps(lampState, lampCount);
}
memset(keypadDown, 0, sizeof(keypadDown));
for (int i = 0; i < profile.keyButtonCount; ++i)
{
profile.keyButtons[i].latched = False;
profile.keyButtons[i].wasDown = False;
}
for (int i = 0; i < profile.padButtonCount; ++i)
{
profile.padButtons[i].latched = False;
profile.padButtons[i].wasDown = False;
}
}
void
PadRIO::ResetThrottle()
{
Check_Pointer(this);
throttleAccum = (Scalar) 0;
Throttle = (Scalar) 0;
analogRequested = True;
}
void
PadRIO::SetLamp(int lampNumber, int state)
{
Check_Pointer(this);
if (lampNumber >= 0 && lampNumber < lampCount)
{
lampState[lampNumber] = (unsigned char) state;
if (keyLightActive)
{
KeyLight_UpdateLamps(lampState, lampCount);
}
}
}
void
PadRIO::QueueEvent(const RIOEvent &an_event)
{
int next = (queueHead + 1) % queueSize;
if (next == queueTail)
{
// full: drop the oldest event
queueTail = (queueTail + 1) % queueSize;
}
eventQueue[queueHead] = an_event;
queueHead = next;
}
void
PadRIO::PollInputs()
{
unsigned long now = GetTickCount();
if (now - lastPollTick < 10)
{
return;
}
Scalar delta_t = (Scalar)(now - lastPollTick) / 1000.0f;
if (delta_t > 0.25f)
{
delta_t = 0.25f;
}
lastPollTick = now;
//---------------------------------------------------------------
// Find / keep the XInput pad. Probing empty slots is slow, so an
// absent pad is only re-probed every 3 seconds.
//---------------------------------------------------------------
XINPUT_STATE pad;
memset(&pad, 0, sizeof(pad));
Logical pad_live = False;
if (padIndex >= 0)
{
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
if (!pad_live)
{
DEBUG_STREAM << "PadRIO: controller " << padIndex << " disconnected\n" << std::flush;
padIndex = -1;
}
}
if (padIndex < 0 && (now - lastPadCheckTick) >= 3000)
{
lastPadCheckTick = now;
for (DWORD i = 0; i < 4; ++i)
{
if (XInputGetState(i, &pad) == ERROR_SUCCESS)
{
padIndex = (int) i;
pad_live = True;
DEBUG_STREAM << "PadRIO: controller " << padIndex << " connected\n" << std::flush;
break;
}
}
if (padIndex < 0 && !padReported)
{
padReported = True;
DEBUG_STREAM << "PadRIO: no controller found - keyboard only\n" << std::flush;
}
}
//---------------------------------------------------------------
// Buttons: build the desired state from the binding profile
// (keyboard + pad, with toggle latches), merge the on-screen
// cockpit buttons, then diff against what we last reported.
// Keypad addresses (0x50-0x6F) collect separately - they become
// arcade KeyEvents, not button events.
//---------------------------------------------------------------
unsigned char desired[buttonUnits];
unsigned char keypadDesired[keypadUnits];
memset(desired, 0, sizeof(desired));
memset(keypadDesired, 0, sizeof(keypadDesired));
for (int i = 0; i < profile.keyButtonCount; ++i)
{
PadKeyButtonBinding *binding = &profile.keyButtons[i];
Logical down = KeyDown(binding->virtualKey);
if (binding->toggle && down && !binding->wasDown)
{
binding->latched = !binding->latched;
}
binding->wasDown = down;
if (binding->toggle ? binding->latched : down)
{
if (binding->address < buttonUnits)
{
desired[binding->address] = 1;
}
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
{
keypadDesired[binding->address - 0x50] = 1;
}
}
}
for (int i = 0; i < profile.padButtonCount; ++i)
{
PadPadButtonBinding *binding = &profile.padButtons[i];
Logical down = pad_live &&
(pad.Gamepad.wButtons & binding->padMask) != 0;
if (binding->toggle && down && !binding->wasDown)
{
binding->latched = !binding->latched;
}
binding->wasDown = down;
if (binding->toggle ? binding->latched : down)
{
if (binding->address < buttonUnits)
{
desired[binding->address] = 1;
}
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
{
keypadDesired[binding->address - 0x50] = 1;
}
}
}
for (int i = 0; i < buttonUnits; ++i)
{
if (screenButton[i])
{
desired[i] = 1;
}
}
for (int unit = 0; unit < buttonUnits; ++unit)
{
if (desired[unit] != buttonDown[unit])
{
buttonDown[unit] = desired[unit];
RIOEvent an_event;
an_event.Type = desired[unit] ? ButtonPressedEvent : ButtonReleasedEvent;
an_event.Data.Unit = unit;
QueueEvent(an_event);
}
}
//---------------------------------------------------------------
// Keypads: presses become the arcade RIO KeyEvents. Unit 0 is the
// pilot's internal keypad (0x50-0x5F), unit 1 the external
// operator keypad (0x60-0x6F); the key is the hex digit 0-15.
//---------------------------------------------------------------
for (int pad_key = 0; pad_key < keypadUnits; ++pad_key)
{
if (keypadDesired[pad_key] != keypadDown[pad_key])
{
keypadDown[pad_key] = keypadDesired[pad_key];
if (keypadDesired[pad_key])
{
RIOEvent an_event;
an_event.Type = KeyEvent;
an_event.Data.Keyboard.Unit = (pad_key >= 0x10) ? 1 : 0;
an_event.Data.Keyboard.Key = pad_key & 0x0F;
QueueEvent(an_event);
}
}
}
//---------------------------------------------------------------
// Axes, from the profile. 'deflect' sources sum into a springy
// position; 'rate' sources integrate the throttle (the pod's only
// sticky axis) by value per second.
//---------------------------------------------------------------
Scalar deflect[BindAxisCount];
Scalar rate[BindAxisCount];
memset(deflect, 0, sizeof(deflect));
memset(rate, 0, sizeof(rate));
for (int i = 0; i < profile.keyAxisCount; ++i)
{
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
if (KeyDown(binding->virtualKey))
{
if (binding->mode == BindKeyRate)
{
rate[binding->axis] += binding->value;
}
else
{
deflect[binding->axis] += binding->value;
}
}
}
if (pad_live)
{
for (int i = 0; i < profile.padAxisCount; ++i)
{
const PadPadAxisBinding *binding = &profile.padAxes[i];
Scalar value = (Scalar) 0;
switch (binding->source)
{
case BindPadLeftStickX:
value = StickValue(pad.Gamepad.sThumbLX, (int)(binding->deadzone * 32767.0f));
break;
case BindPadLeftStickY:
value = StickValue(pad.Gamepad.sThumbLY, (int)(binding->deadzone * 32767.0f));
break;
case BindPadRightStickX:
value = StickValue(pad.Gamepad.sThumbRX, (int)(binding->deadzone * 32767.0f));
break;
case BindPadRightStickY:
value = StickValue(pad.Gamepad.sThumbRY, (int)(binding->deadzone * 32767.0f));
break;
case BindPadLeftTrigger:
value = (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f;
if (value <= binding->deadzone) value = (Scalar) 0;
break;
case BindPadRightTrigger:
value = (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f;
if (value <= binding->deadzone) value = (Scalar) 0;
break;
}
if (binding->invert)
{
value = -value;
}
if (binding->rate > 0.0f)
{
rate[binding->axis] += value * binding->rate;
}
else
{
deflect[binding->axis] += value;
}
}
}
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
Scalar x = deflect[BindAxisJoystickX];
Scalar y = deflect[BindAxisJoystickY];
if (x > 1.0f) x = 1.0f;
if (x < -1.0f) x = -1.0f;
if (y > 1.0f) y = 1.0f;
if (y < -1.0f) y = -1.0f;
Throttle = Clamp01(throttleAccum + deflect[BindAxisThrottle]);
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
// The profile encodes the pod's stick sign convention; L4PADFLIP
// flips on top of it per axis.
JoystickX = invertX ? -x : x;
JoystickY = invertY ? -y : y;
//---------------------------------------------------------------
// Emit an analog event when asked to, or when anything moved
//---------------------------------------------------------------
Logical changed =
(Throttle != sentThrottle) ||
(LeftPedal != sentLeftPedal) ||
(RightPedal != sentRightPedal) ||
(JoystickX != sentJoystickX) ||
(JoystickY != sentJoystickY);
if (analogRequested || changed)
{
analogRequested = False;
sentThrottle = Throttle;
sentLeftPedal = LeftPedal;
sentRightPedal = RightPedal;
sentJoystickX = JoystickX;
sentJoystickY = JoystickY;
RIOEvent an_event;
an_event.Type = AnalogEvent;
an_event.Data.Unit = 0;
QueueEvent(an_event);
}
}
+122
View File
@@ -0,0 +1,122 @@
#pragma once
#include "l4rio.h"
#include "l4padbindings.h"
//########################################################################
//############################### PadRIO #################################
//########################################################################
//
// A cockpit-less RIO: synthesizes the RIOBase control surface from an
// XInput controller and the PC keyboard. Selected with L4CONTROLS=PAD.
//
// Every input is REBINDABLE: bindings.txt beside the exe (vRIO's
// profile format, written with the full default layout on first run)
// maps keys and pad controls to RIO button addresses (0x00-0x47), the
// pilot/external keypads (0x50-0x6F, delivered as arcade KeyEvents;
// unbound by default - the game never reads them), and the five axes
// with deflect/rate semantics. Defaults: vRIO's complete board layout
// (number+letter rows = MFD banks, F-keys = secondary columns) with
// flight on the numpad - 8/2/4/6 stick, 7/9 pedals, 0 trigger - and
// the modifiers: Shift/Ctrl throttle up/down, Alt reverse thrust,
// Space trigger, arrows hat.
//
// Set L4PADFLIP to a string containing 'X' and/or 'Y' to invert the
// stick axes on top of whatever the profile produces.
//
class PadRIO :
public RIOBase
{
public:
PadRIO();
~PadRIO();
Logical
TestInstance() const;
Logical
GetNextEvent(RIOEvent *destinationPointer);
void
RequestAnalogUpdate();
void
GeneralReset();
void
ResetThrottle();
void
SetLamp(int lampNumber, int state);
// Lamp state the game has commanded, indexed by RIO lamp number.
// The on-screen cockpit buttons read this to light themselves.
enum { lampCount = 64 };
unsigned char
lampState[lampCount];
//---------------------------------------------------------------
// On-screen cockpit buttons (MFDSplitView strips) press RIO units
// through these; they are no-ops when no PadRIO is active (e.g.
// real serial hardware selected).
//---------------------------------------------------------------
static void
SetScreenButton(int unit, Logical pressed);
static int
GetLampState(int unit);
static Logical
IsActive()
{ return activeInstance != NULL; }
protected:
void
PollInputs();
void
QueueEvent(const RIOEvent &an_event);
enum { queueSize = 64 };
enum { buttonUnits = 0x48 }; // through LastMappableButton
RIOEvent
eventQueue[queueSize];
int
queueHead, queueTail;
static PadRIO
*activeInstance;
// pressed state driven by the on-screen cockpit buttons
unsigned char
screenButton[0x48];
unsigned long
lastPollTick,
lastPadCheckTick;
int
padIndex; // connected XInput slot, -1 = none
Logical
padReported; // one-time connect/disconnect log flags
Logical
analogRequested;
unsigned char
buttonDown[buttonUnits];
// keypad pressed-state (0x50-0x6F), for KeyEvent edges
enum { keypadUnits = 0x20 };
unsigned char
keypadDown[keypadUnits];
PadBindingProfile
profile;
Scalar
throttleAccum;
Logical
invertX, invertY;
Logical
keyLightActive; // Dynamic Lighting keyboard mirror running
Scalar
sentThrottle, sentLeftPedal, sentRightPedal,
sentJoystickX, sentJoystickY;
};
+96 -27
View File
@@ -109,26 +109,17 @@ protected:
lowestInput;
};
class RIO :
public PCSerialPacket
//########################################################################
//############################### RIOBase ################################
//########################################################################
//
// The control surface the game consumes from the cockpit RIO board,
// independent of transport. RIO (below) is the serial-hardware
// implementation; PadRIO (l4padrio.h) synthesizes the same surface from
// an XInput controller + the PC keyboard for cockpit-less play.
//
class RIOBase
{
protected:
enum RIOCommand{
CheckRequest=0x80,
VersionRequest,
AnalogRequest,
ResetRequest,
LampRequest,
CheckReply,
VersionReply,
AnalogReply,
ButtonPressed,
ButtonReleased,
KeyPressed,
KeyReleased,
TestModeChange
};
public:
enum RIOStatusType {
BoardOk=0, BoardMissing=1, BoardBad=2,
@@ -166,6 +157,90 @@ public:
}Data;
};
RIOBase()
{
TestModeActive = 0;
Throttle = (Scalar) 0;
LeftPedal = (Scalar) 0;
RightPedal = (Scalar) 0;
JoystickX = (Scalar) 0;
JoystickY = (Scalar) 0;
MajorRevision = 0;
MinorRevision = 0;
}
virtual ~RIOBase() {}
virtual Logical
TestInstance() const
{ return True; }
virtual Logical
GetNextEvent(RIOEvent *destinationPointer) = 0;
virtual void
ForceCenterJoystick() {}
virtual void
SetJoystickDeadBand(Scalar) {}
virtual void
SetThrottleDeadBand(Scalar) {}
virtual void
SetPedalsDeadBand(Scalar) {}
virtual void
RequestCheck() {}
virtual void
RequestVersion() {}
virtual void
RequestAnalogUpdate() {}
virtual void
GeneralReset() {}
virtual void
ResetThrottle() {}
virtual void
ResetLeftPedal() {}
virtual void
ResetRightPedal() {}
virtual void
ResetVerticalJoystick() {}
virtual void
ResetHorizontalJoystick() {}
virtual void
SetLamp(int lampNumber, int state) = 0;
int
TestModeActive;
Scalar
Throttle, LeftPedal, RightPedal, JoystickX, JoystickY;
int
MajorRevision, MinorRevision;
};
class RIO :
public RIOBase,
public PCSerialPacket
{
protected:
enum RIOCommand{
CheckRequest=0x80,
VersionRequest,
AnalogRequest,
ResetRequest,
LampRequest,
CheckReply,
VersionReply,
AnalogReply,
ButtonPressed,
ButtonReleased,
KeyPressed,
KeyReleased,
TestModeChange
};
public:
//Win32 Serial support: ADB 02/13/07
//RIO(Word port, Word intNum, Logical perform_tests = True);
RIO(const char* port, Logical perform_tests = True);
@@ -267,14 +342,8 @@ public:
TransmitQueueCount()
{ return PCSerialPacket::TransmitQueueCount(); }
int
TestModeActive;
Scalar
Throttle, LeftPedal, RightPedal, JoystickX, JoystickY;
int
MajorRevision, MinorRevision;
// TestModeActive, the five analog Scalars, and Major/MinorRevision
// now live in RIOBase.
int remoteRetryCount;
int remoteAbandonCount;
+18 -1
View File
@@ -2551,6 +2551,18 @@ void
// which runs in the subsystem-roster walk below (un-skipped under this env).
static const int s_realControls = BTEnvOn("BT_REAL_CONTROLS", 1); // default ON (=0 to disable)
MechControlsMapper *mppr = MappingMapper(); // roster slot 0 (task #7)
//
// BT412: when a RIO device exists (the pod board, or PadRIO's
// pad/keyboard synthesizer under L4CONTROLS=PAD), the ENGINE's
// controls push writes the mapper's published input attributes from
// the device groups -- the WASD stand-in write below would clobber
// it every frame. The bridge speaks for the hardware only when
// there is none; the demand CONSUME side runs either way.
//
LBE4ControlsManager *bridgeControls = (application != 0)
? (LBE4ControlsManager *)application->GetControlsManager() : 0;
const int deviceOwnsInput =
(bridgeControls != 0 && bridgeControls->rioPointer != 0);
if (s_realControls && mppr != 0)
{
// Diagnostic: what the ENGINE controls push left in the attribute since
@@ -2558,10 +2570,13 @@ void
// here as pre != our previous write).
float preThrottle = mppr->throttlePosition;
(void)preThrottle;
if (!deviceOwnsInput)
{
mppr->throttlePosition = (throttle >= 0.0f) ? throttle : -throttle;
mppr->reverseThrust = (throttle < 0.0f) ? 1 : 0; // ControlsButton: >=1 engaged
mppr->stickPosition.x = turn; // Basic mode: turn = stick yaw
mppr->stickPosition.y = 0.0f;
}
// Consume the PREVIOUS frame's interpreted demands (the mapper ticks in
// the roster walk after this block -- one frame of input latency).
// turnDemand is the mode-shaped steering; speedDemand (world u/s, sign =
@@ -2587,7 +2602,9 @@ void
<< " turnDemand=" << mppr->turnDemand
<< " mode=" << mppr->controlMode
<< " mapper=" << (void*)mppr
<< " &mode=" << (void*)&mppr->controlMode << "\n" << std::flush;
<< " &mode=" << (void*)&mppr->controlMode
<< " &stick=" << (void*)&mppr->stickPosition
<< " &thr=" << (void*)&mppr->throttlePosition << "\n" << std::flush;
}
}
+55 -8
View File
@@ -65,6 +65,17 @@
# include <pilot.hpp>
#endif
#include <torso.hpp> // real Torso: analog aim axes @0x1F0/0x1F4, horizontalEnabled @0x250
// LBE4ControlsManager (rioPointer presence gates the key bridge) -- needs
// the btl4app/btl4mode forward-decl order btl4mppr.cpp / mech4.cpp use.
#if !defined(BTL4APP_HPP)
# include <btl4app.hpp>
#endif
#if !defined(BTL4MODE_HPP)
# include <btl4mode.hpp>
#endif
#if !defined(L4CTRL_HPP)
# include <l4ctrl.hpp>
#endif
#include <hud.hpp> // real HUD: freeAimSlew @0x28C
#define JM_CLOSE_ENOUGH 1.0e-4f // _DAT_004b027c
@@ -252,6 +263,15 @@ MechControlsMapper::MessageHandlerSet&
const MechControlsMapper::IndexEntry
MechControlsMapper::AttributePointers[]=
{
//
// BT412 pad slot (see the mechmppr.hpp enum note): the DOS chain below
// the mapper carried a second base attribute the WinTesla chain lacks,
// and Find() is positional -- this entry occupies the missing slot so
// the stream's IDs (stick=3...) land on the right members. No stream
// references ID 2 (verified via BT_CTRLMAP_LOG); a lookup returns NULL.
//
{ Subsystem::NextAttributeID, "BT412BaseSlotPad",
(Simulation::AttributePointer)0 },
ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition), // 0x114
ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition), // 0x11C
ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition), // 0x120
@@ -595,6 +615,18 @@ void
{
static const int s_keyBridge =
(getenv("BT_KEY_BRIDGE") == 0 || *getenv("BT_KEY_BRIDGE") != '0');
//
// BT412: a live RIO device (the pod board, or PadRIO's pad/keyboard
// synthesizer under L4CONTROLS=PAD) feeds the analog input attributes
// through the engine controls push that just ran -- the keyboard
// bridge's ANALOG writes below would clobber the device every frame.
// They stand down when a device exists; the desktop conveniences
// (M mode-cycle, X torso-recenter) stay live either way.
//
LBE4ControlsManager *bridge_controls = (application != 0)
? (LBE4ControlsManager *)application->GetControlsManager() : 0;
const int device_owns_input =
(bridge_controls != 0 && bridge_controls->rioPointer != 0);
if (s_keyBridge && mech != 0 && application != 0
&& (Entity *)mech == application->GetViewpointEntity())
{
@@ -670,8 +702,11 @@ void
}
}
}
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
if (!device_owns_input)
{
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
}
// CONTROL-MODE AXIS ROUTING (2026-07-13, the pod mapping): in
// BASIC the stick steers the legs (turn) and the torso auto-
// centers; in STANDARD/VETERAN the stick is the TORSO (free aim /
@@ -690,13 +725,19 @@ void
}
if (controlMode == BasicMode)
{
stickPosition.x = key_turn;
pedalsPosition = 0.0f;
if (!device_owns_input)
{
stickPosition.x = key_turn;
pedalsPosition = 0.0f;
}
}
else
{
stickPosition.x = gBTTwistAxis; // the torso axis
pedalsPosition = key_turn; // A/D = the pedals
if (!device_owns_input)
{
stickPosition.x = gBTTwistAxis; // the torso axis
pedalsPosition = key_turn; // A/D = the pedals
}
// 'X' recenter pulse (2026-07-13): one-frame centerCommand
// -> the sim arms recenterActive and Recenter (@004b6918)
// slews the torso home; any Q/E input cancels it sim-side.
@@ -712,15 +753,21 @@ void
gBTTorsoRecenter = 0;
rcTorso->CommandRecenter();
}
else
else if (!device_owns_input)
{
// A live device's mapped button owns
// centerCommand; only the keyboard
// bridge clears it while unpressed.
rcTorso->ClearRecenterCommand();
}
}
}
}
}
stickPosition.y = 0.0f;
if (!device_owns_input)
{
stickPosition.y = 0.0f;
}
}
}
+11 -1
View File
@@ -130,9 +130,19 @@ class Pilot;
// table @0050efd0. (The recorded member offsets carry the engine's
// low-bit "scalar" tag, e.g. 0x115 -> &stickPosition @0x114.)
//
// BT412 (databinding, 2026-07-14): the binary's stream IDs put the
// stick at 3, i.e. the DOS chain below the mapper carried TWO base
// attributes. The WinTesla Simulation/Subsystem chain carries ONE
// (SimulationState, Subsystem::NextAttributeID == 2), and the engine's
// AttributeIndexSet::Find is POSITIONAL (ID N -> slot N-1), so the
// enum is locked to the binary (+1) and mechmppr.cpp pads the missing
// base slot -- without it every mapper attribute resolved one entry
// late (the .CTL stick mapping wrote throttlePosition; found the day
// PadRIO first drove the mapper from a live device).
//
public:
enum {
StickPositionAttributeID = Subsystem::NextAttributeID, // 3
StickPositionAttributeID = Subsystem::NextAttributeID + 1, // 3 (binary-locked)
ThrottlePositionAttributeID, // 4
PedalsPositionAttributeID, // 5
ReverseThrustAttributeID, // 6