From 214a8e079c258eb00c0636b34bc4b4e1b60b5b86 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 12 Jul 2026 14:10:52 -0500 Subject: [PATCH] Un-pack the 7-display cockpit in-engine (L4MFDSPLIT=1) The pod drove five monochrome MFDs from the color channels of two video outputs - SVGA16 packs bit-slices of the shared gauge canvas into R/G/B of gauge window 3 (upper MFDs) and R/G of window 4 (lower MFDs), with the map palettized on the secondary and physically mounted portrait. The desktop reconstruction previously required an external BitBlt-mirror wrapper. With L4MFDSPLIT=1, SVGA16 renders each display into its own window (MFDSplitView, plain GDI) straight from the canvas + port bit-masks: five green-screen MFD windows and the 90CW-rotated Map, tiled in the pod grid to the right of the main view (L4MFDSCALE percent, default 50). The packed D3D windows stay hidden but keep presenting off-screen, leaving the original path untouched. Handles spanning mode (2-window setups). Also: the plasma glass now opens directly below the main view (clamped to the work area; L4PLASMAPOS=x,y overrides) per playtest feedback. Verified: window grid comes up as main + 5 MFDs + Map + plasma with the packed windows hidden; screenshots confirm a green MFD score readout and the portrait tactical map rendering correctly. dist packer and BUILD.md updated; the launcher wrapper is obsolete for split-mode use. Co-Authored-By: Claude Fable 5 --- BUILD.md | 15 ++- MUNGA_L4/L4MFDVIEW.cpp | 168 +++++++++++++++++++++++++++++ MUNGA_L4/L4MFDVIEW.h | 53 +++++++++ MUNGA_L4/L4PLASMASCREEN.cpp | 45 +++++++- MUNGA_L4/L4VB16.cpp | 172 +++++++++++++++++++++++++++++- MUNGA_L4/L4VB16.h | 28 +++++ MUNGA_L4/Munga_L4.vcxproj | 2 + MUNGA_L4/Munga_L4.vcxproj.filters | 6 ++ docs/RP412-ROADMAP.md | 6 ++ pack-dist.ps1 | 10 +- 10 files changed, 497 insertions(+), 8 deletions(-) create mode 100644 MUNGA_L4/L4MFDVIEW.cpp create mode 100644 MUNGA_L4/L4MFDVIEW.h diff --git a/BUILD.md b/BUILD.md index 28be5dc..4dddb47 100644 --- a/BUILD.md +++ b/BUILD.md @@ -112,7 +112,19 @@ Two new environment options remove the hardware dependency entirely: Hot-plugging works; with no controller it falls back to keyboard only. - **`L4PLASMA=SCREEN`** — renders the 128×32 plasma display as its own desktop window ("Plasma Display", plasma orange, `L4PLASMASCALE` sets the - pixel size, default 4). No COM port. Closing the window hides it. + pixel size, default 4). It opens directly below the main view; + `L4PLASMAPOS=x,y` overrides. No COM port. Closing the window hides it. +- **`L4MFDSPLIT=1`** — un-packs the 7-display cockpit. The pod hardware + drove five monochrome MFDs from the color channels of two video outputs + (window 3 = upper MFDs in R/G/B, window 4 = lower MFDs in R/G) and + mounted the map display portrait. With split mode the game itself opens + seven proper windows — main view, five green-screen MFDs ("MFD upper + left/center/right", "MFD lower left/right") and the 90°-rotated "Map" — + rendered CPU-side from the shared gauge canvas; the packed windows stay + hidden. `L4MFDSCALE` sets the view size in percent of the gauge canvas + (default 50). Default layout is the pod grid to the right of the main + view; all windows are draggable. This replaces the external + BitBlt-mirror launcher wrapper. Bindings (vRIO's default profile, condensed): @@ -136,6 +148,7 @@ DPLARG=1 L4DPLCFG=RPDPL.INI L4GAUGE=640x480x16 L4PLASMA=SCREEN +L4MFDSPLIT=1 TARGETFPS=60 ``` diff --git a/MUNGA_L4/L4MFDVIEW.cpp b/MUNGA_L4/L4MFDVIEW.cpp new file mode 100644 index 0000000..39a0058 --- /dev/null +++ b/MUNGA_L4/L4MFDVIEW.cpp @@ -0,0 +1,168 @@ +#include "mungal4.h" +#pragma hdrstop + +#include "l4mfdview.h" + +namespace +{ + const char mfdViewClass[] = "RPMFDView"; + const char mfdViewProp[] = "RPMFDViewBlit"; + + struct MFDViewBlit + { + BITMAPINFOHEADER header; + void *pixels; + int sourceWidth; + int sourceHeight; + }; + + LRESULT CALLBACK + MFDViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) + { + switch (message) + { + case WM_PAINT: + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + const MFDViewBlit *blit = + (const MFDViewBlit *) GetPropA(hwnd, mfdViewProp); + if (blit != NULL && blit->pixels != NULL) + { + RECT client; + GetClientRect(hwnd, &client); + SetStretchBltMode(hdc, COLORONCOLOR); + StretchDIBits( + hdc, + 0, 0, client.right, client.bottom, + 0, 0, blit->sourceWidth, blit->sourceHeight, + blit->pixels, (const BITMAPINFO *) &blit->header, + DIB_RGB_COLORS, SRCCOPY + ); + } + EndPaint(hwnd, &ps); + } + return 0; + + case WM_CLOSE: + // Part of the cockpit; just hide it. + ShowWindow(hwnd, SW_HIDE); + return 0; + + case WM_ERASEBKGND: + return 1; + } + return DefWindowProcA(hwnd, message, wParam, lParam); + } +} + +MFDSplitView::MFDSplitView( + const char *title, + int source_width, + int source_height, + int client_width, + int client_height, + int x, + int y +) +{ + Check_Pointer(this); + + sourceWidth = source_width; + sourceHeight = source_height; + + pixels = new unsigned long[source_width * source_height]; + memset(pixels, 0, source_width * source_height * sizeof(unsigned long)); + + MFDViewBlit *blit = new MFDViewBlit; + memset(blit, 0, sizeof(MFDViewBlit)); + blit->header.biSize = sizeof(BITMAPINFOHEADER); + blit->header.biWidth = source_width; + blit->header.biHeight = -source_height; // top-down + blit->header.biPlanes = 1; + blit->header.biBitCount = 32; + blit->header.biCompression = BI_RGB; + blit->pixels = pixels; + blit->sourceWidth = source_width; + blit->sourceHeight = source_height; + blitInfo = blit; + + 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 = MFDViewWndProc; + window_class.hInstance = instance; + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = mfdViewClass; + RegisterClassA(&window_class); + } + + 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 = CreateWindowExA( + 0, + mfdViewClass, + title, + style, + x, y, + bounds.right - bounds.left, + bounds.bottom - bounds.top, + NULL, NULL, instance, NULL + ); + + if (window != NULL) + { + SetPropA((HWND) window, mfdViewProp, blitInfo); + ShowWindow((HWND) window, SW_SHOWNOACTIVATE); + } + else + { + DEBUG_STREAM << "MFDSplitView: window creation failed (" << title << ")\n" << std::flush; + } +} + +MFDSplitView::~MFDSplitView() +{ + Check_Pointer(this); + + if (window != NULL) + { + RemovePropA((HWND) window, mfdViewProp); + DestroyWindow((HWND) window); + window = NULL; + } + delete (MFDViewBlit *) blitInfo; + blitInfo = NULL; + delete [] pixels; + pixels = NULL; +} + +Logical + MFDSplitView::TestInstance() const +{ + return pixels != NULL; +} + +void + MFDSplitView::Repaint() +{ + Check_Pointer(this); + if (window != NULL) + { + InvalidateRect((HWND) window, NULL, FALSE); + } +} diff --git a/MUNGA_L4/L4MFDVIEW.h b/MUNGA_L4/L4MFDVIEW.h new file mode 100644 index 0000000..45b3983 --- /dev/null +++ b/MUNGA_L4/L4MFDVIEW.h @@ -0,0 +1,53 @@ +#pragma once + +#include "..\munga\style.h" + +//######################################################################## +//############################ MFDSplitView ############################## +//######################################################################## +// +// One desktop window showing a single cockpit display, used by SVGA16's +// split mode (L4MFDSPLIT=1). The pod hardware packed the five monochrome +// MFDs into the color channels of two video outputs and mounted the map +// display rotated; on a desktop each display gets its own window instead. +// +// SVGA16 fills Pixels() (32bpp top-down, sourceWidth x sourceHeight as +// passed - already swapped by the caller for the rotated map) and calls +// Repaint(). Closing a view window just hides it. +// +class MFDSplitView +{ +public: + MFDSplitView( + const char *title, + int source_width, + int source_height, + int client_width, + int client_height, + int x, + int y + ); + + ~MFDSplitView(); + + Logical + TestInstance() const; + + unsigned long * + Pixels() + { return pixels; } + + void + Repaint(); + +protected: + void + *window; // HWND + void + *blitInfo; // MFDViewBlit (header + source dims), see .cpp + unsigned long + *pixels; + int + sourceWidth, + sourceHeight; +}; diff --git a/MUNGA_L4/L4PLASMASCREEN.cpp b/MUNGA_L4/L4PLASMASCREEN.cpp index 5aca79e..66388b5 100644 --- a/MUNGA_L4/L4PLASMASCREEN.cpp +++ b/MUNGA_L4/L4PLASMASCREEN.cpp @@ -2,6 +2,7 @@ #pragma hdrstop #include "l4plasmascreen.h" +#include "l4app.h" namespace { @@ -163,14 +164,52 @@ void 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, - CW_USEDEFAULT, CW_USEDEFAULT, - bounds.right - bounds.left, - bounds.bottom - bounds.top, + x, y, + window_w, + window_h, NULL, NULL, instance, NULL ); diff --git a/MUNGA_L4/L4VB16.cpp b/MUNGA_L4/L4VB16.cpp index 942fad7..bdc3e35 100644 --- a/MUNGA_L4/L4VB16.cpp +++ b/MUNGA_L4/L4VB16.cpp @@ -2,6 +2,7 @@ #pragma hdrstop #include "l4vb16.h" +#include "l4mfdview.h" #include "../munga/gaugrend.h" #include "L4VIDEO.h" #include "DXUtils.h" @@ -176,7 +177,12 @@ void SVGA16::BuildWindows(unsigned int width, unsigned int height, bool windowed gaugeWindows[j] = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_BORDER, surfaceWindowRect.left, surfaceWindowRect.top, surfaceWindowRect.right-surfaceWindowRect.left, surfaceWindowRect.bottom-surfaceWindowRect.top, (HWND)NULL, (HMENU)NULL, L4Application::GetAppInstance(), (LPVOID)NULL); if (gaugeWindows[j]) { - ShowWindow(gaugeWindows[j], SW_SHOW); + // In split-view mode the packed windows are only kept as D3D + // device targets; the split windows are what the player sees. + if (!splitViews) + { + ShowWindow(gaugeWindows[j], SW_SHOW); + } memset(&mPresentParams[j], 0, sizeof(D3DPRESENT_PARAMETERS)); @@ -3809,7 +3815,76 @@ SVGA16::SVGA16( int *aux2Index ):Video16BitBuffered(init_width, init_height) { + //------------------------------------------------------------------ + // Split-view mode: decide before BuildWindows so the packed gauge + // windows can stay hidden. + //------------------------------------------------------------------ + splitViews = False; + { + const char *split_string = getenv("L4MFDSPLIT"); + if (split_string != NULL && atoi(split_string) != 0) + { + splitViews = True; + } + } + for (int view = 0; view < SplitViewCount; ++view) + { + splitView[view] = NULL; + } + BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index); + + if (splitViews) + { + //--------------------------------------------------------------- + // One window per cockpit display. L4MFDSCALE is the view size in + // percent of the gauge canvas (default 50). Default layout is the + // pod grid to the right of the main view: + // [ main ] [ UL ] [ C ] [ UR ] + // [ LL ] [ map ] [ LR ] + //--------------------------------------------------------------- + int scale_percent = 50; + const char *scale_string = getenv("L4MFDSCALE"); + if (scale_string != NULL) + { + int requested = atoi(scale_string); + if (requested >= 10 && requested <= 200) + { + scale_percent = requested; + } + } + + int cell_w = (init_width * scale_percent) / 100; + int cell_h = (init_height * scale_percent) / 100; + int frame_h = GetSystemMetrics(SM_CYCAPTION) + 2 * GetSystemMetrics(SM_CYFIXEDFRAME); + int frame_w = 2 * GetSystemMetrics(SM_CXFIXEDFRAME); + int grid_x = init_width + frame_w; + int row2_y = cell_h + frame_h; + + splitView[SplitMFDUpperLeft] = new MFDSplitView( + "MFD upper left", init_width, init_height, + cell_w, cell_h, grid_x, 0); + splitView[SplitMFDUpperCenter] = new MFDSplitView( + "MFD upper center", init_width, init_height, + cell_w, cell_h, grid_x + (cell_w + frame_w), 0); + splitView[SplitMFDUpperRight] = new MFDSplitView( + "MFD upper right", init_width, init_height, + cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), 0); + splitView[SplitMFDLowerLeft] = new MFDSplitView( + "MFD lower left", init_width, init_height, + cell_w, cell_h, grid_x, row2_y); + splitView[SplitMFDLowerRight] = new MFDSplitView( + "MFD lower right", init_width, init_height, + cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), row2_y); + // map is portrait: source rotated 90 degrees clockwise + splitView[SplitMap] = new MFDSplitView( + "Map", init_height, init_width, + cell_h, cell_w, + grid_x + (cell_w + frame_w) + (cell_w - cell_h) / 2, row2_y); + + DEBUG_STREAM << "SVGA16: split views active - 5 MFD windows + rotated map (scale " + << scale_percent << "%)\n" << std::flush; + } //STUBBED: VIDEO RB 1/15/07 # if defined(DEBUG) Tell("SVGA16::SVGA16()\n"); @@ -3993,6 +4068,13 @@ SVGA16::~SVGA16() // Set VWE video splitter clock divider //--------------------------------------------------------- //SVGASetSplitterClock(False); + + for (int view = 0; view < SplitViewCount; ++view) + { + delete splitView[view]; + splitView[view] = NULL; + } + Check_Fpu(); } @@ -4029,6 +4111,43 @@ int GetShiftAmount(DWORD mask) return amount; } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Render one MFD's bit-slice of the shared canvas into its split-view +// window as a green screen. The mask arrives in the duplicated +// (mask | mask<<16) form the packed path uses. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void SVGA16::FillSplitMFD( + SplitViewID view, + DWORD mask, + int shift +) +{ + if (splitView[view] == NULL) + { + return; + } + + Word mask16 = (Word)(mask & 0xFFFF); + Word max_value = (Word)(mask16 >> shift); + if (max_value == 0) + { + return; + } + + Word *source = pixelBuffer.Data.MapPointer; + unsigned long *dest = splitView[view]->Pixels(); + int count = pixelBuffer.Data.Size.x * pixelBuffer.Data.Size.y; + + for (int i = 0; i < count; ++i) + { + unsigned long value = (unsigned long)((*source++ & mask16) >> shift); + unsigned long green = (value * 255ul) / max_value; + *dest++ = green << 8; + } + + splitView[view]->Repaint(); +} + Logical SVGA16::Update(Logical forceAll) { HRESULT hr; @@ -4101,6 +4220,57 @@ Logical SVGA16::Update(Logical forceAll) } } + //------------------------------------------------------------------ + // Split views: render this window's displays into their own desktop + // windows, straight from the shared canvas (CPU-side, no D3D). + //------------------------------------------------------------------ + if (splitViews) + { + if (mDisplayToUpdate == 0) + { + if (splitView[SplitMap] != NULL) + { + // Map is portrait-mounted: rotate 90 degrees clockwise. + // dest(x,y) = source(row = srcH-1-x, col = y) + Word *source_base = pixelBuffer.Data.MapPointer; + unsigned long *dest = splitView[SplitMap]->Pixels(); + int src_w = pixelBuffer.Data.Size.x; + int src_h = pixelBuffer.Data.Size.y; + + for (int dy = 0; dy < src_w; ++dy) + { + for (int dx = 0; dx < src_h; ++dx) + { + Word pixel = source_base[(src_h - 1 - dx) * src_w + dy]; + PaletteTriplet *entry = + &secPalette->paletteData.Color[pixel & secMask]; + *dest++ = ((unsigned long) entry->Red << 16) | + ((unsigned long) entry->Green << 8) | + ((unsigned long) entry->Blue); + } + } + splitView[SplitMap]->Repaint(); + } + } + else if (mDisplayToUpdate == 1) + { + FillSplitMFD(SplitMFDUpperLeft, ulMask, ulMask_sh); + FillSplitMFD(SplitMFDUpperCenter, ucMask, ucMask_sh); + FillSplitMFD(SplitMFDUpperRight, urMask, urMask_sh); + if (NUMGAUGEWINDOWS < 3) + { + // spanning mode: the lower MFDs ride this window too + FillSplitMFD(SplitMFDLowerLeft, llMask, llMask_sh); + FillSplitMFD(SplitMFDLowerRight, lrMask, lrMask_sh); + } + } + else + { + FillSplitMFD(SplitMFDLowerLeft, llMask, llMask_sh); + FillSplitMFD(SplitMFDLowerRight, lrMask, lrMask_sh); + } + } + Word *data = pixelBuffer.Data.MapPointer; D3DLOCKED_RECT rect; diff --git a/MUNGA_L4/L4VB16.h b/MUNGA_L4/L4VB16.h index bc1b987..8fc93ae 100644 --- a/MUNGA_L4/L4VB16.h +++ b/MUNGA_L4/L4VB16.h @@ -7,6 +7,7 @@ class L4graphicsPort; +class MFDSplitView; //######################################################################## //######################### Video16BitBuffered ########################### @@ -291,6 +292,33 @@ private: RECT *mSurfaceRects; int mDisplayToUpdate; + + //------------------------------------------------------------------ + // Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and + // the rotated map render as their own desktop windows; the packed + // D3D gauge windows stay hidden. See L4MFDVIEW.h. + //------------------------------------------------------------------ + enum SplitViewID { + SplitMFDUpperLeft = 0, + SplitMFDUpperCenter, + SplitMFDUpperRight, + SplitMFDLowerLeft, + SplitMFDLowerRight, + SplitMap, + SplitViewCount + }; + + void + FillSplitMFD( + SplitViewID view, + DWORD mask16, + int shift + ); + + Logical + splitViews; + MFDSplitView + *splitView[SplitViewCount]; }; //######################################################################## diff --git a/MUNGA_L4/Munga_L4.vcxproj b/MUNGA_L4/Munga_L4.vcxproj index 184deb5..e721fc9 100644 --- a/MUNGA_L4/Munga_L4.vcxproj +++ b/MUNGA_L4/Munga_L4.vcxproj @@ -249,6 +249,7 @@ + @@ -438,6 +439,7 @@ + diff --git a/MUNGA_L4/Munga_L4.vcxproj.filters b/MUNGA_L4/Munga_L4.vcxproj.filters index 18a32c0..b3070e4 100644 --- a/MUNGA_L4/Munga_L4.vcxproj.filters +++ b/MUNGA_L4/Munga_L4.vcxproj.filters @@ -564,6 +564,9 @@ Source Files\MUNGA_L4 + + Source Files\MUNGA_L4 + Source Files\MUNGA_L4 @@ -1127,6 +1130,9 @@ Header Files\MUNGA_L4 + + Header Files\MUNGA_L4 + Header Files\MUNGA_L4 diff --git a/docs/RP412-ROADMAP.md b/docs/RP412-ROADMAP.md index d2cf4b7..4cd8c8c 100644 --- a/docs/RP412-ROADMAP.md +++ b/docs/RP412-ROADMAP.md @@ -50,6 +50,12 @@ so press-feedback works like the real button field. `Video8BitBuffered` surface into a desktop "Plasma Display" window in plasma orange — verified drawing live game content (score readout). See BUILD.md §4 for bindings and the desktop `environ.ini`. +- ✅ **7-display cockpit in-engine** (`L4MFDSPLIT=1`): the five MFDs the pod + packed into the color channels of two video outputs, plus the + portrait-mounted map, now open as their own windows (green-screen MFDs, + 90°-rotated map), rendered CPU-side from the shared gauge canvas — the + external BitBlt-mirror wrapper is obsolete. Verified visually (MFD score + readout, full tactical map). - Next in A: on-screen RIO panel fed by `PadRIO::lampState[]` (the cockpit-feel layout — vRIO button clusters around the displays); port the full vRIO bindings-file model (deflect/rate/deadzone per axis, rebinding); diff --git a/pack-dist.ps1 b/pack-dist.ps1 index 753accc..d00cbda 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -67,6 +67,7 @@ DPLARG=1 L4DPLCFG=RPDPL.INI L4GAUGE=640x480x16 L4PLASMA=SCREEN +L4MFDSPLIT=1 TARGETFPS=60 "@ @@ -94,9 +95,12 @@ Controls (XInput controller and/or keyboard): DPad / arrow keys joystick hat (look) Start,Back / F1,F2 config buttons -The "Plasma Display" window is the pod's plasma glass. Closing it just -hides it. environ.ini options: L4PADFLIP=XY inverts the stick axes, -L4PLASMASCALE=n sets the plasma pixel size (default 4). +The full pod cockpit comes up as separate windows: the main view, five +green MFD screens, the portrait map, and the orange plasma glass below +the main view. Drag them wherever you like; closing one just hides it. +environ.ini options: L4PADFLIP=XY inverts the stick axes, L4MFDSCALE=n +sizes the MFD/map windows (percent, default 50), L4PLASMASCALE=n sets +the plasma pixel size (default 4), L4PLASMAPOS=x,y places the plasma. Known prototype notes: pods race untextured (the player1-8 skins come from the presets system, not shipped data), and text drawn on the plasma