545 lines
15 KiB
C++
545 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 },
|
|
{ "Throttle", KindColumn, 1, 8, 1, 0, 9, 0x38 },
|
|
{ "Secondary", KindColumn, 1, 8, 1, 4, 9, 0x10 },
|
|
{ "Screen", KindColumn, 1, 8, 1, 6, 9, 0x18 },
|
|
{ "Joystick", KindColumn, 1, 8, 1, 11, 9, 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, "Secondary") == 0 ||
|
|
strcmp(cell.group->title, "Screen") == 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; }
|
|
}
|