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