Input: PadRIO -- play without the pod (L4CONTROLS=PAD), Workstream A.1

Ported from RP412: RIOBase split out of the serial RIO (L4RIO.h),
rioPointer is RIOBase* (L4CTRL.h), PAD token -> new PadRIO() speaking
the RIO surface from an XInput pad + keyboard (L4PADRIO/L4PADBINDINGS,
vRIO bindings.txt grammar, hot-plug), KeyLight RGB mirror TU
(BT412KEYLIGHT, /std:c++17 per-file).

BT-side fixes PadRIO forced into the open:
- Both keyboard input bridges (mech4.cpp, mechmppr.cpp BT_KEY_BRIDGE)
  stand down when a RIO device exists -- they overwrote the engine
  controls push every frame. M/X conveniences stay live.
- Mapper attribute chain OFF BY ONE (latent real-pod bug): the DOS
  chain below MechControlsMapper carried two base attributes, WinTesla
  carries one, and AttributeIndexSet::Find is positional -- the .CTL
  stick mapping wrote throttlePosition. Pad slot + binary-locked enum;
  gotcha ledgered (reconstruction-gotchas #11).

Verified: PAD throttle lever ramps + sticks, stick turns with the
authentic speed-vs-turn clamp (61.5 -> 22.0 u/s), mech drives; keyboard
fallback intact (BT_FORCE_THROTTLE harness). New diags: BT_CTRLMAP_LOG,
BT_STICK_LOG. (Phase 2 of docs/BT412-ROADMAP.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-14 08:16:33 -05:00
co-authored by Claude Fable 5
parent 55e4295dc3
commit fe97746a51
17 changed files with 2050 additions and 60 deletions
+18 -1
View File
@@ -2551,6 +2551,18 @@ void
// which runs in the subsystem-roster walk below (un-skipped under this env).
static const int s_realControls = BTEnvOn("BT_REAL_CONTROLS", 1); // default ON (=0 to disable)
MechControlsMapper *mppr = MappingMapper(); // roster slot 0 (task #7)
//
// BT412: when a RIO device exists (the pod board, or PadRIO's
// pad/keyboard synthesizer under L4CONTROLS=PAD), the ENGINE's
// controls push writes the mapper's published input attributes from
// the device groups -- the WASD stand-in write below would clobber
// it every frame. The bridge speaks for the hardware only when
// there is none; the demand CONSUME side runs either way.
//
LBE4ControlsManager *bridgeControls = (application != 0)
? (LBE4ControlsManager *)application->GetControlsManager() : 0;
const int deviceOwnsInput =
(bridgeControls != 0 && bridgeControls->rioPointer != 0);
if (s_realControls && mppr != 0)
{
// Diagnostic: what the ENGINE controls push left in the attribute since
@@ -2558,10 +2570,13 @@ void
// here as pre != our previous write).
float preThrottle = mppr->throttlePosition;
(void)preThrottle;
if (!deviceOwnsInput)
{
mppr->throttlePosition = (throttle >= 0.0f) ? throttle : -throttle;
mppr->reverseThrust = (throttle < 0.0f) ? 1 : 0; // ControlsButton: >=1 engaged
mppr->stickPosition.x = turn; // Basic mode: turn = stick yaw
mppr->stickPosition.y = 0.0f;
}
// Consume the PREVIOUS frame's interpreted demands (the mapper ticks in
// the roster walk after this block -- one frame of input latency).
// turnDemand is the mode-shaped steering; speedDemand (world u/s, sign =
@@ -2587,7 +2602,9 @@ void
<< " turnDemand=" << mppr->turnDemand
<< " mode=" << mppr->controlMode
<< " mapper=" << (void*)mppr
<< " &mode=" << (void*)&mppr->controlMode << "\n" << std::flush;
<< " &mode=" << (void*)&mppr->controlMode
<< " &stick=" << (void*)&mppr->stickPosition
<< " &thr=" << (void*)&mppr->throttlePosition << "\n" << std::flush;
}
}