diff --git a/BUILD.md b/BUILD.md index 4dddb47..1b24f82 100644 --- a/BUILD.md +++ b/BUILD.md @@ -126,6 +126,17 @@ Two new environment options remove the hardware dependency entirely: view; all windows are draggable. This replaces the external BitBlt-mirror launcher wrapper. + Each split window also carries its display's **physical button bank** + (geometry per vRIO's `CockpitLayout`): 4 red buttons above and below + each MFD glass (RIO addresses descending from the cluster anchor — + upper left 0x2F, upper center 0x27, upper right 0x37, lower left 0x0F, + lower right 0x07), and 6 amber buttons down each side of the map + (Secondary 0x10–0x15 left, Screen 0x18–0x1D right; the columns' other + addresses are Tesla relays, not buttons). They **light from the lamp + states the game commands** (dim/bright, flash modes animate) and press + the corresponding RIO unit with the mouse — active when `PadRIO` is the + control device, dark and inert with real serial hardware. + Bindings (vRIO's default profile, condensed): | Input | Pod control | diff --git a/MUNGA_L4/L4MFDVIEW.cpp b/MUNGA_L4/L4MFDVIEW.cpp index 39a0058..178528b 100644 --- a/MUNGA_L4/L4MFDVIEW.cpp +++ b/MUNGA_L4/L4MFDVIEW.cpp @@ -2,47 +2,83 @@ #pragma hdrstop #include "l4mfdview.h" +#include "l4padrio.h" namespace { const char mfdViewClass[] = "RPMFDView"; - const char mfdViewProp[] = "RPMFDViewBlit"; - struct MFDViewBlit + const int buttonGap = 4; + + //--------------------------------------------------------------- + // Button fill colors by lamp brightness (0 off / 1-2 dim / 3 + // bright), red family for MFD strips, amber for the side columns. + //--------------------------------------------------------------- + COLORREF ButtonFill(int amber, int level) { - BITMAPINFOHEADER header; - void *pixels; - int sourceWidth; - int sourceHeight; - }; + if (amber) + { + if (level >= 3) return RGB(255, 210, 48); + if (level >= 1) return RGB(150, 124, 26); + return RGB(64, 52, 10); + } + if (level >= 3) return RGB(255, 72, 44); + if (level >= 1) return RGB(150, 44, 28); + return RGB(64, 18, 12); + } + + //--------------------------------------------------------------- + // Lamp byte -> current brightness level, animating flash modes. + // Low 2 bits: solid/slow/med/fast; bits 2-3: state 1 brightness; + // bits 4-5: state 2 brightness (the flash alternate). + //--------------------------------------------------------------- + int LampLevel(int lamp_state) + { + int mode = lamp_state & 0x03; + int level1 = (lamp_state >> 2) & 0x03; + int level2 = (lamp_state >> 4) & 0x03; + + if (mode == 0) + { + return level1; + } + + static const int half_period[4] = { 0, 500, 250, 125 }; + return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1; + } LRESULT CALLBACK MFDViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + MFDSplitView *view = + (MFDSplitView *) GetWindowLongPtrA(hwnd, GWLP_USERDATA); + switch (message) { case WM_PAINT: + if (view != NULL) { - 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); + view->Paint(); + return 0; } - return 0; + break; + + case WM_LBUTTONDOWN: + if (view != NULL) + { + view->MouseDown((int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam)); + return 0; + } + break; + + case WM_LBUTTONUP: + case WM_CAPTURECHANGED: + if (view != NULL) + { + view->MouseUp(); + return 0; + } + break; case WM_CLOSE: // Part of the cockpit; just hide it. @@ -56,36 +92,47 @@ namespace } } +//######################################################################## +//############################ MFDSplitView ############################## +//######################################################################## + MFDSplitView::MFDSplitView( const char *title, int source_width, int source_height, - int client_width, - int client_height, + int display_width, + int display_height, int x, - int y + int y, + ButtonStyle button_style, + int anchorA, + int anchorB ) { Check_Pointer(this); sourceWidth = source_width; sourceHeight = source_height; + buttonCount = 0; + pressedIndex = -1; 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; + BITMAPINFOHEADER *header = new BITMAPINFOHEADER; + memset(header, 0, sizeof(BITMAPINFOHEADER)); + header->biSize = sizeof(BITMAPINFOHEADER); + header->biWidth = source_width; + header->biHeight = -source_height; // top-down + header->biPlanes = 1; + header->biBitCount = 32; + header->biCompression = BI_RGB; + blitHeader = header; + + int client_width = display_width; + int client_height = display_height; + LayoutButtons(button_style, anchorA, anchorB, + display_width, display_height, &client_width, &client_height); HINSTANCE instance = GetModuleHandleA(NULL); @@ -126,7 +173,7 @@ MFDSplitView::MFDSplitView( if (window != NULL) { - SetPropA((HWND) window, mfdViewProp, blitInfo); + SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this); ShowWindow((HWND) window, SW_SHOWNOACTIVATE); } else @@ -141,12 +188,12 @@ MFDSplitView::~MFDSplitView() if (window != NULL) { - RemovePropA((HWND) window, mfdViewProp); + SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0); DestroyWindow((HWND) window); window = NULL; } - delete (MFDViewBlit *) blitInfo; - blitInfo = NULL; + delete (BITMAPINFOHEADER *) blitHeader; + blitHeader = NULL; delete [] pixels; pixels = NULL; } @@ -157,6 +204,171 @@ Logical return pixels != NULL; } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Compute the display rectangle and the button rectangles, growing the +// client area to make room for the strips. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void + MFDSplitView::LayoutButtons( + ButtonStyle button_style, + int anchorA, + int anchorB, + int display_width, + int display_height, + int *client_width, + int *client_height + ) +{ + displayX = 0; + displayY = 0; + displayW = display_width; + displayH = display_height; + + if (button_style == MFDStrips) + { + //----------------------------------------------------------- + // 4 buttons above the glass, 4 below; RIO addresses descend + // from the anchor, row-major (vRIO CockpitLayout::Mfd). + //----------------------------------------------------------- + int strip_h = display_height / 8; + if (strip_h < 18) strip_h = 18; + if (strip_h > 40) strip_h = 40; + + int button_w = (display_width - 5 * buttonGap) / 4; + + displayY = strip_h + 2 * buttonGap; + *client_height = display_height + 2 * (strip_h + 2 * buttonGap); + + for (int i = 0; i < 8; ++i) + { + int row = i / 4; // 0 = top strip, 1 = bottom strip + int col = i % 4; + + ScreenButton *button = &buttons[buttonCount++]; + button->unit = anchorA - i; + button->amber = 0; + button->x = buttonGap + col * (button_w + buttonGap); + button->y = row ? (displayY + displayH + buttonGap) : buttonGap; + button->w = button_w; + button->h = strip_h; + } + } + else if (button_style == SideColumns) + { + //----------------------------------------------------------- + // 6 buttons down each side of the glass (the pod wires only + // 6 of each column's 8 addresses to buttons; the rest are + // Tesla relays). Left = anchorA ascending, right = anchorB. + //----------------------------------------------------------- + int col_w = display_width / 8; + if (col_w < 20) col_w = 20; + if (col_w > 40) col_w = 40; + + int button_h = (display_height - 7 * buttonGap) / 6; + + displayX = col_w + 2 * buttonGap; + *client_width = display_width + 2 * (col_w + 2 * buttonGap); + + for (int i = 0; i < 6; ++i) + { + ScreenButton *left = &buttons[buttonCount++]; + left->unit = anchorA + i; + left->amber = 1; + left->x = buttonGap; + left->y = buttonGap + i * (button_h + buttonGap); + left->w = col_w; + left->h = button_h; + + ScreenButton *right = &buttons[buttonCount++]; + right->unit = anchorB + i; + right->amber = 1; + right->x = displayX + displayW + buttonGap; + right->y = left->y; + right->w = col_w; + right->h = button_h; + } + } +} + +void + MFDSplitView::Paint() +{ + PAINTSTRUCT ps; + HDC hdc = BeginPaint((HWND) window, &ps); + + RECT client; + GetClientRect((HWND) window, &client); + FillRect(hdc, &client, (HBRUSH) GetStockObject(BLACK_BRUSH)); + + SetStretchBltMode(hdc, COLORONCOLOR); + StretchDIBits( + hdc, + displayX, displayY, displayW, displayH, + 0, 0, sourceWidth, sourceHeight, + pixels, (const BITMAPINFO *) blitHeader, + DIB_RGB_COLORS, SRCCOPY + ); + + for (int i = 0; i < buttonCount; ++i) + { + const ScreenButton *button = &buttons[i]; + + int level = PadRIO::IsActive() + ? LampLevel(PadRIO::GetLampState(button->unit)) + : 0; + + RECT rect; + rect.left = button->x; + rect.top = button->y; + rect.right = button->x + button->w; + rect.bottom = button->y + button->h; + + HBRUSH fill = CreateSolidBrush(ButtonFill(button->amber, level)); + FillRect(hdc, &rect, fill); + DeleteObject(fill); + + HBRUSH frame = CreateSolidBrush( + (i == pressedIndex) ? RGB(255, 255, 255) : RGB(48, 48, 48)); + FrameRect(hdc, &rect, frame); + DeleteObject(frame); + } + + EndPaint((HWND) window, &ps); +} + +void + MFDSplitView::MouseDown(int x, int y) +{ + for (int i = 0; i < buttonCount; ++i) + { + const ScreenButton *button = &buttons[i]; + if (x >= button->x && x < button->x + button->w && + y >= button->y && y < button->y + button->h) + { + pressedIndex = i; + SetCapture((HWND) window); + PadRIO::SetScreenButton(button->unit, True); + InvalidateRect((HWND) window, NULL, FALSE); + return; + } + } +} + +void + MFDSplitView::MouseUp() +{ + if (pressedIndex >= 0) + { + PadRIO::SetScreenButton(buttons[pressedIndex].unit, False); + pressedIndex = -1; + if (GetCapture() == (HWND) window) + { + ReleaseCapture(); + } + InvalidateRect((HWND) window, NULL, FALSE); + } +} + void MFDSplitView::Repaint() { diff --git a/MUNGA_L4/L4MFDVIEW.h b/MUNGA_L4/L4MFDVIEW.h index 45b3983..0e2f7df 100644 --- a/MUNGA_L4/L4MFDVIEW.h +++ b/MUNGA_L4/L4MFDVIEW.h @@ -11,6 +11,16 @@ // 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. // +// Each window also carries its display's physical button bank, exactly +// as mounted in the pod (geometry per vRIO's CockpitLayout): +// - MFDs: a 4x2 cluster - 4 red buttons above the glass, 4 below, +// RIO addresses descending from the cluster anchor (top-left = hi). +// - The map ("secondary screen"): 6 amber buttons down each side - +// Secondary 0x10-0x15 on the left, Screen 0x18-0x1D on the right +// (the remaining column addresses are Tesla relays, not buttons). +// Buttons light from the lamp state the game commands (via PadRIO) and +// press/release RIO units with the mouse. +// // 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. @@ -18,14 +28,24 @@ class MFDSplitView { public: + enum ButtonStyle + { + NoButtons = 0, + MFDStrips, // 4 above + 4 below, addresses descend from anchorA + SideColumns // 6 per side; anchorA = left base, anchorB = right base + }; + MFDSplitView( const char *title, int source_width, int source_height, - int client_width, - int client_height, + int display_width, + int display_height, int x, - int y + int y, + ButtonStyle button_style = NoButtons, + int anchorA = 0, + int anchorB = 0 ); ~MFDSplitView(); @@ -40,14 +60,53 @@ public: void Repaint(); + // Window-procedure callbacks (public for the registered WndProc) + void + Paint(); + void + MouseDown(int x, int y); + void + MouseUp(); + protected: + void + LayoutButtons( + ButtonStyle button_style, + int anchorA, + int anchorB, + int display_width, + int display_height, + int *client_width, + int *client_height + ); + + struct ScreenButton + { + int unit; + int x, y, w, h; + int amber; // 0 = red family, 1 = amber family + }; + + enum { maxButtons = 20 }; + void *window; // HWND void - *blitInfo; // MFDViewBlit (header + source dims), see .cpp + *blitHeader; // BITMAPINFOHEADER for StretchDIBits unsigned long *pixels; int sourceWidth, sourceHeight; + + int + displayX, displayY, + displayW, displayH; + + ScreenButton + buttons[maxButtons]; + int + buttonCount; + int + pressedIndex; }; diff --git a/MUNGA_L4/L4PADRIO.cpp b/MUNGA_L4/L4PADRIO.cpp index eb4dbfe..24bc8b4 100644 --- a/MUNGA_L4/L4PADRIO.cpp +++ b/MUNGA_L4/L4PADRIO.cpp @@ -87,6 +87,27 @@ namespace //############################### PadRIO ################################# //######################################################################## +PadRIO *PadRIO::activeInstance = NULL; + +void + PadRIO::SetScreenButton(int unit, Logical pressed) +{ + if (activeInstance != NULL && unit >= 0 && unit < buttonUnits) + { + activeInstance->screenButton[unit] = pressed ? 1 : 0; + } +} + +int + PadRIO::GetLampState(int unit) +{ + if (activeInstance != NULL && unit >= 0 && unit < lampCount) + { + return activeInstance->lampState[unit]; + } + return 0; +} + PadRIO::PadRIO() { Check_Pointer(this); @@ -105,6 +126,7 @@ PadRIO::PadRIO() memset(buttonDown, 0, sizeof(buttonDown)); memset(lampState, 0, sizeof(lampState)); + memset(screenButton, 0, sizeof(screenButton)); invertX = False; invertY = False; @@ -125,12 +147,18 @@ PadRIO::PadRIO() MajorRevision = 4; MinorRevision = 2; + activeInstance = this; + DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush; } PadRIO::~PadRIO() { Check_Pointer(this); + if (activeInstance == this) + { + activeInstance = NULL; + } } Logical @@ -290,6 +318,13 @@ void desired[keyboardButtonMap[i].rioUnit] = 1; } } + for (int i = 0; i < buttonUnits; ++i) + { + if (screenButton[i]) + { + desired[i] = 1; + } + } for (int unit = 0; unit < buttonUnits; ++unit) { diff --git a/MUNGA_L4/L4PADRIO.h b/MUNGA_L4/L4PADRIO.h index eafce95..5c09895 100644 --- a/MUNGA_L4/L4PADRIO.h +++ b/MUNGA_L4/L4PADRIO.h @@ -50,11 +50,24 @@ public: SetLamp(int lampNumber, int state); // Lamp state the game has commanded, indexed by RIO lamp number. - // A future on-screen panel reads this to light its buttons. + // The on-screen cockpit buttons read this to light themselves. enum { lampCount = 64 }; unsigned char lampState[lampCount]; + //--------------------------------------------------------------- + // On-screen cockpit buttons (MFDSplitView strips) press RIO units + // through these; they are no-ops when no PadRIO is active (e.g. + // real serial hardware selected). + //--------------------------------------------------------------- + static void + SetScreenButton(int unit, Logical pressed); + static int + GetLampState(int unit); + static Logical + IsActive() + { return activeInstance != NULL; } + protected: void PollInputs(); @@ -69,6 +82,13 @@ protected: int queueHead, queueTail; + static PadRIO + *activeInstance; + + // pressed state driven by the on-screen cockpit buttons + unsigned char + screenButton[0x48]; + unsigned long lastPollTick, lastPadCheckTick; diff --git a/MUNGA_L4/L4VB16.cpp b/MUNGA_L4/L4VB16.cpp index bdc3e35..5d27a5e 100644 --- a/MUNGA_L4/L4VB16.cpp +++ b/MUNGA_L4/L4VB16.cpp @@ -3859,30 +3859,50 @@ SVGA16::SVGA16( 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; + // the MFD button strips grow each window (mirror of + // MFDSplitView::LayoutButtons so the grid rows still tile) + int strip_h = cell_h / 8; + if (strip_h < 18) strip_h = 18; + if (strip_h > 40) strip_h = 40; + int mfd_extra_h = 2 * (strip_h + 8); + int row2_y = cell_h + mfd_extra_h + frame_h; + + //--------------------------------------------------------------- + // Button banks per display (addresses per vRIO CockpitLayout, + // bank-to-display placement per the pod cockpit): + // upper left MFD 0x2F.. / upper center 0x27.. / upper right + // 0x37.. / lower left 0x0F.. / lower right 0x07.. and the map + // flanked by Secondary 0x10-0x15 / Screen 0x18-0x1D. + //--------------------------------------------------------------- splitView[SplitMFDUpperLeft] = new MFDSplitView( "MFD upper left", init_width, init_height, - cell_w, cell_h, grid_x, 0); + cell_w, cell_h, grid_x, 0, + MFDSplitView::MFDStrips, 0x2F); splitView[SplitMFDUpperCenter] = new MFDSplitView( "MFD upper center", init_width, init_height, - cell_w, cell_h, grid_x + (cell_w + frame_w), 0); + cell_w, cell_h, grid_x + (cell_w + frame_w), 0, + MFDSplitView::MFDStrips, 0x27); splitView[SplitMFDUpperRight] = new MFDSplitView( "MFD upper right", init_width, init_height, - cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), 0); + cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), 0, + MFDSplitView::MFDStrips, 0x37); splitView[SplitMFDLowerLeft] = new MFDSplitView( "MFD lower left", init_width, init_height, - cell_w, cell_h, grid_x, row2_y); + cell_w, cell_h, grid_x, row2_y, + MFDSplitView::MFDStrips, 0x0F); splitView[SplitMFDLowerRight] = new MFDSplitView( "MFD lower right", init_width, init_height, - cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), row2_y); + cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), row2_y, + MFDSplitView::MFDStrips, 0x07); // 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); + grid_x + (cell_w + frame_w) + (cell_w - cell_h) / 2, row2_y, + MFDSplitView::SideColumns, 0x10, 0x18); - DEBUG_STREAM << "SVGA16: split views active - 5 MFD windows + rotated map (scale " + DEBUG_STREAM << "SVGA16: split views active - 5 MFD windows + rotated map, cockpit buttons on (scale " << scale_percent << "%)\n" << std::flush; } //STUBBED: VIDEO RB 1/15/07 diff --git a/docs/RP412-ROADMAP.md b/docs/RP412-ROADMAP.md index 4cd8c8c..09708ec 100644 --- a/docs/RP412-ROADMAP.md +++ b/docs/RP412-ROADMAP.md @@ -56,10 +56,20 @@ so press-feedback works like the real button field. 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); - pilot keypad (numpad → KeyEvent); Steam Input once C starts. +- ✅ **Cockpit buttons on the displays** (2026-07-12): each split window + carries its physical button bank — 4+4 red buttons per MFD, 6 amber per + map side (Secondary/Screen columns; addresses per vRIO `CockpitLayout`, + placement per the pod). Mouse presses inject into `PadRIO`; the game's + lamp commands light them (verified: PRESET lamps lit on the map flank, + aligned with the glass's own edge labels). +- Next in A: port the full vRIO bindings-file model (deflect/rate/deadzone + per axis, rebinding); pilot keypad (numpad / on-screen 4×4 → KeyEvent); + Steam Input once C starts. +- Polish pass (queued): **vRIO's Dynamic Lighting RGB-keyboard mirror** — + lamp states glow on the physical keyboard's keys (per-key or zone-lit), + blinking with the flash modes. Needs package identity for background + light control (vRIO's `pkg\Register-vRIO.ps1` pattern; see the vRIO + README for the Settings → Dynamic Lighting steps). - vRIO itself stays useful as a dev harness against unmodified builds. ## Workstream B — In-game sessions (from TeslaConsole) diff --git a/pack-dist.ps1 b/pack-dist.ps1 index d00cbda..6071251 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -98,6 +98,9 @@ Controls (XInput controller and/or keyboard): 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. +The red buttons around each MFD and the amber buttons flanking the map +are the pod's real button banks: click them with the mouse, and they +light up as the game commands their lamps. 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.