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
+10
View File
@@ -580,6 +580,16 @@ void
camera_stream.AdvancePointer(model->instanceSize);
}
res->Unlock();
// BT port diag (BT_CAM_LOG): the camera-seat bring-up needs to know the
// network actually loaded (type-27 res, all 8 maps ship in BTL4.RES)
if (getenv("BT_CAM_LOG"))
{
int n = 0;
SChainIteratorOf<CameraInstance*> it(cameraList);
while (it.ReadAndNext() != NULL) ++n;
DEBUG_STREAM << "[cam] streamed camera network '" << map_name
<< "': " << n << " camera(s) loaded\n" << std::flush;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+19
View File
@@ -314,11 +314,30 @@ void
//
if (!goalEntity)
{
// BT port diag (BT_CAM_LOG, 1 Hz): parked with no goal
if (getenv("BT_CAM_LOG"))
{
static float s_ng = 0.0f; s_ng += time_slice;
if (s_ng >= 1.0f) { s_ng = 0.0f;
DEBUG_STREAM << "[cam] ship: NO goal entity (parked)\n"
<< std::flush; }
}
return;
}
Check(goalEntity);
Point3D target;
target.Multiply(focusOffset, goalEntity->localToWorld);
// BT port diag (BT_CAM_LOG, 1 Hz): live follow state
if (getenv("BT_CAM_LOG"))
{
static float s_fg = 0.0f; s_fg += time_slice;
if (s_fg >= 1.0f) { s_fg = 0.0f;
DEBUG_STREAM << "[cam] ship: goal at (" << target.x << ","
<< target.z << ") cam=" << (void*)currentCamera
<< " sees=" << (currentCamera
? (int)currentCamera->CanCameraSee(target) : -1)
<< "\n" << std::flush; }
}
//
//------------------------------------------------------------------------
+14
View File
@@ -11,6 +11,15 @@
#include "l4padrio.h"
#endif
//
// BT port (input remap): keys claimed by a CONTROLS.MAP binding are consumed
// by the binding engine (game/reconstructed/btinput.cpp) and must NOT also
// reach the key-command channel -- that was the historic double-dispatch
// ('w' drove AND selected pilot 0; F5's key-up value 0x74 aliased to the
// 't' hotkey). The symbol resolves from the game lib at the btl4 exe link.
//
extern "C" int BTInputSuppressKey(unsigned int key_value, int is_char);
// #define LOCAL_TEST
#if defined(LOCAL_TEST)
@@ -1572,6 +1581,11 @@ void
{
// swallowed: navigation key, not a character command
}
else if (BTInputSuppressKey((unsigned int)keyValue,
msg.message == WM_CHAR))
{
// swallowed: remapped by CONTROLS.MAP
}
else
{
keyboardGroup[KeyboardPC].ForceUpdate(&keyValue, mode_mask);
File diff suppressed because it is too large Load Diff
+63
View File
@@ -327,6 +327,50 @@ private:
DroppedMessageHostSocket *dropped_message_host_socket
);
//
// D1 RELAY MODE (BT_RELAY=host:port + BT_SELF=<[pilots] entry>): outbound-
// only topology -- the pod dials the relay (extended btconsole.py) for BOTH
// the console protocol (egg/launch; legacy raw frames, direction flipped)
// and the game traffic (envelope-framed, one TCP connection multiplexing
// every peer; UDP for the unreliable channel in a later phase). BT_RELAY
// unset => relayMode False => every relay branch is dead and the classic
// mesh is untouched.
//
Logical
RelayDiscover(SOCKADDR_IN *console_endpoint);
Logical
RelayRequestSeat();
SOCKET
RelayDialTcp(const SOCKADDR_IN &endpoint, int timeout_seconds);
Logical
RelaySendAll(SOCKET sock, const char *buffer, int length);
NetworkPacket *
BuildRelayPacket(Message *message, ClientID client, int *packet_size);
Logical
RelaySendFrame(int route, const NetworkPacket *packet, int packet_size);
void
ConnectRelayGame(HostID local_host_ID);
void
RelayGameDown(const char *why);
Logical
CheckRelay(NetworkPacket *network_packet);
//
// UDP unreliable channel (restores the 1995 NETNUB reliable/unreliable
// split): the ~60Hz update records ride UDP so a lost/late datagram is
// dropped (dead reckoning absorbs the gap) instead of head-of-line-
// blocking the reliable stream. Reliable traffic + control stay on TCP.
//
void
ConnectRelayUdp(HostID local_host_ID);
Logical
RelayUdpSendFrame(int route, const NetworkPacket *packet, int packet_size);
void
RelayUdpKeepalive(HostID local_host_ID);
Logical
RelayShouldUseUdp(const Message *message) const;
Logical
CheckRelayUdp(NetworkPacket *network_packet);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Private Data
//
@@ -353,6 +397,25 @@ private:
addrinfo* thisHost;
SOCKET gameListenerSocket;
SOCKET consoleListenerSocket;
// D1 relay mode state (see the private-methods block above). All inert
// unless BT_RELAY is set.
Logical relayMode;
char relaySelf[64]; // BT_SELF -- our [pilots] entry string
SOCKADDR_IN relayConsoleAddress; // relay console endpoint (BT_RELAY port)
SOCKADDR_IN relayGameAddress; // relay game endpoint (port + 1, tcp+udp)
SOCKET relayGameSocket; // the ONE multiplexed game connection
SOCKET relayUdpSocket; // unreliable channel (later phase)
char relayPad[16384]; // relay-connection reassembly buffer
int relayPadTail;
Logical relayConsoleDialedOnce; // first dial gets the long timeout
Logical udpUp; // UDP HELLO acked by the relay
HostID relayLocalHostID; // our hostID (for UDP HELLO/keepalive/tx)
unsigned long lastUdpKeepaliveTick;
unsigned long lastUdpHelloTick; // HELLO retry until acked
unsigned long udpTxSeq;
unsigned long udpRxSeq[64]; // per-fromHost last-seen sequence
NetworkMode currentNetworkMode; // Reliable/Unreliable (Mode() stores)
};
//~~~~~~~~~~~~~~~~~~~~~~ L4NetworkManager inlines ~~~~~~~~~~~~~~~~~~~~~~~~~~
+31 -28
View File
@@ -7431,6 +7431,9 @@ void
Check_Pointer(display_rank_window);
mCamShipHUD = new CameraShipHUDRenderable(entity, CameraShipHUDRenderable::Dynamic, player_index, display_rank_window);
if (getenv("BT_CAM_LOG"))
DEBUG_STREAM << "[cam] CameraShipHUDRenderable created\n"
<< std::flush;
}
break;
}
@@ -8518,34 +8521,6 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
BTDrawTranslocationSpheres(mDevice, &viewTransform, (float)dT, mTargetRenderTime);
}
// DIAG (off by default): BT_SHOT=<path> dumps this instance's backbuffer (the rendered
// WORLD, before the 2D HUD overlay) to a PNG once ~180 frames in, for non-disruptive
// per-instance frame capture without foregrounding the window. Each instance sets its
// own path. Built for the -net render glitch (task #53) but general.
{
const char *shotPath = getenv("BT_SHOT");
if (shotPath)
{
static int s_btShotN = 0;
++s_btShotN;
// dump (overwriting) every 90 frames once past 90, so the file always holds a
// recent frame regardless of load timing / debug fps.
if (s_btShotN >= 90 && (s_btShotN % 90) == 0)
{
IDirect3DSurface9 *bb = 0;
if (SUCCEEDED(mDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb)) && bb)
{
HRESULT hr = D3DXSaveSurfaceToFileA(shotPath, D3DXIFF_PNG, bb, 0, 0);
bb->Release();
DEBUG_STREAM << "[btshot] frame " << s_btShotN << " -> " << shotPath
<< " hr=0x" << std::hex << (unsigned)hr << std::dec << "\n" << std::flush;
}
else
DEBUG_STREAM << "[btshot] GetBackBuffer FAILED frame " << s_btShotN << "\n" << std::flush;
}
}
}
// The BT targeting reticle / weapon pips (2D screen space, cockpit view
// only -- the dpl2d layer; see game/reconstructed/dpl2d.cpp).
{
@@ -8588,6 +8563,34 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
if (mCamShipHUD)
mCamShipHUD->Render(0, &viewTransform);
// DIAG (off by default): BT_SHOT=<path> dumps this instance's backbuffer to a
// PNG every 90 frames -- non-disruptive per-instance frame capture without
// foregrounding the window. Built for the -net render glitch (task #53);
// moved AFTER the 2D pass (2026-07-18, camera-seat bring-up) so captures
// include the HUD overlays (reticle, camera-ship ranking window) the screen
// actually shows.
{
const char *shotPath = getenv("BT_SHOT");
if (shotPath)
{
static int s_btShotN = 0;
++s_btShotN;
if (s_btShotN >= 90 && (s_btShotN % 90) == 0)
{
IDirect3DSurface9 *bb = 0;
if (SUCCEEDED(mDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb)) && bb)
{
HRESULT hr = D3DXSaveSurfaceToFileA(shotPath, D3DXIFF_PNG, bb, 0, 0);
bb->Release();
DEBUG_STREAM << "[btshot] frame " << s_btShotN << " -> " << shotPath
<< " hr=0x" << std::hex << (unsigned)hr << std::dec << "\n" << std::flush;
}
else
DEBUG_STREAM << "[btshot] GetBackBuffer FAILED frame " << s_btShotN << "\n" << std::flush;
}
}
}
// DEV-COMPOSITE: in DOCKED mode (BT_DEV_GAUGES_DOCK) blit the 6-surface gauge panel
// into this window as the LAST draw before EndScene (no-op otherwise / off pod).
extern void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device);
+1
View File
@@ -379,6 +379,7 @@ public:
void SetCurrentFogLimits(float fogNear, float fogFar);
LPDIRECT3DTEXTURE9 GetNameTexture(int playerIndex) { return mNameTextures[playerIndex]; }
LPDIRECT3DTEXTURE9 GetOrdinalTexture(int rank) { return mOrdinalTextures[rank]; }
inline LPDIRECT3DDEVICE9 GetDevice() { return mDevice; }
inline LPD3DXMATRIXSTACK GetMatrixStack() { return m_MatrixStack; }
+111 -4
View File
@@ -3045,17 +3045,124 @@ void CameraShipHUDRenderable::Execute()
VideoRenderable::Execute();
}
//
// BT port (camera-seat bring-up, 2026-07-18): draw one screen-space textured
// quad (u/v 0..1) -- shared by the followed-callsign banner and the ranking
// window rows. The name/ordinal bitmaps load black-on-transparent
// (LoadBitSliceTexture, A4R4G4B4), so alpha blending keys them over the scene.
//
static void CameraHUDDrawQuad(
LPDIRECT3DDEVICE9 device,
LPDIRECT3DTEXTURE9 texture,
float x, float y, float w, float h,
float u0 = 0.0f, float u1 = 1.0f)
{
if (texture == NULL)
{
return;
}
L4VERTEX_2D_TEX quad[4];
memset(quad, 0, sizeof(quad));
const DWORD color = D3DCOLOR_XRGB(0, 128, 0);
for (int i = 0; i < 4; ++i)
{
quad[i].z = 0.0f;
quad[i].rhw = 1.0f;
quad[i].color = color;
}
quad[0].x = x + w; quad[0].y = y; quad[0].u = u1; quad[0].v = 0.0f;
quad[1].x = x; quad[1].y = y; quad[1].u = u0; quad[1].v = 0.0f;
quad[2].x = x + w; quad[2].y = y + h; quad[2].u = u1; quad[2].v = 1.0f;
quad[3].x = x; quad[3].y = y + h; quad[3].u = u0; quad[3].v = 1.0f;
device->SetTexture(0, texture);
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad,
sizeof(L4VERTEX_2D_TEX));
}
void CameraShipHUDRenderable::Render(int pass, const D3DXMATRIX *viewTransform)
{
if (application->GetApplicationState() == Application::RunningMission && oldFollowedPlayerIndex >= 0 && oldFollowedPlayerIndex < MAX_PLAYER_NAMES)
if (application->GetApplicationState() != Application::RunningMission)
{
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
return;
}
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
device->SetFVF(L4VERTEX_2D_TEX_FVF);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
device->SetFVF(L4VERTEX_2D_TEX_FVF);
//
// The followed player's callsign banner (bottom center). Execute already
// converts the 1-based goalPlayerIndex to the 0-based texture slot.
//
if (oldFollowedPlayerIndex >= 0 && oldFollowedPlayerIndex < MAX_PLAYER_NAMES)
{
device->SetStreamSource(0, mVB, 0, sizeof(L4VERTEX_2D_TEX));
device->SetTexture(0, myRenderer->GetNameTexture(oldFollowedPlayerIndex));
device->SetTexture(0,
myRenderer->GetNameTexture(oldFollowedPlayerIndex));
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
}
//
// THE RANKING WINDOW (the un-stubbed 1995 display): one row per scoring
// player -- [1st/2nd/... ordinal][callsign] -- in RANK order, right of
// center (the old DCS placement: window at x .22 y .16, ordinal left of
// the name). Visibility is the Director's flash logic (10s on / 15s off,
// solid for the final 30s). playerRank[bmp-1] -> &playerRanking (LIVE
// pointers captured at ctor), so the rows re-sort as the score changes.
//
if (getenv("BT_CAM_LOG"))
{
static int s_rlog = 0;
if ((s_rlog++ % 300) == 0)
DEBUG_STREAM << "[cam] HUD render: followed="
<< oldFollowedPlayerIndex
<< " rankwin=" << (displayRankingWindow
? (int)*displayRankingWindow : -1)
<< " count=" << playerCount << "\n" << std::flush;
}
if (displayRankingWindow != NULL && *displayRankingWindow
&& playerRank != NULL && playerCount > 0)
{
const float width = myRenderer->GetWidth();
const float height = myRenderer->GetHeight();
const float nameW = width * 0.20f;
const float ordW = width * 0.10f;
const float rowH = nameW * (32.0f / 128.0f); // bitmap aspect
const float pad = width * 0.01f;
const float x0 = width * 0.55f;
const float y0 = height * 0.16f;
for (int bmp = 0; bmp < playerCount && bmp < MAX_PLAYER_NAMES; ++bmp)
{
if (playerRank[bmp] == NULL)
{
continue;
}
int rank = *playerRank[bmp];
if (getenv("BT_CAM_LOG"))
{
static int s_rowlog = 0;
if ((s_rowlog++ % 300) == 0)
DEBUG_STREAM << "[cam] rank row: bmp=" << bmp
<< " rank=" << rank << "\n" << std::flush;
}
if (rank < 0 || rank >= MAX_PLAYER_NAMES)
{
continue; // non-scoring (director/camera)
}
const float y = y0 + rank * (rowH * 1.15f);
// each 128x32 ordinal bitmap packs TWO ordinals side by side
// ("1st|2nd", "3rd|4th", ...) -- texture rank/2, u-half by parity
CameraHUDDrawQuad(device, myRenderer->GetOrdinalTexture(rank / 2),
x0, y, ordW, rowH,
(rank & 1) ? 0.5f : 0.0f, (rank & 1) ? 1.0f : 0.5f);
CameraHUDDrawQuad(device, myRenderer->GetNameTexture(bmp),
x0 + ordW + pad, y, nameW, rowH);
}
}
device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~