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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 2–7: 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 3–7: not started — see the roadmap.
|
||||
|
||||
## The seams (what plugs in where) [T0 unless noted]
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -542,9 +542,9 @@ public:
|
||||
|
||||
public:
|
||||
//-------------------------------------------------------------------
|
||||
// RIO data
|
||||
// RIO data (serial hardware or the PadRIO pad/keyboard synthesizer)
|
||||
//-------------------------------------------------------------------
|
||||
RIO
|
||||
RIOBase
|
||||
*rioPointer;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user