#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 #include #include #include // 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 = 0x0021FF42 // standard phosphor green rgb(33,255,66); // 0x00RRGGBB, env BT_COCKPIT_TINT overrides }; // // The mono-MFD phosphor tint for the glass-panel windows: the standard green // (MfdMonoTint), or BT_COCKPIT_TINT=RRGGBB (the SAME env the cockpit surround // honours -- L4VB16.cpp CkTint). Read once. ExpandPlaneToBGRA wants the tint // as 0x00RRGGBB (its palette path builds Red<<16 | Green<<8 | Blue), so the // hex is used verbatim -- no R5G6B5 packing (that is the surround's D3D path). // static unsigned long GlassMfdTint() { static unsigned long t = 0; if (t == 0) { const char *e = getenv("BT_COCKPIT_TINT"); if (e != NULL && strlen(e) >= 6) t = (unsigned long)(strtoul(e, NULL, 16) & 0x00FFFFFF); else t = (unsigned long)MfdMonoTint; if (t == 0) t = (unsigned long)MfdMonoTint; // a literal "000000" -> green, not invisible } return t; } 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 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; int restored; // wantX/wantY came from glass_layout.cfg -> don't re-snap }; 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, "Look Bk" }, { 0x42, "Torso Ctr" }, { 0x43, "Look R" }, { 0x44, "Look 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 = (int)GlassMfdTint(); // standard green or BT_COCKPIT_TINT 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]; if (w.restored) continue; // sticky (BT_GLASS_LAYOUT) -- keep the saved spot 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); } //########################################################################### // Sticky layout -- BT_GLASS_LAYOUT persists each window's on-screen position // to glass_layout.cfg (cwd-relative, beside bindings.txt), so a developer can // drag the ring into place once and have it survive the menu->mission->menu // relaunch loop and future launches. Modes: // off / 0 / unset -- computed pod-faithful ring only, no file I/O (default) // load / restore -- restore saved positions (per-window fallback to // computed); never write // save / adjust / -- restore first, then re-save on finished-drag // on / 1 (WM_EXITSIZEMOVE) and on teardown [the round trip] // Position is restored; size stays deterministic from the window's content, so // a later geometry change can't distort an old save. Windows carry WS_CAPTION // in every mode, so they are always draggable -- the gate only controls whether // a drag STICKS. //########################################################################### enum { LayoutOff = 0, LayoutLoad = 1, LayoutSave = 2 }; static const char *layoutFileName = "glass_layout.cfg"; static int GlassLayoutMode() { static int m = -1; if (m < 0) { const char *e = getenv("BT_GLASS_LAYOUT"); if (e == NULL || e[0] == '\0') m = LayoutOff; else if (_stricmp(e, "off") == 0 || _stricmp(e, "0") == 0) m = LayoutOff; else if (_stricmp(e, "load") == 0 || _stricmp(e, "restore") == 0) m = LayoutLoad; else // save / adjust / on / 1 / anything else truthy m = LayoutSave; if (m != LayoutOff) DEBUG_STREAM << "[glasswin] BT_GLASS_LAYOUT=" << (m == LayoutSave ? "save" : "load") << " (" << layoutFileName << ")\n" << std::flush; } return m; } // Find a window by its (stable) title. Titles carry no '=', so the cfg splits // cleanly on the first '='. static GWin * FindGWinByTitle(const char *title) { for (int i = 0; i < gWinCount; ++i) if (gWins[i].title != NULL && strcmp(gWins[i].title, title) == 0) return &gWins[i]; return NULL; } // Restore saved positions over the just-computed pod-faithful defaults. Called // from Create AFTER ComputeLayout, so any window not named in the file keeps its // computed spot. Restored windows are flagged so the WM_TIMER re-snap (which // re-runs ComputeLayout once the main window appears) leaves them alone. static void LoadLayout() { if (GlassLayoutMode() == LayoutOff) return; FILE *f = fopen(layoutFileName, "rt"); if (f == NULL) { DEBUG_STREAM << "[glasswin] no " << layoutFileName << " yet (using computed placement)\n" << std::flush; return; } int restored = 0; char line[256]; while (fgets(line, sizeof(line), f) != NULL) { char *s = line; while (*s == ' ' || *s == '\t') ++s; if (*s == '#' || *s == '\r' || *s == '\n' || *s == '\0') continue; char *eq = strchr(s, '='); if (eq == NULL) continue; *eq = '\0'; // trim trailing space off the title char *end = eq; while (end > s && (end[-1] == ' ' || end[-1] == '\t')) --end; *end = '\0'; int x = 0, y = 0, w = 0, h = 0; if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2) continue; GWin *gw = FindGWinByTitle(s); if (gw == NULL) continue; gw->wantX = x; gw->wantY = y; gw->restored = 1; ++restored; } fclose(f); DEBUG_STREAM << "[glasswin] restored " << restored << " window position(s) from " << layoutFileName << "\n" << std::flush; } // Write every window's current on-screen frame rect. Whole-file rewrite (it is // tiny), so partial/hard kills never leave a half-written file for long. Called // on finished-drag and on teardown in save mode. static void SaveLayout() { if (GlassLayoutMode() != LayoutSave) return; FILE *f = fopen(layoutFileName, "wt"); if (f == NULL) { DEBUG_STREAM << "[glasswin] could not write " << layoutFileName << "\n" << std::flush; return; } fputs("# BT411 glass cockpit window layout (BT_GLASS_LAYOUT=save writes this\n" "# on finished-drag/exit; =load restores it). =<x>,<y>,<w>,<h>\n", f); int wrote = 0; for (int i = 0; i < gWinCount; ++i) { GWin &gw = gWins[i]; if (gw.hwnd == NULL || !IsWindow(gw.hwnd)) continue; RECT r; if (!GetWindowRect(gw.hwnd, &r)) continue; fprintf(f, "%s=%ld,%ld,%ld,%ld\n", gw.title, (long)r.left, (long)r.top, (long)(r.right - r.left), (long)(r.bottom - r.top)); ++wrote; } fclose(f); DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to " << layoutFileName << "\n" << std::flush; } //########################################################################### // 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_EXITSIZEMOVE: // The user finished dragging (or resizing) a window. In save/adjust // mode, persist the whole ring so the layout survives a hard kill even // if teardown never runs. No-op in load/off mode. SaveLayout(); 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(); LoadLayout(); // BT_GLASS_LAYOUT=load/save: restore saved positions over the ring 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() { SaveLayout(); // backstop for a clean teardown (WM_EXITSIZEMOVE already // caught every finished drag); no-op unless mode==save 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; } }