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>
This commit is contained in:
Cyd
2026-07-26 02:23:40 -05:00
co-authored by Claude Opus 5
parent 1cda880c6d
commit 61563c9efe
15 changed files with 1670 additions and 282 deletions
+348 -70
View File
@@ -2,6 +2,7 @@
#pragma hdrstop
#include "l4vb16.h"
#include "l4riobank.h" // the shared button-bank geometry (with the exploded windows)
#include "../munga/gaugrend.h"
#include "L4VIDEO.h"
#include "DXUtils.h"
@@ -195,41 +196,229 @@ int gBTCockpitCanvasW = 0; // backbuffer canvas dims (set by btl4main; the
int gBTCockpitCanvasH = 0; // aspect calc needs them after a client resize)
// Layout constants (backbuffer px). MFD = 640x480 native x 0.5; radar = 480x640
// portrait x 0.5. The lamp EDGE (kCkLamp) protrudes past the surface; the FULL
// button rect reaches kCkRedCell INTO the display (hit-only, covered by the
// surface). User asked for larger/more-clickable buttons than the glass windows'
// 10px edge -> kCkLamp 16.
// portrait x 0.5.
//
// ⚠ 2026-07-26: the BUTTON rects no longer come from here -- L4RIOBANK owns
// that geometry now and the exploded per-display windows draw the identical
// field. What changed for the surround: its MFD lamps used to sit ENTIRELY
// outside the glass on a kCkRedH=24 strip (a 76x24 click target), while the
// same button in the exploded window reached 128px under the display. Both
// now follow the pod's under-glass rule -- half the glass deep, a scaled lamp
// strip clearing the edge. kCkTopBand still has to reserve room for the top
// row's protruding strip, so the band constants stay.
enum {
kCkMFDW = 320, kCkMFDH = 240,
kCkMFDW = 320, kCkMFDH = 240, // the POD sizes (100%) -- see BTCkSizes
kCkRADW = 240, kCkRADH = 320,
kCkOVL = 44, // (int)(0.14f * 320): corner overlap into the view
kCkLamp = 16, // radar rail / bottom-cell protrusion
kCkRedH = 24, // MFD red lamp height -- sits fully OUTSIDE the surface
kCkRedGap = 3, // gap between the red lamp and the MFD screen edge
kCkRailW = 26, // radar side-rail width
kCkGap = 2,
kCkSideBand = kCkMFDW - kCkOVL, // 276
kCkTopBand = (kCkMFDH - kCkOVL) + kCkRedGap + kCkRedH, // 223 (fits the top red lamps)
kCkBotBand = kCkRADH + kCkLamp // 336 (radar flush below view)
kCkLamp = 16, // legacy band reserve (radar rail / bottom cell)
kCkRedH = 24, // legacy band reserve above the top MFD row
kCkRedGap = 3, // gap between that reserve and the MFD screen edge
kCkGap = 2
};
//===========================================================================//
// PLAYER-SCALED DISPLAYS (2026-07-26).
//
// The pod bolted its instruments down at one size; a desktop panel has room to
// trade viewscreen for instrument, so the player scales them -- BT_MFD_SCALE
// sets all five MFDs, BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR override individually,
// BT_RADAR_SCALE the map. Percentages of the pod size, 25-200; anything
// unreadable or out of range falls back to the group setting, then to 100.
//
// Resolved ONCE (the surround recomputes its layout every frame and this reads
// the environment), and the SURROUND BANDS derive from the result -- which is
// the whole reason the sizes could not just be constants: the band a display
// hangs in has to grow with it or the canvas clips it.
//===========================================================================//
struct BTCkSizes
{
int mfdW[5], mfdH[5]; // UL, UC, UR, LL, LR -- kCkMfdBank order
int radW, radH;
int sideBandL, sideBandR, topBand, botBand;
};
static int CkScalePercent(const char *name, int fallback)
{
const char *text = getenv(name);
if (text == NULL) return fallback;
int percent = atoi(text);
if (percent <= 0) return fallback;
if (percent < 25) percent = 25;
if (percent > 200) percent = 200;
return percent;
}
// Where the map sits. The pod had it dead centre under the viewscreen, which
// on a wide panel is exactly where the road is -- BT_RADAR_POS moves it out of
// the way. In a bottom corner it takes that corner and the lower MFD there
// slides inboard beside it; halfway up a side it leaves the bottom row
// entirely (and the bottom band shrinks to what the lower MFDs need).
enum BTCkRadarPos { CkRadarBottomCenter = 0, CkRadarBottomLeft, CkRadarBottomRight,
CkRadarMidLeft, CkRadarMidRight };
static int CkRadarPosition(void)
{
static int cached = -1;
if (cached >= 0) return cached;
static const struct { const char *name; int pos; } names[] = {
{ "CENTER", CkRadarBottomCenter }, { "CENTRE", CkRadarBottomCenter },
{ "BOTTOM", CkRadarBottomCenter },
{ "LEFT", CkRadarBottomLeft }, { "BOTTOMLEFT", CkRadarBottomLeft },
{ "RIGHT", CkRadarBottomRight }, { "BOTTOMRIGHT", CkRadarBottomRight },
{ "MIDLEFT", CkRadarMidLeft }, { "LEFTCENTER", CkRadarMidLeft },
{ "LEFTCENTRE", CkRadarMidLeft },
{ "MIDRIGHT", CkRadarMidRight }, { "RIGHTCENTER", CkRadarMidRight },
{ "RIGHTCENTRE", CkRadarMidRight }
};
cached = CkRadarBottomCenter;
const char *text = getenv("BT_RADAR_POS");
if (text != NULL)
for (int i = 0; i < (int)(sizeof(names) / sizeof(names[0])); i++)
if (_stricmp(text, names[i].name) == 0) { cached = names[i].pos; break; }
static const char *described[] = { "bottom centre", "bottom left", "bottom right",
"left side, centred", "right side, centred" };
DEBUG_STREAM << "[cockpit] radar on the " << described[cached] << "\n" << std::flush;
return cached;
}
static int CkRadarOnASide(int pos)
{ return (pos == CkRadarMidLeft || pos == CkRadarMidRight) ? 1 : 0; }
static const BTCkSizes *BTCkResolvedSizes(void)
{
static BTCkSizes s;
static int resolved = 0;
if (resolved) return &s;
resolved = 1;
int group = CkScalePercent("BT_MFD_SCALE", 100);
static const char *perDisplay[5] = {
"BT_MFD_SCALE_UL", "BT_MFD_SCALE_UC", "BT_MFD_SCALE_UR",
"BT_MFD_SCALE_LL", "BT_MFD_SCALE_LR"
};
int percent[5];
for (int i = 0; i < 5; i++)
{
percent[i] = CkScalePercent(perDisplay[i], group);
s.mfdW[i] = kCkMFDW * percent[i] / 100;
s.mfdH[i] = kCkMFDH * percent[i] / 100;
}
int radarPercent = CkScalePercent("BT_RADAR_SCALE", 100);
s.radW = kCkRADW * radarPercent / 100;
s.radH = kCkRADH * radarPercent / 100;
//-------------------------------------------------------------------
// The bands: each is whatever the LARGEST display hanging in it needs.
// A corner MFD overlaps the view by kCkOVL, so it only claims
// (its size - the overlap) of the band.
//-------------------------------------------------------------------
int radarPos = CkRadarPosition();
int leftMax = (s.mfdW[0] > s.mfdW[3]) ? s.mfdW[0] : s.mfdW[3]; // UL, LL
int rightMax = (s.mfdW[2] > s.mfdW[4]) ? s.mfdW[2] : s.mfdW[4]; // UR, LR
s.sideBandL = leftMax - kCkOVL;
s.sideBandR = rightMax - kCkOVL;
// top: the corner MFDs claim (h - overlap); UC drops 75% of its height in
int topCorner = ((s.mfdH[0] > s.mfdH[2]) ? s.mfdH[0] : s.mfdH[2]) - kCkOVL;
int topCentre = (int)(0.75f * s.mfdH[1]);
s.topBand = (topCorner > topCentre) ? topCorner : topCentre;
s.topBand += kCkRedGap + kCkRedH; // room for the top lamp strip
// bottom: the lower MFDs hang (h - overlap) below the view; the radar,
// when it is down there, hangs its full height plus its lamp strip
int botCorner = ((s.mfdH[3] > s.mfdH[4]) ? s.mfdH[3] : s.mfdH[4]) - kCkOVL;
s.botBand = botCorner;
if (!CkRadarOnASide(radarPos))
{
int radarBand = s.radH + kCkLamp;
if (radarBand > s.botBand) s.botBand = radarBand;
}
// a side-mounted radar has to fit BESIDE the view, in that side's band
if (CkRadarOnASide(radarPos))
{
int *band = (radarPos == CkRadarMidLeft) ? &s.sideBandL : &s.sideBandR;
if (s.radW + kCkLamp > *band) *band = s.radW + kCkLamp;
}
if (s.sideBandL < 1) s.sideBandL = 1;
if (s.sideBandR < 1) s.sideBandR = 1;
DEBUG_STREAM << "[cockpit] displays UL " << percent[0] << "% UC " << percent[1]
<< "% UR " << percent[2] << "% LL " << percent[3] << "% LR " << percent[4]
<< "% radar " << radarPercent << "% bands L" << s.sideBandL
<< " R" << s.sideBandR << " T" << s.topBand << " B" << s.botBand
<< "\n" << std::flush;
return &s;
}
// Canvas = view region + the surround bands.
void BTCockpitCanvasFor(int viewW, int viewH, int *canvasW, int *canvasH)
{
if (canvasW) *canvasW = viewW + 2 * kCkSideBand;
if (canvasH) *canvasH = viewH + kCkTopBand + kCkBotBand;
const BTCkSizes *s = BTCkResolvedSizes();
if (canvasW) *canvasW = viewW + s->sideBandL + s->sideBandR;
if (canvasH) *canvasH = viewH + s->topBand + s->botBand;
}
//===========================================================================//
// LETTERBOX FIT -- one uniform scale, centred, leftover black. Contract +
// why the swap effect changes: l4vb16.h.
//===========================================================================//
int gBTCockpitLetterbox = 0;
int BTCockpitFitRect(int clientW, int clientH, RECT *out)
{
int cw = gBTCockpitCanvasW, ch = gBTCockpitCanvasH;
if (out == NULL || cw <= 0 || ch <= 0 || clientW <= 0 || clientH <= 0)
return 0;
// The larger axis is the constraint; integer math throughout so the rect
// is stable frame to frame (a float scale wobbles by a pixel and the bars
// shimmer during a drag).
int w = clientW;
int h = (int)((__int64)clientW * ch / cw);
if (h > clientH)
{
h = clientH;
w = (int)((__int64)clientH * cw / ch);
}
if (w < 1) w = 1;
if (h < 1) h = 1;
out->left = (clientW - w) / 2;
out->top = (clientH - h) / 2;
out->right = out->left + w;
out->bottom = out->top + h;
return (w != clientW || h != clientH) ? 1 : 0;
}
const RECT *BTCockpitPresentDest(void *hwnd, RECT *storage)
{
if (!gBTCockpitLetterbox || !gBTGaugeCockpit || hwnd == NULL || storage == NULL)
return NULL;
RECT client;
if (!GetClientRect((HWND)hwnd, &client))
return NULL;
if (!BTCockpitFitRect(client.right, client.bottom, storage))
return NULL; // already an exact fit -- present full client
return storage;
}
// The five MFD banks: {cockpit surface slot, high address}. Top row = bankHi-i
// (descending), bottom row = bankHi-4-i (L4GLASSWIN BuildMfd order + the
// BTGlassPanels_Create address assignment).
static const struct { int slot, bankHi; } kCkMfdBank[5] =
static const struct { int slot, bankHi; const char *name; } kCkMfdBank[5] =
{
{ 0, 0x2F }, // Heat (UL)
{ 1, 0x27 }, // Mfd2 (UC)
{ 5, 0x37 }, // Comm (UR)
{ 2, 0x0F }, // Mfd1 (LL)
{ 3, 0x07 }, // Mfd3 (LR)
{ 0, 0x2F, "surround/Heat MFD" }, // UL
{ 1, 0x27, "surround/Engineering" }, // UC
{ 5, 0x37, "surround/Comm MFD" }, // UR
{ 2, 0x0F, "surround/Left Weapons" }, // LL
{ 3, 0x07, "surround/Right Weapons" }, // LR
};
static void CkPushBtn(BTCockpitLayout *L, int addr, int x, int y, int w, int h,
@@ -247,66 +436,114 @@ void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out)
memset(L, 0, sizeof(*L));
L->canvasW = canvasW;
L->canvasH = canvasH;
int viewW = canvasW - 2 * kCkSideBand;
int viewH = canvasH - kCkTopBand - kCkBotBand;
const BTCkSizes *S = BTCkResolvedSizes();
int viewW = canvasW - S->sideBandL - S->sideBandR;
int viewH = canvasH - S->topBand - S->botBand;
if (viewW < 64) viewW = 64;
if (viewH < 64) viewH = 64;
int vx = kCkSideBand, vy = kCkTopBand;
int vx = S->sideBandL, vy = S->topBand;
L->viewX = vx; L->viewY = vy; L->viewW = viewW; L->viewH = viewH;
// Surface dest rects (slots: 0 Heat, 1 Mfd2, 2 Mfd1, 3 Mfd3, 4 sec, 5 Comm).
// Corner MFDs overlap the view corner by kCkOVL both axes; Mfd2 drops 25% of
// its height into the top; sec is flush below (view bottom edge clean).
// Sizes are the player-scaled ones (BTCkResolvedSizes), indexed in
// kCkMfdBank order: 0 UL, 1 UC, 2 UR, 3 LL, 4 LR.
int cx = vx + viewW / 2;
// Heat TL
L->surfX[0] = vx - kCkMFDW + kCkOVL; L->surfY[0] = vy - kCkMFDH + kCkOVL;
L->surfW[0] = kCkMFDW; L->surfH[0] = kCkMFDH;
L->surfX[0] = vx - S->mfdW[0] + kCkOVL; L->surfY[0] = vy - S->mfdH[0] + kCkOVL;
L->surfW[0] = S->mfdW[0]; L->surfH[0] = S->mfdH[0];
// Mfd2 top-center (25% into view)
L->surfX[1] = cx - kCkMFDW / 2; L->surfY[1] = vy - (int)(0.75f * kCkMFDH);
L->surfW[1] = kCkMFDW; L->surfH[1] = kCkMFDH;
L->surfX[1] = cx - S->mfdW[1] / 2; L->surfY[1] = vy - (int)(0.75f * S->mfdH[1]);
L->surfW[1] = S->mfdW[1]; L->surfH[1] = S->mfdH[1];
// Mfd1 BL
L->surfX[2] = vx - kCkMFDW + kCkOVL; L->surfY[2] = vy + viewH - kCkOVL;
L->surfW[2] = kCkMFDW; L->surfH[2] = kCkMFDH;
L->surfX[2] = vx - S->mfdW[3] + kCkOVL; L->surfY[2] = vy + viewH - kCkOVL;
L->surfW[2] = S->mfdW[3]; L->surfH[2] = S->mfdH[3];
// Mfd3 BR
L->surfX[3] = vx + viewW - kCkOVL; L->surfY[3] = vy + viewH - kCkOVL;
L->surfW[3] = kCkMFDW; L->surfH[3] = kCkMFDH;
// sec center, flush below
L->surfX[4] = cx - kCkRADW / 2; L->surfY[4] = vy + viewH;
L->surfW[4] = kCkRADW; L->surfH[4] = kCkRADH;
L->surfW[3] = S->mfdW[4]; L->surfH[3] = S->mfdH[4];
// Comm TR
L->surfX[5] = vx + viewW - kCkOVL; L->surfY[5] = vy - kCkMFDH + kCkOVL;
L->surfW[5] = kCkMFDW; L->surfH[5] = kCkMFDH;
L->surfX[5] = vx + viewW - kCkOVL; L->surfY[5] = vy - S->mfdH[2] + kCkOVL;
L->surfW[5] = S->mfdW[2]; L->surfH[5] = S->mfdH[2];
// --- MFD red buttons: 4 top (bankHi-i) + 4 bottom (bankHi-4-i) ---
int slotW = kCkMFDW / 4; // 80
for (int m = 0; m < 5; m++)
//-------------------------------------------------------------------
// sec (the map). Bottom centre as the pod had it, either bottom
// corner, or halfway up either side (BT_RADAR_POS). Only when it
// takes a bottom CORNER does a lower MFD have to move -- it slides
// inboard beside the map; on a side the map is out of that row's way
// already.
//-------------------------------------------------------------------
{
int s = kCkMfdBank[m].slot, hi = kCkMfdBank[m].bankHi;
int sx = L->surfX[s], sy = L->surfY[s];
for (int i = 0; i < 4; i++)
int radarPos = CkRadarPosition();
L->surfW[4] = S->radW;
L->surfH[4] = S->radH;
// A corner map goes FLUSH to the canvas edge and the lower MFD whose
// corner it took sits BESIDE it -- measured off the canvas edge, not
// the view edge, or the two overlap by the band width (they did:
// 232px of map drawn over the Right Weapons MFD).
int canvasLeft = vx - S->sideBandL;
int canvasRight = vx + viewW + S->sideBandR;
switch (radarPos)
{
int bx = sx + i * slotW;
// Lamps sit fully OUTSIDE the MFD (a small gap off the edge) so nothing
// occludes them or the DISPLAY/PROGRAM legends -- 4 above, 4 below.
CkPushBtn(L, hi - i, bx, sy - kCkRedGap - kCkRedH, slotW - kCkGap, kCkRedH, 0, 0, 0);
CkPushBtn(L, hi - 4 - i, bx, sy + kCkMFDH + kCkRedGap, slotW - kCkGap, kCkRedH, 0, 0, 0);
case CkRadarBottomLeft:
L->surfX[4] = canvasLeft;
L->surfY[4] = vy + viewH;
L->surfX[2] = L->surfX[4] + S->radW; // LL slides inboard
break;
case CkRadarBottomRight:
L->surfX[4] = canvasRight - S->radW;
L->surfY[4] = vy + viewH;
L->surfX[3] = L->surfX[4] - S->mfdW[4]; // LR slides inboard
break;
case CkRadarMidLeft:
L->surfX[4] = vx - S->radW + kCkOVL;
L->surfY[4] = vy + (viewH - S->radH) / 2;
break;
case CkRadarMidRight:
L->surfX[4] = vx + viewW - kCkOVL;
L->surfY[4] = vy + (viewH - S->radH) / 2;
break;
default:
L->surfX[4] = cx - S->radW / 2;
L->surfY[4] = vy + viewH;
break;
}
}
// --- radar yellow rails (6/side) + 4 bottom cells ---
// --- MFD buttons + radar columns: L4RIOBANK, the shared under-glass field ---
// Each bank is laid out at ITS display's scaled size, so the lamp strips and
// the under-glass reach track a resized display instead of drifting off it.
{
int rx = L->surfX[4], ry = L->surfY[4];
int slotH = kCkRADH / 6;
for (int i = 0; i < 6; i++)
for (int m = 0; m < 5; m++)
{
int y = ry + i * slotH;
CkPushBtn(L, 0x10 + i, rx - kCkLamp, y, kCkLamp + kCkRailW, slotH - kCkGap, 1, 0, 0);
CkPushBtn(L, 0x18 + i, rx + kCkRADW - kCkRailW, y, kCkRailW + kCkLamp, slotH - kCkGap, 1, 0, 0);
int s = kCkMfdBank[m].slot;
BTRioBankMetrics mfdMetrics;
BTRioBankMetricsFor(L->surfW[s], L->surfH[s], &mfdMetrics);
BTRioBank bank;
BTRioBankLayout(BTRioBankMfd, L->surfX[s], L->surfY[s],
L->surfW[s], L->surfH[s], kCkMfdBank[m].bankHi, 0, &mfdMetrics, 0, &bank);
BTRioBankDump(kCkMfdBank[m].name, &bank);
for (int i = 0; i < bank.buttonCount; i++)
{
const BTRioButton &b = bank.buttons[i];
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert, b.label);
}
}
static const int bottomAddr[4] = { 0x16, 0x17, 0x1F, 0x1E };
int botLeft = rx + kCkRailW, botSpan = kCkRADW - 2 * kCkRailW, botW = botSpan / 4;
for (int i = 0; i < 4; i++)
CkPushBtn(L, bottomAddr[i], botLeft + i * botW, ry + kCkRADH - 14, botW - kCkGap, 14 + kCkLamp, 1, 0, 0);
BTRioBankMetrics radarMetrics;
BTRioBankMetricsFor(L->surfW[4], L->surfH[4], &radarMetrics);
BTRioBank radar;
BTRioBankLayout(BTRioBankRadar, L->surfX[4], L->surfY[4],
L->surfW[4], L->surfH[4], 0x10, 0x18, &radarMetrics, bottomAddr, &radar);
BTRioBankDump("surround/radar", &radar);
for (int i = 0; i < radar.buttonCount; i++)
{
const BTRioButton &b = radar.buttons[i];
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert, b.label);
}
}
// --- flight blue blocks: THROTTLE/AUX (0x38-0x3F) under Mfd1, JOYSTICK
@@ -316,18 +553,34 @@ void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out)
static const int thrInert[8] = { 1, 1, 1, 1, 1, 0, 1, 0 };
static const char *joyLabel[8] = { "Main", "Hat Bk", "Hat Up", "Hat R",
"Hat L", "Pinky", "Middle", "Upper" }; // 0x40..0x47
const int cw = 68, ch = 30, g = 4;
int blockW = 4 * cw + 3 * g; // 284
int belowLamp = kCkMFDH + kCkRedGap + kCkRedH + 8; // clear the MFD's bottom red lamps
int thrX = L->surfX[2], thrY = L->surfY[2] + belowLamp;
int joyX = L->surfX[3] + kCkMFDW - blockW, joyY = L->surfY[3] + belowLamp;
for (int i = 0; i < 8; i++)
// These cells carry TEXT, so the surround runs them bigger than the
// exploded window's 58x28 -- the caller owns the cell size
// (l4riobank.h), only the grid walk is shared.
BTRioBankMetrics metrics;
metrics.strip = 0;
metrics.gap = 4;
metrics.cellW = 68;
metrics.cellH = 30;
// Hang each block below ITS lower MFD, clearing that display's own
// (scaled) bottom lamp strip -- a shrunken MFD used to leave the block
// floating in the gap, an enlarged one to overlap it.
int blockW = 4 * metrics.cellW + 3 * metrics.gap; // 284
int thrX = L->surfX[2];
int thrY = L->surfY[2] + L->surfH[2] + kCkRedGap + kCkRedH + 8;
int joyX = L->surfX[3] + L->surfW[3] - blockW;
int joyY = L->surfY[3] + L->surfH[3] + kCkRedGap + kCkRedH + 8;
BTRioBank flight;
memset(&flight, 0, sizeof(flight));
BTRioBankFlightGrid(thrX, thrY, 0x38, 8, 4, thrLabel, thrInert, &metrics, &flight);
BTRioBankFlightGrid(joyX, joyY, 0x40, 8, 4, joyLabel, 0, &metrics, &flight);
BTRioBankDump("surround/flight", &flight);
for (int i = 0; i < flight.buttonCount; i++)
{
int c = i % 4, r = i / 4;
CkPushBtn(L, 0x38 + i, thrX + c * (cw + g), thrY + r * (ch + g), cw, ch, 2,
thrInert[i], thrLabel[i] ? thrLabel[i] : "");
CkPushBtn(L, 0x40 + i, joyX + c * (cw + g), joyY + r * (ch + g), cw, ch, 2,
0, joyLabel[i]);
const BTRioButton &b = flight.buttons[i];
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert,
b.label ? b.label : "");
}
}
}
@@ -959,8 +1212,33 @@ int BTCockpitMouseDown(int cx, int cy, int clientW, int clientH, int rightButton
if (!gBTGaugeCockpit) return 0;
int bbW = gBTCockpitCanvasW, bbH = gBTCockpitCanvasH;
if (bbW <= 0 || bbH <= 0 || clientW <= 0 || clientH <= 0) return 0;
int bx = cx * bbW / clientW;
int by = cy * bbH / clientH;
// Client -> canvas. Under the letterbox the canvas occupies a centred
// sub-rect, so the click has to come back through the SAME transform
// Present used -- mapping against the full client would drift the hit test
// off every button by the bar width. A click on a bar hits nothing.
int bx, by;
if (gBTCockpitLetterbox)
{
RECT fit;
if (BTCockpitFitRect(clientW, clientH, &fit))
{
if (cx < fit.left || cx >= fit.right || cy < fit.top || cy >= fit.bottom)
return 0;
bx = (cx - fit.left) * bbW / (fit.right - fit.left);
by = (cy - fit.top) * bbH / (fit.bottom - fit.top);
}
else
{
bx = cx * bbW / clientW;
by = cy * bbH / clientH;
}
}
else
{
bx = cx * bbW / clientW;
by = cy * bbH / clientH;
}
BTCockpitLayout L;
BTCockpitComputeLayout(bbW, bbH, &L);