Merge origin/master: the D1 relay/operator line + input remap meet the glass layer

33 master commits in (relay TCP/UDP + PySide6 operator console, CONTROLS.MAP
+XInput binding engine, camera seats, torso pitch aim, sign fixes, the 1995
manual, version stamping, 18-mech certification).  Conflicts: .gitignore +
CLAUDE.md router rows (combined).

SEMANTIC RECONCILIATION (the one real overlap): masters btinput binding
engine (ungated, CONTROLS.MAP) and the glass PadRIO (gated, bindings.txt)
would both read the keyboard/pad in a glass+PAD session.  btinput now joins
the stand-down convention: BTInputPoll yields (and BTInputSuppressKey claims
NOTHING, so authentic hotkeys flow) when an operational cockpit device owns
the input path -- BTRIODevicePresent, BT_KEY_BRIDGE force-override honored,
forced harness exempt.  One input system per mode: btinput on pod/dev
desktops, PadRIO on glass.  The mechmppr/mech4 bridge merges composed clean
(masters negate-once sign fix inside our device-gated bridge).  The D1 relay
keeps its own raw sockets by design (an alternative LAN wire; Steam and
relay are separate modes).

Verified post-merge: all 3 configs build; glass boots with [input] binding
engine standing down + PadRIO owning input (30 ticks); pod forced-walk
speedDemand=61.501 with btinput ACTIVE; 2-node loopback MP full 31/31
mission, 76/76 ticks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-18 17:46:41 -05:00
co-authored by Claude Fable 5
48 changed files with 7234 additions and 141 deletions
+144 -84
View File
@@ -155,6 +155,7 @@
#include <messmgr.hpp> // SubsystemMessageManager (task #7 consolidated delivery)
#include <mechweap.hpp> // MechWeapon::GetExplosionResourceID (per-round detonation)
#include <mislanch.hpp> // MissileLauncher::ClassDerivations (splash-radius gate)
#include "btinput.hpp" // the CONTROLS.MAP + XInput binding engine
#if !defined(PLAYER_HPP)
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
#endif
@@ -653,6 +654,8 @@ static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire butto
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
int gBTLookBehind = 0; // 'V' held: the pod's rear-view button (task #68)
float gBTTwistAxis = 0.0f; // Q/E torso-twist deflection (assisted-mode stick X)
float gBTElevAxis = 0.0f; // R/F torso-elevation (pitch aim, stick Y)
int gBTElevRecenter = 0; // X edge: zero the pitch axis
int gBTTorsoRecenter = 0; // 'X' edge: pulse the authentic torso recenter (mapper consumes)
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
@@ -2480,7 +2483,11 @@ void
if (s_replLog >= 1.0f)
{
s_replLog = 0.0f;
DEBUG_STREAM << "[repl] mech " << (long)GetEntityID()
// print host:local -- the (long) cast collapsed distinct
// hosts' mechs onto one number (EntityID::operator int
// returns localID only) and faked "missing replicants"
// during the first 4-pod session analysis (2026-07-18)
DEBUG_STREAM << "[repl] mech " << GetEntityID()
<< " pos=(" << localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.y << ", "
<< localOrigin.linearPosition.z << ")"
@@ -2507,6 +2514,28 @@ void
AuthenticGroundAndCollide(dt, preMovePos);
}
// SPEC AUDIT (BT_SPEC_LOG): once, after all subsystems are built + linked,
// dump each subsystem -> coolant loop (BTCoolingLoopFrame resolves the
// linked condenser's number, the manual's "Loop N"). Diffs the per-mech
// COOLANT LOOPS table vs the 1995 manual (structural, less tuning-prone).
if (isPlayerMech && getenv("BT_SPEC_LOG"))
{
static int s_specDumped = 0;
if (!s_specDumped && dt > 0.0001f)
{
s_specDumped = 1;
extern int BTCoolingLoopFrame(void *subsystem);
for (int i = 0; i < GetSubsystemCount(); ++i)
{
Subsystem *s = GetSubsystem(i);
if (s == 0) continue;
DEBUG_STREAM << "[spec] loop " << BTCoolingLoopFrame(s)
<< " <- " << (s->GetName() ? s->GetName() : "?")
<< "\n" << std::flush;
}
}
}
if (isPlayerMech && dt > 0.0001f && dt < 0.5f) // ignore zero / huge (stall) slices
{
// --- VIRTUAL CONTROLS (dev keyboard -> the pod's analog inputs) --------
@@ -2527,76 +2556,45 @@ void
const float kStickRate = 2.5f; // stick deflect per second
const float kStickCenterRate = 5.0f; // stick auto-center per second
// POLL the real key state. WndProc key messages are unusable: the
// POLL the input state. WndProc key messages are unusable: the
// engine keyboard reader (L4CTRL.cpp:1506) GetMessage()s every
// WM_KEYUP/WM_CHAR out of the queue for its key-command channel, so
// only KEYDOWNs ever reached the WndProc and key state latched on
// forever. GetAsyncKeyState is immune; the foreground guard keeps
// background typing from driving the mech.
// WM_KEYUP/WM_CHAR out of the queue for its key-command channel,
// so the binding engine polls GetAsyncKeyState (+ XInput); the
// foreground guard (BT_KEY_NOFOCUS override) lives inside it.
{
typedef short (__stdcall *AsyncFn)(int);
typedef void *(__stdcall *FgFn)(void);
typedef unsigned long (__stdcall *WtpFn)(void *, unsigned long *);
typedef unsigned long (__stdcall *PidFn)(void);
static AsyncFn pAsync = 0; static FgFn pFg = 0;
static WtpFn pWtp = 0; static PidFn pPid = 0;
if (pAsync == 0)
{
HMODULE u = GetModuleHandleA("user32.dll");
HMODULE k = GetModuleHandleA("kernel32.dll");
pAsync = (AsyncFn)GetProcAddress(u, "GetAsyncKeyState");
pFg = (FgFn)GetProcAddress(u, "GetForegroundWindow");
pWtp = (WtpFn)GetProcAddress(u, "GetWindowThreadProcessId");
pPid = (PidFn)GetProcAddress(k, "GetCurrentProcessId");
}
int focused = 0;
if (pFg && pWtp && pPid)
{
void *fg = pFg();
unsigned long fgPid = 0;
if (fg) pWtp(fg, &fgPid);
focused = (fgPid == pPid());
}
// TEST HOOK (BT_KEY_NOFOCUS=1): accept keys without foreground focus --
// automation harnesses (SendInput from a background shell) can't grant
// real foreground; interactive play never needs this.
static int sNoFocus = -1;
if (sNoFocus < 0) { const char *nf = getenv("BT_KEY_NOFOCUS"); sNoFocus = (nf && *nf == '1') ? 1 : 0; }
if (sNoFocus) focused = 1;
if (pAsync)
{
const int dn = 0x8000;
gBTDrive.keyFwd = focused && ((pAsync('W') | pAsync(0x26 /*VK_UP*/)) & dn) ? 1 : 0;
gBTDrive.keyBack = focused && ((pAsync('S') | pAsync(0x28 /*VK_DOWN*/)) & dn) ? 1 : 0;
gBTDrive.keyLeft = focused && ((pAsync('A') | pAsync(0x25 /*VK_LEFT*/)) & dn) ? 1 : 0;
gBTDrive.keyRight = focused && ((pAsync('D') | pAsync(0x27 /*VK_RIGHT*/)) & dn) ? 1 : 0;
// WEAPON GROUPS (task #43, KEYBOARD only per user): three fire
// channels like three pod buttons -- 1/SPACE = lasers, 2 = PPCs,
// 3/CTRL = missiles. (Interim; the authentic system is the
// ConfigureMappables/ChooseButton mapper channels.)
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
// The pod's 4TH fire button (Pinky, 0x45) -- previously
// unmapped on desktop, so any weapon the authored groups
// put there (the Avatar's NARC etc.) was unreachable.
gBTPinkyKey = focused && (pAsync('4') & dn) ? 1 : 0;
// task #6: HOLD 'G' opens the config session on the selected
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
// held, the fire keys TOGGLE its group membership.
gBTConfigKey = focused && (pAsync('G') & dn) ? 1 : 0;
// task #12: F5..F8 assign the selected weapon to Generator
// A..D; F9 toggles Manual/Auto reconnect.
gBTGenSelKey =
(focused && (pAsync(0x74 /*F5*/) & dn)) ? 4
: (focused && (pAsync(0x75 /*F6*/) & dn)) ? 5
: (focused && (pAsync(0x76 /*F7*/) & dn)) ? 6
: (focused && (pAsync(0x77 /*F8*/) & dn)) ? 7
: (focused && (pAsync(0x78 /*F9*/) & dn)) ? 8
: 0;
// task #13: 'C' cycles the coolant valve (BT_VALVE_SLOT
// picks the condenser roster slot; default = Condenser1).
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
// INPUT REMAP (2026-07-18): the raw GetAsyncKeyState reads
// moved behind the binding engine (btinput.cpp) -- keys +
// XInput pad resolve through content/CONTROLS.MAP (built-in
// WASD-classic profile when the file is absent). RIO lamp
// buttons / keypads / authentic pckey hotkeys are emitted
// into the controls manager inside the poll; the fire
// channels, actions, and analog demands publish through
// gBTInput. Everything below the reads (the lever/stick/
// twist integrators, detents, edge latches) is UNCHANGED.
BTInputPoll(dt);
gBTDrive.keyFwd = gBTInput.leverRate > 0.0001f ? 1 : 0;
gBTDrive.keyBack = gBTInput.leverRate < -0.0001f ? 1 : 0;
gBTDrive.keyLeft = (gBTInput.turnActive && gBTInput.turnTarget < 0.0f) ? 1 : 0;
gBTDrive.keyRight = (gBTInput.turnActive && gBTInput.turnTarget > 0.0f) ? 1 : 0;
// WEAPON GROUPS (task #43): the four pod joystick fire
// buttons -> the bring-up fire channels (0x40 Main = laser
// group, 0x46 ThumbLow = PPCs, 0x47 ThumbHigh = missiles,
// 0x45 Pinky = the 4th group). (Interim; the authentic
// system is the ConfigureMappables/ChooseButton mapper.)
gBTLaserKey = gBTInput.fireTrigger;
gBTPPCKey = gBTInput.fireThumbLow;
gBTMissileKey = gBTInput.fireThumbHigh;
gBTPinkyKey = gBTInput.firePinky;
// task #6: HOLD the ConfigHold action opens the config
// session on the selected weapon; while held, the fire
// keys TOGGLE its group membership.
gBTConfigKey = gBTInput.configHold;
// task #12: Generator1-4 actions assign the selected weapon
// to Generator A..D; Reconnect toggles Manual/Auto.
gBTGenSelKey = gBTInput.genSel;
// task #13: the Valve action cycles the coolant valve.
gBTValveKey = gBTInput.valve;
// TORSO CONTROLS (2026-07-13): 'M' cycles the control mode
// (Basic -> Standard -> Veteran -- the pod console button,
// CycleControlModeMessageHandler); Q/E deflect the torso
@@ -2604,19 +2602,23 @@ void
// become the pedals). Ramped like the turn stick.
{
static int sPrevM = 0;
const int mNow = focused && (pAsync('M') & dn) ? 1 : 0;
const int mNow = gBTInput.modeCycle;
if (mNow && !sPrevM) gBTModeCycle = 1; // edge -> one cycle
sPrevM = mNow;
// (task #68) 'V' HELD = look behind (the pod's rear-view
// button; releases back to the forward view). Rear-mounted
// (task #68) LookBehind action HELD = the pod's rear-view
// button (releases back to the forward view). Rear-mounted
// weapons (blackhawk/owens back racks) fire only in it.
gBTLookBehind = focused && (pAsync('V') & dn) ? 1 : 0;
const int tw = (focused && (pAsync('E') & dn) ? 1 : 0)
- (focused && (pAsync('Q') & dn) ? 1 : 0);
gBTLookBehind = gBTInput.lookBehind;
static float sTwist = 0.0f;
if (tw != 0)
if (gBTInput.twistAbsolute)
{
sTwist += tw * kStickRate * dt;
// pad stick: the axis IS the stick position (the
// spring is in the physical stick, not software)
sTwist = gBTInput.twistTarget;
}
else if (gBTInput.twistActive)
{
sTwist += gBTInput.twistTarget * kStickRate * dt;
if (sTwist > 1.0f) sTwist = 1.0f;
if (sTwist < -1.0f) sTwist = -1.0f;
}
@@ -2642,26 +2644,76 @@ void
// edge detector from the drive all-stop below -- both
// fire on the same press.
static int sPrevXT = 0;
const int xtNow = focused && (pAsync('X') & dn) ? 1 : 0;
const int xtNow = gBTInput.allStop;
if (xtNow && !sPrevXT)
{
sTwist = 0.0f;
gBTTorsoRecenter = 1; // mapper consumes -> CommandRecenter()
{
extern int gBTElevRecenter;
gBTElevRecenter = 1; // pitch axis zeroes below
}
}
sPrevXT = xtNow;
gBTTwistAxis = sTwist;
// TORSO ELEVATION (pitch aim -- the pod stick's Y
// axis; every 1995 control mode routes it to
// Torso::SetAnalogElevationAxis). Same integrator
// model as the twist: keys walk the axis, release
// springs the RATE to 0 (position holds, the sim's
// vertical limits clamp); a pad stick is absolute.
{
extern float gBTElevAxis;
static float sElev = 0.0f;
if (gBTInput.elevAbsolute)
{
sElev = gBTInput.elevTarget;
}
else if (gBTInput.elevActive)
{
sElev += gBTInput.elevTarget * kStickRate * dt;
if (sElev > 1.0f) sElev = 1.0f;
if (sElev < -1.0f) sElev = -1.0f;
}
else if (sElev != 0.0f)
{
const float estep = kStickCenterRate * dt;
if (sElev > estep) sElev -= estep;
else if (sElev < -estep) sElev += estep;
else sElev = 0.0f;
}
extern int gBTElevRecenter; // set by the X edge above
if (gBTElevRecenter)
{
gBTElevRecenter = 0;
sElev = 0.0f; // X recenters pitch too
}
gBTElevAxis = sElev;
if (getenv("BT_INPUT_LOG"))
{
static float s_eacc = 0.0f; s_eacc += dt;
if (s_eacc >= 1.0f) { s_eacc = 0.0f;
DEBUG_STREAM << "[input] elev tgt="
<< gBTInput.elevTarget << " act="
<< gBTInput.elevActive << " axis="
<< sElev << "\n" << std::flush; }
}
}
}
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// damage dispatcher + the beam-visual keepalive)
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
static int sPrevX = 0;
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
const int xNow = gBTInput.allStop;
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
sPrevX = xNow;
// V: toggle the view between the authentic COCKPIT eyepoint
// (the pod's only view) and the external chase camera.
// ViewToggle action: the authentic COCKPIT eyepoint (the
// pod's only view) <-> the external chase camera. Split
// from LookBehind (2026-07-18): the old shim had BOTH on
// 'V', so holding rear-view also flipped the camera.
static int sPrevV = 0, sViewInside = 0;
const int vNow = focused && (pAsync('V') & dn) ? 1 : 0;
const int vNow = gBTInput.viewToggle;
if (vNow && !sPrevV)
{
sViewInside = !sViewInside;
@@ -2849,7 +2901,9 @@ void
}
}
}
float sweep = ((fwd ? 1.0f : 0.0f) - (back ? 1.0f : 0.0f)) * kLeverRate * dt;
// the configured rate (CONTROLS.MAP `axis Throttle rate n`) IS the
// sweep speed; kLeverRate above stays as the documented default
float sweep = gBTInput.leverRate * dt;
if (sweep != 0.0f && !sDetent)
{
const float prev = sLever;
@@ -2866,8 +2920,13 @@ void
if (sLever < -1.0f) sLever = -1.0f;
}
const float want = (gBTDrive.keyRight ? 1.0f : 0.0f) - (gBTDrive.keyLeft ? 1.0f : 0.0f);
if (want != 0.0f)
const float want = gBTInput.turnActive ? gBTInput.turnTarget : 0.0f;
if (gBTInput.turnAbsolute)
{
// pad stick: the axis IS the stick position (physically sprung)
sStick = want;
}
else if (want != 0.0f)
{
sStick += want * kStickRate * dt;
if (sStick > 1.0f) sStick = 1.0f;
@@ -3184,6 +3243,7 @@ void
<< " pre=" << preThrottle
<< " rev=" << mppr->reverseThrust
<< " stickX=" << mppr->stickPosition.x
<< " stickY=" << mppr->stickPosition.y
<< " -> speedDemand=" << mppr->speedDemand
<< " turnDemand=" << mppr->turnDemand
<< " mode=" << mppr->controlMode