Ported RP412's bindings design and made it the DEFAULT: the letter and number rows are the MFD button banks laid out WHERE THEY SIT ON THE PANEL, flight moves to the numpad so the board stays free, F1-F12 are the map's two columns, and G/B stay unbound as the physical gap between the lower clusters. Expressed in BT's OWN grammar (slew/deflect/set, the Turn channel) -- bindings.txt is a documented compatibility surface and was not touched. Coverage: 61 of 72 addresses on the keyboard, and the 11 absent ones are exactly those the pod never wired (0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E intercom/door). All 72 stay clickable on the panel. It lands on BT's addresses unreasonably well: 1-4 + QWER are the ENTIRE coolant system (Condensers 1-6, flush, balance), F6/F7 the display and control-mode cycles, F9-F12 Generators A-D, F4 the crouch button reconstructed earlier today. THE DELIBERATE COST. A bound key is removed from the authentic 1995 typed-hotkey channel so it cannot double-dispatch, and this board binds nearly everything -- so 5 (Quad page), z (Eng1), t/y/u/i/o (pilot select) and +/- (target zoom) are given up. Unbind a key to get its 1995 meaning back. Documented in the file header, the markdown and the page. NEW RULE: A BINDINGS ROW WINS OVER A BUILT-IN CONVENIENCE KEY (PadRIO::KeyHasBinding). V and J/K/L are board buttons now, and those polls read GetAsyncKeyState directly -- without this they would have fired BOTH the button and the built-in. View toggle lives on BACKTICK alone. CONTROLS.MAP rewritten to mirror the board so glass/pod/dev still feel identical (the 2026-07-21 settlement): 90 bindings, 0 parse complaints. HAT LABELS CORRECTED [T1]. INPUT_PATH_AUDIT flagged 0x41-0x44 and was right. Settled from the streamed mapping (BT_CTRLMAP_LOG): elem 66 -> subsys 17 attrID 14, i.e. 0x42 is the TORSO subsystem, not a look; 0x44/0x43/0x41 are the mapper's LookLeft/LookRight/LookBehind (attrID 10/11/12). The .RES has no "TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so the audit's wording was loose but its substance correct. Swept L4GLASSWIN, L4PADPANEL and L4VB16. docs/CONTROLS.html: interactive keyboard (hover a key for its address and meaning, MFD clusters outlined), pad diagram, panel map, the under-glass press-target figure, radar placement thumbnails and the environ.ini table -- modelled on RP412's page, rewritten for BT's addresses and hazards. mkdist wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt. Verified: bindings.txt regenerates and loads (74 keys, 0 errors); CONTROLS.MAP 90 bindings, 0 errors; 72/72 panel addresses still dispatch; surround / exploded / dock / pod / dev boot and simulate with zero faults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
809 lines
26 KiB
C++
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, "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 = 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; }
|
|
}
|