From 5285117d3b2f2b3f84e5b12cba0d4cfb4dd5a002 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 14 Jul 2026 08:23:43 -0500 Subject: [PATCH] Cockpit 3a: PlasmaScreen on the desktop (L4PLASMA=SCREEN) Renders BT's 128x32 plasma display into a desktop window instead of streaming to serial hardware. L4PLASMASCREEN.* copied verbatim from RP412; the L4GREND.cpp selector diff applied (BT's only divergence there is a comment -- no collision). Verified: a 'Plasma Display' window opens and the game ticks normally beside it (L4GAUGE=640x480x16 L4PLASMA=SCREEN). Phase 3b (single-window MFD-split cockpit) is DEFERRED -- blocked on the missing BT cockpit-layout geometry and BT's unfinished upstream MFD dev-composite; the L4VB16 hand-merge is HIGH-risk (BT's dev-gauge docking overlaps RP412's split rewrite). See context/steamification.md and docs/BT412-ROADMAP.md. (Phase 3 of the roadmap) Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 1 + context/steamification.md | 25 ++- docs/BT412-ROADMAP.md | 7 + engine/MUNGA_L4/L4GREND.cpp | 13 +- engine/MUNGA_L4/L4PLASMASCREEN.cpp | 320 +++++++++++++++++++++++++++++ engine/MUNGA_L4/L4PLASMASCREEN.h | 64 ++++++ 6 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 engine/MUNGA_L4/L4PLASMASCREEN.cpp create mode 100644 engine/MUNGA_L4/L4PLASMASCREEN.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d32a85..fb55628 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,6 +204,7 @@ add_library(munga_engine STATIC "engine/MUNGA_L4/L4PADRIO.cpp" "engine/MUNGA_L4/L4KEYLIGHT.cpp" "engine/MUNGA_L4/L4PLASMA.cpp" + "engine/MUNGA_L4/L4PLASMASCREEN.cpp" "engine/MUNGA_L4/L4RIO.cpp" "engine/MUNGA_L4/L4SERIAL.cpp" "engine/MUNGA_L4/L4SPLR.cpp" diff --git a/context/steamification.md b/context/steamification.md index c41706b..b9d1fc3 100644 --- a/context/steamification.md +++ b/context/steamification.md @@ -64,7 +64,30 @@ pattern). Forked at BT411 `4e72f0c` (2026-07-14). parse (59 key buttons/8 axes/12 pad buttons/5 pad axes). Keyboard fallback regression-verified headless (`BT_FORCE_THROTTLE=1` walks). New diagnostics: `BT_CTRLMAP_LOG=1` (streamed-mapping dump), `BT_STICK_LOG=1` (RIO stick push trace). -- Phases 3–7: not started — see the roadmap. +- **Phase 3a (PlasmaScreen, `L4PLASMA=SCREEN`) — DONE 2026-07-14.** `L4PLASMASCREEN.*` + copied verbatim; `L4GREND.cpp` selector diff applied (BT's only divergence there is a + comment at line 440 — no collision). Renders BT's 128×32 plasma into a desktop window. + Verified: a window titled "Plasma Display" opens and the game ticks normally beside it + (`L4GAUGE=640x480x16 L4PLASMA=SCREEN`). Note: the `L4GAUGE` gauge path binds a listen + socket at startup → a first-run Windows Firewall prompt (expected; unrelated to plasma). +- **Phase 3b (single-window MFD-split cockpit, `L4MFDSPLIT=1`) — DEFERRED 2026-07-14.** + Two blockers make this the wrong thing to land now: + 1. **No BT cockpit-layout geometry.** RP412's `L4MFDVIEW` button-bank anchors/addresses + come from vRIO's `CockpitLayout` for the Red Planet pod; vRIO has **zero** BattleTech + data. The BT pod's panel geometry has no reference (needs pod photos / `l4gauge.cfg` + mining / Nick) — the clickable lamp-lit banks can't be authentically placed. + 2. **BT's MFD compositing is an unfinished upstream beachhead.** Per `docs/GAUGE_COMPOSITE.md`, + only the radar/secondary surface composites on a dev box; the five mono MFDs don't + render yet (port-name mismatch, Step 2). RP412's split panes would be empty until that + upstream work lands. + 3. **Merge is HIGH-risk** (quantified above): BT's 521-line L4VB16 divergence (dev-gauge + docking + a 476-line block) overlaps RP412's 402-line split rewrite in the ctor and + `Update` regions — a true hand-merge, not a drop-in. The 3-way diffs are staged in the + scratchpad (`rp412-L4VB16.diff`, `bt-L4VB16.diff`). + Prerequisite order when resumed: finish BT's MFD dev-composite (GAUGE_COMPOSITE.md Steps + 2–3) → obtain BT cockpit geometry → then hand-merge `MFDSplitView`. Until then the game + ships single-window with the dev-gauge dock (`BT_DEV_GAUGES`) as its cockpit composite. +- Phases 4–7: Phase 4 (NetTransport seam) next — unblocked and independent of 3b. ## The seams (what plugs in where) [T0 unless noted] diff --git a/docs/BT412-ROADMAP.md b/docs/BT412-ROADMAP.md index a64f70c..bc63fde 100644 --- a/docs/BT412-ROADMAP.md +++ b/docs/BT412-ROADMAP.md @@ -97,6 +97,13 @@ identical-to-baseline → take RP412's file verbatim, diverged → hand-apply th present path EARLY), live MFDs/radar/plasma; pod multi-surface path (`L4GAUGE`) unbroken. +> **Status (2026-07-14):** 3a landed. **3b DEFERRED** — blocked on (1) the missing BT +> cockpit geometry and (2) BT's own MFD dev-composite being an unfinished upstream +> beachhead (`docs/GAUGE_COMPOSITE.md`: only radar/secondary composites; the five mono +> MFDs don't render yet). Prereq order when resumed: finish the MFD dev-composite → +> obtain BT cockpit geometry → hand-merge `MFDSplitView`. Details in +> `context/steamification.md`. Phase 4 does not depend on 3b. + ## Phase 4 — NetTransport seam + Steam transport (Workstream C.1) - `L4NETTRANSPORT.*` verbatim (abstract seam + `WinsockNetTransport` default; diff --git a/engine/MUNGA_L4/L4GREND.cpp b/engine/MUNGA_L4/L4GREND.cpp index b357511..5e47ebb 100644 --- a/engine/MUNGA_L4/L4GREND.cpp +++ b/engine/MUNGA_L4/L4GREND.cpp @@ -7,6 +7,7 @@ #include "l4gauge.h" #include "..\munga\mission.h" #include "l4plasma.h" +#include "l4plasmascreen.h" #include "..\munga\notation.h" // #define LOCAL_TEST @@ -100,8 +101,16 @@ L4GaugeRenderer::L4GaugeRenderer(bool windowed, int *secondaryIndex, int *aux1In // Tell("Plasma display created on COM1\n"); // externalDisplay = new PlasmaDisplay(PCS_COM1); //} - Tell("Plasma display created on "); Tell(plasma_string); Tell("\n"); - externalDisplay = new PlasmaDisplay(plasma_string); + if (_stricmp(plasma_string, "SCREEN") == 0) + { + Tell("Plasma display created as an on-screen window\n"); + externalDisplay = new PlasmaScreen(); + } + else + { + Tell("Plasma display created on "); Tell(plasma_string); Tell("\n"); + externalDisplay = new PlasmaDisplay(plasma_string); + } if (externalDisplay != NULL) { diff --git a/engine/MUNGA_L4/L4PLASMASCREEN.cpp b/engine/MUNGA_L4/L4PLASMASCREEN.cpp new file mode 100644 index 0000000..4b13807 --- /dev/null +++ b/engine/MUNGA_L4/L4PLASMASCREEN.cpp @@ -0,0 +1,320 @@ +#include "mungal4.h" +#pragma hdrstop + +#include "l4plasmascreen.h" +#include "l4app.h" + +namespace +{ + const char plasmaWindowClass[] = "RPPlasmaScreen"; + + LRESULT CALLBACK + PlasmaWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) + { + PlasmaScreen *screen = + (PlasmaScreen *) GetWindowLongPtrA(hwnd, GWLP_USERDATA); + + switch (message) + { + case WM_PAINT: + if (screen != NULL) + { + // handled below through the friend-free public surface: + // the window procedure only asks the screen to repaint. + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + // PlasmaScreen stores what StretchDIBits needs in + // window properties to keep the class decoupled. + const BITMAPINFO *bmi = + (const BITMAPINFO *) GetPropA(hwnd, "PlasmaBitmapInfo"); + const void *pixels = GetPropA(hwnd, "PlasmaPixels"); + RECT client; + GetClientRect(hwnd, &client); + if (bmi != NULL && pixels != NULL) + { + SetStretchBltMode(hdc, COLORONCOLOR); + StretchDIBits( + hdc, + 0, 0, client.right, client.bottom, + 0, 0, PlasmaScreen::plasmaWidth, PlasmaScreen::plasmaHeight, + pixels, bmi, DIB_RGB_COLORS, SRCCOPY + ); + } + EndPaint(hwnd, &ps); + return 0; + } + break; + + case WM_CLOSE: + // The glass is part of the cockpit; just hide it. + ShowWindow(hwnd, SW_HIDE); + return 0; + + case WM_ERASEBKGND: + return 1; + } + return DefWindowProcA(hwnd, message, wParam, lParam); + } +} + +//######################################################################## +//############################ PlasmaScreen ############################## +//######################################################################## + +PlasmaScreen *PlasmaScreen::activeInstance = NULL; + +void + PlasmaScreen::Position(void *parent, int x, int y, int client_width, int client_height) +{ + if (activeInstance == NULL || activeInstance->window == NULL) + { + return; + } + + HWND glass = (HWND) activeInstance->window; + int window_w = client_width; + int window_h = client_height; + + if (parent != NULL) + { + // fold the glass into the cockpit window as a chrome-less pane + SetWindowLongPtrA(glass, GWL_STYLE, WS_CHILD | WS_VISIBLE); + SetParent(glass, (HWND) parent); + } + else + { + DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; + RECT bounds; + bounds.left = 0; + bounds.top = 0; + bounds.right = client_width; + bounds.bottom = client_height; + AdjustWindowRect(&bounds, style, FALSE); + window_w = bounds.right - bounds.left; + window_h = bounds.bottom - bounds.top; + } + + SetWindowPos( + glass, NULL, + x, y, + window_w, + window_h, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW + ); +} + +void + PlasmaScreen::Hide() +{ + if (activeInstance != NULL && activeInstance->window != NULL) + { + ShowWindow((HWND) activeInstance->window, SW_HIDE); + } +} + +PlasmaScreen::PlasmaScreen(): + Video8BitBuffered(plasmaWidth, plasmaHeight) +{ + Check_Pointer(this); + + window = NULL; + bitmapInfo = NULL; + + scale = 4; + const char *scale_string = getenv("L4PLASMASCALE"); + if (scale_string != NULL) + { + int requested = atoi(scale_string); + if (requested >= 1 && requested <= 16) + { + scale = requested; + } + } + + //--------------------------------------------------------------- + // Build the 8bpp DIB header with the plasma-orange palette: + // index 0 is the dark glass, everything else lights up. + //--------------------------------------------------------------- + BITMAPINFOHEADER *header = (BITMAPINFOHEADER *) new char[ + sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)]; + memset(header, 0, sizeof(BITMAPINFOHEADER)); + header->biSize = sizeof(BITMAPINFOHEADER); + header->biWidth = plasmaWidth; + header->biHeight = -plasmaHeight; // top-down, matching PixelMap8 + header->biPlanes = 1; + header->biBitCount = 8; + header->biCompression = BI_RGB; + header->biClrUsed = 256; + + RGBQUAD *palette = (RGBQUAD *)((char *) header + sizeof(BITMAPINFOHEADER)); + for (int i = 0; i < 256; ++i) + { + if (i == 0) + { + palette[i].rgbRed = 24; palette[i].rgbGreen = 10; palette[i].rgbBlue = 4; + } + else + { + palette[i].rgbRed = 255; palette[i].rgbGreen = 144; palette[i].rgbBlue = 32; + } + palette[i].rgbReserved = 0; + } + bitmapInfo = header; + + CreateGlassWindow(); + + Tell("PlasmaScreen: on-screen plasma display, scale x" << scale << "\n"); +} + +PlasmaScreen::~PlasmaScreen() +{ + Check_Pointer(this); + + if (activeInstance == this) + { + activeInstance = NULL; + } + if (window != NULL) + { + RemovePropA((HWND) window, "PlasmaBitmapInfo"); + RemovePropA((HWND) window, "PlasmaPixels"); + DestroyWindow((HWND) window); + window = NULL; + } + delete [] (char *) bitmapInfo; + bitmapInfo = NULL; +} + +Logical + PlasmaScreen::TestInstance() const +{ + return valid; +} + +void + PlasmaScreen::CreateGlassWindow() +{ + HINSTANCE instance = GetModuleHandleA(NULL); + + static Logical class_registered = False; + if (!class_registered) + { + class_registered = True; + + WNDCLASSA window_class; + memset(&window_class, 0, sizeof(window_class)); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = PlasmaWndProc; + window_class.hInstance = instance; + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = plasmaWindowClass; + RegisterClassA(&window_class); + } + + RECT bounds; + bounds.left = 0; + bounds.top = 0; + bounds.right = plasmaWidth * scale; + bounds.bottom = plasmaHeight * scale; + AdjustWindowRect(&bounds, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE); + + int window_w = bounds.right - bounds.left; + int window_h = bounds.bottom - bounds.top; + + //--------------------------------------------------------------- + // Default position: directly below the main view (the pod mounts + // the glass adjacent to the displays). The main window sits at + // (0,0) with the -res size; clamp to the work area in case the + // main view fills the screen. L4PLASMAPOS=x,y overrides. + //--------------------------------------------------------------- + int x = 0; + int y = (int) L4Application::GetScreenHeight(); + + const char *pos_string = getenv("L4PLASMAPOS"); + if (pos_string != NULL) + { + int px = 0, py = 0; + if (sscanf(pos_string, "%d,%d", &px, &py) == 2) + { + x = px; + y = py; + } + } + else + { + RECT work; + if (SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0)) + { + if (y + window_h > work.bottom) + { + y = work.bottom - window_h; + } + if (y < 0) + { + y = 0; + } + } + } + + window = CreateWindowExA( + 0, + plasmaWindowClass, + "Plasma Display", + WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + x, y, + window_w, + window_h, + NULL, NULL, instance, NULL + ); + + if (window != NULL) + { + SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this); + SetPropA((HWND) window, "PlasmaBitmapInfo", bitmapInfo); + SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer); + ShowWindow((HWND) window, SW_SHOWNOACTIVATE); + activeInstance = this; + } + else + { + DEBUG_STREAM << "PlasmaScreen: window creation failed\n" << std::flush; + } +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Called from the gauge renderer's background task, same slot the serial +// plasma used for streaming. Repaint when any line changed. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Logical + PlasmaScreen::Update(Logical forceall) +{ + Check(this); + + Logical dirty = forceall; + + for (int y = 0; y < plasmaHeight; ++y) + { + if (changedLine[y]) + { + changedLine[y] = 0; + dirty = True; + } + } + + if (dirty && window != NULL) + { + // paint now - queued WM_PAINTs starve behind the game loop's + // one-message-per-frame pump + RedrawWindow((HWND) window, NULL, NULL, + RDW_INVALIDATE | RDW_UPDATENOW); + } + + return False; // 'done' - nothing left to stream +} + +void + PlasmaScreen::WaitForUpdate() +{ + Check(this); +} diff --git a/engine/MUNGA_L4/L4PLASMASCREEN.h b/engine/MUNGA_L4/L4PLASMASCREEN.h new file mode 100644 index 0000000..d64153a --- /dev/null +++ b/engine/MUNGA_L4/L4PLASMASCREEN.h @@ -0,0 +1,64 @@ +#pragma once + +#include "l4vb8.h" + +//######################################################################## +//############################ PlasmaScreen ############################## +//######################################################################## +// +// The cockpit's 128x32 plasma display rendered as a desktop window +// instead of streamed to serial hardware. The gauge system draws into +// the same Video8BitBuffered surface it always has; Update() just blits +// the buffer into a small "glass" window in plasma orange. +// +// Selected with L4PLASMA=SCREEN. L4PLASMASCALE sets the pixel size +// (default 4 -> a 512x128 window). +// +class PlasmaScreen : + public Video8BitBuffered +{ +public: + enum + { + plasmaWidth=128, + plasmaHeight=32 + }; + + PlasmaScreen(); + + ~PlasmaScreen(); + + Logical + TestInstance() const; + + Logical + Update(Logical forceall); + + void + WaitForUpdate(); + + // Reposition/resize the live glass (client-area size; the paint + // stretches to fit, so this also rescales it). With a parent the + // glass becomes a chrome-less child pane of the cockpit window. + // Used by SVGA16's split-view layout; no-op when no plasma exists. + static void + Position(void *parent, int x, int y, int client_width, int client_height); + + // Hide the glass entirely (cockpit currently runs without it). + static void + Hide(); + +protected: + static PlasmaScreen + *activeInstance; + + void + CreateGlassWindow(); + + void + *window; // HWND, kept untyped so this header stays lean + void + *bitmapInfo; // BITMAPINFOHEADER + 256-entry orange palette + int + scale; +};