Files
BT411/engine/MUNGA_L4/L4PADPANEL.cpp
T
CydandClaude Fable 5 ec3985e9da Panel: condensed layout -- keypads dropped, board columns centered + raised
Per Cyd: the two 4x4 hex keypads leave the glass panel (engine keypad units
stay reachable via bindings.txt); the four board columns (Thr/Sec/Scr/Joy)
move to the center between the lower MFD stacks and rise into the vacated
internal-keypad space -- 72 controls, ~5 rows shorter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:47:28 -05:00

549 lines
15 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).
//
// Layout + coloration follow vRIO (C:\VWE\vrio, src/VRio.Core/Panel/
// CockpitLayout.cs + src/VRio.App/PanelCanvas.cs) -- the software replica
// of the RIO board whose panel mirrors RIOJoy's profile editor (the
// original Win32 RIO control-panel mockup): five 4x2 MFD clusters, four
// vertical 1x8 board columns, two 4x4 hex keypads; red lamp cells (white
// text), yellow Secondary/Screen columns (black text), neutral-blue
// lampless keypads, lamp flash at 500/250/125 ms half-periods. Keeping
// the same layout means a button here sits where it sits in vRIO/RIOJoy.
//
// Left-click = momentary press. Right-click = latch toggle. Keypad cells
// (addresses 0x50-0x6F) emit keypad key events; everything else emits RIO
// button events -- both via PadRIO::SetScreenButton.
//###########################################################################
#include "l4padpanel.h"
#include "l4padrio.h"
#include <windows.h>
#include <stdio.h>
#include <string.h>
//###########################################################################
// The vRIO cockpit layout (CockpitLayout.cs transcribed)
//###########################################################################
enum
{
CellW = 66,
CellH = 34,
PanelMargin = 6,
FirstRow = 1, // the shared layout leaves grid row 0 empty
RepaintTimerId = 1,
RepaintMilliseconds = 100
};
enum GroupKind { KindMfd, KindColumn, KindKeypad };
struct PanelGroupDef
{
const char *title;
GroupKind kind;
int cols, rows;
int lampCapable;
int originCol, originRow;
int baseAddress; // Mfd: hi address; Column: base; Keypad: base
};
//
// Groups exactly as CockpitLayout.Groups: MFD blocks list top row then
// bottom row with addresses DESCENDING from the hi address; columns run
// base..base+7 top to bottom; keypads place hex keys 1 2 3 C / 4 5 6 D /
// 7 8 9 E / A 0 B F at base+digit.
//
static const PanelGroupDef panelGroups[] =
{
{ "Upper Left MFD", KindMfd, 4, 2, 1, 0, 1, 0x2F },
{ "Upper Middle MFD", KindMfd, 4, 2, 1, 4, 1, 0x27 },
{ "Upper Right MFD", KindMfd, 4, 2, 1, 8, 1, 0x37 },
{ "Lower Left MFD", KindMfd, 4, 2, 1, 0, 5, 0x0F },
{ "Lower Right MFD", KindMfd, 4, 2, 1, 8, 5, 0x07 },
// The four board columns sit CENTERED between the lower MFD stacks,
// raised into the space the internal keypad vacated (per Cyd
// 2026-07-18: condensed panel, no keypads). Short titles fit the
// one-cell frames.
{ "Thr", KindColumn, 1, 8, 1, 4, 4, 0x38 },
{ "Sec", KindColumn, 1, 8, 1, 5, 4, 0x10 },
{ "Scr", KindColumn, 1, 8, 1, 6, 4, 0x18 },
{ "Joy", KindColumn, 1, 8, 1, 7, 4, 0x40 },
// (The two 4x4 hex keypads -- Internal 0x50 / External 0x60 -- were
// dropped from the glass panel per Cyd 2026-07-18; the engine keypad
// units remain reachable through bindings.txt `keypad` rows.)
};
enum { PanelGroupCount = sizeof(panelGroups)/sizeof(panelGroups[0]) };
static const int keypadDigits[16] =
{ 0x1, 0x2, 0x3, 0xC, 0x4, 0x5, 0x6, 0xD,
0x7, 0x8, 0x9, 0xE, 0xA, 0x0, 0xB, 0xF };
struct NamedButton
{
int address;
const char *name;
};
static const NamedButton namedButtons[] =
{
{ 0x3D, "Panic" },
{ 0x3F, "Throttle" },
{ 0x40, "Main" },
{ 0x41, "Hat Back" },
{ 0x42, "Hat Up" },
{ 0x43, "Hat Right" },
{ 0x44, "Hat Left" },
{ 0x45, "Pinky" },
{ 0x46, "Middle" },
{ 0x47, "Upper" },
};
enum { NamedButtonCount = sizeof(namedButtons)/sizeof(namedButtons[0]) };
struct PanelCell
{
int address;
const PanelGroupDef *group;
RECT rect;
};
static PanelCell panelCells[128];
static int panelCellCount = 0;
static HWND panelWindow = NULL;
static int pressedAddress = -1;
static unsigned char latched[128];
static HFONT titleFont = NULL; // group titles (bold)
static HFONT buttonFont = NULL; // cell labels
static HFONT subFont = NULL; // the small hex under a name
//###########################################################################
static RECT
CellRect(int col, int row)
{
RECT r;
r.left = col * CellW + 2;
r.top = PanelMargin + (row - FirstRow) * CellH + 2;
r.right = r.left + CellW - 4;
r.bottom = r.top + CellH - 4;
return r;
}
static void
LayoutCells()
{
panelCellCount = 0;
for (int g = 0; g < PanelGroupCount; ++g)
{
const PanelGroupDef &group = panelGroups[g];
int count = group.cols * group.rows;
for (int i = 0; i < count; ++i)
{
int address;
switch (group.kind)
{
case KindMfd: address = group.baseAddress - i; break;
case KindColumn: address = group.baseAddress + i; break;
default: address = group.baseAddress + keypadDigits[i]; break;
}
PanelCell &cell = panelCells[panelCellCount++];
cell.address = address;
cell.group = &group;
// +1 row leaves space under the group title.
cell.rect = CellRect(group.originCol + i % group.cols,
group.originRow + 1 + i / group.cols);
}
}
}
static const char *
PhysicalName(int address)
{
for (int i = 0; i < NamedButtonCount; ++i)
{
if (namedButtons[i].address == address)
{
return namedButtons[i].name;
}
}
return NULL;
}
static int
HitTest(int x, int y)
{
for (int i = 0; i < panelCellCount; ++i)
{
const RECT &r = panelCells[i].rect;
if (x >= r.left && x < r.right && y >= r.top && y < r.bottom)
{
return panelCells[i].address;
}
}
return -1;
}
//###########################################################################
// Lamp decode (RIOBase::LampState): flash = low 2 bits (solid/slow/med/
// fast), filament brightness = max of the state1 (>>2) and state2 (>>4)
// 2-bit fields (0 off / 1 dim / 3 bright). Flash blinks between the
// commanded brightness and off, half-periods 500/250/125 ms (vRIO).
//###########################################################################
static int
LampBrightnessOf(int state, unsigned long tick)
{
int level1 = (state >> 2) & 0x3;
int level2 = (state >> 4) & 0x3;
int level = (level1 > level2) ? level1 : level2;
if (level == 0)
{
return 0;
}
int flash = state & 0x3;
if (flash != 0)
{
unsigned long half_period =
(flash == 1) ? 500 : (flash == 2) ? 250 : 125;
if ((tick / half_period) & 1)
{
return 0;
}
}
return level;
}
//###########################################################################
// Painting (PanelCanvas.cs coloration)
//###########################################################################
static void
DrawLabelText(HDC dc, const char *text, HFONT font, RECT *rect,
COLORREF color, UINT format)
{
WCHAR wide[32];
int n = 0;
for (const char *s = text; *s && n < 31; ++s)
{
wide[n++] = (WCHAR)*s;
}
wide[n] = 0;
HFONT old_font = (HFONT)SelectObject(dc, font);
SetTextColor(dc, color);
DrawTextW(dc, wide, -1, rect, format);
SelectObject(dc, old_font);
}
static void
PaintPanel(HWND window)
{
PAINTSTRUCT paint;
HDC dc = BeginPaint(window, &paint);
unsigned long tick = GetTickCount();
RECT client;
GetClientRect(window, &client);
HBRUSH background = CreateSolidBrush(RGB(28, 28, 28));
FillRect(dc, &client, background);
DeleteObject(background);
SetBkMode(dc, TRANSPARENT);
//
// Group frames + titles.
//
HPEN frame_pen = CreatePen(PS_SOLID, 1, RGB(80, 80, 80));
HPEN old_pen = (HPEN)SelectObject(dc, frame_pen);
HBRUSH old_brush = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH));
for (int g = 0; g < PanelGroupCount; ++g)
{
const PanelGroupDef &group = panelGroups[g];
int x = group.originCol * CellW + 1;
int y = PanelMargin + (group.originRow - FirstRow) * CellH + 1;
Rectangle(dc, x, y,
x + group.cols * CellW, y + (group.rows + 1) * CellH);
RECT title_rect = { x + 4, y, x + group.cols * CellW - 4, y + CellH };
DrawLabelText(dc, group.title, titleFont, &title_rect,
RGB(220, 220, 220),
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
SelectObject(dc, old_brush);
SelectObject(dc, old_pen);
DeleteObject(frame_pen);
//
// Cells.
//
for (int i = 0; i < panelCellCount; ++i)
{
const PanelCell &cell = panelCells[i];
const RECT &r = cell.rect;
int held = (cell.address == pressedAddress) || latched[cell.address & 0x7F];
int lamp_capable = cell.group->lampCapable;
int yellow = (strcmp(cell.group->title, "Sec") == 0 ||
strcmp(cell.group->title, "Scr") == 0);
//
// Lamp shade: a locally held cap floods bright; else the host-
// commanded lamp state (with flash phase) decides.
//
int shade = 0;
if (lamp_capable)
{
shade = LampBrightnessOf(PadRIO::GetLampState(cell.address), tick);
}
if (held)
{
shade = 3;
}
COLORREF fill;
COLORREF text_color;
if (!lamp_capable)
{
fill = held ? RGB(205, 228, 255) : RGB(150, 182, 226);
text_color = RGB(0, 0, 0);
}
else if (yellow)
{
fill = (shade == 3) ? RGB(245, 210, 60)
: (shade != 0) ? RGB(140, 118, 38)
: RGB(70, 60, 24);
text_color = RGB(0, 0, 0);
}
else
{
fill = (shade == 3) ? RGB(230, 70, 70)
: (shade != 0) ? RGB(120, 50, 50)
: RGB(64, 40, 40);
text_color = RGB(255, 255, 255);
}
HBRUSH face = CreateSolidBrush(fill);
RECT fill_rect = r;
FillRect(dc, &fill_rect, face);
DeleteObject(face);
//
// Label: keypads show the hex digit; named controls show the name
// over the small hex address; the rest show the address.
//
char hex[8];
sprintf(hex, "%02X", cell.address);
const char *name = PhysicalName(cell.address);
if (cell.group->kind == KindKeypad)
{
char digit[4];
sprintf(digit, "%X", cell.address & 0x0F);
RECT cell_rect = r;
DrawLabelText(dc, digit, buttonFont, &cell_rect, text_color,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
else if (name != NULL)
{
RECT top_rect = { r.left, r.top + 1, r.right, (r.top + r.bottom) / 2 + 3 };
RECT bottom_rect = { r.left, (r.top + r.bottom) / 2, r.right, r.bottom - 1 };
DrawLabelText(dc, name, buttonFont, &top_rect, text_color,
DT_CENTER | DT_TOP | DT_SINGLELINE | DT_END_ELLIPSIS);
DrawLabelText(dc, hex, subFont, &bottom_rect, text_color,
DT_CENTER | DT_BOTTOM | DT_SINGLELINE);
}
else
{
RECT cell_rect = r;
DrawLabelText(dc, hex, buttonFont, &cell_rect, text_color,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
//
// Latch = gold outline, held = white outline (vRIO).
//
if (latched[cell.address & 0x7F])
{
HBRUSH gold = CreateSolidBrush(RGB(255, 215, 0));
RECT outline = r;
FrameRect(dc, &outline, gold);
outline.left += 1; outline.top += 1;
outline.right -= 1; outline.bottom -= 1;
FrameRect(dc, &outline, gold);
DeleteObject(gold);
}
else if (held)
{
RECT outline = r;
FrameRect(dc, &outline, (HBRUSH)GetStockObject(WHITE_BRUSH));
outline.left += 1; outline.top += 1;
outline.right -= 1; outline.bottom -= 1;
FrameRect(dc, &outline, (HBRUSH)GetStockObject(WHITE_BRUSH));
}
}
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)
{
if (latched[address & 0x7F])
{
// Left-clicking a latched button releases it.
latched[address & 0x7F] = 0;
PadRIO::SetScreenButton(address, 0);
}
else
{
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_RBUTTONDOWN:
{
// Latch toggle: press-and-hold without keeping the mouse down.
int address = HitTest((int)(short)LOWORD(lparam),
(int)(short)HIWORD(lparam));
if (address >= 0)
{
if (latched[address & 0x7F])
{
latched[address & 0x7F] = 0;
PadRIO::SetScreenButton(address, 0);
}
else
{
latched[address & 0x7F] = 1;
PadRIO::SetScreenButton(address, 1);
}
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;
}
LayoutCells();
memset(latched, 0, sizeof(latched));
titleFont = CreateFontW(-11, 0, 0, 0, FW_BOLD, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
buttonFont = CreateFontW(-10, 0, 0, 0, FW_NORMAL, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
subFont = CreateFontW(-9, 0, 0, 0, FW_NORMAL, 0, 0, 0,
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
int client_width = 0, client_height = 0;
for (int i = 0; i < panelCellCount; ++i)
{
if (panelCells[i].rect.right > client_width)
{
client_width = panelCells[i].rect.right;
}
if (panelCells[i].rect.bottom > client_height)
{
client_height = panelCells[i].rect.bottom;
}
}
client_width += PanelMargin;
client_height += 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 ("
<< panelCellCount << " controls, vRIO layout)\n" << std::flush;
}
void
BTPadPanel_Destroy()
{
if (panelWindow != NULL)
{
KillTimer(panelWindow, RepaintTimerId);
DestroyWindow(panelWindow);
panelWindow = NULL;
}
if (titleFont) { DeleteObject(titleFont); titleFont = NULL; }
if (buttonFont) { DeleteObject(buttonFont); buttonFont = NULL; }
if (subFont) { DeleteObject(subFont); subFont = NULL; }
}