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:
+9
-3
@@ -18,6 +18,7 @@
|
||||
// #define BT_HEAPCHECK // enable to validate the heap on every alloc (heap-corruption hunt)
|
||||
#include <windows.h>
|
||||
#include <shellapi.h> // CommandLineToArgvW
|
||||
#include <btversion.h> // generated: 4.11.<commit count> (<hash>)
|
||||
#include <crtdbg.h> // _CrtSetDbgFlag (heap validation, gated by BT_HEAPCHECK)
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -448,7 +449,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
std::cout << "BattleTech v4.10 (reconstructed port)" << std::endl << std::flush;
|
||||
// Version scheme: 4.10 = the 1995 arcade release; 4.11 = this win32
|
||||
// reconstruction; build = git commit count, hash pins exact source
|
||||
// ('+' = built from an uncommitted tree). Stamped by tools/btversion.cmake.
|
||||
std::cout << "BattleTech " << BT_VERSION_FULL
|
||||
<< " (win32 reconstruction of Tesla 4.10)" << std::endl << std::flush;
|
||||
|
||||
// CPU pin (timing stability). BT_AFFINITY overrides the mask; =0 disables --
|
||||
// required for multi-instance runs (two instances pinned to core 0 starve
|
||||
@@ -550,9 +555,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
if (np != 0)
|
||||
sscanf(np + 4, " %d", &netPort);
|
||||
if (netPort > 0)
|
||||
swprintf(winTitle, 64, L"BattleTech \x2014 node %d", netPort);
|
||||
swprintf(winTitle, 64, L"BattleTech %S \x2014 node %d",
|
||||
BT_VERSION_STRING, netPort);
|
||||
else
|
||||
wcscpy(winTitle, L"BattleTech");
|
||||
swprintf(winTitle, 64, L"BattleTech %S", BT_VERSION_STRING);
|
||||
}
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", winTitle, WS_OVERLAPPEDWINDOW,
|
||||
0, 0, wr.right - wr.left, wr.bottom - wr.top,
|
||||
|
||||
@@ -247,6 +247,39 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// BT port diag (BT_CAM_LOG, 1 Hz): why is/isn't the director tracking
|
||||
if (getenv("BT_CAM_LOG"))
|
||||
{
|
||||
static float s_acc = 0.0f; s_acc += time_slice;
|
||||
if (s_acc >= 1.0f)
|
||||
{
|
||||
s_acc = 0.0f;
|
||||
DEBUG_STREAM << "[cam] director pick: player=" << (void*)player
|
||||
<< " vehicle=" << (void*)vehicle
|
||||
<< " goal=" << (void*)GetGoalEntity()
|
||||
<< " timer=" << timeLeftOnPlayer;
|
||||
// who IS in the Players group here?
|
||||
EntityGroup *pg =
|
||||
application->GetEntityManager()->FindGroup("Players");
|
||||
if (pg)
|
||||
{
|
||||
DEBUG_STREAM << " group:[";
|
||||
Player *p;
|
||||
ChainIteratorOf<Node*> it(pg->groupMembers);
|
||||
while ((p = (Player *)it.ReadAndNext()) != NULL)
|
||||
{
|
||||
DEBUG_STREAM << " bmp=" << p->playerBitmapIndex
|
||||
<< (p->GetPlayerVehicle() ? "+veh" : "-veh");
|
||||
}
|
||||
DEBUG_STREAM << " ]";
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << " group:MISSING";
|
||||
}
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (player == 0 || vehicle == 0)
|
||||
{
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
//#############################################################################
|
||||
// btinput.hpp -- the desktop input binding engine (CONTROLS.MAP + XInput).
|
||||
//
|
||||
// The pod's controls are a fixed physical device set (stick, throttle lever,
|
||||
// pedals, ~44 lamp buttons, keypads) reaching the game through the
|
||||
// LBE4ControlsManager push model. On a desktop there is no RIO, so this
|
||||
// engine maps PC keys and an XInput gamepad onto the SAME channels:
|
||||
//
|
||||
// axes -> the virtual-controls integrators in mech4.cpp (lever / turn
|
||||
// stick / torso twist), exactly where the BT_KEY_BRIDGE writes
|
||||
// the analog values the RIO would have produced
|
||||
// buttons -> real buttonGroup[addr] ForceUpdates (aux banks, hotbox,
|
||||
// panic, throttle-head...) so the AUTHENTIC handlers fire;
|
||||
// the four joystick fire buttons (0x40/45/46/47) drive the
|
||||
// bring-up fire channels instead (see btinput.cpp note)
|
||||
// keypads -> keyboardGroup[KeyboardPilot/KeyboardExternal] key values
|
||||
// pckey -> keyboardGroup[KeyboardPC] (any authentic '+'-style hotkey)
|
||||
// actions -> the port-side dev controls (view toggle, all-stop, ...)
|
||||
//
|
||||
// Bindings load from content/CONTROLS.MAP at first poll; if the file is
|
||||
// absent the compiled-in WASD-classic profile (identical text) applies.
|
||||
// Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/-KEYUP
|
||||
// keyboard feed (BTInputSuppressKey), ending the historic double-dispatch
|
||||
// (W = drive AND pilot-select, F5 keyup aliasing to 't' = pilot 3, ...).
|
||||
//#############################################################################
|
||||
#ifndef BTINPUT_HPP
|
||||
#define BTINPUT_HPP
|
||||
|
||||
struct BTInputState
|
||||
{
|
||||
// analog demands (consumed by the mech4 virtual-controls block)
|
||||
float leverRate; // throttle lever sweep, per second (+fwd/-back)
|
||||
float turnTarget; // turn-stick deflection target [-1,1]
|
||||
int turnActive; // any turn input held (else spring-center)
|
||||
float twistTarget; // torso-twist deflection target [-1,1]
|
||||
int twistActive;
|
||||
float elevTarget; // torso-elevation (pitch) target [-1,1]
|
||||
int elevActive;
|
||||
int turnAbsolute; // pad stick overrides the integrator outright
|
||||
int twistAbsolute;
|
||||
int elevAbsolute;
|
||||
|
||||
// the four pod joystick fire buttons (levels)
|
||||
int fireTrigger; // 0x40 Main
|
||||
int firePinky; // 0x45
|
||||
int fireThumbLow; // 0x46 Middle
|
||||
int fireThumbHigh; // 0x47 Upper
|
||||
|
||||
// port-side actions (levels; consumers keep their own edge detectors)
|
||||
int viewToggle;
|
||||
int lookBehind;
|
||||
int allStop;
|
||||
int modeCycle;
|
||||
int valve;
|
||||
int configHold;
|
||||
int genSel; // 0 = none, 4..7 = Generator A..D, 8 = reconnect
|
||||
};
|
||||
|
||||
extern BTInputState gBTInput;
|
||||
|
||||
// Poll everything (keys + pad), evaluate the binding table, fire button /
|
||||
// keypad edges into the controls manager. Call ONCE per sim frame from the
|
||||
// player drive block. Handles the foreground-focus guard (BT_KEY_NOFOCUS)
|
||||
// internally.
|
||||
void BTInputPoll(float dt);
|
||||
|
||||
// The legacy keyboard feed asks before dispatching a typed character /
|
||||
// key-up value: nonzero = this key is claimed by a binding, swallow it.
|
||||
extern "C" int BTInputSuppressKey(unsigned int key_value, int is_char);
|
||||
|
||||
#endif // BTINPUT_HPP
|
||||
@@ -1074,6 +1074,10 @@ void
|
||||
|
||||
playerVehicle->Dispatch(&player_link_message);
|
||||
playerVehicle->DispatchToReplicants(&player_link_message);
|
||||
if (getenv("BT_CAM_LOG"))
|
||||
DEBUG_STREAM << "[cam] InitializePlayerLink dispatched (player "
|
||||
<< GetEntityID() << " -> vehicle "
|
||||
<< playerVehicle->GetEntityID() << ")\n" << std::flush;
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
|
||||
@@ -546,6 +546,14 @@ Reservoir::Reservoir(
|
||||
// *this = &PTR_LAB_0050ecd4;
|
||||
squirtEfficiency = 0.5f; // this[0x8b] @0x22C
|
||||
thermalCapacity = subsystem_resource->coolantCapacity; // this[0x4a] @0x128 (coolantCapacity reuses thermalCapacity)
|
||||
|
||||
// SPEC AUDIT (BT_SPEC_LOG): streamed CoolantCapacity vs the manual's
|
||||
// "Reservoir Size (Liters)".
|
||||
if (getenv("BT_SPEC_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[spec] reservoir: coolantCapacity="
|
||||
<< subsystem_resource->coolantCapacity << "\n" << std::flush;
|
||||
}
|
||||
coolantSquirtMass = subsystem_resource->coolantSquirtMass; // this[0x89] @0x224 = res +0x100
|
||||
coolantLevel = thermalCapacity; // this[0x4b] @0x12C
|
||||
coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57")
|
||||
@@ -765,6 +773,23 @@ AggregateHeatSink::AttributeIndexSet&
|
||||
return attributeIndex;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// BTSetBankAmbientTemperature -- bridge for Mech::PlayerLinkMessageHandler
|
||||
// (@0049f624 writes bank+0x1d4 = mission temperature): mech.cpp cannot see
|
||||
// AggregateHeatSink (local-stub collision), so the write lands here where the
|
||||
// type is complete. This is the AUTHENTIC ambientTemperature writer -- the
|
||||
// old "frozen 300" note stands corrected (the ctor default only holds until
|
||||
// the mission's PlayerLink pass runs on the master).
|
||||
//
|
||||
void
|
||||
BTSetBankAmbientTemperature(Subsystem *bank, Scalar temperature)
|
||||
{
|
||||
if (bank != NULL)
|
||||
{
|
||||
((AggregateHeatSink *)bank)->ambientTemperature = temperature;
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// Shared Data Support
|
||||
//
|
||||
@@ -806,6 +831,14 @@ AggregateHeatSink::AggregateHeatSink(
|
||||
heatSinkCount = subsystem_resource->heatSinkCount; // this[0x74] @0x1D0 = res +0xFC
|
||||
ambientTemperature = 300.0f; // this[0x75] @0x1D4 (_DAT_004ae89c)
|
||||
|
||||
// SPEC AUDIT (BT_SPEC_LOG): the streamed HeatSinkCount vs the manual's
|
||||
// "Number of Heat Sinks" per-mech stat.
|
||||
if (getenv("BT_SPEC_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[spec] heatsinks: count=" << heatSinkCount
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
// task #9 (the ambient radiator lands): the binary ctor @4ae8d0 scales the
|
||||
// bank's conductance by 0.1 x HeatSinkCount (_DAT_004ae974 float80 = 0.1
|
||||
// byte-verified; madcat count 14 -> x1.4) and installs the RADIATOR
|
||||
|
||||
@@ -80,6 +80,12 @@
|
||||
// for the same reason.
|
||||
#include "dmgtable.hpp"
|
||||
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
|
||||
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
|
||||
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
|
||||
extern void BTSetBankAmbientTemperature(Subsystem *bank, Scalar temperature);
|
||||
#if !defined(BTMSSN_HPP)
|
||||
# include <btmssn.hpp> // BTMission::GetMissionTemperature (PlayerLink)
|
||||
#endif
|
||||
#if !defined(APP_HPP)
|
||||
# include <app.hpp>
|
||||
#endif
|
||||
@@ -454,6 +460,7 @@ const Receiver::HandlerEntry
|
||||
Mech::MessageHandlerEntries[] =
|
||||
{
|
||||
MESSAGE_ENTRY(Mech, TakeDamage),
|
||||
MESSAGE_ENTRY(Mech, PlayerLink),
|
||||
};
|
||||
|
||||
Receiver::MessageHandlerSet
|
||||
@@ -609,6 +616,52 @@ Scalar
|
||||
return tmpl ? tmpl->maxY : 0.0f; // ResolveHit guards <= 0
|
||||
}
|
||||
|
||||
//
|
||||
// Mech override of Entity::PlayerLinkMessageHandler (binary @0049f624). The
|
||||
// engine base resolves the FORWARD link (mech->playerLink @0x190 from the
|
||||
// message's playerID); BT then:
|
||||
// 1. sets the REVERSE link player->playerVehicle = this -- on EVERY node,
|
||||
// replicants included. This is how the 1995 camera director
|
||||
// (BeABTDirector's GetPlayerByIndex -> GetPlayerVehicle) and the
|
||||
// scoreboard find a REMOTE player's mech; without it a spectator seat
|
||||
// parks forever ("no goal entity", camera-seat bring-up 2026-07-18).
|
||||
// 2. clears the player's NonScoringPlayerFlag (0x4000 == bit 14): a pilot
|
||||
// with a vehicle is a SCORING player -- this is what admits them to the
|
||||
// ranking pass (Player::IsScoringPlayer gates RankPlayers).
|
||||
// 3. master only: seeds the heat bank's ambientTemperature (@0x1d4 on the
|
||||
// 0xBBE AggregateHeatSink at mech+0x7dc) from the mission's [mission]
|
||||
// temperature (BTMission+0xf4) -- resolving the heat family's
|
||||
// "frozen 300" deviation note: it was never frozen, THIS writes it.
|
||||
//
|
||||
void
|
||||
Mech::PlayerLinkMessageHandler(PlayerLinkMessage *message)
|
||||
{
|
||||
Check(message);
|
||||
Entity::PlayerLinkMessageHandler(message); // FUN_0041fd18: mech+0x190
|
||||
|
||||
Player *owner = GetPlayerLink();
|
||||
if (getenv("BT_CAM_LOG"))
|
||||
DEBUG_STREAM << "[cam] Mech::PlayerLink inst="
|
||||
<< (int)(GetInstance() == ReplicantInstance)
|
||||
<< " owner=" << (void*)owner << "\n" << std::flush;
|
||||
if (owner != NULL)
|
||||
{
|
||||
owner->SetPlayerVehicle(this); // player+0x1fc = this
|
||||
owner->simulationFlags &= ~Player::NonScoringPlayerFlag; // &= ~0x4000
|
||||
|
||||
if (GetInstance() != ReplicantInstance && sensorSubsystem != NULL)
|
||||
{
|
||||
Mission *mission = owner->GetMission();
|
||||
if (mission != NULL)
|
||||
{
|
||||
BTSetBankAmbientTemperature(sensorSubsystem,
|
||||
((BTMission *)mission)->GetMissionTemperature());
|
||||
}
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
// Mech override of Entity::TakeDamageMessageHandler (binary @0x4a037a, the two
|
||||
// call sites into the glue @0x49ed0c). An unaimed hit arrives with
|
||||
|
||||
@@ -1165,6 +1165,14 @@ protected:
|
||||
// Mech override of Entity::TakeDamageMessageHandler: resolve an unaimed
|
||||
// (invalidDamageZone) hit's zone via the cylinder table, then base-route.
|
||||
void TakeDamageMessageHandler(TakeDamageMessage *message);
|
||||
// Mech override of Entity::PlayerLinkMessageHandler (@0049f624): the
|
||||
// base resolves mech->player; BT adds the REVERSE link
|
||||
// player->playerVehicle = this on EVERY node (replicants included --
|
||||
// this is how the camera director / scoreboard find a remote player's
|
||||
// mech), clears the player's NonScoringPlayerFlag (a pilot with a
|
||||
// vehicle SCORES/ranks), and on the master seeds the heat bank's
|
||||
// ambientTemperature from the mission's [mission] temperature.
|
||||
void PlayerLinkMessageHandler(PlayerLinkMessage *message);
|
||||
|
||||
// --- damage-routing support (mechdmg / mech4) -----------------------
|
||||
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
|
||||
|
||||
+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
|
||||
|
||||
@@ -641,7 +641,13 @@ void
|
||||
{
|
||||
float key_throttle = gBTDrive.forced
|
||||
? gBTDrive.forcedThrottle : gBTDrive.throttle;
|
||||
float key_turn = gBTDrive.forced ? 0.0f : gBTDrive.turn;
|
||||
// PORT SIGN (user live-verified 2026-07-18: left arrow turned the
|
||||
// mech RIGHT): the sim's positive turnDemand is CCW (LEFT) -- math
|
||||
// convention, same as the torso twist. On the pod the RIO Ranger
|
||||
// owned the hardware sign; the desktop bridge must negate so that
|
||||
// input-right (positive) = turn RIGHT. Forced/BT_GOTO harness
|
||||
// demands are sim-frame already and stay un-negated.
|
||||
float key_turn = gBTDrive.forced ? 0.0f : -gBTDrive.turn;
|
||||
// Headless harness (forced mode only): BT_FORCE_TURN holds a steering
|
||||
// demand; BT_FORCE_SECONDS releases the forced throttle after n
|
||||
// sim-seconds while KEEPING the turn (the turn-in-place repro).
|
||||
@@ -760,6 +766,14 @@ void
|
||||
gBTModeCycle = 0;
|
||||
CycleControlModeNow();
|
||||
}
|
||||
// TORSO ELEVATION (pitch): the pod stick's Y axis -- every
|
||||
// control mode routes stickPosition.y into
|
||||
// Torso::SetAnalogElevationAxis below, so the bridge feeds
|
||||
// it in BOTH branches (keys R/F or a pad stick via btinput).
|
||||
{
|
||||
extern float gBTElevAxis;
|
||||
stickPosition.y = gBTElevAxis;
|
||||
}
|
||||
if (controlMode == BasicMode)
|
||||
{
|
||||
stickPosition.x = key_turn;
|
||||
@@ -767,7 +781,14 @@ void
|
||||
}
|
||||
else
|
||||
{
|
||||
stickPosition.x = gBTTwistAxis; // the torso axis
|
||||
// PORT SIGN: positive twist is CCW (left) like the turn;
|
||||
// negate so stick/E-key right = torso right (manual p8:
|
||||
// "pulling your joystick to the right torso twists your
|
||||
// 'Mech to the right"). The earlier "MadCat twist was
|
||||
// correct" call was based on the same misread screenshot
|
||||
// forensics as the turn -- the user's live left-arrow
|
||||
// test invalidated both.
|
||||
stickPosition.x = -gBTTwistAxis; // the torso axis
|
||||
pedalsPosition = key_turn; // A/D = the pedals
|
||||
// 'X' recenter pulse (2026-07-13): one-frame centerCommand
|
||||
// -> the sim arms recenterActive and Recenter (@004b6918)
|
||||
@@ -792,7 +813,8 @@ void
|
||||
}
|
||||
}
|
||||
}
|
||||
stickPosition.y = 0.0f;
|
||||
// (stickPosition.y no longer zeroed here -- the bridge above
|
||||
// feeds the elevation axis every bridged frame, 2026-07-18)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +885,18 @@ void
|
||||
{
|
||||
torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4 (500)
|
||||
torso->SetAnalogTwistAxis(0.0f); // raw torso+0x1f0
|
||||
if (getenv("BT_INPUT_LOG"))
|
||||
{
|
||||
static float s_eacc2 = 0.0f; s_eacc2 += time_slice;
|
||||
if (s_eacc2 >= 0.5f) { s_eacc2 = 0.0f;
|
||||
YawPitchRoll ypr;
|
||||
ypr = mech->localOrigin.angularPosition;
|
||||
DEBUG_STREAM << "[input] mppr elev in=" << stick_y
|
||||
<< " torsoElev=" << torso->CurrentElevation()
|
||||
<< " vel=" << torso->ElevationVelocity()
|
||||
<< " mechYaw=" << (Scalar)ypr.yaw
|
||||
<< "\n" << std::flush; }
|
||||
}
|
||||
}
|
||||
if (cockpit)
|
||||
{
|
||||
@@ -887,6 +921,8 @@ void
|
||||
//
|
||||
if (torso == 0 || !torso->GetHorizontalEnabled()) // raw torso+0x250 == 0
|
||||
{
|
||||
// (sign handled ONCE at the key bridge: positive sim twist/slew
|
||||
// = CCW/left; the bridge negates so input-right = right)
|
||||
if (cockpit) cockpit->SetFreeAimSlew(stick_x); // raw cockpit+0x28c
|
||||
}
|
||||
else
|
||||
@@ -894,6 +930,14 @@ void
|
||||
torso->SetAnalogTwistAxis(stick_x); // raw torso+0x1f0
|
||||
}
|
||||
if (torso) torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4
|
||||
if (torso && getenv("BT_INPUT_LOG"))
|
||||
{
|
||||
static float s_tacc = 0.0f; s_tacc += time_slice;
|
||||
if (s_tacc >= 0.5f) { s_tacc = 0.0f;
|
||||
DEBUG_STREAM << "[input] twist in=" << stick_x
|
||||
<< " currentTwist=" << torso->CurrentTwist()
|
||||
<< "\n" << std::flush; }
|
||||
}
|
||||
|
||||
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
|
||||
|
||||
@@ -914,7 +958,7 @@ void
|
||||
//
|
||||
if (torso == 0 || !torso->GetHorizontalEnabled())
|
||||
{
|
||||
if (cockpit) cockpit->SetFreeAimSlew(stick_x);
|
||||
if (cockpit) cockpit->SetFreeAimSlew(stick_x); // (sign at the bridge)
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -244,6 +244,24 @@ Torso::Torso(
|
||||
elevationCenter = verticalLimitTop; // @0x228 = @0x220
|
||||
elevationHalfBottom = verticalLimitBottom * 0.5f; // @0x22C (_DAT_004b6fbc)
|
||||
|
||||
// SPEC AUDIT (BT_SPEC_LOG, 2026-07-18): dump the streamed torso config so
|
||||
// it can be diffed against the 1995 manual's per-mech stat sheet
|
||||
// ("Torso speed (deg/sec)" / "Torso Limit (degrees)"). Values printed in
|
||||
// DEGREES (the manual's units); horizontalRotationPerSecond IS deg/sec in
|
||||
// the resource, the limits are stored in degrees pre-DegToRad.
|
||||
if (getenv("BT_SPEC_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[spec] torso: speed=" << r->horizontalRotationPerSecond
|
||||
<< " deg/s limitL=" << r->horizontalLimitLeft
|
||||
<< " limitR=" << r->horizontalLimitRight
|
||||
<< " (arc=" << (r->horizontalLimitLeft - r->horizontalLimitRight)
|
||||
<< ") vRate=" << r->verticalRotationPerSecond
|
||||
<< " vTop=" << r->verticalLimitTop
|
||||
<< " vBot=" << r->verticalLimitBottom
|
||||
<< " hEnabled=" << (int)r->torsoHorizontalEnabled
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
buttonRampActive = 0; // @0x268
|
||||
buttonRamp = 0.0f;// @0x26C
|
||||
|
||||
|
||||
@@ -171,6 +171,10 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
// Address of the twist scalar (task #56: the binary bt_mech stream tail
|
||||
// re-points gyro+0x258 to torso+0x1D8; the gyro damage-response reads it).
|
||||
Scalar* CurrentTwistAddr() { return ¤tTwist; }
|
||||
// Current vertical (pitch) torso aim in radians (binary torso+0x1e4) --
|
||||
// elevation-axis bring-up diagnostics (2026-07-18).
|
||||
Scalar CurrentElevation() const { return currentElevation; }
|
||||
Scalar ElevationVelocity() const { return elevationVelocity; }
|
||||
protected:
|
||||
// Resolve a named skeleton node to its live Joint* via the owning Mech
|
||||
// (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined
|
||||
|
||||
Reference in New Issue
Block a user