Two pieces of MFD work that had been sitting in the tree (already in every build and dist since). The compact cockpit glass shows the full 640x480 gauge canvas at about half size, and COLORONCOLOR did that by dropping every other row and column - which shredded the 1-bit vector strokes and small text. HALFTONE area-averages instead (with the brush origin set, as MSDN requires) so the downscaled MFDs stay legible. L4MFDSPLIT=2 adds an exploded diagnostic view: every display in its own full-size desktop window at native resolution, decoded exactly as the pod VDB split them from the single gauge canvas, with no cockpit compositing and no downscale. It makes an individual MFD readable and screenshottable at full resolution for comparison against the emulator per-channel reference windows. L4MFDSPLIT=1 keeps the composited glass cockpit and stays the default. Also: .gitignore now covers the packaged RedPlanet-*.zip releases, which live on the Gitea release page rather than in the tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
451 lines
11 KiB
C++
451 lines
11 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;
|
|
}
|
|
// no instance (e.g. the plain viewscreen child): default paint
|
|
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:
|
|
if (view != NULL)
|
|
{
|
|
return 1;
|
|
}
|
|
break; // viewscreen child: erase with the class black brush
|
|
}
|
|
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,
|
|
void *parent
|
|
)
|
|
{
|
|
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);
|
|
clientWidth = client_width;
|
|
clientHeight = 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;
|
|
int window_w, window_h;
|
|
|
|
if (parent != NULL)
|
|
{
|
|
// chrome-less pane inside the cockpit window; clips siblings so
|
|
// overlapping panes and the viewscreen never paint over it
|
|
style = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS;
|
|
window_w = client_width;
|
|
window_h = client_height;
|
|
}
|
|
else
|
|
{
|
|
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_w = bounds.right - bounds.left;
|
|
window_h = bounds.bottom - bounds.top;
|
|
}
|
|
|
|
window = CreateWindowExA(
|
|
0,
|
|
mfdViewClass,
|
|
title,
|
|
style,
|
|
x, y,
|
|
window_w,
|
|
window_h,
|
|
(HWND) parent, 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, flush to the pane
|
|
// edges (no border against the viewscreen behind); 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 - 3 * buttonGap) / 4;
|
|
|
|
displayY = strip_h;
|
|
*client_height = display_height + 2 * strip_h;
|
|
|
|
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 = col * (button_w + buttonGap);
|
|
button->y = row ? (displayY + displayH) : 0;
|
|
button->w = (col == 3)
|
|
? (display_width - 3 * (button_w + buttonGap))
|
|
: button_w;
|
|
button->h = strip_h;
|
|
}
|
|
}
|
|
else if (button_style == SideColumns)
|
|
{
|
|
//-----------------------------------------------------------
|
|
// 6 buttons down each side of the glass, stacked contiguous
|
|
// and flush to the pane edges, like the pod's strips (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 / 6;
|
|
|
|
displayX = col_w;
|
|
*client_width = display_width + 2 * col_w;
|
|
|
|
for (int i = 0; i < 6; ++i)
|
|
{
|
|
int h = (i == 5) ? (display_height - 5 * button_h) : button_h;
|
|
|
|
ScreenButton *left = &buttons[buttonCount++];
|
|
left->unit = anchorA + i;
|
|
left->amber = 1;
|
|
left->x = 0;
|
|
left->y = i * button_h;
|
|
left->w = col_w;
|
|
left->h = h;
|
|
|
|
ScreenButton *right = &buttons[buttonCount++];
|
|
right->unit = anchorB + i;
|
|
right->amber = 1;
|
|
right->x = displayX + displayW;
|
|
right->y = left->y;
|
|
right->w = col_w;
|
|
right->h = h;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::Paint()
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint((HWND) window, &ps);
|
|
|
|
RECT client;
|
|
GetClientRect((HWND) window, &client);
|
|
|
|
//---------------------------------------------------------------
|
|
// Double-buffered: compose the whole pane off screen and blit it
|
|
// in one operation, otherwise the clear-then-draw sequence
|
|
// flickers at the repaint cadence.
|
|
//---------------------------------------------------------------
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
|
|
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
|
|
|
|
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
|
|
// HALFTONE area-averages the downscale (the compact glass shows the
|
|
// full 640x480 canvas at ~half size); COLORONCOLOR dropped every other
|
|
// row/column, which shredded the 1-bit vector strokes and small text.
|
|
// HALFTONE requires the brush origin be set (MSDN).
|
|
SetStretchBltMode(mem, HALFTONE);
|
|
SetBrushOrgEx(mem, 0, 0, NULL);
|
|
StretchDIBits(
|
|
mem,
|
|
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(mem, &rect, fill);
|
|
DeleteObject(fill);
|
|
|
|
HBRUSH frame = CreateSolidBrush(
|
|
(i == pressedIndex) ? RGB(255, 255, 255) : RGB(48, 48, 48));
|
|
FrameRect(mem, &rect, frame);
|
|
DeleteObject(frame);
|
|
}
|
|
|
|
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
|
|
|
SelectObject(mem, old_surface);
|
|
DeleteObject(surface);
|
|
DeleteDC(mem);
|
|
|
|
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);
|
|
Repaint();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::MouseUp()
|
|
{
|
|
if (pressedIndex >= 0)
|
|
{
|
|
PadRIO::SetScreenButton(buttons[pressedIndex].unit, False);
|
|
pressedIndex = -1;
|
|
if (GetCapture() == (HWND) window)
|
|
{
|
|
ReleaseCapture();
|
|
}
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Paint NOW rather than queueing an invalidation: the game loop pumps
|
|
// one message per frame, and queued WM_PAINTs (lowest priority) starve
|
|
// behind it - panes would freeze on their first frame.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
void
|
|
MFDSplitView::SetPosition(int x, int y)
|
|
{
|
|
Check_Pointer(this);
|
|
if (window != NULL)
|
|
{
|
|
SetWindowPos((HWND) window, NULL, x, y, 0, 0,
|
|
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
|
|
void
|
|
MFDSplitView::Repaint()
|
|
{
|
|
Check_Pointer(this);
|
|
if (window != NULL)
|
|
{
|
|
RedrawWindow((HWND) window, NULL, NULL,
|
|
RDW_INVALIDATE | RDW_UPDATENOW);
|
|
}
|
|
}
|