From 99ea3f07e9358e1999ce6a531b7450d41442e592 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 16 Jul 2026 20:46:57 -0500 Subject: [PATCH] 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 --- engine/MUNGA_L4/L4VB16.cpp | 143 +++++++++++++++++++++++++++++++++++++ game/btl4main.cpp | 23 ++++++ 2 files changed, 166 insertions(+) diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index cfbaac8..d88cad9 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -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(); } } //===========================================================================// diff --git a/game/btl4main.cpp b/game/btl4main.cpp index e4932a2..521c468 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -100,6 +100,29 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) L4NotifyWindowResized((int)LOWORD(lParam), (int)HIWORD(lParam)); } return 0; + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + { + // COCKPIT BUTTONS (L4MFDSPLIT): a mouse press on an on-screen MFD/radar + // button injects into PadRIO exactly like a physical RIO key. A single + // pressed unit is tracked so the matching release fires even if the + // cursor moved off the button first. + extern int BTCockpitButtonAt(int px, int py, int clientW, int clientH); + extern void BTPadRIOScreenButton(int unit, int pressed); + static int s_pressedUnit = -1; + RECT rc; GetClientRect(hWnd, &rc); + int mx = (short)LOWORD(lParam), my = (short)HIWORD(lParam); + if (uMsg == WM_LBUTTONDOWN) + { + int unit = BTCockpitButtonAt(mx, my, rc.right, rc.bottom); + if (unit >= 0) { s_pressedUnit = unit; BTPadRIOScreenButton(unit, 1); SetCapture(hWnd); } + } + else + { + if (s_pressedUnit >= 0) { BTPadRIOScreenButton(s_pressedUnit, 0); s_pressedUnit = -1; ReleaseCapture(); } + } + return 0; + } case WM_CLOSE: DestroyWindow(hWnd); return 0;