Files
BT411/engine/MUNGA_L4/L4GLASSWIN.cpp
T
CydandClaude Opus 5 61563c9efe Glass cockpit refit: one mode resolver, one button geometry, and a cockpit that scales
Replicates the RP412 cockpit line into BT411's own architecture -- porting the
geometry and keeping our renderers, so BT_SHOT single-frame verification stays
intact.

MODE RESOLVER.  Where the secondary displays go was decided in TWO places with
duplicated precedence, and the boot banner read NEITHER -- it announced
"per-display cockpit windows" for every glass boot, surround included.  The
split had also broken BT_COCKPIT=0: documented as the dock-bottom opt-out, the
profile block converted it to BT_GLASS_PANELS=1, so the docked strip was
UNREACHABLE under the glass profile.  One resolver now, consumed by the banner,
the pad-panel decision and the window sizing; BT_GLASS_PANELS is explicit-only;
dock/window modes auto-raise BT_PAD_PANEL so the button field always has a home.

L4RIOBANK -- one button geometry.  Both renderers carried their own copy and had
drifted: an MFD button was 156x138 reaching under the glass in the exploded
window and a 76x24 sliver entirely OUTSIDE the glass in the surround.  One
module owns it now, both are consumers, placement stays per-renderer.  The pod's
under-glass rule (RP412 L4MFDVIEW): reach half the glass in behind the display,
leave a lamp strip clearing the edge, paint buttons first and imagery over --
so the lamp reads as a bar and practically the whole display is the press
target.  The strip scales off the display's SHORT axis (the map is portrait)
with a readable floor.  Retired L4GLASSWIN's three local placers and seven
layout constants.

LAMP FLASH DECODE was wrong [T1].  BTLampBrightnessOf returned max(state1,
state2) and blanked on the alternate phase; RIO::LampState (L4RIO.h [T0]) says
solid shows state 1 and flashing ALTERNATES the two.  Agrees only when one state
is Off -- true for the Panic lamp, which is why it survived -- but L4LAMP.cpp:252
commands flashFast + state1Dim + state2Bright, a dim->bright pulse that rendered
as a hard bright->off blink.  Three copies existed (l4vb16.h, L4GLASSWIN,
L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline.

THE COCKPIT SCALES.  The fixed canvas was stretched into whatever the client
area was, so a window dragged to a different shape squashed the instruments (the
projection was aspect-corrected in task #20; the panels never were).  Now one
uniform scale, centred, leftover black.  D3D9 applies it as a Present
destination rect, which DISCARD forbids -- so the WINDOWED swap effect becomes
COPY when the surround is up and multisampling is off.  The click mapping had to
follow (mapping against the full client drifts the hit test off every button by
the bar width), as did the world aspect (under a uniform scale it is the view
rect's own).  -fit / -windowed-fullscreen: borderless over the monitor.

 - ordering trap: the first WM_SIZE beats the device, so a -fit boot logged
   aspect=3.14 and applied it on frame 1.  The letterbox INTENT is decided in
   btl4main; L4VIDEO only confirms or withdraws it.

PLAYER-TUNABLE DISPLAYS.  BT_MFD_SCALE (+ _UL/_UC/_UR/_LL/_LR), BT_RADAR_SCALE,
BT_RADAR_POS (CENTER/LEFT/RIGHT/MIDLEFT/MIDRIGHT).  The surround BANDS derive
from the resolved sizes -- that is why the sizes could not stay constants: the
band a display hangs in has to grow with it or the canvas clips it.  100%
reproduces the historical L276 R276 T223 B336 exactly.  A corner map goes flush
to the CANVAS edge and the lower MFD slides beside it (measuring off the view
edge overlapped them by 232px).

MAP LEGEND GRID -- measured, not inherited.  scratchpad/measurelegend.py over a
native capture: top 3, cell 102, pitch 107 of 640.  RP412's map is 13 + 6x102 @
105 -- same cell height, different top and pitch, so its numbers do NOT
transfer.  Our old even division had the pitch right by luck and sat 3px high of
the labels.

environ.ini.  It was read ~300 lines into WinMain, AFTER the platform-profile
block had run its getenv()s -- so every setting the profile reads was silently
ignored FROM THE FILE and only worked as a real env var.  It also putenv()'d
comments verbatim.  Now loaded immediately after the first-breath line, comments
skipped, the real environment WINS over the file, and a fully documented default
is written on first run (the bindings.txt convention: untracked, so
extract-over-top never clobbers a player's settings).

VERIFICATION HARNESS (new, reusable): BT_RIOBANK_LOG=1 dumps every bank;
checkbank.py proves no address is SHADOWED (an address whose rect is covered by
earlier buttons is dead however big it looks -- the overlapping under-glass banks
make that a live hazard); clickbank.py posts a real click at every button centre.

Verified: 72/72 placed, 0 shadowed, 72/72 dispatched in BOTH modes, after a
resize, at 150%/135%, and at 75%+BOTTOMRIGHT; wide/tall drags and -fit
undistorted on a 3440x1440; exploded/dock/pod/dev un-regressed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 02:23:40 -05:00

809 lines
26 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 "l4riobank.h" // the shared button-bank geometry (with the surround)
#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;
}
//###########################################################################
// Window constants. The BUTTON GEOMETRY (how far a bank reaches under the
// glass, the lamp strip, the column pitch, the per-column nudge against the
// imagery legends) moved to L4RIOBANK on 2026-07-26 so the cockpit surround
// draws the identical field; what is left here is this window's own chrome.
//###########################################################################
enum
{
CellW = 58, // flight-controls cell (no glass behind it)
CellH = 28,
Gap = 4,
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 };
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: the ONE copy lives in l4vb16.h (BTLampBrightnessOf). This TU
// used to carry its own, as did L4PADPANEL -- three copies of the same
// formula, all three carrying the same dim-to-bright flash bug (see the
// header's 2026-07-26 note).
//###########################################################################
static int
LampBrightnessOf(int state, unsigned long tick)
{
return BTLampBrightnessOf(state, tick);
}
//###########################################################################
// Layout builders
//###########################################################################
// (The local PlaceCellAt / PlaceLine / PlaceRect placers retired 2026-07-26 --
// the geometry they carried now comes from L4RIOBANK, shared with the cockpit
// surround. AdoptBank below is the only thing that writes GButton rects.)
//---------------------------------------------------------------------------
// Copy a laid-out bank into this window at the given offset. A glass window
// passes -bounds so the bank's bounding box lands at the client origin (the
// bank reaches OUTSIDE the display, so its rects arrive with coordinates
// left of / above it); the flight window, which has no glass, passes 0.
//---------------------------------------------------------------------------
static void
AdoptBank(GWin &w, const BTRioBank &bank, ColorClass color, int dx, int dy)
{
for (int i = 0; i < bank.buttonCount &&
w.buttonCount < (int)(sizeof(w.buttons) / sizeof(w.buttons[0])); ++i)
{
const BTRioButton &src = bank.buttons[i];
GButton &b = w.buttons[w.buttonCount++];
b.address = src.address;
b.color = (src.colorClass == 1) ? ClrYellow
: (src.colorClass == 2) ? ClrBlue : color;
b.rect.left = src.x + dx;
b.rect.top = src.y + dy;
b.rect.right = src.x + dx + src.w;
b.rect.bottom = src.y + dy + src.h;
}
}
// 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).
// Geometry is L4RIOBANK's -- the under-glass rule, shared with the surround.
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;
BTRioBankMetrics metrics;
BTRioBankMetricsFor(MfdSurfW, MfdSurfH, &metrics);
BTRioBank bank;
BTRioBankLayout(BTRioBankMfd, 0, 0, MfdSurfW, MfdSurfH,
bankHi, 0, &metrics, 0, &bank);
BTRioBankDump(title, &bank);
int dx = -bank.boundsX, dy = -bank.boundsY;
AdoptBank(w, bank, ClrRed, dx, dy);
SetRect(&w.surfaceRect, dx, dy, dx + MfdSurfW, dy + MfdSurfH);
w.clientW = bank.boundsW;
w.clientH = bank.boundsH;
}
// 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;
// Left column 0x10-0x15, right 0x18-0x1D, foot row 0x16/0x17/0x1F/0x1E.
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
BTRioBankMetrics metrics;
BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics);
BTRioBank bank;
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
0x10, 0x18, &metrics, bottomAddrs, &bank);
BTRioBankDump("Secondary / Radar", &bank);
int dx = -bank.boundsX, dy = -bank.boundsY;
AdoptBank(w, bank, ClrYellow, dx, dy);
SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH);
w.clientW = bank.boundsW;
w.clientH = bank.boundsH;
}
// 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
// No glass to hide behind, so these cells carry their own size (see
// l4riobank.h) -- two 1-wide columns, eight rows each. Cell minus the
// 4px gap keeps the historical CellW/CellH PITCH and the 2px inset the
// old PlaceCellAt drew, so the window is pixel-identical.
BTRioBankMetrics metrics;
metrics.strip = 0;
metrics.gap = Gap;
metrics.cellW = CellW - Gap;
metrics.cellH = CellH - Gap;
BTRioBank bank;
memset(&bank, 0, sizeof(bank));
BTRioBankFlightGrid(2, 2, 0x38, 8, 1, 0, 0, &metrics, &bank); // throttle/panic/door/icom
BTRioBankFlightGrid(CellW + Gap + 2, 2, 0x40, 8, 1, 0, 0, &metrics, &bank); // joystick + fire
BTRioBankDump("Flight Controls", &bank);
AdoptBank(w, bank, ClrBlue, 0, 0);
// no translation here (the cells are already at their 2px inset); the
// client is the historical two-column extent
w.clientW = CellW + Gap + CellW;
w.clientH = 8 * CellH;
}
//###########################################################################
// 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; }
}