Files
BT411/engine/MUNGA_L4/L4GLASSWIN.cpp
T
arcattackandClaude Fable 5 5ba5697a08 Fix glass panel dead-button crash: null-init MessageHandlerSet gap slots (Gitea #18)
Clicking a per-display glass Engineering panel button (0x21 on an Eng page)
hard-crashed via a wild call through an uninitialised pointer (eip=cdcdcdcd
debug / 0x01048748 release), zero btl4 frames above Receiver::Receive.

Root cause is the dense message-handler-table gap hazard (reconstruction-
gotchas #11), the message-side twin of the attribute-table one -- NOT a /FORCE
unresolved external (the glass link log carried only the known-benign mech3.obj
CreateStreamedSubsystem set). Receiver::MessageHandlerSet::Find(id) returns
messageHandlers[id-1].entryHandler for any id <= entryCount with no populated-
check, and Receive() calls it when != NullHandler. The streamed Eng-page .CTL
dispatches subsystem msg id 3/0xb to whatever subsystem is shown; a weapon
(Emitter) registers only PoweredSubsystem 4-8 + MechWeapon 9-10, so id 3 is a
gap below entryCount 10. Build did new HandlerEntry[entryCount] and left gaps
uninitialised -> (this->*garbage)(msg). The 1995 binary has the identical non-
zeroing new[] (part_002.c Build) and survived only on fresh-heap-zero luck: a
weapon receiving id 3 was always meant to IGNORE it (id 3 = a Condenser/
Reservoir action in a different subsystem branch; the uniform Eng-page button
template makes buttons for unimplemented actions authentically inert). The new
per-display windows just made the id reachable live.

Fix (engine, class-wide, faithful): Build now null-inits every slot
(entryID=0/entryName=""/entryHandler=NullHandler) before copying inherited /
placing supplied, so a gap is deterministically NullHandler -> Receive drops it
(the authentic "Receiver ignores unhandled messages"), with an empty never-NULL
name so the name-based Find() strcmp can't deref a gap. Correct dispatch
contract, not a glass-path guard -> protects the whole dead-button class (#14).

Also lands the [glasswin] CLICK crash-forensics log (names the button before
dispatch).

Verified under cdb: click-soaked every MFD bank (Engineering/L+R Weapons/Heat/
Comm, ~130 clicks through Quad<->Eng page cycles) -> zero AVs, reconstructed
mapper preset selects still fire ([mode] preset (1,1)/(2,1)...); pod build +
solo mission un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 15:59:37 -05:00

843 lines
27 KiB
C++

#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4GLASSWIN -- per-display glass cockpit windows. Design + layout rules:
// l4glasswin.h. Each pod secondary display is its OWN GDI window carrying
// the display surface (CPU-blit from the shared gauge buffer) with its RIO
// button bank placed around it; a Flight Controls window hosts the
// no-display banks. Modeled on L4PADPANEL (GDI cells + lamp decode) and
// L4PLASMAWIN (CPU pixelBuffer -> StretchDIBits), so no D3D is involved.
//###########################################################################
#include "l4glasswin.h"
#include "l4padrio.h"
#include "l4vb16.h"
#include "../munga/gaugrend.h"
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// The main game window (for pod-faithful placement) is reached via
// BTResolveMainWindow() (l4vb16.h) -- NOT the `ghWnd` global directly, which this
// newly-added engine TU can bind as the NULL /FORCE-duplicate copy.
//###########################################################################
// Mode gate
//###########################################################################
int
BTGlassPanelsActive()
{
static int v = -1;
if (v < 0)
{
const char *e = getenv("BT_GLASS_PANELS");
v = (e != NULL && e[0] != '0') ? 1 : 0;
}
return v;
}
//###########################################################################
// Layout constants (tunable -- Cyd flagged refinements to the exact button
// sub-positions + the yellow left/right split).
//###########################################################################
enum
{
Margin = 0, // no padding around the edges (surface fills the window)
TitleH = 0, // no in-client title band (the OS caption labels the window)
CellW = 58, // default button cell (flight-controls window)
CellH = 28,
RedCellH = 128, // red MFD buttons -- tall, extend ~100px into the display
EdgeProtrude = 10, // ...and this far OUT past the display edge
Gap = 4,
RailW = 40, // radar side-rail button thickness (rotated 90deg, fill height)
RailSlotAdd = 1, // rail pitch = 640/6 + 1 = 107 -> 104px buttons + 3px gaps = 639 (fits 640)
RailGap = 3, // vertical gap between rail buttons (own value; red/horizontals use Gap)
RedOffsetX = 5, // nudge the red MFD buttons right to line up with the imagery
MfdSurfW = 640, MfdSurfH = 480, // mono MFD surface at native 640x480
RadarSurfW = 480, RadarSurfH = 640, // portrait secondary/radar CRT at native (rotated 640x480)
RepaintTimerId = 1,
RepaintMilliseconds = 62, // == L4PADPANEL (half the fastest lamp half-period)
MfdMonoTint = 0x00FFFFFF // mono MFD phosphor colour (BGRA); tunable
};
enum ColorClass { ClrRed, ClrYellow, ClrBlue };
// Per-column horizontal nudge (px) for the 4 red MFD buttons, left->right, to line
// them up with the (unevenly-spaced) imagery legends. Tunable -- Cyd is dialing
// these in against DISPLAY / PROGRAM / etc. Applied on top of RedOffsetX.
static const int kRedColDX[4] = { -5, -4, 0, 0 };
struct GButton
{
int address;
ColorClass color;
RECT rect; // client-space
};
struct GWin
{
const char *title;
HWND hwnd;
// Surface (portPrimary == NULL -> no surface, e.g. Flight Controls).
const char *portPrimary;
const char *portAlt; // the Eng<n> preset plane sibling, or NULL
int monoTint; // -1 = palette-expand (radar); else BGRA mono tint
int rotate; // 0 / 1 / 3 (portrait secondary)
RECT surfaceRect;
GButton buttons[24];
int buttonCount;
int showLabels; // draw the hex/name on each button? (Flight Controls only)
int clientW, clientH;
int frameW, frameH;
int wantX, wantY;
};
static GWin gWins[8];
static int gWinCount = 0;
static int pressedAddress = -1;
static unsigned char latched[128];
static int gRadarRot = 3; // BT_GAUGE_SEC_ROT (default CW, upright)
static bool sRepositionedToMain = false;
static unsigned long *gStage = NULL; // 640*480 BGRA staging (shared, main-thread)
static HFONT titleFont = NULL;
static HFONT buttonFont = NULL;
static HFONT subFont = NULL;
//###########################################################################
// Named handle/joystick buttons (the Flight Controls window)
//###########################################################################
struct NamedButton { int address; const char *name; };
static const NamedButton namedButtons[] =
{
{ 0x3D, "Panic" }, { 0x3F, "Throttle" },
{ 0x40, "Main" }, { 0x41, "Hat Bk" }, { 0x42, "Hat Up" },
{ 0x43, "Hat R" }, { 0x44, "Hat L" }, { 0x45, "Pinky" },
{ 0x46, "Middle"}, { 0x47, "Upper" },
};
enum { NamedButtonCount = sizeof(namedButtons) / sizeof(namedButtons[0]) };
static const char *
PhysicalName(int address)
{
for (int i = 0; i < NamedButtonCount; ++i)
if (namedButtons[i].address == address)
return namedButtons[i].name;
return NULL;
}
//###########################################################################
// Lamp decode (identical to L4PADPANEL::LampBrightnessOf -- RIOBase::LampState)
//###########################################################################
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;
}
//###########################################################################
// Layout builders
//###########################################################################
static void
PlaceCellAt(GWin &w, int address, int x, int y, ColorClass color)
{
GButton &b = w.buttons[w.buttonCount++];
b.address = address;
b.color = color;
b.rect.left = x + 2;
b.rect.top = y + 2;
b.rect.right = x + CellW - 2;
b.rect.bottom = y + CellH - 2;
}
static void
PlaceLine(GWin &w, int startAddr, int count, int step,
int x, int y, int horizontal, ColorClass color)
{
for (int i = 0; i < count; ++i)
{
int cx = horizontal ? (x + i * CellW) : x;
int cy = horizontal ? y : (y + i * CellH);
PlaceCellAt(w, startAddr + i * step, cx, cy, color);
}
}
// Place a button with an explicit size (custom-shaped cells: wide red buttons,
// tall rotated radar rails).
static void
PlaceRect(GWin &w, int address, int x, int y, int cw, int ch, ColorClass color)
{
GButton &b = w.buttons[w.buttonCount++];
b.address = address;
b.color = color;
b.rect.left = x;
b.rect.top = y;
b.rect.right = x + cw;
b.rect.bottom = y + ch;
}
// An MFD display: 8 red buttons split 4 above / 4 below the surface (top row =
// the high 4 addresses descending, matching the L4PADPANEL cluster order).
static void
BuildMfd(GWin &w, const char *title, const char *portP, const char *portA, int bankHi)
{
memset(&w, 0, sizeof(w));
w.title = title;
w.portPrimary = portP;
w.portAlt = portA;
w.monoTint = MfdMonoTint;
w.rotate = 0;
int surfX = Margin;
int surfY = Margin + TitleH + EdgeProtrude; // room for the top buttons to poke out
// Buttons OVERLAY the display: 4 tile the full width, reaching RedCellH INTO the
// display and EdgeProtrude OUT past the edge (top row + bottom row; hi addresses
// descending -- L4PADPANEL order).
int slotW = MfdSurfW / 4;
for (int i = 0; i < 4; ++i)
{
int x = surfX + i * slotW + RedOffsetX + kRedColDX[i];
PlaceRect(w, bankHi - i, x, surfY - EdgeProtrude, slotW - Gap, RedCellH + EdgeProtrude, ClrRed); // top
PlaceRect(w, bankHi - 4 - i, x, surfY + MfdSurfH - RedCellH, slotW - Gap, RedCellH + EdgeProtrude, ClrRed); // bottom
}
SetRect(&w.surfaceRect, surfX, surfY, surfX + MfdSurfW, surfY + MfdSurfH);
w.clientW = Margin + MfdSurfW + Margin;
w.clientH = surfY + MfdSurfH + EdgeProtrude + Margin;
}
// The secondary/radar display: 12 yellow Secondary buttons split left / right
// of the portrait surface (0x10-0x15 left, 0x16-0x1B right).
static void
BuildRadar(GWin &w)
{
memset(&w, 0, sizeof(w));
w.title = "Secondary / Radar";
w.portPrimary = "sec";
w.portAlt = NULL;
w.monoTint = -1; // palette
w.rotate = gRadarRot;
int surfX = Margin + EdgeProtrude; // room for the left rail to poke out
int surfY = Margin + TitleH;
// Rails: big clickable strips that reach RailW INTO the display (hidden under the
// imagery) and poke EdgeProtrude OUT past the side edge (the lamp light).
// Left 0x10..0x15, right 0x18 (top)..0x1D (bottom). (0x15 kept on the left rail
// -- Cyd listed 0x10-0x14; move it if wanted.)
int railCount = 6;
int slotH = RadarSurfH / railCount + RailSlotAdd;
int leftX = surfX - EdgeProtrude;
int rightX = surfX + RadarSurfW - RailW;
for (int i = 0; i < railCount; ++i)
{
int y = surfY + i * slotH;
PlaceRect(w, 0x10 + i, leftX, y, EdgeProtrude + RailW, slotH - RailGap, ClrYellow);
PlaceRect(w, 0x18 + i, rightX, y, RailW + EdgeProtrude, slotH - RailGap, ClrYellow);
}
SetRect(&w.surfaceRect, surfX, surfY, surfX + RadarSurfW, surfY + RadarSurfH);
// Bottom row: 16, 17, 1F, 1E (left to right), BETWEEN the rails -- reaches CellH
// up into the display, pokes EdgeProtrude out the bottom.
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
int botLeft = surfX + RailW;
int botSpan = RadarSurfW - 2 * RailW;
int botW = botSpan / 4;
int botY = surfY + RadarSurfH - CellH;
for (int i = 0; i < 4; ++i)
PlaceRect(w, bottomAddrs[i], botLeft + i * botW, botY, botW - Gap, CellH + EdgeProtrude, ClrYellow);
w.clientW = surfX + RadarSurfW + EdgeProtrude + Margin;
w.clientH = surfY + RadarSurfH + EdgeProtrude + Margin;
}
// The Flight Controls window: the no-display banks -- throttle/panic/door/icom
// (0x38-0x3F) and joystick/fire (0x40-0x47), two neutral columns.
static void
BuildFlight(GWin &w)
{
memset(&w, 0, sizeof(w));
w.title = "Flight Controls";
w.portPrimary = NULL;
w.showLabels = 1; // the only window that shows hex/name on its buttons
int topY = Margin + TitleH;
int col1 = Margin;
int col2 = Margin + CellW + Gap;
PlaceLine(w, 0x38, 8, +1, col1, topY, 0, ClrBlue); // throttle / panic / door / icom
PlaceLine(w, 0x40, 8, +1, col2, topY, 0, ClrBlue); // joystick + fire buttons
w.clientW = col2 + CellW + Margin;
w.clientH = topY + 8 * CellH + Margin;
}
//###########################################################################
// Placement -- pod-faithful ring around the main game window
//###########################################################################
static void
WorkArea(RECT *wa)
{
if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, wa, 0))
{
wa->left = 0; wa->top = 0;
wa->right = GetSystemMetrics(SM_CXSCREEN);
wa->bottom = GetSystemMetrics(SM_CYSCREEN);
}
}
static bool
GetMainRect(RECT *out)
{
HWND mh = (HWND)BTResolveMainWindow();
if (mh != NULL && IsWindow(mh) && GetWindowRect(mh, out))
return true;
// Fallback before the main window exists: assume 800x600 centered.
RECT wa; WorkArea(&wa);
int cw = 800, ch = 600;
out->left = (wa.left + wa.right - cw) / 2;
out->top = (wa.top + wa.bottom - ch) / 2;
out->right = out->left + cw;
out->bottom = out->top + ch;
return false;
}
static void
ComputeLayout()
{
RECT m; GetMainRect(&m);
RECT wa; WorkArea(&wa);
int gap = 10;
int cx = (m.left + m.right) / 2;
int cy = (m.top + m.bottom) / 2;
for (int i = 0; i < gWinCount; ++i)
{
GWin &w = gWins[i];
int fw = w.frameW, fh = w.frameH;
int x = m.left, y = m.top;
switch (i)
{
case 0: x = m.left; y = m.top - fh - gap; break; // Heat (UL)
case 1: x = cx - fw / 2; y = m.top - fh - gap; break; // Mfd2 (UC)
case 2: x = m.right - fw; y = m.top - fh - gap; break; // Comm (UR)
case 3: x = m.left - fw - gap; y = m.bottom - fh; break; // Mfd1 (LL)
case 4: x = m.right + gap; y = m.bottom - fh; break; // Mfd3 (LR)
case 5: x = cx - fw / 2; y = m.bottom + gap; break; // Radar (center)
case 6: x = m.left - fw - gap; y = cy - fh / 2; break; // Flight (left of seat)
}
// Clamp fully on-screen (small desktops -> panels pile at the edges).
if (x + fw > wa.right) x = wa.right - fw;
if (y + fh > wa.bottom) y = wa.bottom - fh;
if (x < wa.left) x = wa.left;
if (y < wa.top) y = wa.top;
w.wantX = x;
w.wantY = y;
}
}
static void
ApplyLayout()
{
for (int i = 0; i < gWinCount; ++i)
if (gWins[i].hwnd != NULL)
SetWindowPos(gWins[i].hwnd, HWND_TOPMOST, gWins[i].wantX, gWins[i].wantY,
0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
}
//###########################################################################
// Painting
//###########################################################################
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 int sGlassLog = -1;
static unsigned long sBlitCalls = 0;
static void
BlitSurface(HDC dc, GWin *w)
{
if (sGlassLog < 0)
sGlassLog = getenv("BT_GLASS_LOG") ? 1 : 0;
int log = sGlassLog && ((sBlitCalls++ % 240) == 0);
// Reach the renderer via BTResolveGaugeRenderer (l4vb16.h; DEFINED in
// game/btl4main.cpp off the real btl4App pointer), NOT the `application` global
// directly -- a fresh engine TU can bind the NULL /FORCE-duplicate copy.
GaugeRenderer *gr = BTResolveGaugeRenderer();
if (gr == NULL)
{
if (log) DEBUG_STREAM << "[glass] '" << w->title << "' gauge renderer not ready\n" << std::flush;
return;
}
L4GraphicsPort *port = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->portPrimary));
if (w->portAlt != NULL)
{
L4GraphicsPort *alt = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->portAlt));
// Show whichever of the Mfd<n>/Eng<n> pair is the live (non-blank) plane,
// exactly like the pod monitor + BTDrawGaugeSurfaces (Gitea #9).
bool primaryBlank = (port == NULL) ||
(port->GetEnableID() == L4GraphicsPort::BlankColor);
if (primaryBlank && alt != NULL &&
alt->GetEnableID() != L4GraphicsPort::BlankColor)
port = alt;
else if (port == NULL)
port = alt;
}
if (port == NULL)
{
if (log) DEBUG_STREAM << "[glass] '" << w->title << "' port '" << w->portPrimary
<< "' NULL (not installed)\n" << std::flush;
return;
}
SVGA16 *svga = static_cast<SVGA16*>(port->graphicsDisplay);
if (svga == NULL || gStage == NULL)
{
if (log) DEBUG_STREAM << "[glass] '" << w->title << "' svga=" << (void*)svga
<< " stage=" << (void*)gStage << "\n" << std::flush;
return;
}
// Surface expand. BT_GLASS_MFD_PAL (experiment): render the mono MFD windows
// through the palette LUT (low byte, secondary palette) instead of the 1-bit
// mono tint, to compare. NB the MFD's own graphics live in a HIGH bit-plane
// the 256-entry palette can't index, so this shows the shared low-byte content.
int mask = port->GetBitMask();
int palId = port->paletteID;
int tint = w->monoTint;
static int sMfdPal = -1;
if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
if (sMfdPal && tint >= 0)
{
tint = -1; // palette-expand
mask = 0xFF; // index the low (palette) byte
palId = SVGA16::SecondaryPalette;
}
int ow = 0, oh = 0;
svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh);
if (log)
{
int nz = 0, n = ow * oh;
for (int i = 0; i < n; ++i) if (gStage[i] != 0) nz++;
DEBUG_STREAM << "[glass] '" << w->title << "' port=" << w->portPrimary
<< " mask=0x" << std::hex << port->GetBitMask() << std::dec
<< " pal=" << port->paletteID << " enable=" << port->GetEnableID()
<< " ow=" << ow << " oh=" << oh << " nonzero=" << nz << "/" << n
<< "\n" << std::flush;
}
if (ow <= 0 || oh <= 0)
return;
BITMAPINFO info;
memset(&info, 0, sizeof(info));
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = ow;
info.bmiHeader.biHeight = -oh; // top-down
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
const RECT &r = w->surfaceRect;
SetStretchBltMode(dc, HALFTONE);
SetBrushOrgEx(dc, 0, 0, NULL);
StretchDIBits(dc,
r.left, r.top, r.right - r.left, r.bottom - r.top,
0, 0, ow, oh,
gStage, &info, DIB_RGB_COLORS, SRCCOPY);
}
static void
PaintGlass(HWND window, GWin *w)
{
PAINTSTRUCT paint;
HDC winDC = BeginPaint(window, &paint);
unsigned long tick = GetTickCount();
RECT client;
GetClientRect(window, &client);
// Double-buffer the whole frame (== L4PADPANEL issue #13).
HDC dc = CreateCompatibleDC(winDC);
HBITMAP backing = CreateCompatibleBitmap(winDC,
client.right - client.left, client.bottom - client.top);
HBITMAP oldBacking = (HBITMAP)SelectObject(dc, backing);
HBRUSH background = CreateSolidBrush(RGB(24, 24, 24));
FillRect(dc, &client, background);
DeleteObject(background);
SetBkMode(dc, TRANSPARENT);
// (No in-client title -- the OS window caption labels each window.)
// Buttons are drawn FIRST (UNDER the display). Each is a big clickable rect;
// the surface blit further down masks the part inside the display, leaving only
// the bit that protrudes past the display edge visible -- a solid lamp light.
// (The Flight Controls window has no surface, so its buttons show in full.)
for (int i = 0; i < w->buttonCount; ++i)
{
const GButton &b = w->buttons[i];
const RECT &r = b.rect;
int held = (b.address == pressedAddress) || latched[b.address & 0x7F];
int shade = LampBrightnessOf(PadRIO::GetLampState(b.address), tick);
if (held)
shade = 3;
COLORREF fill, text_color;
if (b.color == ClrYellow)
{
fill = (shade == 3) ? RGB(245, 210, 60)
: (shade != 0) ? RGB(140, 118, 38)
: RGB(70, 60, 24);
text_color = RGB(0, 0, 0);
}
else if (b.color == ClrBlue)
{
fill = (shade == 3) ? RGB(205, 228, 255)
: (shade != 0) ? RGB(110, 135, 180)
: RGB(70, 86, 120);
text_color = RGB(0, 0, 0);
}
else // ClrRed (MFD)
{
fill = (shade == 3) ? RGB(230, 70, 70)
: (shade != 0) ? RGB(120, 50, 50)
: RGB(64, 40, 40);
text_color = RGB(255, 255, 255);
}
// Solid lamp fill + a bright border. Under the imagery, only the protruding
// edge shows once the surface is blitted on top.
{
COLORREF bright = (b.color == ClrYellow) ? RGB(245, 210, 60)
: (b.color == ClrBlue) ? RGB(205, 228, 255)
: RGB(230, 70, 70);
HBRUSH face = CreateSolidBrush(fill);
FillRect(dc, &r, face);
DeleteObject(face);
HPEN pen = CreatePen(PS_SOLID, 1, bright);
HGDIOBJ oldPen = SelectObject(dc, pen);
HGDIOBJ oldBr = SelectObject(dc, GetStockObject(NULL_BRUSH));
Rectangle(dc, r.left, r.top, r.right, r.bottom);
SelectObject(dc, oldBr);
SelectObject(dc, oldPen);
DeleteObject(pen);
}
// Labels only on the Flight Controls window; MFD/radar buttons are bare.
if (w->showLabels)
{
char hex[8];
sprintf(hex, "%02X", b.address);
const char *name = PhysicalName(b.address);
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; momentary held = white outline (== L4PADPANEL).
if (latched[b.address & 0x7F])
{
HBRUSH gold = CreateSolidBrush(RGB(255, 215, 0));
RECT o = r;
FrameRect(dc, &o, gold);
DeleteObject(gold);
}
else if (held)
{
RECT o = r;
FrameRect(dc, &o, (HBRUSH)GetStockObject(WHITE_BRUSH));
}
}
// Surface (imagery) LAST -- ON TOP of the buttons, masking their inside so only
// the protruding lamp edges show. (No-op for the Flight Controls window.)
if (w->portPrimary != NULL)
{
BlitSurface(dc, w);
HPEN pen = CreatePen(PS_SOLID, 1, RGB(70, 70, 70));
HPEN oldPen = (HPEN)SelectObject(dc, pen);
HBRUSH oldBrush = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH));
Rectangle(dc, w->surfaceRect.left - 1, w->surfaceRect.top - 1,
w->surfaceRect.right + 1, w->surfaceRect.bottom + 1);
SelectObject(dc, oldBrush);
SelectObject(dc, oldPen);
DeleteObject(pen);
}
BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top,
dc, 0, 0, SRCCOPY);
SelectObject(dc, oldBacking);
DeleteObject(backing);
DeleteDC(dc);
EndPaint(window, &paint);
}
//###########################################################################
// WndProc / input
//###########################################################################
static int
HitTest(GWin *w, int x, int y)
{
for (int i = 0; i < w->buttonCount; ++i)
{
const RECT &r = w->buttons[i].rect;
if (x >= r.left && x < r.right && y >= r.top && y < r.bottom)
return w->buttons[i].address;
}
return -1;
}
static LRESULT CALLBACK
GlassWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
GWin *w = (GWin*)GetWindowLongPtrW(window, GWLP_USERDATA);
switch (message)
{
case WM_PAINT:
if (w != NULL) { PaintGlass(window, w); return 0; }
break;
case WM_ERASEBKGND:
return 1; // PaintGlass fills the frame (double-buffered)
case WM_TIMER:
if (wparam == RepaintTimerId)
{
// One-shot: once the real main window is up, snap the ring onto it.
HWND mh = (HWND)BTResolveMainWindow();
if (!sRepositionedToMain && mh != NULL && IsWindow(mh))
{
ComputeLayout();
ApplyLayout();
sRepositionedToMain = true;
}
InvalidateRect(window, NULL, FALSE);
}
return 0;
case WM_LBUTTONDOWN:
if (w != NULL)
{
int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam));
if (a >= 0)
{
// crash forensics (always-on, flushed BEFORE dispatch): if a
// click kills the process, the last log line names the button.
DEBUG_STREAM << "[glasswin] CLICK '" << (w->title ? w->title : "?")
<< "' addr=0x" << std::hex << a << std::dec
<< " at(" << (int)(short)LOWORD(lparam) << ","
<< (int)(short)HIWORD(lparam) << ")"
<< (latched[a & 0x7F] ? " unlatch" : " press")
<< "\n" << std::flush;
if (latched[a & 0x7F])
{
latched[a & 0x7F] = 0;
PadRIO::SetScreenButton(a, 0);
}
else
{
pressedAddress = a;
SetCapture(window);
PadRIO::SetScreenButton(a, 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:
if (w != NULL)
{
int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam));
if (a >= 0)
{
DEBUG_STREAM << "[glasswin] CLICK '" << (w->title ? w->title : "?")
<< "' addr=0x" << std::hex << a << std::dec
<< " latch-toggle\n" << std::flush;
latched[a & 0x7F] = latched[a & 0x7F] ? 0 : 1;
PadRIO::SetScreenButton(a, latched[a & 0x7F]);
InvalidateRect(window, NULL, FALSE);
}
}
return 0;
case WM_CLOSE:
ShowWindow(window, SW_HIDE); // hide; the device owns the lifetime
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
//###########################################################################
// Create / destroy
//###########################################################################
void
BTGlassPanels_Create()
{
if (gWinCount != 0)
return; // already up
// BT_GAUGE_SEC_ROT: the portrait-CRT unrotation direction (shared with the
// D3D dev path; default 3 = CW = user-verified upright).
{
const char *rv = getenv("BT_GAUGE_SEC_ROT");
gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
}
memset(latched, 0, sizeof(latched));
pressedAddress = -1;
sRepositionedToMain = false;
if (gStage == NULL)
gStage = new unsigned long[640 * 480];
// Build the seven windows. Order fixes the ComputeLayout roles (0..6).
BuildMfd (gWins[0], "Heat MFD", "Heat", NULL, 0x2F); // UL 0x28-0x2F
BuildMfd (gWins[1], "Engineering", "Mfd2", "Eng2", 0x27); // UC 0x20-0x27
BuildMfd (gWins[2], "Comm MFD", "Comm", NULL, 0x37); // UR 0x30-0x37
BuildMfd (gWins[3], "Left Weapons", "Mfd1", "Eng1", 0x0F); // LL 0x08-0x0F
BuildMfd (gWins[4], "Right Weapons", "Mfd3", "Eng3", 0x07); // LR 0x00-0x07
BuildRadar(gWins[5]); // center 0x10-0x1B
BuildFlight(gWins[6]); // 0x38-0x47
gWinCount = 7;
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");
WNDCLASSW wc;
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = GlassWndProc;
wc.hInstance = GetModuleHandleW(NULL);
wc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
wc.lpszClassName = L"BTGlassWin";
RegisterClassW(&wc);
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
// Frame sizes first (ComputeLayout positions by frame dims).
for (int i = 0; i < gWinCount; ++i)
{
RECT fr = { 0, 0, gWins[i].clientW, gWins[i].clientH };
AdjustWindowRect(&fr, style, FALSE);
gWins[i].frameW = fr.right - fr.left;
gWins[i].frameH = fr.bottom - fr.top;
}
ComputeLayout();
for (int i = 0; i < gWinCount; ++i)
{
GWin &w = gWins[i];
WCHAR wtitle[64];
int n = 0;
for (const char *s = w.title; *s && n < 63; ++s) wtitle[n++] = (WCHAR)*s;
wtitle[n] = 0;
// Layered + topmost (== L4PADPANEL): DWM composites the panel independently
// of the game's D3D swap chain, ending GDI-vs-D3D presentation strobing.
w.hwnd = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_LAYERED,
L"BTGlassWin", wtitle, style,
w.wantX, w.wantY, w.frameW, w.frameH,
NULL, NULL, GetModuleHandleW(NULL), NULL);
if (w.hwnd == NULL)
{
DEBUG_STREAM << "[glasswin] CreateWindow failed for '" << w.title
<< "'\n" << std::flush;
continue;
}
SetWindowLongPtrW(w.hwnd, GWLP_USERDATA, (LONG_PTR)&w);
SetLayeredWindowAttributes(w.hwnd, 0, 255, LWA_ALPHA);
SetTimer(w.hwnd, RepaintTimerId, RepaintMilliseconds, NULL);
ShowWindow(w.hwnd, SW_SHOWNOACTIVATE);
}
DEBUG_STREAM << "[glasswin] per-display cockpit up (" << gWinCount
<< " windows: 5 MFD + radar + flight)\n" << std::flush;
}
void
BTGlassPanels_Destroy()
{
for (int i = 0; i < gWinCount; ++i)
{
if (gWins[i].hwnd != NULL)
{
KillTimer(gWins[i].hwnd, RepaintTimerId);
DestroyWindow(gWins[i].hwnd);
gWins[i].hwnd = NULL;
}
}
gWinCount = 0;
if (titleFont) { DeleteObject(titleFont); titleFont = NULL; }
if (buttonFont) { DeleteObject(buttonFont); buttonFont = NULL; }
if (subFont) { DeleteObject(subFont); subFont = NULL; }
if (gStage) { delete[] gStage; gStage = NULL; }
}