#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 #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; } //########################################################################### // 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 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(gr->GetGraphicsPort(w->portPrimary)); if (w->portAlt != NULL) { L4GraphicsPort *alt = static_cast(gr->GetGraphicsPort(w->portAlt)); // Show whichever of the Mfd/Eng 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(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) { 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) { 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; } }