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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user