Displays: on-screen cockpit buttons + desktop plasma + the glass preset (steps 2d/2e)

NEW gated TUs: L4PADPANEL (GDI top-level window, all-W APIs: the pod button
banks -- upper/lower aux, 12 secondary, specials/hat/handle, 63 buttons --
clickable via PadRIO::SetScreenButton with lamp-lit faces from GetLampState,
10 Hz repaint; created by the PadRIO ctor on BT_PAD_PANEL=1) and L4PLASMAWIN
(PlasmaWindow : Video8BitBuffered -- the gauge renderer draws the 128x32
plasma into its pixelBuffer through the SAME code path as the serial device;
Update() blits orange-on-black at L4PLASMASCALE, default x4).  Gated seams:
L4GREND.cpp L4PLASMA=SCREEN branch; btl4main.cpp -platform glass preset
(PAD,KEYBOARD + BT_DEV_GAUGES + SCREEN plasma + panel; env always overrides;
non-glass builds log+fall back to DEV); run.cmd glass token.  MFD surfaces
need NO new code: the existing dock-bottom / BT_DEV_GAUGES_WINDOW=1 /
BT_DEV_GAUGES_DOCK=1 modes are the display story.

Verified live: -platform glass boots GLASS profile, panel + plasma windows
up, pad detected, dev gauges awake, mission loop clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-17 22:21:22 -05:00
co-authored by Claude Fable 5
parent 9f35a8034a
commit db9ac947f7
9 changed files with 629 additions and 8 deletions
+18
View File
@@ -8,6 +8,9 @@
#include "..\munga\mission.h"
#include "l4plasma.h"
#include "..\munga\notation.h"
#ifdef BT_GLASS
#include "l4plasmawin.h"
#endif
// #define LOCAL_TEST
@@ -100,8 +103,23 @@ L4GaugeRenderer::L4GaugeRenderer(bool windowed, int *secondaryIndex, int *aux1In
// Tell("Plasma display created on COM1\n");
// externalDisplay = new PlasmaDisplay(PCS_COM1);
//}
#ifdef BT_GLASS
//
// Glass cockpit (step 2d): L4PLASMA=SCREEN renders the 128x32
// plasma into a desktop window instead of a serial device.
// Any other value keeps the authentic serial path below.
//
if (_stricmp(plasma_string, "SCREEN") == 0)
{
Tell("Plasma display created on desktop window\n");
externalDisplay = new PlasmaWindow();
}
else
#endif
{
Tell("Plasma display created on "); Tell(plasma_string); Tell("\n");
externalDisplay = new PlasmaDisplay(plasma_string);
}
if (externalDisplay != NULL)
{
+314
View File
@@ -0,0 +1,314 @@
#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 <windows.h>
#include <stdio.h>
//###########################################################################
// 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;
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
//###########################################################################
//
// L4PADPANEL -- the on-screen cockpit button panel (BT_GLASS only).
//
// A developer-grade GDI window presenting the pod's RIO button banks as
// clickable, lamp-lit buttons: upper aux banks (0x20-0x37), the 12
// secondary-panel buttons (0x10-0x15, 0x18-0x1D), lower aux banks
// (0x00-0x0F), and the special/handle row (door, panic, reverse, trigger,
// hat, pinky/thumbs). Lamps render from PadRIO::GetLampState; clicks
// inject through PadRIO::SetScreenButton. Created by the PadRIO ctor when
// BT_PAD_PANEL=1 (the glass preset sets it); destroyed with the PadRIO.
//
// All window-management uses the W APIs (the L4VB16 mojibake lesson). The
// window lives on the app thread and repaints on a ~10 Hz timer.
//
//###########################################################################
void
BTPadPanel_Create();
void
BTPadPanel_Destroy();
+11
View File
@@ -8,6 +8,7 @@
//###########################################################################
#include "l4padrio.h"
#include "l4padpanel.h"
#include "l4ctrl.h"
#include <windows.h>
@@ -121,10 +122,20 @@ PadRIO::PadRIO():
activeInstance = this;
DEBUG_STREAM << "[padrio] PadRIO up (XInput probe pending; keyboard "
<< "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush;
//
// The on-screen cockpit button panel (step 2d) rides the device: the
// glass preset sets BT_PAD_PANEL=1.
//
if (getenv("BT_PAD_PANEL") != NULL && *getenv("BT_PAD_PANEL") != '0')
{
BTPadPanel_Create();
}
}
PadRIO::~PadRIO()
{
BTPadPanel_Destroy();
if (activeInstance == this)
{
activeInstance = NULL;
+167
View File
@@ -0,0 +1,167 @@
#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4PLASMAWIN -- the desktop plasma display (BT_GLASS only; this TU is only
// in the build when the gate is on -- see CMakeLists.txt).
// Design: L4PLASMAWIN.h.
//###########################################################################
#include "l4plasmawin.h"
#include <windows.h>
#include <stdlib.h>
#include <string.h>
static LRESULT CALLBACK
PlasmaWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_CLOSE:
ShowWindow(window, SW_HIDE); // hide only; the renderer owns it
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
//###########################################################################
PlasmaWindow::PlasmaWindow():
Video8BitBuffered(plasmaWidth, plasmaHeight),
window(NULL),
blitBuffer(NULL)
{
scale = 4;
const char *scale_string = getenv("L4PLASMASCALE");
if (scale_string != NULL)
{
int v = atoi(scale_string);
if (v >= 1 && v <= 16)
{
scale = v;
}
}
blitBuffer = new unsigned long[plasmaWidth * plasmaHeight];
memset(blitBuffer, 0, plasmaWidth * plasmaHeight * sizeof(unsigned long));
DEBUG_STREAM << "[plasmawin] desktop plasma display up (scale x"
<< scale << ")\n" << std::flush;
}
PlasmaWindow::~PlasmaWindow()
{
if (window != NULL)
{
DestroyWindow((HWND)window);
window = NULL;
}
delete[] blitBuffer;
blitBuffer = NULL;
}
void
PlasmaWindow::EnsureWindow()
{
if (window != NULL)
{
return;
}
WNDCLASSW window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.lpfnWndProc = PlasmaWndProc;
window_class.hInstance = GetModuleHandleW(NULL);
window_class.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
window_class.lpszClassName = L"BTPlasmaWnd";
RegisterClassW(&window_class);
RECT frame = { 0, 0, plasmaWidth * scale, plasmaHeight * scale };
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
AdjustWindowRect(&frame, style, FALSE);
window = CreateWindowW(
L"BTPlasmaWnd", L"BattleTech - Plasma",
style,
40, 480,
frame.right - frame.left, frame.bottom - frame.top,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (window == NULL)
{
DEBUG_STREAM << "[plasmawin] CreateWindow failed\n" << std::flush;
return;
}
ShowWindow((HWND)window, SW_SHOWNOACTIVATE);
}
//
// The per-frame external-display flush (the gauge renderer calls this
// where the serial path streams changed lines out the COM port). Blits
// the whole 8-bit buffer as orange-on-black. Returns False = done (no
// multi-call transfer in progress, unlike the serial path).
//
Logical
PlasmaWindow::Update(Logical force_all)
{
(void)force_all;
EnsureWindow();
if (window == NULL || pixelBuffer == NULL ||
pixelBuffer->Data.MapPointer == NULL)
{
return False;
}
//
// 8-bit index -> BGRA: any lit index renders plasma orange, intensity
// stepped a little by index so shaded glyphs keep their shape. The
// engine's graphics coordinate system puts (0,0) bottom-left; the
// window is top-down, so flip Y in the copy.
//
const Byte *source = pixelBuffer->Data.MapPointer;
for (int y = 0; y < plasmaHeight; ++y)
{
const Byte *line = source + (plasmaHeight - 1 - y) * plasmaWidth;
unsigned long *destination = blitBuffer + y * plasmaWidth;
for (int x = 0; x < plasmaWidth; ++x)
{
Byte index = line[x];
if (index == 0)
{
destination[x] = 0x00180800; // near-black
}
else
{
unsigned long intensity = 160 + (index % 6) * 19; // 160..255
destination[x] =
(intensity << 16) | // R
((intensity * 140 / 255) << 8); // G (amber)
}
}
}
BITMAPINFO info;
memset(&info, 0, sizeof(info));
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = plasmaWidth;
info.bmiHeader.biHeight = -plasmaHeight; // top-down
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
HDC dc = GetDC((HWND)window);
if (dc != NULL)
{
RECT client;
GetClientRect((HWND)window, &client);
StretchDIBits(dc,
0, 0, client.right, client.bottom,
0, 0, plasmaWidth, plasmaHeight,
blitBuffer, &info, DIB_RGB_COLORS, SRCCOPY);
ReleaseDC((HWND)window, dc);
}
return False;
}
void
PlasmaWindow::WaitForUpdate()
{
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
//###########################################################################
//
// L4PLASMAWIN -- the desktop plasma display (BT_GLASS only).
//
// PlasmaWindow renders the pod's 128x32 serial plasma marquee (pilot-name /
// rank display) into a small orange-on-black desktop window instead of a
// serial device. It derives from Video8BitBuffered exactly like the serial
// PlasmaDisplay, so the L4GaugeRenderer draws into its 8-bit pixelBuffer
// with the same code path; Update() (the per-frame external-display flush)
// blits the buffer to the window at L4PLASMASCALE x zoom (default 4).
// Selected with L4PLASMA=SCREEN (the gated branch in L4GREND.cpp); any
// other L4PLASMA value keeps the serial device.
//
//###########################################################################
#include "l4vb8.h"
class PlasmaWindow :
public Video8BitBuffered
{
public:
enum
{
plasmaWidth = 128,
plasmaHeight = 32
};
PlasmaWindow();
~PlasmaWindow();
Logical
Update(Logical force_all);
void
WaitForUpdate();
protected:
void
EnsureWindow();
void
*window; // HWND (kept void* -- no <windows.h> in headers)
int
scale;
unsigned long
*blitBuffer; // plasmaWidth*plasmaHeight BGRA staging
};