#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 #include #include //########################################################################### // 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; } }