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>
297 lines
9.9 KiB
C++
297 lines
9.9 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
//###########################################################################
|
|
// L4RIOBANK -- the ONE button-bank geometry for a pod display. Design +
|
|
// the under-glass rule: l4riobank.h. Consumed by the cockpit surround
|
|
// (L4VB16.cpp, canvas space) and the exploded per-display windows
|
|
// (L4GLASSWIN.cpp, client space); neither owns a copy any more.
|
|
//###########################################################################
|
|
|
|
#include "l4riobank.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
namespace
|
|
{
|
|
//-------------------------------------------------------------------
|
|
// The lamp strip at NATIVE glass size, scaled down with the display
|
|
// but floored so a half-scale surround lamp is still a bar and not a
|
|
// smudge. RP412 L4MFDVIEW works at native only and hardcodes 10.
|
|
//-------------------------------------------------------------------
|
|
const int nativeShortAxis = 480; // MFD 640x480 / map 480x640
|
|
const int nativeStrip = 10;
|
|
const int minimumStrip = 6;
|
|
const int defaultGap = 4;
|
|
|
|
//-------------------------------------------------------------------
|
|
// Per-column horizontal nudge for the 4 MFD buttons, left->right, so
|
|
// they line up with the (unevenly spaced) DISPLAY / PROGRAM / ...
|
|
// legends painted in the imagery. Measured against the native 640
|
|
// glass in L4GLASSWIN (RedOffsetX + kRedColDX) and scaled here, so
|
|
// both renderers inherit the same alignment. Tunable.
|
|
//-------------------------------------------------------------------
|
|
const int nativeMfdW = 640;
|
|
const int mfdColumnDX[4] = { 0, 1, 5, 5 };
|
|
|
|
//-------------------------------------------------------------------
|
|
// The map paints its own legend beside each side button (the six
|
|
// cells: MAP+, MAP-, IR, crouch, searchlight, display-mode), and the
|
|
// buttons have to line up with THAT grid, not with height/6.
|
|
//
|
|
// MEASURED off a native 480x640 capture (scratchpad/measurelegend.py
|
|
// over an exploded Secondary/Radar window): the first cell border sits
|
|
// 3 rows down and the six cells are 102 tall on a 107 pitch. RP412's
|
|
// map measures 13 + 6x102 on a 105 pitch -- same cell height, different
|
|
// top and pitch, so its numbers do NOT transfer; these are ours.
|
|
//
|
|
// Our previous even division (displayH/6 + 1 = 107) had the pitch right
|
|
// by luck and sat 3px high of the labels.
|
|
//-------------------------------------------------------------------
|
|
const int mapLegendSpan = 640;
|
|
const int mapLegendTop = 3;
|
|
const int mapLegendCell = 102;
|
|
const int mapLegendPitch = 107;
|
|
|
|
int Scaled(int nativeValue, int actual, int nativeExtent)
|
|
{
|
|
if (nativeExtent <= 0) return nativeValue;
|
|
return (nativeValue * actual) / nativeExtent;
|
|
}
|
|
|
|
void Push(
|
|
BTRioBank *bank,
|
|
int address, int x, int y, int w, int h,
|
|
int colorClass, int inert, const char *label
|
|
)
|
|
{
|
|
if (bank->buttonCount >= BTRioBankMaxButtons) return;
|
|
if (w <= 0 || h <= 0) return;
|
|
BTRioButton *button = &bank->buttons[bank->buttonCount++];
|
|
button->address = address;
|
|
button->x = x;
|
|
button->y = y;
|
|
button->w = w;
|
|
button->h = h;
|
|
button->colorClass = colorClass;
|
|
button->inert = inert;
|
|
button->label = label;
|
|
}
|
|
|
|
void GrowBounds(BTRioBank *bank, int x, int y, int w, int h)
|
|
{
|
|
if (bank->boundsW == 0 && bank->boundsH == 0)
|
|
{
|
|
bank->boundsX = x; bank->boundsY = y;
|
|
bank->boundsW = w; bank->boundsH = h;
|
|
return;
|
|
}
|
|
int left = (x < bank->boundsX) ? x : bank->boundsX;
|
|
int top = (y < bank->boundsY) ? y : bank->boundsY;
|
|
int right = bank->boundsX + bank->boundsW;
|
|
int bottom = bank->boundsY + bank->boundsH;
|
|
if (x + w > right) right = x + w;
|
|
if (y + h > bottom) bottom = y + h;
|
|
bank->boundsX = left;
|
|
bank->boundsY = top;
|
|
bank->boundsW = right - left;
|
|
bank->boundsH = bottom - top;
|
|
}
|
|
}
|
|
|
|
//###########################################################################
|
|
|
|
void
|
|
BTRioBankMetricsFor(int displayW, int displayH, BTRioBankMetrics *out)
|
|
{
|
|
if (out == 0) return;
|
|
|
|
int shortAxis = (displayW < displayH) ? displayW : displayH;
|
|
|
|
int strip = Scaled(nativeStrip, shortAxis, nativeShortAxis);
|
|
if (strip < minimumStrip) strip = minimumStrip;
|
|
|
|
out->strip = strip;
|
|
out->gap = defaultGap;
|
|
|
|
// The cells with no glass behind them (the flight bank, the map's foot
|
|
// row) are drawn in full, so they get a plain readable size scaled off
|
|
// the same axis rather than a derived depth.
|
|
out->cellW = Scaled(58, shortAxis, nativeShortAxis);
|
|
out->cellH = Scaled(28, shortAxis, nativeShortAxis);
|
|
if (out->cellW < 12) out->cellW = 12;
|
|
if (out->cellH < 12) out->cellH = 12;
|
|
}
|
|
|
|
//###########################################################################
|
|
|
|
void
|
|
BTRioBankLayout(
|
|
BTRioBankStyle style,
|
|
int originX,
|
|
int originY,
|
|
int displayW,
|
|
int displayH,
|
|
int anchorA,
|
|
int anchorB,
|
|
const BTRioBankMetrics *metrics,
|
|
const int *bottomAddrs,
|
|
BTRioBank *out
|
|
)
|
|
{
|
|
if (out == 0 || metrics == 0) return;
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
const int strip = metrics->strip;
|
|
const int gap = metrics->gap;
|
|
|
|
// the glass itself is always inside the bounds
|
|
GrowBounds(out, originX, originY, displayW, displayH);
|
|
|
|
if (style == BTRioBankMfd)
|
|
{
|
|
//---------------------------------------------------------------
|
|
// 4 above the glass, 4 below. Addresses descend from the anchor
|
|
// row-major (vRIO CockpitLayout::Mfd): top = anchorA-i, bottom =
|
|
// anchorA-4-i. Each button reaches HALF THE GLASS in and clears
|
|
// the edge by `strip`, so the two banks meet in the middle and
|
|
// every pixel of the display is some button's press target.
|
|
//---------------------------------------------------------------
|
|
int depth = displayH / 2;
|
|
if (depth < strip) depth = strip;
|
|
|
|
int slotW = displayW / 4;
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
int dx = Scaled(mfdColumnDX[i], displayW, nativeMfdW);
|
|
int x = originX + i * slotW + dx;
|
|
int w = slotW - gap;
|
|
|
|
int topY = originY - strip;
|
|
int botY = originY + displayH - depth;
|
|
Push(out, anchorA - i, x, topY, w, depth + strip, 0, 0, 0);
|
|
Push(out, anchorA - 4 - i, x, botY, w, depth + strip, 0, 0, 0);
|
|
GrowBounds(out, x, topY, w, depth + strip);
|
|
GrowBounds(out, x, botY, w, depth + strip);
|
|
}
|
|
}
|
|
else if (style == BTRioBankRadar)
|
|
{
|
|
//---------------------------------------------------------------
|
|
// The portrait map: 6 buttons down each side (anchorA left,
|
|
// anchorB right) plus an optional 4 along the foot. Same rule
|
|
// turned on its side -- each column reaches half the map in and
|
|
// leaves `strip` clearing the edge.
|
|
//
|
|
// The FOOT ROW IS PUSHED FIRST, deliberately: the columns now
|
|
// reach half the width each, so they cover the foot band too,
|
|
// and the hit test takes the first match. Foot-first keeps
|
|
// those four addresses reachable.
|
|
//---------------------------------------------------------------
|
|
int depth = displayW / 2;
|
|
if (depth < strip) depth = strip;
|
|
|
|
if (bottomAddrs != 0)
|
|
{
|
|
int bandH = metrics->cellH;
|
|
int bandY = originY + displayH - bandH;
|
|
int bandW = displayW / 4;
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
int x = originX + i * bandW;
|
|
Push(out, bottomAddrs[i], x, bandY, bandW - gap,
|
|
bandH + strip, 1, 0, 0);
|
|
GrowBounds(out, x, bandY, bandW - gap, bandH + strip);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Six cells down the height, on the map's MEASURED legend grid
|
|
// (see mapLegend* above). Top and bottom are scaled SEPARATELY
|
|
// and subtracted rather than scaling a height directly: rounding
|
|
// a height would let the buttons drift out of step with the
|
|
// labels a display or two down the column.
|
|
//---------------------------------------------------------------
|
|
int leftX = originX - strip;
|
|
int rightX = originX + displayW - depth;
|
|
for (int i = 0; i < 6; ++i)
|
|
{
|
|
int cellTop = mapLegendTop + i * mapLegendPitch;
|
|
int top = (cellTop * displayH) / mapLegendSpan;
|
|
int bottom = ((cellTop + mapLegendCell) * displayH) / mapLegendSpan;
|
|
int h = bottom - top;
|
|
if (h < 1) h = 1;
|
|
int y = originY + top;
|
|
Push(out, anchorA + i, leftX, y, depth + strip, h, 1, 0, 0);
|
|
Push(out, anchorB + i, rightX, y, depth + strip, h, 1, 0, 0);
|
|
GrowBounds(out, leftX, y, depth + strip, h);
|
|
GrowBounds(out, rightX, y, depth + strip, h);
|
|
}
|
|
}
|
|
}
|
|
|
|
//###########################################################################
|
|
|
|
void
|
|
BTRioBankFlightGrid(
|
|
int originX,
|
|
int originY,
|
|
int baseAddress,
|
|
int count,
|
|
int columns,
|
|
const char *const *labels,
|
|
const int *inert,
|
|
const BTRioBankMetrics *metrics,
|
|
BTRioBank *out
|
|
)
|
|
{
|
|
if (out == 0 || metrics == 0 || columns <= 0) return;
|
|
|
|
const int cw = metrics->cellW;
|
|
const int ch = metrics->cellH;
|
|
const int gap = metrics->gap;
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
int column = i % columns;
|
|
int row = i / columns;
|
|
int x = originX + column * (cw + gap);
|
|
int y = originY + row * (ch + gap);
|
|
Push(out, baseAddress + i, x, y, cw, ch, 2,
|
|
(inert != 0) ? inert[i] : 0,
|
|
(labels != 0) ? labels[i] : 0);
|
|
GrowBounds(out, x, y, cw, ch);
|
|
}
|
|
}
|
|
|
|
//###########################################################################
|
|
|
|
void
|
|
BTRioBankDump(const char *tag, const BTRioBank *bank)
|
|
{
|
|
static int enabled = -1;
|
|
static int printed = 0;
|
|
|
|
if (enabled < 0)
|
|
{
|
|
const char *e = getenv("BT_RIOBANK_LOG");
|
|
enabled = (e != 0 && e[0] != '0') ? 1 : 0;
|
|
}
|
|
if (!enabled || bank == 0 || printed >= 16) return;
|
|
++printed;
|
|
|
|
DEBUG_STREAM << "[riobank] " << (tag ? tag : "?")
|
|
<< " bounds=(" << bank->boundsX << "," << bank->boundsY << ","
|
|
<< bank->boundsW << "," << bank->boundsH << ") "
|
|
<< bank->buttonCount << " buttons\n";
|
|
for (int i = 0; i < bank->buttonCount; ++i)
|
|
{
|
|
const BTRioButton &b = bank->buttons[i];
|
|
DEBUG_STREAM << "[riobank] addr=0x" << std::hex << b.address << std::dec
|
|
<< " rect=(" << b.x << "," << b.y << "," << b.w << "," << b.h << ")"
|
|
<< " class=" << b.colorClass << "\n";
|
|
}
|
|
DEBUG_STREAM << std::flush;
|
|
}
|