diff --git a/handoff/padrio/PADRIO-IMPLEMENTATION.md b/handoff/padrio/PADRIO-IMPLEMENTATION.md new file mode 100644 index 0000000..3835e9d --- /dev/null +++ b/handoff/padrio/PADRIO-IMPLEMENTATION.md @@ -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 button [toggle] + key axis deflect # hold at n while down, spring back + key axis rate # walk by n/sec while down, position holds + pad