Handoff: PadRIO module + implementation guide for BT411
A self-contained kit so the BT411 author can add cockpit-less play (XInput
pad + keyboard driving the stock RIO path) upstream. Under handoff/padrio/:
- PADRIO-IMPLEMENTATION.md -- the guide: the RIOBase seam (split RIO into an
abstract control surface + serial RIO + PadRIO), the three-file integration
(L4RIO split, L4CTRL.h rioPointer -> RIOBase*, the L4CTRL.cpp PAD token ->
primaryControlType=PrimaryRIO so MechRIOMapper engages unchanged), the
build/XInput note, the bindings.txt format, config, and verification.
- src/ -- the two NEW drop-in files (L4PADRIO.*, L4PADBINDINGS.*).
- reference/L4RIO.h -- the header after the RIOBase split.
- bindings.default.txt -- a sample of the profile written on first run.
The guide also documents the two game-side gotchas BT411 shares and will hit
when a real device first drives the mapper: (1) keyboard bring-up bridges must
stand down when rioPointer != 0, and (2) the .CTL streamed mapping resolves one
attribute slot early under the shorter WinTesla parent chain (stick writes
throttle) -- a LATENT real-pod bug PadRIO exposes; fix = a named pad slot at the
front of the mapper's AttributePointers[] (reconstruction-gotchas §11).
Handoff files only; does not touch the BT411 remote.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
#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[];
|
||||||
|
};
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
#include "mungal4.h"
|
||||||
|
#pragma hdrstop
|
||||||
|
|
||||||
|
#include "l4padbindings.h"
|
||||||
|
|
||||||
|
#include <XInput.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Name tables: .NET Keys names (what vRIO uses) to virtual keys, pad
|
||||||
|
// names to XInput masks. Matching is case-insensitive.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct NameValue
|
||||||
|
{
|
||||||
|
const char *name;
|
||||||
|
int value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameValue kKeyNames[] =
|
||||||
|
{
|
||||||
|
// letters and digits are generated in LookupKey
|
||||||
|
{ "F1", VK_F1 }, { "F2", VK_F2 }, { "F3", VK_F3 }, { "F4", VK_F4 },
|
||||||
|
{ "F5", VK_F5 }, { "F6", VK_F6 }, { "F7", VK_F7 }, { "F8", VK_F8 },
|
||||||
|
{ "F9", VK_F9 }, { "F10", VK_F10 }, { "F11", VK_F11 }, { "F12", VK_F12 },
|
||||||
|
{ "NumPad0", VK_NUMPAD0 }, { "NumPad1", VK_NUMPAD1 },
|
||||||
|
{ "NumPad2", VK_NUMPAD2 }, { "NumPad3", VK_NUMPAD3 },
|
||||||
|
{ "NumPad4", VK_NUMPAD4 }, { "NumPad5", VK_NUMPAD5 },
|
||||||
|
{ "NumPad6", VK_NUMPAD6 }, { "NumPad7", VK_NUMPAD7 },
|
||||||
|
{ "NumPad8", VK_NUMPAD8 }, { "NumPad9", VK_NUMPAD9 },
|
||||||
|
{ "Divide", VK_DIVIDE }, { "Multiply", VK_MULTIPLY },
|
||||||
|
{ "Subtract", VK_SUBTRACT }, { "Add", VK_ADD },
|
||||||
|
{ "Decimal", VK_DECIMAL },
|
||||||
|
{ "Up", VK_UP }, { "Down", VK_DOWN },
|
||||||
|
{ "Left", VK_LEFT }, { "Right", VK_RIGHT },
|
||||||
|
{ "Space", VK_SPACE }, { "Return", VK_RETURN }, { "Enter", VK_RETURN },
|
||||||
|
{ "Escape", VK_ESCAPE }, { "Tab", VK_TAB }, { "Back", VK_BACK },
|
||||||
|
{ "Home", VK_HOME }, { "End", VK_END },
|
||||||
|
{ "PageUp", VK_PRIOR }, { "PageDown", VK_NEXT },
|
||||||
|
{ "Prior", VK_PRIOR }, { "Next", VK_NEXT },
|
||||||
|
{ "Insert", VK_INSERT }, { "Delete", VK_DELETE },
|
||||||
|
{ "ShiftKey", VK_SHIFT }, { "Shift", VK_SHIFT },
|
||||||
|
{ "ControlKey", VK_CONTROL }, { "Ctrl", VK_CONTROL }, { "Control", VK_CONTROL },
|
||||||
|
{ "Menu", VK_MENU }, { "Alt", VK_MENU },
|
||||||
|
{ "OemMinus", VK_OEM_MINUS }, { "Oemplus", VK_OEM_PLUS },
|
||||||
|
{ "Oemcomma", VK_OEM_COMMA }, { "OemPeriod", VK_OEM_PERIOD },
|
||||||
|
{ "OemOpenBrackets", VK_OEM_4 }, { "OemCloseBrackets", VK_OEM_6 },
|
||||||
|
{ "OemSemicolon", VK_OEM_1 }, { "OemQuotes", VK_OEM_7 },
|
||||||
|
{ "OemQuestion", VK_OEM_2 }, { "OemPipe", VK_OEM_5 },
|
||||||
|
{ "Oemtilde", VK_OEM_3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameValue kPadButtonNames[] =
|
||||||
|
{
|
||||||
|
{ "A", XINPUT_GAMEPAD_A }, { "B", XINPUT_GAMEPAD_B },
|
||||||
|
{ "X", XINPUT_GAMEPAD_X }, { "Y", XINPUT_GAMEPAD_Y },
|
||||||
|
{ "DPadUp", XINPUT_GAMEPAD_DPAD_UP },
|
||||||
|
{ "DPadDown", XINPUT_GAMEPAD_DPAD_DOWN },
|
||||||
|
{ "DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT },
|
||||||
|
{ "DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT },
|
||||||
|
{ "Start", XINPUT_GAMEPAD_START }, { "Back", XINPUT_GAMEPAD_BACK },
|
||||||
|
{ "LeftShoulder", XINPUT_GAMEPAD_LEFT_SHOULDER },
|
||||||
|
{ "RightShoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER },
|
||||||
|
{ "LeftThumb", XINPUT_GAMEPAD_LEFT_THUMB },
|
||||||
|
{ "RightThumb", XINPUT_GAMEPAD_RIGHT_THUMB },
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameValue kPadAxisNames[] =
|
||||||
|
{
|
||||||
|
{ "LeftStickX", BindPadLeftStickX }, { "LeftStickY", BindPadLeftStickY },
|
||||||
|
{ "RightStickX", BindPadRightStickX }, { "RightStickY", BindPadRightStickY },
|
||||||
|
{ "LeftTrigger", BindPadLeftTrigger }, { "RightTrigger", BindPadRightTrigger },
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameValue kRioAxisNames[] =
|
||||||
|
{
|
||||||
|
{ "Throttle", BindAxisThrottle },
|
||||||
|
{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
|
||||||
|
{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
|
||||||
|
};
|
||||||
|
|
||||||
|
Logical NameEquals(const char *a, const char *b)
|
||||||
|
{
|
||||||
|
return _stricmp(a, b) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LookupTable(const NameValue *table, int count, const char *name)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
if (NameEquals(table[i].name, name))
|
||||||
|
{
|
||||||
|
return table[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LookupKey(const char *name)
|
||||||
|
{
|
||||||
|
// single letter A-Z (VK == uppercase ASCII)
|
||||||
|
if (name[1] == '\0' &&
|
||||||
|
((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')))
|
||||||
|
{
|
||||||
|
return toupper(name[0]);
|
||||||
|
}
|
||||||
|
// digit row: D0-D9 (VK == '0'-'9')
|
||||||
|
if ((name[0] == 'D' || name[0] == 'd') &&
|
||||||
|
name[1] >= '0' && name[1] <= '9' && name[2] == '\0')
|
||||||
|
{
|
||||||
|
return name[1];
|
||||||
|
}
|
||||||
|
return LookupTable(kKeyNames, sizeof(kKeyNames) / sizeof(kKeyNames[0]), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical ParseAddress(const char *text, int *address)
|
||||||
|
{
|
||||||
|
int value = -1;
|
||||||
|
if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X'))
|
||||||
|
{
|
||||||
|
if (sscanf(text + 2, "%x", &value) != 1)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sscanf(text, "%d", &value) != 1)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
if ((value >= 0x00 && value <= 0x47) || (value >= 0x50 && value <= 0x6F))
|
||||||
|
{
|
||||||
|
*address = value;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical ParseNumber(const char *text, Scalar *value)
|
||||||
|
{
|
||||||
|
float parsed;
|
||||||
|
if (sscanf(text, "%f", &parsed) != 1)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
*value = (Scalar) parsed;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Line tokenizer: whitespace separated, '#' starts a comment
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
int Tokenize(char *line, char *tokens[], int max_tokens)
|
||||||
|
{
|
||||||
|
char *hash = strchr(line, '#');
|
||||||
|
if (hash != NULL)
|
||||||
|
{
|
||||||
|
*hash = '\0';
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
char *cursor = line;
|
||||||
|
while (count < max_tokens)
|
||||||
|
{
|
||||||
|
while (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n')
|
||||||
|
{
|
||||||
|
++cursor;
|
||||||
|
}
|
||||||
|
if (*cursor == '\0')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokens[count++] = cursor;
|
||||||
|
while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' &&
|
||||||
|
*cursor != '\r' && *cursor != '\n')
|
||||||
|
{
|
||||||
|
++cursor;
|
||||||
|
}
|
||||||
|
if (*cursor != '\0')
|
||||||
|
{
|
||||||
|
*cursor++ = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// One line of the profile grammar
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
|
||||||
|
{
|
||||||
|
if (token_count < 4)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "button"))
|
||||||
|
{
|
||||||
|
int key = LookupKey(tokens[1]);
|
||||||
|
int address;
|
||||||
|
if (key < 0 || !ParseAddress(tokens[3], &address) ||
|
||||||
|
profile->keyButtonCount >= PadBindingProfile::maxKeyButtons)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
|
||||||
|
PadKeyButtonBinding *binding = &profile->keyButtons[profile->keyButtonCount++];
|
||||||
|
memset(binding, 0, sizeof(*binding));
|
||||||
|
binding->virtualKey = key;
|
||||||
|
binding->address = address;
|
||||||
|
binding->toggle = toggle;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "axis"))
|
||||||
|
{
|
||||||
|
if (token_count < 6)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
int key = LookupKey(tokens[1]);
|
||||||
|
int axis = LookupTable(kRioAxisNames,
|
||||||
|
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
|
||||||
|
int mode = NameEquals(tokens[4], "deflect") ? BindKeyDeflect
|
||||||
|
: NameEquals(tokens[4], "rate") ? BindKeyRate : -1;
|
||||||
|
Scalar value;
|
||||||
|
if (key < 0 || axis < 0 || mode < 0 || !ParseNumber(tokens[5], &value) ||
|
||||||
|
profile->keyAxisCount >= PadBindingProfile::maxKeyAxes)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
PadKeyAxisBinding *binding = &profile->keyAxes[profile->keyAxisCount++];
|
||||||
|
binding->virtualKey = key;
|
||||||
|
binding->axis = axis;
|
||||||
|
binding->mode = mode;
|
||||||
|
binding->value = value;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameEquals(tokens[0], "pad") && NameEquals(tokens[2], "button"))
|
||||||
|
{
|
||||||
|
int mask = LookupTable(kPadButtonNames,
|
||||||
|
sizeof(kPadButtonNames) / sizeof(kPadButtonNames[0]), tokens[1]);
|
||||||
|
int address;
|
||||||
|
if (mask < 0 || !ParseAddress(tokens[3], &address) ||
|
||||||
|
profile->padButtonCount >= PadBindingProfile::maxPadButtons)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
|
||||||
|
PadPadButtonBinding *binding = &profile->padButtons[profile->padButtonCount++];
|
||||||
|
memset(binding, 0, sizeof(*binding));
|
||||||
|
binding->padMask = (unsigned short) mask;
|
||||||
|
binding->address = address;
|
||||||
|
binding->toggle = toggle;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameEquals(tokens[0], "padaxis") && NameEquals(tokens[2], "axis"))
|
||||||
|
{
|
||||||
|
int source = LookupTable(kPadAxisNames,
|
||||||
|
sizeof(kPadAxisNames) / sizeof(kPadAxisNames[0]), tokens[1]);
|
||||||
|
int axis = LookupTable(kRioAxisNames,
|
||||||
|
sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
|
||||||
|
if (source < 0 || axis < 0 ||
|
||||||
|
profile->padAxisCount >= PadBindingProfile::maxPadAxes)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
|
||||||
|
memset(binding, 0, sizeof(*binding));
|
||||||
|
binding->source = source;
|
||||||
|
binding->axis = axis;
|
||||||
|
for (int i = 4; i < token_count; ++i)
|
||||||
|
{
|
||||||
|
if (NameEquals(tokens[i], "invert"))
|
||||||
|
{
|
||||||
|
binding->invert = True;
|
||||||
|
}
|
||||||
|
else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
|
||||||
|
{
|
||||||
|
if (!ParseNumber(tokens[++i], &binding->deadzone))
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
|
||||||
|
{
|
||||||
|
if (!ParseNumber(tokens[++i], &binding->rate))
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// The default profile: vRIO's board-complete layout, adapted for
|
||||||
|
// the desktop game - the driving keys (WASD/QE/PgUp/PgDn) stay
|
||||||
|
// axes, so six bank buttons lose their keyboard keys (they are
|
||||||
|
// still clickable on the on-screen cockpit, and rebindable here).
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
const char kDefaultProfile[] =
|
||||||
|
"# BT412 input bindings - keyboard and Xbox (XInput) controller.\n"
|
||||||
|
"#\n"
|
||||||
|
"# One binding per line, '#' starts a comment, keywords are case-\n"
|
||||||
|
"# insensitive. Loaded at game start; delete this file to restore\n"
|
||||||
|
"# the defaults.\n"
|
||||||
|
"#\n"
|
||||||
|
"# key <name> button <addr> [toggle]\n"
|
||||||
|
"# key <name> axis <axis> deflect <n>\n"
|
||||||
|
"# key <name> axis <axis> rate <n-per-second>\n"
|
||||||
|
"# pad <button> button <addr> [toggle]\n"
|
||||||
|
"# padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
|
||||||
|
"#\n"
|
||||||
|
"# <addr> RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
|
||||||
|
"# 0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
|
||||||
|
"# <axis> Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
|
||||||
|
"# <name> Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
|
||||||
|
"# Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
|
||||||
|
"# OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
|
||||||
|
"# <button> A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back\n"
|
||||||
|
"# LeftShoulder RightShoulder LeftThumb RightThumb\n"
|
||||||
|
"# <src> LeftStickX LeftStickY RightStickX RightStickY\n"
|
||||||
|
"# LeftTrigger RightTrigger\n"
|
||||||
|
"#\n"
|
||||||
|
"# 'deflect' holds the axis there while the key is down and springs\n"
|
||||||
|
"# back on release; 'rate' walks the axis by <n> per second and the\n"
|
||||||
|
"# position sticks (the throttle). Every lamp button is also clickable\n"
|
||||||
|
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Driving: number pad + modifiers ------------------------------\n"
|
||||||
|
"# The whole letter board stays free for the MFD banks; driving lives\n"
|
||||||
|
"# on the numpad with the throttle lever on the modifier keys.\n"
|
||||||
|
"key NumPad8 axis JoystickY deflect -1 # stick forward\n"
|
||||||
|
"key NumPad2 axis JoystickY deflect 1 # stick back\n"
|
||||||
|
"key NumPad4 axis JoystickX deflect 1 # stick left\n"
|
||||||
|
"key NumPad6 axis JoystickX deflect -1 # stick right\n"
|
||||||
|
"key NumPad7 axis LeftPedal deflect 1\n"
|
||||||
|
"key NumPad9 axis RightPedal deflect 1\n"
|
||||||
|
"key NumPad0 button 0x40 # Main trigger (Space works too)\n"
|
||||||
|
"key Shift axis Throttle rate 0.75 # throttle up\n"
|
||||||
|
"key Ctrl axis Throttle rate -0.75 # throttle down\n"
|
||||||
|
"key Alt button 0x3F # Throttle-head button\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Driving: Xbox controller (signs match the pod convention) ----\n"
|
||||||
|
"padaxis LeftStickX axis JoystickX invert deadzone 0.24\n"
|
||||||
|
"padaxis LeftStickY axis JoystickY invert deadzone 0.24\n"
|
||||||
|
"padaxis RightStickY axis Throttle deadzone 0.24 rate 0.75\n"
|
||||||
|
"padaxis LeftTrigger axis LeftPedal deadzone 0.12\n"
|
||||||
|
"padaxis RightTrigger axis RightPedal deadzone 0.12\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Joystick column: hat on the arrows, Main on Space ------------\n"
|
||||||
|
"key Space button 0x40 # Main trigger\n"
|
||||||
|
"key Up button 0x42 # Hat Up\n"
|
||||||
|
"key Down button 0x41 # Hat Back\n"
|
||||||
|
"key Left button 0x44 # Hat Left\n"
|
||||||
|
"key Right button 0x43 # Hat Right\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Xbox controller: buttons -------------------------------------\n"
|
||||||
|
"pad A button 0x40 # Main\n"
|
||||||
|
"pad B button 0x46 # Middle\n"
|
||||||
|
"pad X button 0x45 # Pinky\n"
|
||||||
|
"pad Y button 0x47 # Upper\n"
|
||||||
|
"pad DPadUp button 0x42 # Hat Up\n"
|
||||||
|
"pad DPadDown button 0x41 # Hat Back\n"
|
||||||
|
"pad DPadLeft button 0x44 # Hat Left\n"
|
||||||
|
"pad DPadRight button 0x43 # Hat Right\n"
|
||||||
|
"pad LeftShoulder button 0x3D # Panic\n"
|
||||||
|
"pad RightShoulder button 0x3F # Throttle-head button\n"
|
||||||
|
"pad Start button 0x37 # config 1\n"
|
||||||
|
"pad Back button 0x36 # config 2\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
|
||||||
|
"# ---- QWERTY row is the row under it, left to right across the\n"
|
||||||
|
"# ---- Left / Middle / Right MFDs.\n"
|
||||||
|
"key D1 button 0x2F\n"
|
||||||
|
"key D2 button 0x2E\n"
|
||||||
|
"key D3 button 0x2D\n"
|
||||||
|
"key D4 button 0x2C\n"
|
||||||
|
"key D5 button 0x27\n"
|
||||||
|
"key D6 button 0x26\n"
|
||||||
|
"key D7 button 0x25\n"
|
||||||
|
"key D8 button 0x24\n"
|
||||||
|
"key D9 button 0x37\n"
|
||||||
|
"key D0 button 0x36\n"
|
||||||
|
"key OemMinus button 0x35\n"
|
||||||
|
"key Oemplus button 0x34\n"
|
||||||
|
"key Q button 0x2B\n"
|
||||||
|
"key W button 0x2A\n"
|
||||||
|
"key E button 0x29\n"
|
||||||
|
"key R button 0x28\n"
|
||||||
|
"key T button 0x23\n"
|
||||||
|
"key Y button 0x22\n"
|
||||||
|
"key U button 0x21\n"
|
||||||
|
"key I button 0x20\n"
|
||||||
|
"key O button 0x33\n"
|
||||||
|
"key P button 0x32\n"
|
||||||
|
"key OemOpenBrackets button 0x31\n"
|
||||||
|
"key OemCloseBrackets button 0x30\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Lower MFD bank: home row and the row below it, two 4-key\n"
|
||||||
|
"# ---- blocks split by an unbound gap key (G / B), mirroring the\n"
|
||||||
|
"# ---- keypad gap between the Lower Left and Lower Right MFDs.\n"
|
||||||
|
"key A button 0x0F\n"
|
||||||
|
"key S button 0x0E\n"
|
||||||
|
"key D button 0x0D\n"
|
||||||
|
"key F button 0x0C\n"
|
||||||
|
"key H button 0x07\n"
|
||||||
|
"key J button 0x06\n"
|
||||||
|
"key K button 0x05\n"
|
||||||
|
"key L button 0x04\n"
|
||||||
|
"key Z button 0x0B\n"
|
||||||
|
"key X button 0x0A\n"
|
||||||
|
"key C button 0x09\n"
|
||||||
|
"key V button 0x08\n"
|
||||||
|
"key N button 0x03\n"
|
||||||
|
"key M button 0x02\n"
|
||||||
|
"key Oemcomma button 0x01\n"
|
||||||
|
"key OemPeriod button 0x00\n"
|
||||||
|
"\n"
|
||||||
|
"# ---- Secondary / Screen columns on the function keys, top to\n"
|
||||||
|
"# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).\n"
|
||||||
|
"key F1 button 0x10\n"
|
||||||
|
"key F2 button 0x11\n"
|
||||||
|
"key F3 button 0x12\n"
|
||||||
|
"key F4 button 0x13\n"
|
||||||
|
"key F5 button 0x14\n"
|
||||||
|
"key F6 button 0x15\n"
|
||||||
|
"key F7 button 0x18\n"
|
||||||
|
"key F8 button 0x19\n"
|
||||||
|
"key F9 button 0x1A\n"
|
||||||
|
"key F10 button 0x1B\n"
|
||||||
|
"key F11 button 0x1C\n"
|
||||||
|
"key F12 button 0x1D\n"
|
||||||
|
"\n"
|
||||||
|
"# The pilot keypad (0x50-0x5F) and external keypad (0x60-0x6F) are\n"
|
||||||
|
"# unbound - the game never reads them - but remain bindable here\n"
|
||||||
|
"# (they arrive as arcade RIO key events).\n";
|
||||||
|
|
||||||
|
const char kBindingsFileName[] = "bindings.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Loader
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
void
|
||||||
|
PadBindings_Load(PadBindingProfile *profile)
|
||||||
|
{
|
||||||
|
memset(profile, 0, sizeof(*profile));
|
||||||
|
|
||||||
|
//
|
||||||
|
// First run: write the default profile so the player has a
|
||||||
|
// self-documenting file to edit
|
||||||
|
//
|
||||||
|
FILE *file = fopen(kBindingsFileName, "rb");
|
||||||
|
if (file == NULL)
|
||||||
|
{
|
||||||
|
FILE *out = fopen(kBindingsFileName, "wb");
|
||||||
|
if (out != NULL)
|
||||||
|
{
|
||||||
|
fwrite(kDefaultProfile, 1, strlen(kDefaultProfile), out);
|
||||||
|
fclose(out);
|
||||||
|
DEBUG_STREAM << "PadBindings: wrote default " << kBindingsFileName
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
|
file = fopen(kBindingsFileName, "rb");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parse: file if we have one, the built-in default if not
|
||||||
|
//
|
||||||
|
char *text = NULL;
|
||||||
|
long size = 0;
|
||||||
|
if (file != NULL)
|
||||||
|
{
|
||||||
|
fseek(file, 0, SEEK_END);
|
||||||
|
size = ftell(file);
|
||||||
|
fseek(file, 0, SEEK_SET);
|
||||||
|
text = new char[size + 1];
|
||||||
|
fread(text, 1, size, file);
|
||||||
|
text[size] = '\0';
|
||||||
|
fclose(file);
|
||||||
|
}
|
||||||
|
const char *source = (text != NULL) ? text : kDefaultProfile;
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
int line_number = 0;
|
||||||
|
int error_count = 0;
|
||||||
|
const char *cursor = source;
|
||||||
|
while (*cursor != '\0')
|
||||||
|
{
|
||||||
|
int length = 0;
|
||||||
|
while (cursor[length] != '\0' && cursor[length] != '\n' &&
|
||||||
|
length < (int) sizeof(line) - 1)
|
||||||
|
{
|
||||||
|
line[length] = cursor[length];
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
line[length] = '\0';
|
||||||
|
cursor += length;
|
||||||
|
if (*cursor == '\n')
|
||||||
|
{
|
||||||
|
++cursor;
|
||||||
|
}
|
||||||
|
++line_number;
|
||||||
|
|
||||||
|
char *tokens[12];
|
||||||
|
int token_count = Tokenize(line, tokens, 12);
|
||||||
|
if (token_count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!ParseLine(tokens, token_count, profile))
|
||||||
|
{
|
||||||
|
++error_count;
|
||||||
|
DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
|
||||||
|
<< line_number << " rejected\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text != NULL)
|
||||||
|
{
|
||||||
|
delete[] text;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_STREAM << "PadBindings: " << profile->keyButtonCount << " key buttons, "
|
||||||
|
<< profile->keyAxisCount << " key axes, "
|
||||||
|
<< profile->padButtonCount << " pad buttons, "
|
||||||
|
<< profile->padAxisCount << " pad axes"
|
||||||
|
<< (error_count ? " (with rejected lines)" : "")
|
||||||
|
<< "\n" << std::flush;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//===========================================================================//
|
||||||
|
// File: l4padbindings.h //
|
||||||
|
// Project: MUNGA Brick: PadRIO binding profiles //
|
||||||
|
// Contents: The rebindable input profile (vRIO bindings format) //
|
||||||
|
//---------------------------------------------------------------------------//
|
||||||
|
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||||
|
// PROPRIETARY AND CONFIDENTIAL //
|
||||||
|
//===========================================================================//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "..\munga\style.h"
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// The bindings file (vRIO's format, shared grammar):
|
||||||
|
//
|
||||||
|
// key <name> button <addr> [toggle]
|
||||||
|
// key <name> axis <axis> deflect <n> | rate <n>
|
||||||
|
// pad <button> button <addr> [toggle]
|
||||||
|
// padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
|
||||||
|
//
|
||||||
|
// <addr> is a RIO input address: lamp buttons 0x00-0x47, internal
|
||||||
|
// keypad 0x50-0x5F, external keypad 0x60-0x6F. Loaded from
|
||||||
|
// bindings.txt beside the exe; written there (self-documenting, with
|
||||||
|
// the full default profile) on first run. Bad lines are logged and
|
||||||
|
// skipped, good lines always win.
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
enum PadBindRioAxis
|
||||||
|
{
|
||||||
|
BindAxisThrottle = 0,
|
||||||
|
BindAxisLeftPedal,
|
||||||
|
BindAxisRightPedal,
|
||||||
|
BindAxisJoystickY,
|
||||||
|
BindAxisJoystickX,
|
||||||
|
BindAxisCount
|
||||||
|
};
|
||||||
|
|
||||||
|
enum PadBindKeyMode
|
||||||
|
{
|
||||||
|
BindKeyDeflect = 0, // hold at value while down, spring back
|
||||||
|
BindKeyRate // walk by value/second while down, position holds
|
||||||
|
};
|
||||||
|
|
||||||
|
enum PadBindPadAxis
|
||||||
|
{
|
||||||
|
BindPadLeftStickX = 0,
|
||||||
|
BindPadLeftStickY,
|
||||||
|
BindPadRightStickX,
|
||||||
|
BindPadRightStickY,
|
||||||
|
BindPadLeftTrigger,
|
||||||
|
BindPadRightTrigger,
|
||||||
|
BindPadAxisCount
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadKeyButtonBinding
|
||||||
|
{
|
||||||
|
int virtualKey;
|
||||||
|
int address; // 0x00-0x47 button, 0x50-0x6F keypad
|
||||||
|
Logical toggle;
|
||||||
|
// runtime edge/latch state
|
||||||
|
Logical wasDown;
|
||||||
|
Logical latched;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadKeyAxisBinding
|
||||||
|
{
|
||||||
|
int virtualKey;
|
||||||
|
int axis; // PadBindRioAxis
|
||||||
|
int mode; // PadBindKeyMode
|
||||||
|
Scalar value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadPadButtonBinding
|
||||||
|
{
|
||||||
|
unsigned short padMask; // XINPUT_GAMEPAD_*
|
||||||
|
int address;
|
||||||
|
Logical toggle;
|
||||||
|
Logical wasDown;
|
||||||
|
Logical latched;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadPadAxisBinding
|
||||||
|
{
|
||||||
|
int source; // PadBindPadAxis
|
||||||
|
int axis; // PadBindRioAxis
|
||||||
|
Logical invert;
|
||||||
|
Scalar deadzone; // normalized 0..1
|
||||||
|
Scalar rate; // 0 = direct position, >0 = speed integrate
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PadBindingProfile
|
||||||
|
{
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
maxKeyButtons = 128,
|
||||||
|
maxKeyAxes = 32,
|
||||||
|
maxPadButtons = 32,
|
||||||
|
maxPadAxes = 16
|
||||||
|
};
|
||||||
|
|
||||||
|
PadKeyButtonBinding keyButtons[maxKeyButtons];
|
||||||
|
int keyButtonCount;
|
||||||
|
PadKeyAxisBinding keyAxes[maxKeyAxes];
|
||||||
|
int keyAxisCount;
|
||||||
|
PadPadButtonBinding padButtons[maxPadButtons];
|
||||||
|
int padButtonCount;
|
||||||
|
PadPadAxisBinding padAxes[maxPadAxes];
|
||||||
|
int padAxisCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load bindings.txt from the working directory into the profile,
|
||||||
|
// writing the default profile file first when absent. Errors go to
|
||||||
|
// the debug log with line numbers.
|
||||||
|
void
|
||||||
|
PadBindings_Load(PadBindingProfile *profile);
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
#include "mungal4.h"
|
||||||
|
#pragma hdrstop
|
||||||
|
|
||||||
|
#include "l4padrio.h"
|
||||||
|
#include "l4keylight.h"
|
||||||
|
|
||||||
|
#include <XInput.h>
|
||||||
|
#pragma comment(lib, "xinput9_1_0.lib")
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Input helpers; the binding tables live in bindings.txt now
|
||||||
|
// (l4padbindings.cpp writes and parses the vRIO-format profile)
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Scalar StickValue(int raw, int dead_zone)
|
||||||
|
{
|
||||||
|
if (raw > -dead_zone && raw < dead_zone)
|
||||||
|
{
|
||||||
|
return (Scalar) 0;
|
||||||
|
}
|
||||||
|
Scalar value =
|
||||||
|
(raw > 0)
|
||||||
|
? (Scalar)(raw - dead_zone) / (Scalar)(32767 - dead_zone)
|
||||||
|
: (Scalar)(raw + dead_zone) / (Scalar)(32768 - dead_zone);
|
||||||
|
if (value > 1.0f) value = 1.0f;
|
||||||
|
if (value < -1.0f) value = -1.0f;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar Clamp01(Scalar value)
|
||||||
|
{
|
||||||
|
if (value < 0.0f) return 0.0f;
|
||||||
|
if (value > 1.0f) return 1.0f;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical KeyDown(int virtual_key)
|
||||||
|
{
|
||||||
|
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyLightLog(const char *line)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << line << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
//############################### PadRIO #################################
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
PadRIO *PadRIO::activeInstance = NULL;
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::SetScreenButton(int unit, Logical pressed)
|
||||||
|
{
|
||||||
|
if (activeInstance != NULL && unit >= 0 && unit < buttonUnits)
|
||||||
|
{
|
||||||
|
activeInstance->screenButton[unit] = pressed ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
PadRIO::GetLampState(int unit)
|
||||||
|
{
|
||||||
|
if (activeInstance != NULL && unit >= 0 && unit < lampCount)
|
||||||
|
{
|
||||||
|
return activeInstance->lampState[unit];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PadRIO::PadRIO()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
queueHead = 0;
|
||||||
|
queueTail = 0;
|
||||||
|
lastPollTick = GetTickCount();
|
||||||
|
lastPadCheckTick = 0;
|
||||||
|
padIndex = -1;
|
||||||
|
padReported = False;
|
||||||
|
analogRequested = False;
|
||||||
|
throttleAccum = (Scalar) 0;
|
||||||
|
|
||||||
|
sentThrottle = sentLeftPedal = sentRightPedal = (Scalar) 0;
|
||||||
|
sentJoystickX = sentJoystickY = (Scalar) 0;
|
||||||
|
|
||||||
|
memset(buttonDown, 0, sizeof(buttonDown));
|
||||||
|
memset(keypadDown, 0, sizeof(keypadDown));
|
||||||
|
memset(lampState, 0, sizeof(lampState));
|
||||||
|
memset(screenButton, 0, sizeof(screenButton));
|
||||||
|
|
||||||
|
PadBindings_Load(&profile);
|
||||||
|
|
||||||
|
//
|
||||||
|
// RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound
|
||||||
|
// to lamp addresses glow with the panel. Yellow = the Secondary
|
||||||
|
// columns (0x10-0x1F), red = everything else, like the physical
|
||||||
|
// panel. BT412KEYLIGHT=0 opts out.
|
||||||
|
//
|
||||||
|
keyLightActive = False;
|
||||||
|
const char *keylight = getenv("BT412KEYLIGHT");
|
||||||
|
if (keylight == NULL || atoi(keylight) != 0)
|
||||||
|
{
|
||||||
|
int light_keys[PadBindingProfile::maxKeyButtons];
|
||||||
|
int light_addresses[PadBindingProfile::maxKeyButtons];
|
||||||
|
unsigned char light_yellow[PadBindingProfile::maxKeyButtons];
|
||||||
|
int light_count = 0;
|
||||||
|
for (int i = 0; i < profile.keyButtonCount; ++i)
|
||||||
|
{
|
||||||
|
int address = profile.keyButtons[i].address;
|
||||||
|
if (address >= buttonUnits)
|
||||||
|
{
|
||||||
|
continue; // keypads have no lamps
|
||||||
|
}
|
||||||
|
Logical duplicate = False;
|
||||||
|
for (int j = 0; j < light_count; ++j)
|
||||||
|
{
|
||||||
|
if (light_keys[j] == profile.keyButtons[i].virtualKey)
|
||||||
|
{
|
||||||
|
duplicate = True; // first binding wins
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (duplicate)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
light_keys[light_count] = profile.keyButtons[i].virtualKey;
|
||||||
|
light_addresses[light_count] = address;
|
||||||
|
light_yellow[light_count] =
|
||||||
|
(address >= 0x10 && address <= 0x1F) ? 1 : 0;
|
||||||
|
++light_count;
|
||||||
|
}
|
||||||
|
if (light_count > 0)
|
||||||
|
{
|
||||||
|
KeyLight_SetLogger(&KeyLightLog);
|
||||||
|
KeyLight_SetMap(light_keys, light_addresses, light_yellow, light_count);
|
||||||
|
KeyLight_Start();
|
||||||
|
keyLightActive = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invertX = False;
|
||||||
|
invertY = False;
|
||||||
|
const char *flip = getenv("L4PADFLIP");
|
||||||
|
if (flip != NULL)
|
||||||
|
{
|
||||||
|
if (strchr(flip, 'X') || strchr(flip, 'x'))
|
||||||
|
{
|
||||||
|
invertX = True;
|
||||||
|
}
|
||||||
|
if (strchr(flip, 'Y') || strchr(flip, 'y'))
|
||||||
|
{
|
||||||
|
invertY = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report as a v4.2 board, like vRIO does
|
||||||
|
MajorRevision = 4;
|
||||||
|
MinorRevision = 2;
|
||||||
|
|
||||||
|
activeInstance = this;
|
||||||
|
|
||||||
|
DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
PadRIO::~PadRIO()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
if (keyLightActive)
|
||||||
|
{
|
||||||
|
KeyLight_Stop();
|
||||||
|
keyLightActive = False;
|
||||||
|
}
|
||||||
|
if (activeInstance == this)
|
||||||
|
{
|
||||||
|
activeInstance = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical
|
||||||
|
PadRIO::TestInstance() const
|
||||||
|
{
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// The controls manager drains events every frame; sampling lives here so
|
||||||
|
// button latency does not depend on the analog request cadence (which is
|
||||||
|
// 15 s outside of missions).
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Logical
|
||||||
|
PadRIO::GetNextEvent(RIOEvent *destinationPointer)
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
Check_Pointer(destinationPointer);
|
||||||
|
|
||||||
|
PollInputs();
|
||||||
|
|
||||||
|
if (queueTail == queueHead)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
*destinationPointer = eventQueue[queueTail];
|
||||||
|
queueTail = (queueTail + 1) % queueSize;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::RequestAnalogUpdate()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
analogRequested = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::GeneralReset()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
throttleAccum = (Scalar) 0;
|
||||||
|
Throttle = (Scalar) 0;
|
||||||
|
LeftPedal = (Scalar) 0;
|
||||||
|
RightPedal = (Scalar) 0;
|
||||||
|
JoystickX = (Scalar) 0;
|
||||||
|
JoystickY = (Scalar) 0;
|
||||||
|
analogRequested = True;
|
||||||
|
memset(lampState, 0, sizeof(lampState));
|
||||||
|
if (keyLightActive)
|
||||||
|
{
|
||||||
|
KeyLight_UpdateLamps(lampState, lampCount);
|
||||||
|
}
|
||||||
|
memset(keypadDown, 0, sizeof(keypadDown));
|
||||||
|
for (int i = 0; i < profile.keyButtonCount; ++i)
|
||||||
|
{
|
||||||
|
profile.keyButtons[i].latched = False;
|
||||||
|
profile.keyButtons[i].wasDown = False;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < profile.padButtonCount; ++i)
|
||||||
|
{
|
||||||
|
profile.padButtons[i].latched = False;
|
||||||
|
profile.padButtons[i].wasDown = False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::ResetThrottle()
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
throttleAccum = (Scalar) 0;
|
||||||
|
Throttle = (Scalar) 0;
|
||||||
|
analogRequested = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::SetLamp(int lampNumber, int state)
|
||||||
|
{
|
||||||
|
Check_Pointer(this);
|
||||||
|
if (lampNumber >= 0 && lampNumber < lampCount)
|
||||||
|
{
|
||||||
|
lampState[lampNumber] = (unsigned char) state;
|
||||||
|
if (keyLightActive)
|
||||||
|
{
|
||||||
|
KeyLight_UpdateLamps(lampState, lampCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::QueueEvent(const RIOEvent &an_event)
|
||||||
|
{
|
||||||
|
int next = (queueHead + 1) % queueSize;
|
||||||
|
if (next == queueTail)
|
||||||
|
{
|
||||||
|
// full: drop the oldest event
|
||||||
|
queueTail = (queueTail + 1) % queueSize;
|
||||||
|
}
|
||||||
|
eventQueue[queueHead] = an_event;
|
||||||
|
queueHead = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PadRIO::PollInputs()
|
||||||
|
{
|
||||||
|
unsigned long now = GetTickCount();
|
||||||
|
if (now - lastPollTick < 10)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Scalar delta_t = (Scalar)(now - lastPollTick) / 1000.0f;
|
||||||
|
if (delta_t > 0.25f)
|
||||||
|
{
|
||||||
|
delta_t = 0.25f;
|
||||||
|
}
|
||||||
|
lastPollTick = now;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Find / keep the XInput pad. Probing empty slots is slow, so an
|
||||||
|
// absent pad is only re-probed every 3 seconds.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
XINPUT_STATE pad;
|
||||||
|
memset(&pad, 0, sizeof(pad));
|
||||||
|
Logical pad_live = False;
|
||||||
|
|
||||||
|
if (padIndex >= 0)
|
||||||
|
{
|
||||||
|
pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS);
|
||||||
|
if (!pad_live)
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "PadRIO: controller " << padIndex << " disconnected\n" << std::flush;
|
||||||
|
padIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (padIndex < 0 && (now - lastPadCheckTick) >= 3000)
|
||||||
|
{
|
||||||
|
lastPadCheckTick = now;
|
||||||
|
for (DWORD i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
if (XInputGetState(i, &pad) == ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
padIndex = (int) i;
|
||||||
|
pad_live = True;
|
||||||
|
DEBUG_STREAM << "PadRIO: controller " << padIndex << " connected\n" << std::flush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (padIndex < 0 && !padReported)
|
||||||
|
{
|
||||||
|
padReported = True;
|
||||||
|
DEBUG_STREAM << "PadRIO: no controller found - keyboard only\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Buttons: build the desired state from the binding profile
|
||||||
|
// (keyboard + pad, with toggle latches), merge the on-screen
|
||||||
|
// cockpit buttons, then diff against what we last reported.
|
||||||
|
// Keypad addresses (0x50-0x6F) collect separately - they become
|
||||||
|
// arcade KeyEvents, not button events.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
unsigned char desired[buttonUnits];
|
||||||
|
unsigned char keypadDesired[keypadUnits];
|
||||||
|
memset(desired, 0, sizeof(desired));
|
||||||
|
memset(keypadDesired, 0, sizeof(keypadDesired));
|
||||||
|
|
||||||
|
for (int i = 0; i < profile.keyButtonCount; ++i)
|
||||||
|
{
|
||||||
|
PadKeyButtonBinding *binding = &profile.keyButtons[i];
|
||||||
|
Logical down = KeyDown(binding->virtualKey);
|
||||||
|
if (binding->toggle && down && !binding->wasDown)
|
||||||
|
{
|
||||||
|
binding->latched = !binding->latched;
|
||||||
|
}
|
||||||
|
binding->wasDown = down;
|
||||||
|
|
||||||
|
if (binding->toggle ? binding->latched : down)
|
||||||
|
{
|
||||||
|
if (binding->address < buttonUnits)
|
||||||
|
{
|
||||||
|
desired[binding->address] = 1;
|
||||||
|
}
|
||||||
|
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
|
||||||
|
{
|
||||||
|
keypadDesired[binding->address - 0x50] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < profile.padButtonCount; ++i)
|
||||||
|
{
|
||||||
|
PadPadButtonBinding *binding = &profile.padButtons[i];
|
||||||
|
Logical down = pad_live &&
|
||||||
|
(pad.Gamepad.wButtons & binding->padMask) != 0;
|
||||||
|
if (binding->toggle && down && !binding->wasDown)
|
||||||
|
{
|
||||||
|
binding->latched = !binding->latched;
|
||||||
|
}
|
||||||
|
binding->wasDown = down;
|
||||||
|
|
||||||
|
if (binding->toggle ? binding->latched : down)
|
||||||
|
{
|
||||||
|
if (binding->address < buttonUnits)
|
||||||
|
{
|
||||||
|
desired[binding->address] = 1;
|
||||||
|
}
|
||||||
|
else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits)
|
||||||
|
{
|
||||||
|
keypadDesired[binding->address - 0x50] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < buttonUnits; ++i)
|
||||||
|
{
|
||||||
|
if (screenButton[i])
|
||||||
|
{
|
||||||
|
desired[i] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int unit = 0; unit < buttonUnits; ++unit)
|
||||||
|
{
|
||||||
|
if (desired[unit] != buttonDown[unit])
|
||||||
|
{
|
||||||
|
buttonDown[unit] = desired[unit];
|
||||||
|
|
||||||
|
RIOEvent an_event;
|
||||||
|
an_event.Type = desired[unit] ? ButtonPressedEvent : ButtonReleasedEvent;
|
||||||
|
an_event.Data.Unit = unit;
|
||||||
|
QueueEvent(an_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Keypads: presses become the arcade RIO KeyEvents. Unit 0 is the
|
||||||
|
// pilot's internal keypad (0x50-0x5F), unit 1 the external
|
||||||
|
// operator keypad (0x60-0x6F); the key is the hex digit 0-15.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
for (int pad_key = 0; pad_key < keypadUnits; ++pad_key)
|
||||||
|
{
|
||||||
|
if (keypadDesired[pad_key] != keypadDown[pad_key])
|
||||||
|
{
|
||||||
|
keypadDown[pad_key] = keypadDesired[pad_key];
|
||||||
|
if (keypadDesired[pad_key])
|
||||||
|
{
|
||||||
|
RIOEvent an_event;
|
||||||
|
an_event.Type = KeyEvent;
|
||||||
|
an_event.Data.Keyboard.Unit = (pad_key >= 0x10) ? 1 : 0;
|
||||||
|
an_event.Data.Keyboard.Key = pad_key & 0x0F;
|
||||||
|
QueueEvent(an_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Axes, from the profile. 'deflect' sources sum into a springy
|
||||||
|
// position; 'rate' sources integrate the throttle (the pod's only
|
||||||
|
// sticky axis) by value per second.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
Scalar deflect[BindAxisCount];
|
||||||
|
Scalar rate[BindAxisCount];
|
||||||
|
memset(deflect, 0, sizeof(deflect));
|
||||||
|
memset(rate, 0, sizeof(rate));
|
||||||
|
|
||||||
|
for (int i = 0; i < profile.keyAxisCount; ++i)
|
||||||
|
{
|
||||||
|
const PadKeyAxisBinding *binding = &profile.keyAxes[i];
|
||||||
|
if (KeyDown(binding->virtualKey))
|
||||||
|
{
|
||||||
|
if (binding->mode == BindKeyRate)
|
||||||
|
{
|
||||||
|
rate[binding->axis] += binding->value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deflect[binding->axis] += binding->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pad_live)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < profile.padAxisCount; ++i)
|
||||||
|
{
|
||||||
|
const PadPadAxisBinding *binding = &profile.padAxes[i];
|
||||||
|
Scalar value = (Scalar) 0;
|
||||||
|
switch (binding->source)
|
||||||
|
{
|
||||||
|
case BindPadLeftStickX:
|
||||||
|
value = StickValue(pad.Gamepad.sThumbLX, (int)(binding->deadzone * 32767.0f));
|
||||||
|
break;
|
||||||
|
case BindPadLeftStickY:
|
||||||
|
value = StickValue(pad.Gamepad.sThumbLY, (int)(binding->deadzone * 32767.0f));
|
||||||
|
break;
|
||||||
|
case BindPadRightStickX:
|
||||||
|
value = StickValue(pad.Gamepad.sThumbRX, (int)(binding->deadzone * 32767.0f));
|
||||||
|
break;
|
||||||
|
case BindPadRightStickY:
|
||||||
|
value = StickValue(pad.Gamepad.sThumbRY, (int)(binding->deadzone * 32767.0f));
|
||||||
|
break;
|
||||||
|
case BindPadLeftTrigger:
|
||||||
|
value = (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f;
|
||||||
|
if (value <= binding->deadzone) value = (Scalar) 0;
|
||||||
|
break;
|
||||||
|
case BindPadRightTrigger:
|
||||||
|
value = (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f;
|
||||||
|
if (value <= binding->deadzone) value = (Scalar) 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (binding->invert)
|
||||||
|
{
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
if (binding->rate > 0.0f)
|
||||||
|
{
|
||||||
|
rate[binding->axis] += value * binding->rate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deflect[binding->axis] += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t);
|
||||||
|
|
||||||
|
Scalar x = deflect[BindAxisJoystickX];
|
||||||
|
Scalar y = deflect[BindAxisJoystickY];
|
||||||
|
if (x > 1.0f) x = 1.0f;
|
||||||
|
if (x < -1.0f) x = -1.0f;
|
||||||
|
if (y > 1.0f) y = 1.0f;
|
||||||
|
if (y < -1.0f) y = -1.0f;
|
||||||
|
|
||||||
|
Throttle = Clamp01(throttleAccum + deflect[BindAxisThrottle]);
|
||||||
|
LeftPedal = Clamp01(deflect[BindAxisLeftPedal]);
|
||||||
|
RightPedal = Clamp01(deflect[BindAxisRightPedal]);
|
||||||
|
// The profile encodes the pod's stick sign convention; L4PADFLIP
|
||||||
|
// flips on top of it per axis.
|
||||||
|
JoystickX = invertX ? -x : x;
|
||||||
|
JoystickY = invertY ? -y : y;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Emit an analog event when asked to, or when anything moved
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
Logical changed =
|
||||||
|
(Throttle != sentThrottle) ||
|
||||||
|
(LeftPedal != sentLeftPedal) ||
|
||||||
|
(RightPedal != sentRightPedal) ||
|
||||||
|
(JoystickX != sentJoystickX) ||
|
||||||
|
(JoystickY != sentJoystickY);
|
||||||
|
|
||||||
|
if (analogRequested || changed)
|
||||||
|
{
|
||||||
|
analogRequested = False;
|
||||||
|
sentThrottle = Throttle;
|
||||||
|
sentLeftPedal = LeftPedal;
|
||||||
|
sentRightPedal = RightPedal;
|
||||||
|
sentJoystickX = JoystickX;
|
||||||
|
sentJoystickY = JoystickY;
|
||||||
|
|
||||||
|
RIOEvent an_event;
|
||||||
|
an_event.Type = AnalogEvent;
|
||||||
|
an_event.Data.Unit = 0;
|
||||||
|
QueueEvent(an_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "l4rio.h"
|
||||||
|
#include "l4padbindings.h"
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
//############################### PadRIO #################################
|
||||||
|
//########################################################################
|
||||||
|
//
|
||||||
|
// A cockpit-less RIO: synthesizes the RIOBase control surface from an
|
||||||
|
// XInput controller and the PC keyboard. Selected with L4CONTROLS=PAD.
|
||||||
|
//
|
||||||
|
// Every input is REBINDABLE: bindings.txt beside the exe (vRIO's
|
||||||
|
// profile format, written with the full default layout on first run)
|
||||||
|
// maps keys and pad controls to RIO button addresses (0x00-0x47), the
|
||||||
|
// pilot/external keypads (0x50-0x6F, delivered as arcade KeyEvents;
|
||||||
|
// unbound by default - the game never reads them), and the five axes
|
||||||
|
// with deflect/rate semantics. Defaults: vRIO's complete board layout
|
||||||
|
// (number+letter rows = MFD banks, F-keys = secondary columns) with
|
||||||
|
// flight on the numpad - 8/2/4/6 stick, 7/9 pedals, 0 trigger - and
|
||||||
|
// the modifiers: Shift/Ctrl throttle up/down, Alt reverse thrust,
|
||||||
|
// Space trigger, arrows hat.
|
||||||
|
//
|
||||||
|
// Set L4PADFLIP to a string containing 'X' and/or 'Y' to invert the
|
||||||
|
// stick axes on top of whatever the profile produces.
|
||||||
|
//
|
||||||
|
class PadRIO :
|
||||||
|
public RIOBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PadRIO();
|
||||||
|
~PadRIO();
|
||||||
|
|
||||||
|
Logical
|
||||||
|
TestInstance() const;
|
||||||
|
|
||||||
|
Logical
|
||||||
|
GetNextEvent(RIOEvent *destinationPointer);
|
||||||
|
|
||||||
|
void
|
||||||
|
RequestAnalogUpdate();
|
||||||
|
|
||||||
|
void
|
||||||
|
GeneralReset();
|
||||||
|
void
|
||||||
|
ResetThrottle();
|
||||||
|
|
||||||
|
void
|
||||||
|
SetLamp(int lampNumber, int state);
|
||||||
|
|
||||||
|
// Lamp state the game has commanded, indexed by RIO lamp number.
|
||||||
|
// The on-screen cockpit buttons read this to light themselves.
|
||||||
|
enum { lampCount = 64 };
|
||||||
|
unsigned char
|
||||||
|
lampState[lampCount];
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// On-screen cockpit buttons (MFDSplitView strips) press RIO units
|
||||||
|
// through these; they are no-ops when no PadRIO is active (e.g.
|
||||||
|
// real serial hardware selected).
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
static void
|
||||||
|
SetScreenButton(int unit, Logical pressed);
|
||||||
|
static int
|
||||||
|
GetLampState(int unit);
|
||||||
|
static Logical
|
||||||
|
IsActive()
|
||||||
|
{ return activeInstance != NULL; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void
|
||||||
|
PollInputs();
|
||||||
|
void
|
||||||
|
QueueEvent(const RIOEvent &an_event);
|
||||||
|
|
||||||
|
enum { queueSize = 64 };
|
||||||
|
enum { buttonUnits = 0x48 }; // through LastMappableButton
|
||||||
|
|
||||||
|
RIOEvent
|
||||||
|
eventQueue[queueSize];
|
||||||
|
int
|
||||||
|
queueHead, queueTail;
|
||||||
|
|
||||||
|
static PadRIO
|
||||||
|
*activeInstance;
|
||||||
|
|
||||||
|
// pressed state driven by the on-screen cockpit buttons
|
||||||
|
unsigned char
|
||||||
|
screenButton[0x48];
|
||||||
|
|
||||||
|
unsigned long
|
||||||
|
lastPollTick,
|
||||||
|
lastPadCheckTick;
|
||||||
|
int
|
||||||
|
padIndex; // connected XInput slot, -1 = none
|
||||||
|
Logical
|
||||||
|
padReported; // one-time connect/disconnect log flags
|
||||||
|
Logical
|
||||||
|
analogRequested;
|
||||||
|
|
||||||
|
unsigned char
|
||||||
|
buttonDown[buttonUnits];
|
||||||
|
|
||||||
|
// keypad pressed-state (0x50-0x6F), for KeyEvent edges
|
||||||
|
enum { keypadUnits = 0x20 };
|
||||||
|
unsigned char
|
||||||
|
keypadDown[keypadUnits];
|
||||||
|
|
||||||
|
PadBindingProfile
|
||||||
|
profile;
|
||||||
|
|
||||||
|
Scalar
|
||||||
|
throttleAccum;
|
||||||
|
Logical
|
||||||
|
invertX, invertY;
|
||||||
|
Logical
|
||||||
|
keyLightActive; // Dynamic Lighting keyboard mirror running
|
||||||
|
|
||||||
|
Scalar
|
||||||
|
sentThrottle, sentLeftPedal, sentRightPedal,
|
||||||
|
sentJoystickX, sentJoystickY;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user