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>
315 lines
8.2 KiB
C++
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;
|
|
}
|
|
}
|