diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index d19f9e5..943ec10 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -169,6 +169,19 @@ Verified live: two flagged windows came up caption-less while the other five kep still dispatched on the frameless radar (`CLICK 'Secondary / Radar' addr=0x18`), and a finished-drag save round-tripped both flags back into the file. +**The plasma window is in the list too (2026-08-04) [T2 round-trip-verified].** The desktop plasma +display (`L4PLASMAWIN`, title `BattleTech - Plasma`) is created by a DIFFERENT TU than the +per-display panels, so it can't be a `gWins[]` entry. Instead it registers with a small +extern-window registry in `L4GLASSWIN` (`BTGlassLayout_RegisterExtern`), and on creation it reads +its saved rect + `,noframe` via `BTGlassLayout_QueryWindow` (BEFORE sizing — `WS_POPUP`'s frame +extent differs). `SaveLayout` then writes the plasma's line alongside the panels (with a +last-known-rect cache so a teardown before the save still preserves its line), and the plasma +WndProc calls `BTGlassLayout_Save` on `WM_EXITSIZEMOVE`/teardown. So `BattleTech - Plasma=x,y,w,h` +appears in the cfg like any panel and honours `,noframe`. Verified: a drag wrote +`BattleTech - Plasma=321,222,…`; a reload with `,noframe` brought it up `WS_POPUP` (no `WS_CAPTION`) +at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike +the panels it has no `WM_TIMER` focus-throttle to worry about. + **Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects; `scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index d818445..7d7a7ee 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -190,7 +190,9 @@ static void GButton &b = w.buttons[w.buttonCount++]; b.address = src.address; b.color = (src.colorClass == 1) ? ClrYellow - : (src.colorClass == 2) ? ClrBlue : color; + : (src.colorClass == 2) ? ClrBlue + : (src.colorClass == 0) ? ClrRed // 0 = red (MFD, and the red Panic/Eject 0x3D) + : color; b.rect.left = src.x + dx; b.rect.top = src.y + dy; b.rect.right = src.x + dx + src.w; @@ -449,6 +451,91 @@ static GWin * return NULL; } +//########################################################################### +// External windows that ride glass_layout.cfg (2026-08-04). A window created +// by ANOTHER TU -- the plasma window (L4PLASMAWIN) -- can register here so it +// shares the SAME position + ",noframe" persistence as the per-display windows: +// it queries its saved rect/flag on creation, and Save() writes its line too. +// Tiny (HWND + title + last-known rect); no dynamic state. +//########################################################################### + +struct ExternWin +{ + HWND hwnd; + char title[64]; + int noFrame; + RECT last; + int haveLast; +}; +static ExternWin gExtern[4]; +static int gExternCount = 0; + +// Read the saved rect + ",noframe" flag for a title (glass window OR extern). +// Returns 1 if the title has a line in the cfg. rect / noframe may be NULL. +int + BTGlassLayout_QueryWindow(const char *title, RECT *rect, int *noframe) +{ + if (title == NULL || GlassLayoutMode() == LayoutOff) + return 0; + FILE *f = fopen(layoutFileName, "rt"); + if (f == NULL) + return 0; + + int found = 0; + char line[256]; + while (fgets(line, sizeof(line), f) != NULL) + { + char *s = line; + while (*s == ' ' || *s == '\t') ++s; + if (*s == '#' || *s == '\r' || *s == '\n' || *s == '\0') + continue; + char *eq = strchr(s, '='); + if (eq == NULL) + continue; + *eq = '\0'; + char *end = eq; + while (end > s && (end[-1] == ' ' || end[-1] == '\t')) --end; + *end = '\0'; + if (strcmp(s, title) != 0) + continue; + + int x = 0, y = 0, w = 0, h = 0; + if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2) + continue; + int nf = 0; + for (const char *opt = strchr(eq + 1, ','); opt != NULL; opt = strchr(opt + 1, ',')) + { + const char *t = opt + 1; + while (*t == ' ' || *t == '\t') ++t; + if (_strnicmp(t, "noframe", 7) == 0) { nf = 1; break; } + } + if (rect) { rect->left = x; rect->top = y; rect->right = x + w; rect->bottom = y + h; } + if (noframe) *noframe = nf; + found = 1; + break; + } + fclose(f); + return found; +} + +// Register an externally-created window so Save() writes its line. Loads its +// current ",noframe" flag from the cfg so a save round-trips the flag. +void + BTGlassLayout_RegisterExtern(HWND hwnd, const char *title) +{ + if (hwnd == NULL || title == NULL) + return; + if (gExternCount >= (int)(sizeof(gExtern) / sizeof(gExtern[0]))) + return; + ExternWin &e = gExtern[gExternCount++]; + e.hwnd = hwnd; + strncpy(e.title, title, sizeof(e.title) - 1); + e.title[sizeof(e.title) - 1] = '\0'; + e.noFrame = 0; + e.haveLast = 0; + BTGlassLayout_QueryWindow(title, NULL, &e.noFrame); +} + // Restore saved positions over the just-computed pod-faithful defaults. Called // from Create AFTER ComputeLayout, so any window not named in the file keeps its // computed spot. Restored windows are flagged so the WM_TIMER re-snap (which @@ -545,7 +632,8 @@ static void "# Heat MFD=1920,0,657,539,noframe\n" "# A frameless window is also PINNED (no caption = nothing to drag it\n" "# by), so do the arranging first. Delete the flag to get the frame\n" - "# back. Saves preserve it.\n", + "# back. Saves preserve it. The 'BattleTech - Plasma' window is in\n" + "# this list too -- it can be dragged, remembered and set ,noframe.\n", f); int wrote = 0; for (int i = 0; i < gWinCount; ++i) @@ -562,11 +650,41 @@ static void gw.noFrame ? ",noframe" : ""); // keep the hand-added option ++wrote; } + // External windows (the plasma window) ride the same file. Cache the last + // good rect so a window torn down before this save still keeps its line. + for (int i = 0; i < gExternCount; ++i) + { + ExternWin &e = gExtern[i]; + RECT r; + if (e.hwnd != NULL && IsWindow(e.hwnd) && GetWindowRect(e.hwnd, &r)) + { + e.last = r; e.haveLast = 1; + } + else if (e.haveLast) + { + r = e.last; // window gone -> preserve its last-known line + } + else + continue; + fprintf(f, "%s=%ld,%ld,%ld,%ld%s\n", e.title, + (long)r.left, (long)r.top, + (long)(r.right - r.left), (long)(r.bottom - r.top), + e.noFrame ? ",noframe" : ""); + ++wrote; + } fclose(f); DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to " << layoutFileName << "\n" << std::flush; } +// Public save trigger for registered external windows -- their WndProc calls +// this on WM_EXITSIZEMOVE / teardown. No-op unless BT_GLASS_LAYOUT=save. +void + BTGlassLayout_Save() +{ + SaveLayout(); +} + //########################################################################### // Painting //########################################################################### @@ -1054,3 +1172,43 @@ void if (subFont) { DeleteObject(subFont); subFont = NULL; } if (gStage) { delete[] gStage; gStage = NULL; } } + +//########################################################################### +// Per-frame repaint pump (2026-08-04, "indicators flash slowly unless focused"). +// +// These windows repaint off a 62 ms WM_TIMER. Windows COALESCES/THROTTLES timer +// AND paint messages for a window that is in the BACKGROUND (not focused) -- so +// when the game window (or another app) holds focus, the panel's WM_TIMER slows +// to a crawl and the lamp FLASH (a repaint-driven animation: BTLampBrightnessOf +// alternates the shade off GetTickCount every paint) drags. Giving the panel +// focus un-throttles its timer, which is exactly what playtesters saw. +// +// Fix: drive the repaint from the MAIN render loop (BTGlassPanels_Tick, called +// once per frame from L4VIDEO), gated to the same ~16 Hz flash cadence. The main +// loop runs every frame while the game is up regardless of which window has focus, +// and InvalidateRect + UpdateWindow forces a SYNCHRONOUS WM_PAINT -- bypassing the +// throttled timer-message path entirely. The WM_TIMER stays (it still owns the +// one-shot re-snap); a focused window just repaints from whichever fires first. +//########################################################################### + +void + BTGlassPanels_Tick() +{ + if (gWinCount == 0) + return; + + static unsigned long sLastPaint = 0; + unsigned long now = GetTickCount(); + if (now - sLastPaint < (unsigned long)RepaintMilliseconds) + return; + sLastPaint = now; + + for (int i = 0; i < gWinCount; ++i) + { + if (gWins[i].hwnd != NULL) + { + InvalidateRect(gWins[i].hwnd, NULL, FALSE); + UpdateWindow(gWins[i].hwnd); // synchronous paint, not the throttled queue + } + } +} diff --git a/engine/MUNGA_L4/L4PLASMAWIN.cpp b/engine/MUNGA_L4/L4PLASMAWIN.cpp index 7265c83..9bb699f 100644 --- a/engine/MUNGA_L4/L4PLASMAWIN.cpp +++ b/engine/MUNGA_L4/L4PLASMAWIN.cpp @@ -18,6 +18,12 @@ static LRESULT CALLBACK { switch (message) { + case WM_EXITSIZEMOVE: + // Finished dragging (framed mode): persist the new position so it sticks + // -- same contract as the per-display panels. No-op unless save mode. + { extern void BTGlassLayout_Save(); BTGlassLayout_Save(); } + return 0; + case WM_CLOSE: ShowWindow(window, SW_HIDE); // hide only; the renderer owns it return 0; @@ -52,6 +58,7 @@ PlasmaWindow::~PlasmaWindow() { if (window != NULL) { + { extern void BTGlassLayout_Save(); BTGlassLayout_Save(); } // backstop before the HWND goes DestroyWindow((HWND)window); window = NULL; } @@ -75,20 +82,43 @@ void window_class.lpszClassName = L"BTPlasmaWnd"; RegisterClassW(&window_class); - RECT frame = { 0, 0, plasmaWidth * scale, plasmaHeight * scale }; - DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; - AdjustWindowRect(&frame, style, FALSE); + // glass_layout.cfg integration (2026-08-04): the plasma window rides the same + // file as the per-display panels, so it can be dragged-and-remembered and set + // frameless with ",noframe" -- BT_GLASS_LAYOUT selects load/save. Read its + // saved rect + flag BEFORE sizing (WS_POPUP has a different frame extent than + // the framed tool window). + extern int BTGlassLayout_QueryWindow(const char *title, RECT *rect, int *noframe); + extern void BTGlassLayout_RegisterExtern(HWND hwnd, const char *title); + RECT saved; + int noframe = 0; + int have_saved = BTGlassLayout_QueryWindow("BattleTech - Plasma", &saved, &noframe); - // - // Bottom-right, TOPMOST -- under the (topmost, right-parked) button - // panel, clear of the game window (which would otherwise bury it). - // + DWORD style = noframe + ? (DWORD)WS_POPUP // bare + pinned, same as a ",noframe" glass panel + : (DWORD)(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX); + + RECT frame = { 0, 0, plasmaWidth * scale, plasmaHeight * scale }; + AdjustWindowRect(&frame, style, FALSE); int frame_width = frame.right - frame.left; int frame_height = frame.bottom - frame.top; - int plasma_x = GetSystemMetrics(SM_CXSCREEN) - frame_width - 12; - int plasma_y = GetSystemMetrics(SM_CYSCREEN) - frame_height - 60; - if (plasma_x < 0) plasma_x = 0; - if (plasma_y < 0) plasma_y = 0; + + int plasma_x, plasma_y; + if (have_saved) + { + plasma_x = saved.left; // position restored; size stays native + plasma_y = saved.top; + } + else + { + // + // Default: bottom-right, TOPMOST -- under the (topmost, right-parked) + // button panel, clear of the game window (which would otherwise bury it). + // + plasma_x = GetSystemMetrics(SM_CXSCREEN) - frame_width - 12; + plasma_y = GetSystemMetrics(SM_CYSCREEN) - frame_height - 60; + if (plasma_x < 0) plasma_x = 0; + if (plasma_y < 0) plasma_y = 0; + } window = CreateWindowExW( WS_EX_TOPMOST, @@ -102,6 +132,7 @@ void DEBUG_STREAM << "[plasmawin] CreateWindow failed\n" << std::flush; return; } + BTGlassLayout_RegisterExtern((HWND)window, "BattleTech - Plasma"); ShowWindow((HWND)window, SW_SHOWNOACTIVATE); } diff --git a/engine/MUNGA_L4/L4RIOBANK.cpp b/engine/MUNGA_L4/L4RIOBANK.cpp index c196f22..f1e991d 100644 --- a/engine/MUNGA_L4/L4RIOBANK.cpp +++ b/engine/MUNGA_L4/L4RIOBANK.cpp @@ -258,7 +258,10 @@ void 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, + // Flight blocks are blue (colorClass 2), EXCEPT the Panic/Eject at 0x3D: + // red (colorClass 0) so the eject stands out on the blue panel. + int cc = (baseAddress + i == 0x3D) ? 0 : 2; + Push(out, baseAddress + i, x, y, cw, ch, cc, (inert != 0) ? inert[i] : 0, (labels != 0) ? labels[i] : 0); GrowBounds(out, x, y, cw, ch); diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index b02d778..4fd3a02 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -8960,6 +8960,17 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte extern void BTGaugeWindowRenderAndPresent(LPDIRECT3DDEVICE9 device); BTGaugeWindowRenderAndPresent(mDevice); +#ifdef BT_GLASS + // Drive the per-display glass panels' repaint from the (always-running) main + // loop so their lamp flash keeps animating when they're in the background -- + // Windows throttles an unfocused window's own WM_TIMER ("indicators flash + // slowly unless you focus that window"). No-op unless the panels are up. + { + extern void BTGlassPanels_Tick(); + BTGlassPanels_Tick(); + } +#endif + // DIAG (turn-hitch hunt): draw CPU is _rt0..here; Present blocks on the GPU. LARGE_INTEGER _rt1; QueryPerformanceCounter(&_rt1); // COCKPIT LETTERBOX: uniform-scale the canvas into the client (NULL = the diff --git a/engine/MUNGA_L4/l4glasswin.h b/engine/MUNGA_L4/l4glasswin.h index caec3df..5000a29 100644 --- a/engine/MUNGA_L4/l4glasswin.h +++ b/engine/MUNGA_L4/l4glasswin.h @@ -31,6 +31,16 @@ void void BTGlassPanels_Destroy(); +// +// Per-frame repaint pump. Call once per frame from the main render loop so the +// per-display windows' lamp flash keeps animating even when they are in the +// background (Windows throttles a background window's own WM_TIMER; this drives a +// synchronous repaint at the flash cadence instead). No-op unless the windows +// are up. See the note at the definition (L4GLASSWIN.cpp). +// +void + BTGlassPanels_Tick(); + // // True when BT_GLASS_PANELS mode is selected (default ON under `-platform // glass`, OFF otherwise). Read by the render loop / L4VB16 dev-composite to