Handoff: retire handoff/padrio -- superseded by the merge-back into BT411

The handoff bundle was the interim vehicle for carrying PadRIO to BT411 by
hand.  With the whole steamification line now merged back (BT411 fast-forwards
onto this history), the real modules live in the shared tree behind the BT412
compile gate -- a second copy in the same repo would only drift.  The bundle
stays available in history at 2e475f4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-17 15:45:53 -05:00
co-authored by Claude Fable 5
parent 335396912e
commit bf9f841706
7 changed files with 0 additions and 2166 deletions
-315
View File
@@ -1,315 +0,0 @@
# PadRIO — implementation guide for BattleTech (BT411)
**What it gives you:** *cockpit-less play.* PadRIO synthesizes the pod's RIO
control surface from an **XInput controller + the PC keyboard**, so the full
stock RIO path — the `MechRIOMapper`, the lamp commands, the button banks —
runs unchanged with no serial hardware attached. Selected at runtime with
`L4CONTROLS=PAD`. Every input is rebindable through a `bindings.txt` file.
This kit was built in **BT412** (the Steam fork) and is a straight port of the
PadRIO from **RP412** (Red Planet); the RIO surface is shared MUNGA/L4 code, so
it drops into BT411 with the same three-file seam. It is a point-in-time
snapshot — treat the sources here as the reference and adapt to your tree.
> Serial-hardware RIO is untouched: `L4CONTROLS=RIO` still builds a real
> `RIO` on COM1 exactly as before. PadRIO is an *additional* implementation of
> the same base class, not a replacement.
---
## 1. The design — the `RIOBase` seam
The pod code already consumes an **abstract control surface**, it just wasn't
factored out. The monolithic `RIO` class (serial packet transport + the control
surface the game reads) is split into two:
```
RIOBase (abstract control surface: RIOEvent stream + 5 analog Scalars + SetLamp)
/ \
RIO PadRIO
(serial) (XInput + keyboard) <-- new
```
Everything the game reads from a RIO — `GetNextEvent()` (button press/release,
keypad key, analog update), the five analog `Scalar`s (`Throttle`, `LeftPedal`,
`RightPedal`, `JoystickX`, `JoystickY`), and `SetLamp()` — is declared **pure
virtual on `RIOBase`**. `RIO` implements it over the serial packet layer;
`PadRIO` implements it over XInput + the keyboard. The controls manager holds a
`RIOBase*`, so the mapper/lamp/button code above the seam never changes.
That is the whole trick. No game logic moves; you add one subclass and re-point
one pointer.
---
## 2. What's in this kit
```
handoff/padrio/
├── PADRIO-IMPLEMENTATION.md <- this guide
├── src/
│ ├── L4PADRIO.h L4PADRIO.cpp <- NEW: the pad/keyboard RIO
│ └── L4PADBINDINGS.h L4PADBINDINGS.cpp <- NEW: the bindings.txt parser/profile
├── reference/
│ └── L4RIO.h <- the header AFTER the RIOBase split (your reference)
└── bindings.default.txt <- a sample of the profile the game writes on first run
```
Two **new** files to add, three **existing** files to edit (`L4RIO.h`,
`L4RIO.cpp`, `L4CTRL.h`, `L4CTRL.cpp`), one build-system change, and **two
game-side gotchas** you will hit because PadRIO is the first thing to drive a
real device through your mapper (§5 — read those, they include a *latent
real-pod bug* PadRIO exposes).
---
## 3. Integration steps
### 3a. Split `RIO` into `RIOBase` + `RIO` (`L4RIO.h`, `L4RIO.cpp`)
In `L4RIO.h`, introduce `RIOBase` and re-parent `RIO` onto it. See
`reference/L4RIO.h` for the exact end state. The essentials:
- **`RIOBase`** owns the shared surface: the enums (`RIOStatusType`,
`LampState`, `RIOEventType`, `RIOKeyPair`, `RIOEvent`), the state members
`TestModeActive`, `Throttle`, `LeftPedal`, `RightPedal`, `JoystickX`,
`JoystickY`, `MajorRevision`, `MinorRevision`, and the virtual interface:
```cpp
virtual ~RIOBase() {}
virtual Logical GetNextEvent(RIOEvent *destinationPointer) = 0; // pure
virtual void SetLamp(int lampNumber, int state) = 0; // pure
virtual void RequestAnalogUpdate() {} // + the
virtual void GeneralReset() {} /* ...ResetThrottle, deadbands... */
```
The base ctor zero-initializes the shared members (they used to live on `RIO`).
- **`RIO`** now `: public RIOBase, public PCSerialPacket` and simply **removes**
the members that moved up (`TestModeActive`, the five analog `Scalar`s,
`Major/MinorRevision` are now inherited). Its methods keep the same
signatures; they are now overrides.
- In `L4RIO.cpp`, delete the moved-member initializations from the `RIO`
constructor (the `RIOBase` ctor does them now). Nothing else in `RIO` changes.
### 3b. Re-type the controls-manager pointer (`L4CTRL.h`)
The manager's RIO pointer becomes a base pointer:
```cpp
//-------------------------------------------------------------------
// RIO data (serial hardware or the PadRIO pad/keyboard synthesizer)
//-------------------------------------------------------------------
RIOBase
*rioPointer;
```
`L4CTRL.h` already `#include "l4rio.h"`, so `RIOBase` is in scope. Every existing
`rioPointer->…` call is part of the `RIOBase` interface, so nothing else in the
manager changes.
### 3c. Add the `PAD` token (`L4CTRL.cpp`)
At the top: `#include "l4padrio.h"`. Then in the `L4CONTROLS` token loop, beside
the existing `RIO` / joystick tokens, add:
```cpp
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;
}
```
**The critical line is `primaryControlType = PrimaryRIO`.** That is what makes
the stock `MechRIOMapper` engage against PadRIO exactly as it would against
serial hardware — no mapper change, no new control-type. (In BT this selection
is the switch on `controls->primaryControlType` in
`btl4app.cpp MakeViewpointEntity`; leaving it `PrimaryRIO` means `MechRIOMapper`
is chosen for the viewpoint mech, unchanged.)
### 3d. Add the two new files
Drop `L4PADRIO.{h,cpp}` and `L4PADBINDINGS.{h,cpp}` into `MUNGA_L4/`.
- **`L4PADRIO`** is the `RIOBase` subclass: it polls XInput + `GetAsyncKeyState`,
turns edges into `RIOEvent`s (`GetNextEvent`), integrates the analog axes
(deflect vs. rate semantics), records commanded lamp state (`SetLamp`), and
re-probes for a hot-plugged pad every ~3 s.
- **`L4PADBINDINGS`** parses `bindings.txt` into a `PadBindingProfile` and writes
the self-documenting default file on first run.
XInput links itself — `L4PADRIO.cpp` carries
`#pragma comment(lib, "xinput9_1_0.lib")`, so you do **not** need a linker-line
change if your toolchain honors `#pragma comment` (MSVC does).
**Optional dependency — KeyLight.** `L4PADRIO.cpp` includes `l4keylight.h` to
mirror lamp state onto an RGB keyboard (Windows Dynamic Lighting), gated on the
`BT412KEYLIGHT` env var. If you do not want that feature, either take
`L4KEYLIGHT.*` too, or make a **minimal build**: delete the `#include
"l4keylight.h"` and the `keyLightActive` block (the `KeyLight_*` calls at the
ctor, dtor, and the two `UpdateLamps` sites). PadRIO is fully functional without
it.
### 3e. Build system
Add the two `.cpp` files to your build (this repo uses explicit CMake
`add_library` source lists — no globs):
```cmake
"engine/MUNGA_L4/L4PADRIO.cpp"
"engine/MUNGA_L4/L4PADBINDINGS.cpp"
```
No extra `target_link_libraries` for XInput (the `#pragma comment` handles it).
If your toolchain ignores `#pragma comment`, add `xinput9_1_0.lib` to the link.
---
## 4. Two game-side gotchas you WILL hit (shared game code)
PadRIO is very likely the **first thing that drives a real device through your
`MechControlsMapper`**. That surfaces two issues in the *game* layer (not in
PadRIO). BT411 shares this code, so both apply to you.
### 4a. The keyboard bring-up bridge must stand down
Any dev/bring-up "keyboard → mapper" bridge you added to move the mech without a
device (e.g. a WASD stand-in that writes the mapper's analog attributes, or an
`mechmppr.cpp` bring-up bridge) will now **clobber the real device push every
frame** — the bridge and PadRIO both write the same attributes, and the bridge
wins last. Gate every such bridge to stand down when a device exists:
```cpp
LBE4ControlsManager *bridge_controls = /* the active controls manager */;
bool device_present =
(bridge_controls != 0 && bridge_controls->rioPointer != 0);
if (!device_present) {
// ...the keyboard bring-up writes...
}
```
Keep the pure conveniences (mode-cycle, recenter) live if you like; it is only
the **analog/attribute writes** that must yield to the device. In BT412 this is
`mech4.cpp` (the mapper-attr write) and `mechmppr.cpp` (`BT_KEY_BRIDGE`).
### 4b. The `.CTL` mapping resolves one slot early — a LATENT REAL-POD BUG
This one is subtle and important: **PadRIO exposes a pre-existing bug that a real
serial RIO would also have hit** (you just may never have driven one under
WinTesla yet).
`MechControlsMapper` publishes its mappable members through the attribute system,
and `AttributeIndexSet::Find(ID)` is **positional** — it indexes
`attributeIndex[ID - 1]`. The binary's streamed control map (the `.CTL` /
`CreateStreamedMappings` path) uses fixed numeric IDs from the **DOS** class
chain (e.g. stick = 3, throttle = 4, because the DOS `Subsystem` base carried two
attributes). The **WinTesla** parent chain carries *fewer* base attributes (one:
`SimulationState`), so every child attribute resolves **one slot early**: the
stick mapping lands in `throttlePosition`, the throttle mapping lands in
`pedalsPosition`, and so on. No crash — values silently write the **wrong
member**, so the mech won't steer/throttle correctly under any real device.
Only the **numeric-ID** path is affected (`CreateStreamedMappings` / `.CTL`);
gauge databinding resolves by **name** and never saw it, which is why it stayed
latent.
**Fix:** put a **named pad slot at the front** of the mapper's
`AttributePointers[]` (`mechmppr.cpp`) so the positional index lines up with the
binary's numbering, and lock the attribute-ID enum to the binary's values
(`static_assert` them). This is BT412 reconstruction-gotchas §11 ("short parent
chain / positional trap").
**Diagnostic:** dump each streamed mapping's *resolved pointer* and compare it to
`&mapper->member`. BT412 gates this behind `BT_CTRLMAP_LOG=1`; the per-frame
`[mppr]` log prints `&stick` / `&thr` so you can see the mapping land in the
right member.
---
## 5. The `bindings.txt` format
`bindings.txt` (beside the exe, working directory) maps keys and pad controls to
RIO input addresses and the five analog axes. It is written with the full,
self-documenting default profile on first run; delete it to restore defaults.
See `bindings.default.txt` for a complete example. Grammar:
```
key <name> button <addr> [toggle]
key <name> axis <axis> deflect <n> # hold at n while down, spring back
key <name> axis <axis> rate <n-per-sec> # walk by n/sec while down, position holds
pad <button> button <addr> [toggle]
padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
```
- `<addr>` — a RIO input address: **lamp buttons `0x00``0x47`**, internal keypad
`0x50``0x5F`, external keypad `0x60``0x6F` (the keypads are delivered as
arcade `KeyEvent`s; unbound by default).
- `<axis>` — `Throttle | LeftPedal | RightPedal | JoystickY | JoystickX`.
- `<name>` — `A``Z`, `D0``D9`, `F1``F12`, `NumPad0``NumPad9`, `Up/Down/
Left/Right`, `Space`, `Enter`, `OemMinus`, … (see the default file).
- `<button>` — `A B X Y DPadUp… Start Back LeftShoulder RightShoulder
LeftThumb RightThumb`; `<src>` — `LeftStickX/Y RightStickX/Y LeftTrigger
RightTrigger`.
The default profile is the shared Tesla board layout: number+letter rows = MFD
banks, F-keys = Secondary columns, joystick hat `0x40``0x47` on Space/arrows/pad;
flight on the numpad (8/2/4/6 stick, 7/9 pedals, 0 trigger) with Shift/Ctrl =
throttle up/down, Alt = reverse.
Bad lines are logged with a line number and skipped; good lines always win.
---
## 6. Config / runtime
| Env var | Effect |
|---|---|
| `L4CONTROLS=PAD` | select PadRIO (this is the whole switch). `RIO`, `KEYBOARD`, etc. still work. |
| `L4PADFLIP=XY` | invert the stick axes (`X`, `Y`, or `XY`) on top of the profile. |
| `BT412KEYLIGHT=0` | opt out of the RGB keyboard lamp mirror (only if you kept the KeyLight dep). |
---
## 7. Verify
1. **Pad + keyboard drive.** `L4CONTROLS=PAD`, no COM ports: the mech throttles
(rate axis, holds position), steers (stick deflect), and the authentic
speed-vs-turn clamp engages. Confirm the pad is detected and hot-plugs
(pull/replug — it re-acquires within ~3 s).
2. **Mapper lands in the right members** (gotcha 4b): with the `.CTL` diagnostic
on, the stick mapping resolves to `&stick`, throttle to `&thr` — not one slot
early.
3. **Bindings parse.** The log reports the counts (e.g. "N key buttons / 8 axes /
M pad buttons / K pad axes"); a bad line names its line number.
4. **Serial regression.** `L4CONTROLS=RIO` still builds a real `RIO` on COM1 and
initializes (or logs the serial-open failure and falls back), unchanged.
5. **Keyboard fallback** (no device): with both bridges gated (gotcha 4a), a
keyboard-only run still moves the mech.
---
## 8. Notes
- **Lamps → on-screen buttons.** `PadRIO::SetLamp` records commanded lamp state
in `lampState[]`, and the class exposes `SetScreenButton` / `GetLampState` /
`IsActive` statics. Those are only needed if you also build an **on-screen
cockpit** (mouse-clickable MFD buttons that light from the game's lamp
commands). For pad+keyboard play alone they are inert — ignore them.
- **`RequestAnalogUpdate`** is how the mapper pulls a fresh analog frame; PadRIO
polls on demand and on a timer, so the stock request cadence is fine.
- **Provenance:** RIO surface + `RIOBase` seam are shared MUNGA/L4; PadRIO and
the bindings parser are the RP412→BT412 port. Nothing here depends on the
Steam or front-end work in BT412.
**Repo of record for questions:** BT412 `context/steamification.md` (Phase 2)
and `context/reconstruction-gotchas.md` §11.
-136
View File
@@ -1,136 +0,0 @@
# BT412 input bindings - keyboard and Xbox (XInput) controller.
#
# One binding per line, '#' starts a comment, keywords are case-
# insensitive. Loaded at game start; delete this file to restore
# the defaults.
#
# key <name> button <addr> [toggle]
# key <name> axis <axis> deflect <n>
# key <name> axis <axis> rate <n-per-second>
# pad <button> button <addr> [toggle]
# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]
#
# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad
# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).
# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX
# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,
# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,
# OemMinus, Oemplus, Oemcomma, OemPeriod, ...
# <button> A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back
# LeftShoulder RightShoulder LeftThumb RightThumb
# <src> LeftStickX LeftStickY RightStickX RightStickY
# LeftTrigger RightTrigger
#
# 'deflect' holds the axis there while the key is down and springs
# back on release; 'rate' walks the axis by <n> per second and the
# position sticks (the throttle). Every lamp button is also clickable
# on the on-screen cockpit, so unbound addresses are never stranded.
# ---- Driving: number pad + modifiers ------------------------------
# The whole letter board stays free for the MFD banks; driving lives
# on the numpad with the throttle lever on the modifier keys.
key NumPad8 axis JoystickY deflect -1 # stick forward
key NumPad2 axis JoystickY deflect 1 # stick back
key NumPad4 axis JoystickX deflect 1 # stick left
key NumPad6 axis JoystickX deflect -1 # stick right
key NumPad7 axis LeftPedal deflect 1
key NumPad9 axis RightPedal deflect 1
key NumPad0 button 0x40 # Main trigger (Space works too)
key Shift axis Throttle rate 0.75 # throttle up
key Ctrl axis Throttle rate -0.75 # throttle down
key Alt button 0x3F # Throttle-head button
# ---- Driving: Xbox controller (signs match the pod convention) ----
padaxis LeftStickX axis JoystickX invert deadzone 0.24
padaxis LeftStickY axis JoystickY invert deadzone 0.24
padaxis RightStickY axis Throttle deadzone 0.24 rate 0.75
padaxis LeftTrigger axis LeftPedal deadzone 0.12
padaxis RightTrigger axis RightPedal deadzone 0.12
# ---- Joystick column: hat on the arrows, Main on Space ------------
key Space button 0x40 # Main trigger
key Up button 0x42 # Hat Up
key Down button 0x41 # Hat Back
key Left button 0x44 # Hat Left
key Right button 0x43 # Hat Right
# ---- Xbox controller: buttons -------------------------------------
pad A button 0x40 # Main
pad B button 0x46 # Middle
pad X button 0x45 # Pinky
pad Y button 0x47 # Upper
pad DPadUp button 0x42 # Hat Up
pad DPadDown button 0x41 # Hat Back
pad DPadLeft button 0x44 # Hat Left
pad DPadRight button 0x43 # Hat Right
pad LeftShoulder button 0x3D # Panic
pad RightShoulder button 0x3F # Throttle-head button
pad Start button 0x37 # config 1
pad Back button 0x36 # config 2
# ---- Upper MFD bank: the number row is the top MFD row and the
# ---- QWERTY row is the row under it, left to right across the
# ---- Left / Middle / Right MFDs.
key D1 button 0x2F
key D2 button 0x2E
key D3 button 0x2D
key D4 button 0x2C
key D5 button 0x27
key D6 button 0x26
key D7 button 0x25
key D8 button 0x24
key D9 button 0x37
key D0 button 0x36
key OemMinus button 0x35
key Oemplus button 0x34
key Q button 0x2B
key W button 0x2A
key E button 0x29
key R button 0x28
key T button 0x23
key Y button 0x22
key U button 0x21
key I button 0x20
key O button 0x33
key P button 0x32
key OemOpenBrackets button 0x31
key OemCloseBrackets button 0x30
# ---- Lower MFD bank: home row and the row below it, two 4-key
# ---- blocks split by an unbound gap key (G / B), mirroring the
# ---- keypad gap between the Lower Left and Lower Right MFDs.
key A button 0x0F
key S button 0x0E
key D button 0x0D
key F button 0x0C
key H button 0x07
key J button 0x06
key K button 0x05
key L button 0x04
key Z button 0x0B
key X button 0x0A
key C button 0x09
key V button 0x08
key N button 0x03
key M button 0x02
key Oemcomma button 0x01
key OemPeriod button 0x00
# ---- Secondary / Screen columns on the function keys, top to
# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).
key F1 button 0x10
key F2 button 0x11
key F3 button 0x12
key F4 button 0x13
key F5 button 0x14
key F6 button 0x15
key F7 button 0x18
key F8 button 0x19
key F9 button 0x1A
key F10 button 0x1B
key F11 button 0x1C
key F12 button 0x1D
# The pilot keypad (0x50-0x5F) and external keypad (0x60-0x6F) are
# unbound - the game never reads them - but remain bindable here
# (they arrive as arcade RIO key events).
-389
View File
@@ -1,389 +0,0 @@
#pragma once
#include "l4pcspak.h"
#include "..\munga\notation.h"
#include "..\munga\average.h"
// These are purposely grouped as two parameters in one equate
// (See PCSPAK.HH).
// The standard PC COM ports ALWAYS use these combinations.
//#define RIO_COM1 PCSP_COM1
//#define RIO_COM2 PCSP_COM2
//#define RIO_COM3 PCSP_COM3
//#define RIO_COM4 PCSP_COM4
class FilterChannel SIGNATURED
{
public:
static const char *highName;
static const char *centerName;
static const char *lowName;
static const char *joystickXName;
static const char *joystickYName;
static const char *throttleName;
static const char *leftPedalName;
static const char *rightPedalName;
enum AlignMode { unaligned, normal };
enum PolarMode { unipolar, bipolar };
FilterChannel();
FilterChannel(
NotationFile *init_file,
const char *page_name,
int default_min, int default_max
);
FilterChannel(
NotationFile *init_file,
const char *page_name,
int default_min, int default_center, int default_max
);
~FilterChannel();
void
SetPolarity(PolarMode newPolarity);
void
SetDeadBand(Scalar amount_of_deadband);
virtual void
BeginAlignment();
virtual void
EndAlignment(NotationFile *init_file=NULL);
virtual Scalar
Update(int value);
Logical
TestInstance() const;
protected:
void
CalculateDeadBands();
const char *pageName;
PolarMode polarity;
AlignMode mode;
int previousValue, min, max, center;
int lowerRange, lowerDeadband;
int upperRange, upperDeadband;
Scalar deadbandScalar;
Logical valuesFromFile;
AverageOf<int> average;
};
class RIO;
class Ranger SIGNATURED
{
friend class RIO;
protected:
Ranger(
const char *page_name,
int default_min,
int default_max,
Scalar deadband_scalar
);
~Ranger();
void
ForceToZero();
void
SetDeadBand(Scalar amount_of_deadband);
Scalar
Update(int input);
Logical
TestInstance() const;
void
Statistics(NotationFile *stat_file);
const char
*pageName;
Logical
sampledInputFlag;
int
offset,
hardwareMinimum,
hardwareMaximum,
hardwareRange,
deadbandInteger,
highestInput,
lowestInput;
};
//########################################################################
//############################### 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
{
public:
enum RIOStatusType {
BoardOk=0, BoardMissing=1, BoardBad=2,
LampBad=3,
RestartCount=4, AbandonCount=5, FullBufferCount=6
};
enum LampState{
solid=0, flashSlow=1, flashMed=2, flashFast=3,
state1Off=0x00, state1Dim=0x04, state1Bright=0x0C,
state2Off=0x00, state2Dim=0x10, state2Bright=0x30,
};
enum RIOEventType {
ButtonPressedEvent,
ButtonReleasedEvent,
KeyEvent,
AnalogEvent,
VersionEvent
};
struct RIOKeyPair
{
int Unit;
int Key;
};
struct RIOEvent
{
RIOEventType Type;
union
{
int Unit;
RIOKeyPair Keyboard;
}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);
~RIO();
Logical
TestInstance() const;
Logical
GetNextEvent(RIOEvent *destinationPointer);
void
ForceCenterJoystick(),
SetJoystickDeadBand(Scalar dead_band),
SetThrottleDeadBand(Scalar dead_band),
SetPedalsDeadBand(Scalar dead_band);
void
RequestCheck(),
RequestVersion(),
RequestAnalogUpdate();
void
GeneralReset(),
ResetThrottle(),
ResetLeftPedal(),
ResetRightPedal(),
ResetVerticalJoystick(),
ResetHorizontalJoystick();
void
SetLamp(int lampNumber, int state);
void
TestCheckReply(int type, int location)
{
RIO::reply_check_string[1] = (Byte) (type & 0x7F);
RIO::reply_check_string[2] = (Byte) (location & 0x7F);
SendPacket((Byte *)reply_check_string);
}
void
TestVersionReply(int major, int minor)
{
RIO::reply_version_string[1] = (Byte) (major & 0x7F);
RIO::reply_version_string[2] = (Byte) (minor & 0x7F);
SendPacket((Byte *)reply_version_string);
}
void
TestAnalogReply()
{
SendPacket((Byte *)reply_analog_string);
}
void
TestButtonPressed(int button)
{
RIO::reply_button_press_string[1] = (Byte) (button & 0x7F);
SendPacket((Byte *)reply_button_press_string);
}
void
TestButtonReleased(int button)
{
RIO::reply_button_release_string[1] = (Byte) (button & 0x7F);
SendPacket((Byte *)reply_button_release_string);
}
void
TestKeyPressed(int board, int key)
{
RIO::reply_key_press_string[1] = (Byte) (board & 0x7F);
RIO::reply_key_press_string[2] = (Byte) (key & 0x7F);
SendPacket((Byte *)reply_key_press_string);
}
void
TestKeyReleased(int board, int key)
{
RIO::reply_key_release_string[1] = (Byte) (board & 0x7F);
RIO::reply_key_release_string[2] = (Byte) (key & 0x7F);
SendPacket((Byte *)reply_key_release_string);
}
void
TestEnterTestMode()
{SendPacket((Byte *)reply_test_enter_string);}
void
TestExitTestMode()
{SendPacket((Byte *)reply_test_exit_string);}
int
ReceiveQueueCount()
{ return PCSerialPacket::ReceiveQueueCount(); }
int
TransmitQueueCount()
{ return PCSerialPacket::TransmitQueueCount(); }
// TestModeActive, the five analog Scalars, and Major/MinorRevision
// now live in RIOBase.
int remoteRetryCount;
int remoteAbandonCount;
int remoteFullBufferCount;
int lineErrorCount;
int abandonCount;
int overrunCount;
int discardCount; // used by BeginAnalogCalibration()
protected:
void
OpenFailureFile();
void
CloseFailureFile();
void
CheckErrors();
Logical
operational;
Ranger
*leftPedalRanger, *rightPedalRanger;
Ranger
*throttleRanger;
Ranger
*joystickXRanger, *joystickYRanger;
NotationFile
*failureFile;
int
failureFileOpenCount;
static Byte reply_check_string[];
static Byte reply_version_string[];
static Byte reply_analog_string[];
static Byte reply_button_press_string[];
static Byte reply_button_release_string[];
static Byte reply_key_press_string[];
static Byte reply_key_release_string[];
static Byte reply_test_enter_string[];
static Byte reply_test_exit_string[];
};
-542
View File
@@ -1,542 +0,0 @@
#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
@@ -1,116 +0,0 @@
//===========================================================================//
// 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
@@ -1,546 +0,0 @@
#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
@@ -1,122 +0,0 @@
#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;
};