Cockpit: on-screen vRIO button banks around the MFDs (clickable, lamp-lit)

Each MFD carries its 4+4 physical button strip and the radar its 6+6
amber side columns, exactly as mounted on the pod (RP412 MFDSplitView
geometry + the shared Tesla-board RIO addresses; anchors 0x2F/0x27/0x37/
0x0F/0x07 for the five MFDs, 0x10/0x18 for the radar).

- L4VB16.cpp: BTBuildCockpitButtons computes the button rects from the
  same cockpit surface layout (strips toward the screen interior to stay
  on-canvas); BTDrawCockpitButtons draws them as coloured quads, lit from
  PadRIO::GetLampState (bright red/amber) or dim; state-block wrapped so
  the button pass doesn't leak render state into the next world frame.
  BTCockpitButtonAt hit-tests; BTPadRIOScreenButton bridges to PadRIO.
- btl4main.cpp WndProc: WM_LBUTTONDOWN/UP hit-test the buttons and inject
  press/release into PadRIO (SetCapture so the release always fires).

Verified: the button banks render around all six surfaces in the
L4MFDSPLIT cockpit; mouse plumbing wired through the already-active
PadRIO screen-button path (Phase 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-16 20:46:57 -05:00
co-authored by Claude Fable 5
parent 0c964c0ee7
commit 99ea3f07e9
2 changed files with 166 additions and 0 deletions
+143
View File
@@ -1,4 +1,5 @@
#include "mungal4.h"
#include "l4padrio.h" // PadRIO::GetLampState / SetScreenButton (cockpit buttons)
#pragma hdrstop
#include "l4vb16.h"
@@ -450,6 +451,141 @@ void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
// runs at the FULL viewport in this mode (no strip carve-out), so the surfaces
// occlude the corners like the pod bezels. Called from L4VIDEO alongside the
// dev-gauge inset; the two modes are mutually exclusive.
//
// ON-SCREEN BUTTON BANKS (the vRIO control surface, on the cockpit glass):
// each MFD carries its 4+4 physical button strip and the radar its 6+6 side
// columns, exactly as mounted on the pod (RP412 MFDSplitView geometry, shared
// Tesla-board RIO addresses). Mouse presses inject into PadRIO
// (SetScreenButton); the game's lamp commands light them (GetLampState).
// Positions are computed from the same cockpit surface layout each frame so
// draw and hit-test agree. MFD strips sit toward the screen interior (below
// the top-row glass, above the bottom-row glass) to stay on-canvas.
//
struct BTCockpitBtn { int unit; int amber; float x, y, w, h; };
// {surface index in kBTCockpitSurfaces, anchorA, anchorB, style, topRow}
// style: 0 = MFDStrips (4+4, unit = anchorA - i); 1 = SideColumns (6+6).
static const struct BTBtnBank {
int surf; int anchorA; int anchorB; int style; int topRow;
} kBTBtnBanks[6] = {
{ 0, 0x2F, 0, 0, 1 }, // Heat (UL) strips below
{ 1, 0x27, 0, 0, 1 }, // Mfd2 (UC) strips below
{ 2, 0x37, 0, 0, 1 }, // Comm (UR) strips below
{ 3, 0x0F, 0, 0, 0 }, // Mfd1 (LL) strips above
{ 4, 0x07, 0, 0, 0 }, // Mfd3 (LR) strips above
{ 5, 0x10, 0x18, 1, 0 },// radar (sec) side columns 0x10 left / 0x18 right
};
static int BTBuildCockpitButtons(float W, float H, BTCockpitBtn *out, int maxOut)
{
int n = 0;
for (int b = 0; b < 6 && n < maxOut; ++b)
{
const BTBtnBank &bank = kBTBtnBanks[b];
const BTGaugeSurfaceDesc &s = kBTCockpitSurfaces[bank.surf];
float sx = s.cellX * W, sy = s.cellY * H;
float sw = s.cellW * W, sh = s.cellH * H;
if (bank.style == 0)
{
// 4+4 grid; strip height ~ glass/8 clamped, width = glass/4.
float striph = sh / 6.0f; if (striph < 14.0f) striph = 14.0f; if (striph > 44.0f) striph = 44.0f;
float bw = sw / 4.0f;
// the two rows sit toward the screen interior
float row0y = bank.topRow ? (sy + sh) : (sy - 2.0f * striph);
float row1y = bank.topRow ? (sy + sh + striph) : (sy - striph);
for (int i = 0; i < 8 && n < maxOut; ++i)
{
int row = i / 4, col = i % 4;
BTCockpitBtn &bt = out[n++];
bt.unit = bank.anchorA - i;
bt.amber = 0;
bt.x = sx + col * bw;
bt.y = (row == 0) ? row0y : row1y;
bt.w = bw - 2.0f;
bt.h = striph - 2.0f;
}
}
else
{
// 6 per side, flush to the glass edges.
float colw = sw / 6.0f; if (colw < 16.0f) colw = 16.0f; if (colw > 40.0f) colw = 40.0f;
float bh = sh / 6.0f;
for (int i = 0; i < 6 && n + 1 < maxOut; ++i)
{
BTCockpitBtn &l = out[n++];
l.unit = bank.anchorA + i; l.amber = 1;
l.x = sx - colw; l.y = sy + i * bh; l.w = colw - 2.0f; l.h = bh - 2.0f;
BTCockpitBtn &r = out[n++];
r.unit = bank.anchorB + i; r.amber = 1;
r.x = sx + sw + 2.0f; r.y = sy + i * bh; r.w = colw - 2.0f; r.h = bh - 2.0f;
}
}
}
return n;
}
// A flat coloured quad (no texture) for the buttons.
static void BTFillRect(LPDIRECT3DDEVICE9 device, float x, float y, float w, float h, DWORD color)
{
struct CVert { float x, y, z, rhw; DWORD c; };
CVert quad[4] = {
{ x, y, 0.0f, 1.0f, color },
{ x + w, y, 0.0f, 1.0f, color },
{ x + w, y + h, 0.0f, 1.0f, color },
{ x, y + h, 0.0f, 1.0f, color },
};
device->SetTexture(0, NULL);
device->SetFVF(D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
device->SetRenderState(D3DRS_ZENABLE, FALSE);
device->SetRenderState(D3DRS_LIGHTING, FALSE);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
device->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, quad, sizeof(CVert));
}
static void BTDrawCockpitButtons(LPDIRECT3DDEVICE9 device, float W, float H)
{
BTCockpitBtn btn[64];
int count = BTBuildCockpitButtons(W, H, btn, 64);
for (int i = 0; i < count; ++i)
{
int lit = PadRIO::IsActive() ? PadRIO::GetLampState(btn[i].unit) : 0;
DWORD color;
if (btn[i].amber)
color = lit ? 0xFFFFB000 : 0xFF3A2A00; // amber lit / dim
else
color = lit ? 0xFFFF2020 : 0xFF3A0808; // red lit / dim
// a thin dark border reads as a raised key
BTFillRect(device, btn[i].x - 1, btn[i].y - 1, btn[i].w + 2, btn[i].h + 2, 0xFF101010);
BTFillRect(device, btn[i].x, btn[i].y, btn[i].w, btn[i].h, color);
}
}
// Bridge for the main WndProc (which does not include l4padrio.h): inject a
// screen-button press/release into the active PadRIO.
void BTPadRIOScreenButton(int unit, int pressed)
{
if (PadRIO::IsActive())
PadRIO::SetScreenButton(unit, pressed ? True : False);
}
// Mouse hit-test (window CLIENT pixels): the RIO unit under (px,py), or -1.
// Called from the main WndProc; needs the current window client size.
int BTCockpitButtonAt(int px, int py, int clientW, int clientH)
{
if (!DevCockpitPanel()) return -1;
BTCockpitBtn btn[64];
int count = BTBuildCockpitButtons((float)clientW, (float)clientH, btn, 64);
for (int i = 0; i < count; ++i)
if (px >= btn[i].x && px < btn[i].x + btn[i].w &&
py >= btn[i].y && py < btn[i].y + btn[i].h)
return btn[i].unit;
return -1;
}
//
void BTDrawCockpitPanel(LPDIRECT3DDEVICE9 device)
{
@@ -461,6 +597,13 @@ void BTDrawCockpitPanel(LPDIRECT3DDEVICE9 device)
rt->Release();
BTDrawGaugeSurfaces(device, 0.0f, 0.0f, (float)d.Width, (float)d.Height,
kBTCockpitSurfaces);
// The button quads dirty device state (texture/FVF/render states); snapshot
// and restore around them so the next world frame isn't rendered with them
// (the "floating rocks" depth-state leak documented in BTDrawGaugeSurfaces).
IDirect3DStateBlock9 *sb = NULL;
device->CreateStateBlock(D3DSBT_ALL, &sb);
BTDrawCockpitButtons(device, (float)d.Width, (float)d.Height);
if (sb != NULL) { sb->Apply(); sb->Release(); }
}
//===========================================================================//