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:
+144
-84
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user