Files
BT411/engine/MUNGA_L4/L4PADPANEL.cpp
T
CydandClaude Fable 5 db9ac947f7 Displays: on-screen cockpit buttons + desktop plasma + the glass preset (steps 2d/2e)
NEW gated TUs: L4PADPANEL (GDI top-level window, all-W APIs: the pod button
banks -- upper/lower aux, 12 secondary, specials/hat/handle, 63 buttons --
clickable via PadRIO::SetScreenButton with lamp-lit faces from GetLampState,
10 Hz repaint; created by the PadRIO ctor on BT_PAD_PANEL=1) and L4PLASMAWIN
(PlasmaWindow : Video8BitBuffered -- the gauge renderer draws the 128x32
plasma into its pixelBuffer through the SAME code path as the serial device;
Update() blits orange-on-black at L4PLASMASCALE, default x4).  Gated seams:
L4GREND.cpp L4PLASMA=SCREEN branch; btl4main.cpp -platform glass preset
(PAD,KEYBOARD + BT_DEV_GAUGES + SCREEN plasma + panel; env always overrides;
non-glass builds log+fall back to DEV); run.cmd glass token.  MFD surfaces
need NO new code: the existing dock-bottom / BT_DEV_GAUGES_WINDOW=1 /
BT_DEV_GAUGES_DOCK=1 modes are the display story.

Verified live: -platform glass boots GLASS profile, panel + plasma windows
up, pad detected, dev gauges awake, mission loop clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:21:22 -05:00

315 lines
8.2 KiB
C++

#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4PADPANEL -- the on-screen cockpit button panel (BT_GLASS only; this TU
// is only in the build when the gate is on -- see CMakeLists.txt).
// Design: L4PADPANEL.h.
//###########################################################################
#include "l4padpanel.h"
#include "l4padrio.h"
#include <windows.h>
#include <stdio.h>
//###########################################################################
// Layout: rows of banks, each button carries its RIO address + label.
//###########################################################################
struct PanelButton
{
int address;
const char *label;
RECT rect; // filled at layout time
};
//
// Bank numbering follows the L4CTRL.h enum names: aux banks count DOWN
// from 8 at the low address (ButtonAuxLowerRight8=0x00 .. 1=0x07), the
// secondary panel counts UP (Secondary1=0x10 .. 6=0x15, 7=0x18 .. 12=0x1D).
//
static PanelButton panelButtons[] =
{
// row 0: upper aux left / center / right
{0x28,"UL8"},{0x29,"UL7"},{0x2A,"UL6"},{0x2B,"UL5"},
{0x2C,"UL4"},{0x2D,"UL3"},{0x2E,"UL2"},{0x2F,"UL1"},
{0x20,"UC8"},{0x21,"UC7"},{0x22,"UC6"},{0x23,"UC5"},
{0x24,"UC4"},{0x25,"UC3"},{0x26,"UC2"},{0x27,"UC1"},
{0x30,"UR8"},{0x31,"UR7"},{0x32,"UR6"},{0x33,"UR5"},
{0x34,"UR4"},{0x35,"UR3"},{0x36,"UR2"},{0x37,"UR1"},
// row 1: the 12 secondary-panel buttons
{0x10,"S1"},{0x11,"S2"},{0x12,"S3"},{0x13,"S4"},{0x14,"S5"},{0x15,"S6"},
{0x18,"S7"},{0x19,"S8"},{0x1A,"S9"},{0x1B,"S10"},{0x1C,"S11"},{0x1D,"S12"},
// row 2: lower aux left / right
{0x08,"LL8"},{0x09,"LL7"},{0x0A,"LL6"},{0x0B,"LL5"},
{0x0C,"LL4"},{0x0D,"LL3"},{0x0E,"LL2"},{0x0F,"LL1"},
{0x00,"LR8"},{0x01,"LR7"},{0x02,"LR6"},{0x03,"LR5"},
{0x04,"LR4"},{0x05,"LR3"},{0x06,"LR2"},{0x07,"LR1"},
// row 3: specials + the stick/throttle handle
{0x3C,"DOOR"},{0x3D,"PANIC"},{0x3F,"REV"},{0x40,"TRIG"},
{0x41,"HAT-D"},{0x42,"HAT-U"},{0x43,"HAT-R"},{0x44,"HAT-L"},
{0x45,"PNKY"},{0x46,"THB-"},{0x47,"THB+"},
};
enum { PanelButtonCount = sizeof(panelButtons)/sizeof(panelButtons[0]) };
//
// Row boundaries (index into panelButtons) + per-row bank gaps (an extra
// horizontal gap after these column indices, visually separating banks).
//
struct PanelRow
{
int first, count;
int gapAfterA, gapAfterB; // -1 = none
};
static const PanelRow panelRows[] =
{
{ 0, 24, 7, 15 }, // UL | UC | UR
{ 24, 12, 5, -1 }, // S1-6 | S7-12
{ 36, 16, 7, -1 }, // LL | LR
{ 52, 11, 3, 7 }, // specials | hat | thumbs
};
enum { PanelRowCount = sizeof(panelRows)/sizeof(panelRows[0]) };
enum
{
ButtonWidth = 42,
ButtonHeight = 30,
ButtonGap = 4,
BankGap = 14,
PanelMargin = 10,
RepaintTimerId = 1,
RepaintMilliseconds = 100
};
static HWND panelWindow = NULL;
static int pressedAddress = -1;
//###########################################################################
static void
LayoutButtons()
{
int y = PanelMargin;
for (int r = 0; r < PanelRowCount; ++r)
{
int x = PanelMargin;
for (int c = 0; c < panelRows[r].count; ++c)
{
PanelButton &button = panelButtons[panelRows[r].first + c];
button.rect.left = x;
button.rect.top = y;
button.rect.right = x + ButtonWidth;
button.rect.bottom = y + ButtonHeight;
x += ButtonWidth + ButtonGap;
if (c == panelRows[r].gapAfterA || c == panelRows[r].gapAfterB)
{
x += BankGap;
}
}
y += ButtonHeight + ButtonGap + 4;
}
}
static int
HitTest(int x, int y)
{
for (int i = 0; i < PanelButtonCount; ++i)
{
const RECT &r = panelButtons[i].rect;
if (x >= r.left && x < r.right && y >= r.top && y < r.bottom)
{
return panelButtons[i].address;
}
}
return -1;
}
//
// Lamp color from the recorded RIO lamp state: the two filament fields
// (state1 0x04 dim / 0x0C bright, state2 0x10 dim / 0x30 bright) collapse
// to a single dev-panel brightness; flash modes render as lit (the pod's
// flash timing is not simulated on the panel).
//
static COLORREF
LampColor(int state, int pressed)
{
int level1 = (state >> 2) & 0x3; // 0/1/3
int level2 = (state >> 4) & 0x3;
int level = (level1 > level2) ? level1 : level2;
if (pressed)
{
return RGB(255, 255, 160);
}
switch (level)
{
case 0: return RGB(52, 56, 52); // off: dead face
case 1: return RGB(64, 120, 64); // dim
default: return RGB(80, 220, 80); // bright
}
}
static void
PaintPanel(HWND window)
{
PAINTSTRUCT paint;
HDC dc = BeginPaint(window, &paint);
RECT client;
GetClientRect(window, &client);
HBRUSH background = CreateSolidBrush(RGB(24, 26, 28));
FillRect(dc, &client, background);
DeleteObject(background);
SetBkMode(dc, TRANSPARENT);
HFONT font = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
HFONT old_font = (HFONT)SelectObject(dc, font);
for (int i = 0; i < PanelButtonCount; ++i)
{
const PanelButton &button = panelButtons[i];
int lamp = PadRIO::GetLampState(button.address);
int pressed = (button.address == pressedAddress);
HBRUSH face = CreateSolidBrush(LampColor(lamp, pressed));
FillRect(dc, &button.rect, face);
DeleteObject(face);
FrameRect(dc, &button.rect,
(HBRUSH)GetStockObject(pressed ? WHITE_BRUSH : GRAY_BRUSH));
SetTextColor(dc, RGB(230, 230, 230));
WCHAR label[16];
int n = 0;
for (const char *s = button.label; *s && n < 15; ++s)
{
label[n++] = (WCHAR)*s;
}
label[n] = 0;
RECT text_rect = button.rect;
DrawTextW(dc, label, -1, &text_rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
SelectObject(dc, old_font);
EndPaint(window, &paint);
}
static LRESULT CALLBACK
PanelWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_PAINT:
PaintPanel(window);
return 0;
case WM_TIMER:
if (wparam == RepaintTimerId)
{
InvalidateRect(window, NULL, FALSE);
}
return 0;
case WM_LBUTTONDOWN:
{
int address = HitTest((int)(short)LOWORD(lparam),
(int)(short)HIWORD(lparam));
if (address >= 0)
{
pressedAddress = address;
SetCapture(window);
PadRIO::SetScreenButton(address, 1);
InvalidateRect(window, NULL, FALSE);
}
}
return 0;
case WM_LBUTTONUP:
if (pressedAddress >= 0)
{
PadRIO::SetScreenButton(pressedAddress, 0);
pressedAddress = -1;
ReleaseCapture();
InvalidateRect(window, NULL, FALSE);
}
return 0;
case WM_CLOSE:
//
// Closing the panel just hides it -- the device stays up; the
// window returns on the next run. (No DestroyWindow: the game
// owns the lifetime through BTPadPanel_Destroy.)
//
ShowWindow(window, SW_HIDE);
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
//###########################################################################
void
BTPadPanel_Create()
{
if (panelWindow != NULL)
{
return;
}
LayoutButtons();
//
// Window size from the widest row (row 0: 24 buttons + 2 bank gaps).
//
int client_width = 0;
for (int i = 0; i < PanelButtonCount; ++i)
{
if (panelButtons[i].rect.right > client_width)
{
client_width = panelButtons[i].rect.right;
}
}
client_width += PanelMargin;
int client_height = panelButtons[PanelButtonCount-1].rect.bottom
+ PanelMargin;
WNDCLASSW window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.lpfnWndProc = PanelWndProc;
window_class.hInstance = GetModuleHandleW(NULL);
window_class.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
window_class.lpszClassName = L"BTPadPanelWnd";
RegisterClassW(&window_class);
RECT frame = { 0, 0, client_width, client_height };
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
AdjustWindowRect(&frame, style, FALSE);
panelWindow = CreateWindowW(
L"BTPadPanelWnd", L"BattleTech - Cockpit Buttons",
style,
40, 40,
frame.right - frame.left, frame.bottom - frame.top,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (panelWindow == NULL)
{
DEBUG_STREAM << "[padpanel] CreateWindow failed\n" << std::flush;
return;
}
SetTimer(panelWindow, RepaintTimerId, RepaintMilliseconds, NULL);
ShowWindow(panelWindow, SW_SHOWNOACTIVATE);
DEBUG_STREAM << "[padpanel] cockpit button panel up ("
<< PanelButtonCount << " buttons)\n" << std::flush;
}
void
BTPadPanel_Destroy()
{
if (panelWindow != NULL)
{
KillTimer(panelWindow, RepaintTimerId);
DestroyWindow(panelWindow);
panelWindow = NULL;
}
}