Files
RP412/MUNGA_L4/L4MFDVIEW.cpp
T
CydandClaude Fable 5 1058de326d Cockpit buttons on the split displays, lamp-lit and clickable
Each MFDSplitView window now carries its display's physical button bank,
placed as in the pod (addresses per vRIO CockpitLayout): a 4x2 red
cluster around each MFD glass (anchors 0x2F/0x27/0x37 upper, 0x0F/0x07
lower, addresses descending row-major) and 6 amber buttons down each
side of the map - Secondary 0x10-0x15 left, Screen 0x18-0x1D right; the
remaining column addresses are Tesla relays, per the pod wiring, so they
get no buttons.

Buttons light from the lamp state the game commands: PadRIO grows a
static active-instance hook (SetScreenButton/GetLampState); mouse
press/release feeds PadRIO's desired-state sampling alongside pad and
keyboard, and paint decodes the lamp byte (state1/state2 brightness,
solid/slow/med/fast flash animated by tick). With real serial hardware
(no PadRIO) the buttons draw dark and inert.

Verified: map flank buttons light per the game's preset lamps, aligned
with the labels the glass draws at its edges; MFD clusters render 4+4.
Roadmap: queued the vRIO Dynamic Lighting RGB-keyboard lamp mirror as a
polish-pass item. dist repacked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:22:38 -05:00

381 lines
9.1 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4mfdview.h"
#include "l4padrio.h"
namespace
{
const char mfdViewClass[] = "RPMFDView";
const int buttonGap = 4;
//---------------------------------------------------------------
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
// bright), red family for MFD strips, amber for the side columns.
//---------------------------------------------------------------
COLORREF ButtonFill(int amber, int level)
{
if (amber)
{
if (level >= 3) return RGB(255, 210, 48);
if (level >= 1) return RGB(150, 124, 26);
return RGB(64, 52, 10);
}
if (level >= 3) return RGB(255, 72, 44);
if (level >= 1) return RGB(150, 44, 28);
return RGB(64, 18, 12);
}
//---------------------------------------------------------------
// Lamp byte -> current brightness level, animating flash modes.
// Low 2 bits: solid/slow/med/fast; bits 2-3: state 1 brightness;
// bits 4-5: state 2 brightness (the flash alternate).
//---------------------------------------------------------------
int LampLevel(int lamp_state)
{
int mode = lamp_state & 0x03;
int level1 = (lamp_state >> 2) & 0x03;
int level2 = (lamp_state >> 4) & 0x03;
if (mode == 0)
{
return level1;
}
static const int half_period[4] = { 0, 500, 250, 125 };
return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
}
LRESULT CALLBACK
MFDViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
MFDSplitView *view =
(MFDSplitView *) GetWindowLongPtrA(hwnd, GWLP_USERDATA);
switch (message)
{
case WM_PAINT:
if (view != NULL)
{
view->Paint();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (view != NULL)
{
view->MouseDown((int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam));
return 0;
}
break;
case WM_LBUTTONUP:
case WM_CAPTURECHANGED:
if (view != NULL)
{
view->MouseUp();
return 0;
}
break;
case WM_CLOSE:
// Part of the cockpit; just hide it.
ShowWindow(hwnd, SW_HIDE);
return 0;
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcA(hwnd, message, wParam, lParam);
}
}
//########################################################################
//############################ MFDSplitView ##############################
//########################################################################
MFDSplitView::MFDSplitView(
const char *title,
int source_width,
int source_height,
int display_width,
int display_height,
int x,
int y,
ButtonStyle button_style,
int anchorA,
int anchorB
)
{
Check_Pointer(this);
sourceWidth = source_width;
sourceHeight = source_height;
buttonCount = 0;
pressedIndex = -1;
pixels = new unsigned long[source_width * source_height];
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
BITMAPINFOHEADER *header = new BITMAPINFOHEADER;
memset(header, 0, sizeof(BITMAPINFOHEADER));
header->biSize = sizeof(BITMAPINFOHEADER);
header->biWidth = source_width;
header->biHeight = -source_height; // top-down
header->biPlanes = 1;
header->biBitCount = 32;
header->biCompression = BI_RGB;
blitHeader = header;
int client_width = display_width;
int client_height = display_height;
LayoutButtons(button_style, anchorA, anchorB,
display_width, display_height, &client_width, &client_height);
HINSTANCE instance = GetModuleHandleA(NULL);
static Logical class_registered = False;
if (!class_registered)
{
class_registered = True;
WNDCLASSA window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.lpfnWndProc = MFDViewWndProc;
window_class.hInstance = instance;
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
window_class.lpszClassName = mfdViewClass;
RegisterClassA(&window_class);
}
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
RECT bounds;
bounds.left = 0;
bounds.top = 0;
bounds.right = client_width;
bounds.bottom = client_height;
AdjustWindowRect(&bounds, style, FALSE);
window = CreateWindowExA(
0,
mfdViewClass,
title,
style,
x, y,
bounds.right - bounds.left,
bounds.bottom - bounds.top,
NULL, NULL, instance, NULL
);
if (window != NULL)
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
}
else
{
DEBUG_STREAM << "MFDSplitView: window creation failed (" << title << ")\n" << std::flush;
}
}
MFDSplitView::~MFDSplitView()
{
Check_Pointer(this);
if (window != NULL)
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
DestroyWindow((HWND) window);
window = NULL;
}
delete (BITMAPINFOHEADER *) blitHeader;
blitHeader = NULL;
delete [] pixels;
pixels = NULL;
}
Logical
MFDSplitView::TestInstance() const
{
return pixels != NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Compute the display rectangle and the button rectangles, growing the
// client area to make room for the strips.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
MFDSplitView::LayoutButtons(
ButtonStyle button_style,
int anchorA,
int anchorB,
int display_width,
int display_height,
int *client_width,
int *client_height
)
{
displayX = 0;
displayY = 0;
displayW = display_width;
displayH = display_height;
if (button_style == MFDStrips)
{
//-----------------------------------------------------------
// 4 buttons above the glass, 4 below; RIO addresses descend
// from the anchor, row-major (vRIO CockpitLayout::Mfd).
//-----------------------------------------------------------
int strip_h = display_height / 8;
if (strip_h < 18) strip_h = 18;
if (strip_h > 40) strip_h = 40;
int button_w = (display_width - 5 * buttonGap) / 4;
displayY = strip_h + 2 * buttonGap;
*client_height = display_height + 2 * (strip_h + 2 * buttonGap);
for (int i = 0; i < 8; ++i)
{
int row = i / 4; // 0 = top strip, 1 = bottom strip
int col = i % 4;
ScreenButton *button = &buttons[buttonCount++];
button->unit = anchorA - i;
button->amber = 0;
button->x = buttonGap + col * (button_w + buttonGap);
button->y = row ? (displayY + displayH + buttonGap) : buttonGap;
button->w = button_w;
button->h = strip_h;
}
}
else if (button_style == SideColumns)
{
//-----------------------------------------------------------
// 6 buttons down each side of the glass (the pod wires only
// 6 of each column's 8 addresses to buttons; the rest are
// Tesla relays). Left = anchorA ascending, right = anchorB.
//-----------------------------------------------------------
int col_w = display_width / 8;
if (col_w < 20) col_w = 20;
if (col_w > 40) col_w = 40;
int button_h = (display_height - 7 * buttonGap) / 6;
displayX = col_w + 2 * buttonGap;
*client_width = display_width + 2 * (col_w + 2 * buttonGap);
for (int i = 0; i < 6; ++i)
{
ScreenButton *left = &buttons[buttonCount++];
left->unit = anchorA + i;
left->amber = 1;
left->x = buttonGap;
left->y = buttonGap + i * (button_h + buttonGap);
left->w = col_w;
left->h = button_h;
ScreenButton *right = &buttons[buttonCount++];
right->unit = anchorB + i;
right->amber = 1;
right->x = displayX + displayW + buttonGap;
right->y = left->y;
right->w = col_w;
right->h = button_h;
}
}
}
void
MFDSplitView::Paint()
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint((HWND) window, &ps);
RECT client;
GetClientRect((HWND) window, &client);
FillRect(hdc, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
SetStretchBltMode(hdc, COLORONCOLOR);
StretchDIBits(
hdc,
displayX, displayY, displayW, displayH,
0, 0, sourceWidth, sourceHeight,
pixels, (const BITMAPINFO *) blitHeader,
DIB_RGB_COLORS, SRCCOPY
);
for (int i = 0; i < buttonCount; ++i)
{
const ScreenButton *button = &buttons[i];
int level = PadRIO::IsActive()
? LampLevel(PadRIO::GetLampState(button->unit))
: 0;
RECT rect;
rect.left = button->x;
rect.top = button->y;
rect.right = button->x + button->w;
rect.bottom = button->y + button->h;
HBRUSH fill = CreateSolidBrush(ButtonFill(button->amber, level));
FillRect(hdc, &rect, fill);
DeleteObject(fill);
HBRUSH frame = CreateSolidBrush(
(i == pressedIndex) ? RGB(255, 255, 255) : RGB(48, 48, 48));
FrameRect(hdc, &rect, frame);
DeleteObject(frame);
}
EndPaint((HWND) window, &ps);
}
void
MFDSplitView::MouseDown(int x, int y)
{
for (int i = 0; i < buttonCount; ++i)
{
const ScreenButton *button = &buttons[i];
if (x >= button->x && x < button->x + button->w &&
y >= button->y && y < button->y + button->h)
{
pressedIndex = i;
SetCapture((HWND) window);
PadRIO::SetScreenButton(button->unit, True);
InvalidateRect((HWND) window, NULL, FALSE);
return;
}
}
}
void
MFDSplitView::MouseUp()
{
if (pressedIndex >= 0)
{
PadRIO::SetScreenButton(buttons[pressedIndex].unit, False);
pressedIndex = -1;
if (GetCapture() == (HWND) window)
{
ReleaseCapture();
}
InvalidateRect((HWND) window, NULL, FALSE);
}
}
void
MFDSplitView::Repaint()
{
Check_Pointer(this);
if (window != NULL)
{
InvalidateRect((HWND) window, NULL, FALSE);
}
}