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
+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;