diff --git a/BUILD.md b/BUILD.md index 79bb82a..4b33782 100644 --- a/BUILD.md +++ b/BUILD.md @@ -96,6 +96,47 @@ For runtime debugging the v143 build produces full PDBs — run `cdb -g -G -lines -y Release rpl4opt.exe ...` from the working directory (cdb ships in this machine's Windows Kits). +### Running without the cockpit (Workstream A prototype) + +Two new environment options remove the hardware dependency entirely: + +- **`L4CONTROLS=PAD;KEYBOARD`** — selects **PadRIO** + ([MUNGA_L4/L4PADRIO.cpp](MUNGA_L4/L4PADRIO.cpp)), an in-process RIO that + speaks the full RIO control surface (buttons, axes, lamps) from an XInput + controller + the PC keyboard. The stock `VTVRIOMapper` path runs unchanged. + 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. + +Bindings (vRIO's default profile, condensed): + +| Input | Pod control | +|-------|-------------| +| Left stick / WASD | joystick X/Y | +| LT / RT, Q / E | left / right pedal | +| Right stick Y, PgUp / PgDn | throttle (rate; position holds) | +| A / Space | joystick trigger | +| B / R | reverse thrust (ButtonThrottle1) | +| DPad / arrows | joystick hat (look) | +| X, Y, LB, RB | pinky / thumb-low / thumb-low / thumb-high | +| Start / F1, Back / F2 | config buttons (AuxUpperRight 1 / 2) | + +`L4PADFLIP=XY` (or `X` / `Y`) inverts the stick axes if the feel is wrong. +Example desktop `environ.ini`: + +```ini +L4CONTROLS=PAD;KEYBOARD +DPLARG=1 +L4DPLCFG=RPDPL.INI +L4GAUGE=640x480x16 +L4PLASMA=SCREEN +TARGETFPS=60 +``` + +Lamp commands land in `PadRIO::lampState[]` — the hook for the planned +on-screen cockpit panel (vRIO buttons arranged around the displays). + Also note: `Fail()` compiles to `abort()` in release (`MUNGA/DEBUGOFF.h`) — e.g. running with `L4CONTROLS=KEYBOARD` alone aborts at VTV creation because no keyboard-only pod mapper exists (the old CRT showed a blocking R6010 diff --git a/MUNGA_L4/L4CTRL.cpp b/MUNGA_L4/L4CTRL.cpp index 3dbd041..98cb2e0 100644 --- a/MUNGA_L4/L4CTRL.cpp +++ b/MUNGA_L4/L4CTRL.cpp @@ -1,6 +1,8 @@ #include "mungal4.h" #pragma hdrstop +#include "l4padrio.h" + #include "l4ctrl.h" #include "l4keybd.h" #include "l4app.h" @@ -830,6 +832,19 @@ LBE4ControlsManager::LBE4ControlsManager(): joystickPointer = NULL; } } + else if (strcmpi(temp, "PAD") == 0) + { + //------------------------------------------------------ + // Cockpit-less play: PadRIO speaks the RIO control + // surface from an XInput pad + the PC keyboard, so the + // full RIO mapper/lamp/button path runs unchanged. + //------------------------------------------------------ + rioPointer = new PadRIO(); + Check(rioPointer); + Register_Object(rioPointer); + flags.RIOExists = 1; + primaryControlType = LBE4ControlsManager::PrimaryRIO; + } } while (temp[0] != '\0'); } diff --git a/MUNGA_L4/L4CTRL.h b/MUNGA_L4/L4CTRL.h index 9743d37..c396706 100644 --- a/MUNGA_L4/L4CTRL.h +++ b/MUNGA_L4/L4CTRL.h @@ -542,9 +542,9 @@ public: public: //------------------------------------------------------------------- - // RIO data + // RIO data (serial hardware or the PadRIO pad/keyboard synthesizer) //------------------------------------------------------------------- - RIO + RIOBase *rioPointer; protected: diff --git a/MUNGA_L4/L4GREND.cpp b/MUNGA_L4/L4GREND.cpp index 1d2c8fb..694952a 100644 --- a/MUNGA_L4/L4GREND.cpp +++ b/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/MUNGA_L4/L4PADRIO.cpp b/MUNGA_L4/L4PADRIO.cpp new file mode 100644 index 0000000..9d7df5d --- /dev/null +++ b/MUNGA_L4/L4PADRIO.cpp @@ -0,0 +1,377 @@ +#include "mungal4.h" +#pragma hdrstop + +#include "l4padrio.h" + +#include +#pragma comment(lib, "xinput9_1_0.lib") + +//######################################################################## +// Binding tables (vRIO default profile, condensed) +//######################################################################## + +namespace +{ + struct KeyBinding + { + int virtualKey; + int rioUnit; + }; + + const KeyBinding keyboardButtonMap[] = + { + { VK_SPACE, 0x40 }, // ButtonJoystickTrigger + { 'R', 0x3F }, // ButtonThrottle1 (reverse thrust) + { VK_UP, 0x42 }, // ButtonJoystickHatUp + { VK_DOWN, 0x41 }, // ButtonJoystickHatDown + { VK_RIGHT, 0x43 }, // ButtonJoystickHatRight + { VK_LEFT, 0x44 }, // ButtonJoystickHatLeft + { VK_F1, 0x37 }, // ButtonAuxUpperRight1 (config) + { VK_F2, 0x36 }, // ButtonAuxUpperRight2 (config) + }; + + struct PadBinding + { + unsigned short padMask; + int rioUnit; + }; + + const PadBinding padButtonMap[] = + { + { XINPUT_GAMEPAD_A, 0x40 }, // trigger + { XINPUT_GAMEPAD_B, 0x3F }, // reverse thrust + { XINPUT_GAMEPAD_X, 0x45 }, // pinky + { XINPUT_GAMEPAD_Y, 0x46 }, // thumb low + { XINPUT_GAMEPAD_LEFT_SHOULDER, 0x46 }, // thumb low + { XINPUT_GAMEPAD_RIGHT_SHOULDER, 0x47 }, // thumb high + { XINPUT_GAMEPAD_DPAD_UP, 0x42 }, + { XINPUT_GAMEPAD_DPAD_DOWN, 0x41 }, + { XINPUT_GAMEPAD_DPAD_RIGHT, 0x43 }, + { XINPUT_GAMEPAD_DPAD_LEFT, 0x44 }, + { XINPUT_GAMEPAD_START, 0x37 }, // config 1 + { XINPUT_GAMEPAD_BACK, 0x36 }, // config 2 + }; + + // Full throttle sweep takes ~1.3 s at full stick deflection. + const Scalar throttleRatePerSecond = 0.75f; + + Scalar StickValue(int raw, int dead_zone) + { + if (raw > -dead_zone && raw < dead_zone) + { + return (Scalar) 0; + } + Scalar value = + (raw > 0) + ? (Scalar)(raw - dead_zone) / (Scalar)(32767 - dead_zone) + : (Scalar)(raw + dead_zone) / (Scalar)(32768 - dead_zone); + if (value > 1.0f) value = 1.0f; + if (value < -1.0f) value = -1.0f; + return value; + } + + Scalar Clamp01(Scalar value) + { + if (value < 0.0f) return 0.0f; + if (value > 1.0f) return 1.0f; + return value; + } + + Logical KeyDown(int virtual_key) + { + return (GetAsyncKeyState(virtual_key) & 0x8000) != 0; + } +} + +//######################################################################## +//############################### PadRIO ################################# +//######################################################################## + +PadRIO::PadRIO() +{ + Check_Pointer(this); + + queueHead = 0; + queueTail = 0; + lastPollTick = GetTickCount(); + lastPadCheckTick = 0; + padIndex = -1; + padReported = False; + analogRequested = False; + throttleAccum = (Scalar) 0; + + sentThrottle = sentLeftPedal = sentRightPedal = (Scalar) 0; + sentJoystickX = sentJoystickY = (Scalar) 0; + + memset(buttonDown, 0, sizeof(buttonDown)); + memset(lampState, 0, sizeof(lampState)); + + invertX = False; + invertY = False; + const char *flip = getenv("L4PADFLIP"); + if (flip != NULL) + { + if (strchr(flip, 'X') || strchr(flip, 'x')) + { + invertX = True; + } + if (strchr(flip, 'Y') || strchr(flip, 'y')) + { + invertY = True; + } + } + + // Report as a v4.2 board, like vRIO does + MajorRevision = 4; + MinorRevision = 2; + + DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush; +} + +PadRIO::~PadRIO() +{ + Check_Pointer(this); +} + +Logical + PadRIO::TestInstance() const +{ + return True; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// The controls manager drains events every frame; sampling lives here so +// button latency does not depend on the analog request cadence (which is +// 15 s outside of missions). +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Logical + PadRIO::GetNextEvent(RIOEvent *destinationPointer) +{ + Check_Pointer(this); + Check_Pointer(destinationPointer); + + PollInputs(); + + if (queueTail == queueHead) + { + return False; + } + *destinationPointer = eventQueue[queueTail]; + queueTail = (queueTail + 1) % queueSize; + return True; +} + +void + PadRIO::RequestAnalogUpdate() +{ + Check_Pointer(this); + analogRequested = True; +} + +void + PadRIO::GeneralReset() +{ + Check_Pointer(this); + throttleAccum = (Scalar) 0; + Throttle = (Scalar) 0; + LeftPedal = (Scalar) 0; + RightPedal = (Scalar) 0; + JoystickX = (Scalar) 0; + JoystickY = (Scalar) 0; + analogRequested = True; + memset(lampState, 0, sizeof(lampState)); +} + +void + PadRIO::ResetThrottle() +{ + Check_Pointer(this); + throttleAccum = (Scalar) 0; + Throttle = (Scalar) 0; + analogRequested = True; +} + +void + PadRIO::SetLamp(int lampNumber, int state) +{ + Check_Pointer(this); + if (lampNumber >= 0 && lampNumber < lampCount) + { + lampState[lampNumber] = (unsigned char) state; + } +} + +void + PadRIO::QueueEvent(const RIOEvent &an_event) +{ + int next = (queueHead + 1) % queueSize; + if (next == queueTail) + { + // full: drop the oldest event + queueTail = (queueTail + 1) % queueSize; + } + eventQueue[queueHead] = an_event; + queueHead = next; +} + +void + PadRIO::PollInputs() +{ + unsigned long now = GetTickCount(); + if (now - lastPollTick < 10) + { + return; + } + Scalar delta_t = (Scalar)(now - lastPollTick) / 1000.0f; + if (delta_t > 0.25f) + { + delta_t = 0.25f; + } + lastPollTick = now; + + //--------------------------------------------------------------- + // Find / keep the XInput pad. Probing empty slots is slow, so an + // absent pad is only re-probed every 3 seconds. + //--------------------------------------------------------------- + XINPUT_STATE pad; + memset(&pad, 0, sizeof(pad)); + Logical pad_live = False; + + if (padIndex >= 0) + { + pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS); + if (!pad_live) + { + DEBUG_STREAM << "PadRIO: controller " << padIndex << " disconnected\n" << std::flush; + padIndex = -1; + } + } + if (padIndex < 0 && (now - lastPadCheckTick) >= 3000) + { + lastPadCheckTick = now; + for (DWORD i = 0; i < 4; ++i) + { + if (XInputGetState(i, &pad) == ERROR_SUCCESS) + { + padIndex = (int) i; + pad_live = True; + DEBUG_STREAM << "PadRIO: controller " << padIndex << " connected\n" << std::flush; + break; + } + } + if (padIndex < 0 && !padReported) + { + padReported = True; + DEBUG_STREAM << "PadRIO: no controller found - keyboard only\n" << std::flush; + } + } + + //--------------------------------------------------------------- + // Buttons: build the desired state across pad + keyboard, then + // diff against what we last reported. + //--------------------------------------------------------------- + unsigned char desired[buttonUnits]; + memset(desired, 0, sizeof(desired)); + + if (pad_live) + { + for (int i = 0; i < (int)(sizeof(padButtonMap)/sizeof(padButtonMap[0])); ++i) + { + if (pad.Gamepad.wButtons & padButtonMap[i].padMask) + { + desired[padButtonMap[i].rioUnit] = 1; + } + } + } + for (int i = 0; i < (int)(sizeof(keyboardButtonMap)/sizeof(keyboardButtonMap[0])); ++i) + { + if (KeyDown(keyboardButtonMap[i].virtualKey)) + { + desired[keyboardButtonMap[i].rioUnit] = 1; + } + } + + for (int unit = 0; unit < buttonUnits; ++unit) + { + if (desired[unit] != buttonDown[unit]) + { + buttonDown[unit] = desired[unit]; + + RIOEvent an_event; + an_event.Type = desired[unit] ? ButtonPressedEvent : ButtonReleasedEvent; + an_event.Data.Unit = unit; + QueueEvent(an_event); + } + } + + //--------------------------------------------------------------- + // Axes + //--------------------------------------------------------------- + Scalar x = (Scalar) 0, y = (Scalar) 0; + Scalar left_pedal = (Scalar) 0, right_pedal = (Scalar) 0; + Scalar throttle_rate = (Scalar) 0; + + if (pad_live) + { + x += StickValue(pad.Gamepad.sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + y += StickValue(pad.Gamepad.sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + throttle_rate += StickValue(pad.Gamepad.sThumbRY, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + + if (pad.Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) + { + left_pedal += (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f; + } + if (pad.Gamepad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) + { + right_pedal += (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f; + } + } + + // keyboard: WASD stick deflect, Q/E pedals, PgUp/PgDn throttle + if (KeyDown('A')) x -= 1.0f; + if (KeyDown('D')) x += 1.0f; + if (KeyDown('W')) y += 1.0f; + if (KeyDown('S')) y -= 1.0f; + if (KeyDown('Q')) left_pedal += 1.0f; + if (KeyDown('E')) right_pedal += 1.0f; + if (KeyDown(VK_PRIOR)) throttle_rate += 1.0f; // PgUp + if (KeyDown(VK_NEXT)) throttle_rate -= 1.0f; // PgDn + + if (x > 1.0f) x = 1.0f; + if (x < -1.0f) x = -1.0f; + if (y > 1.0f) y = 1.0f; + if (y < -1.0f) y = -1.0f; + + throttleAccum = Clamp01(throttleAccum + throttle_rate * throttleRatePerSecond * delta_t); + + Throttle = throttleAccum; + LeftPedal = Clamp01(left_pedal); + RightPedal = Clamp01(right_pedal); + JoystickX = invertX ? -x : x; + JoystickY = invertY ? -y : y; + + //--------------------------------------------------------------- + // Emit an analog event when asked to, or when anything moved + //--------------------------------------------------------------- + Logical changed = + (Throttle != sentThrottle) || + (LeftPedal != sentLeftPedal) || + (RightPedal != sentRightPedal) || + (JoystickX != sentJoystickX) || + (JoystickY != sentJoystickY); + + if (analogRequested || changed) + { + analogRequested = False; + sentThrottle = Throttle; + sentLeftPedal = LeftPedal; + sentRightPedal = RightPedal; + sentJoystickX = JoystickX; + sentJoystickY = JoystickY; + + RIOEvent an_event; + an_event.Type = AnalogEvent; + an_event.Data.Unit = 0; + QueueEvent(an_event); + } +} diff --git a/MUNGA_L4/L4PADRIO.h b/MUNGA_L4/L4PADRIO.h new file mode 100644 index 0000000..eafce95 --- /dev/null +++ b/MUNGA_L4/L4PADRIO.h @@ -0,0 +1,93 @@ +#pragma once + +#include "l4rio.h" + +//######################################################################## +//############################### PadRIO ################################# +//######################################################################## +// +// A cockpit-less RIO: synthesizes the RIOBase control surface from an +// XInput controller and the PC keyboard, following vRIO's default +// binding profile. Selected with L4CONTROLS=PAD. +// +// Left stick -> joystick X/Y [-1..1] +// Left/right trigger-> left/right pedal [0..1] +// Right stick Y -> throttle rate (position holds) [0..1] +// A / B -> joystick trigger / reverse thrust +// X / Y / LB / RB -> pinky / thumb-low / thumb-low / thumb-high +// DPad -> joystick hat +// Back / Start -> config buttons (AuxUpperRight2 / 1) +// +// Keyboard: WASD = stick deflect (spring-back), PgUp/PgDn = throttle, +// Q/E = pedals, Space = trigger, R = reverse, arrows = hat, F1/F2 = +// config buttons. +// +// Set L4PADFLIP to a string containing 'X' and/or 'Y' to invert the +// stick axes. +// +class PadRIO : + public RIOBase +{ +public: + PadRIO(); + ~PadRIO(); + + Logical + TestInstance() const; + + Logical + GetNextEvent(RIOEvent *destinationPointer); + + void + RequestAnalogUpdate(); + + void + GeneralReset(); + void + ResetThrottle(); + + void + 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. + enum { lampCount = 64 }; + unsigned char + lampState[lampCount]; + +protected: + void + PollInputs(); + void + QueueEvent(const RIOEvent &an_event); + + enum { queueSize = 64 }; + enum { buttonUnits = 0x48 }; // through LastMappableButton + + RIOEvent + eventQueue[queueSize]; + int + queueHead, queueTail; + + unsigned long + lastPollTick, + lastPadCheckTick; + int + padIndex; // connected XInput slot, -1 = none + Logical + padReported; // one-time connect/disconnect log flags + Logical + analogRequested; + + unsigned char + buttonDown[buttonUnits]; + + Scalar + throttleAccum; + Logical + invertX, invertY; + + Scalar + sentThrottle, sentLeftPedal, sentRightPedal, + sentJoystickX, sentJoystickY; +}; diff --git a/MUNGA_L4/L4PLASMASCREEN.cpp b/MUNGA_L4/L4PLASMASCREEN.cpp new file mode 100644 index 0000000..5aca79e --- /dev/null +++ b/MUNGA_L4/L4PLASMASCREEN.cpp @@ -0,0 +1,222 @@ +#include "mungal4.h" +#pragma hdrstop + +#include "l4plasmascreen.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(): + 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 (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); + + 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, + 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); + } + 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) + { + InvalidateRect((HWND) window, NULL, FALSE); + } + + return False; // 'done' - nothing left to stream +} + +void + PlasmaScreen::WaitForUpdate() +{ + Check(this); +} diff --git a/MUNGA_L4/L4PLASMASCREEN.h b/MUNGA_L4/L4PLASMASCREEN.h new file mode 100644 index 0000000..063ecef --- /dev/null +++ b/MUNGA_L4/L4PLASMASCREEN.h @@ -0,0 +1,50 @@ +#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(); + +protected: + void + CreateGlassWindow(); + + void + *window; // HWND, kept untyped so this header stays lean + void + *bitmapInfo; // BITMAPINFOHEADER + 256-entry orange palette + int + scale; +}; diff --git a/MUNGA_L4/L4RIO.h b/MUNGA_L4/L4RIO.h index 0007210..d35fb18 100644 --- a/MUNGA_L4/L4RIO.h +++ b/MUNGA_L4/L4RIO.h @@ -109,26 +109,17 @@ protected: lowestInput; }; -class RIO : - public PCSerialPacket +//######################################################################## +//############################### RIOBase ################################ +//######################################################################## +// +// The control surface the game consumes from the cockpit RIO board, +// independent of transport. RIO (below) is the serial-hardware +// implementation; PadRIO (l4padrio.h) synthesizes the same surface from +// an XInput controller + the PC keyboard for cockpit-less play. +// +class RIOBase { -protected: - enum RIOCommand{ - CheckRequest=0x80, - VersionRequest, - AnalogRequest, - ResetRequest, - LampRequest, - CheckReply, - VersionReply, - AnalogReply, - ButtonPressed, - ButtonReleased, - KeyPressed, - KeyReleased, - TestModeChange - }; - public: enum RIOStatusType { BoardOk=0, BoardMissing=1, BoardBad=2, @@ -166,6 +157,90 @@ public: }Data; }; + RIOBase() + { + TestModeActive = 0; + Throttle = (Scalar) 0; + LeftPedal = (Scalar) 0; + RightPedal = (Scalar) 0; + JoystickX = (Scalar) 0; + JoystickY = (Scalar) 0; + MajorRevision = 0; + MinorRevision = 0; + } + virtual ~RIOBase() {} + + virtual Logical + TestInstance() const + { return True; } + + virtual Logical + GetNextEvent(RIOEvent *destinationPointer) = 0; + + virtual void + ForceCenterJoystick() {} + virtual void + SetJoystickDeadBand(Scalar) {} + virtual void + SetThrottleDeadBand(Scalar) {} + virtual void + SetPedalsDeadBand(Scalar) {} + + virtual void + RequestCheck() {} + virtual void + RequestVersion() {} + virtual void + RequestAnalogUpdate() {} + + virtual void + GeneralReset() {} + virtual void + ResetThrottle() {} + virtual void + ResetLeftPedal() {} + virtual void + ResetRightPedal() {} + virtual void + ResetVerticalJoystick() {} + virtual void + ResetHorizontalJoystick() {} + + virtual void + SetLamp(int lampNumber, int state) = 0; + + int + TestModeActive; + + Scalar + Throttle, LeftPedal, RightPedal, JoystickX, JoystickY; + + int + MajorRevision, MinorRevision; +}; + +class RIO : + public RIOBase, + public PCSerialPacket +{ +protected: + enum RIOCommand{ + CheckRequest=0x80, + VersionRequest, + AnalogRequest, + ResetRequest, + LampRequest, + CheckReply, + VersionReply, + AnalogReply, + ButtonPressed, + ButtonReleased, + KeyPressed, + KeyReleased, + TestModeChange + }; + +public: //Win32 Serial support: ADB 02/13/07 //RIO(Word port, Word intNum, Logical perform_tests = True); RIO(const char* port, Logical perform_tests = True); @@ -267,14 +342,8 @@ public: TransmitQueueCount() { return PCSerialPacket::TransmitQueueCount(); } - int - TestModeActive; - - Scalar - Throttle, LeftPedal, RightPedal, JoystickX, JoystickY; - - int - MajorRevision, MinorRevision; + // TestModeActive, the five analog Scalars, and Major/MinorRevision + // now live in RIOBase. int remoteRetryCount; int remoteAbandonCount; diff --git a/MUNGA_L4/Munga_L4.vcxproj b/MUNGA_L4/Munga_L4.vcxproj index 626338e..184deb5 100644 --- a/MUNGA_L4/Munga_L4.vcxproj +++ b/MUNGA_L4/Munga_L4.vcxproj @@ -249,8 +249,10 @@ + + @@ -436,8 +438,10 @@ + + diff --git a/MUNGA_L4/Munga_L4.vcxproj.filters b/MUNGA_L4/Munga_L4.vcxproj.filters index bb838ad..18a32c0 100644 --- a/MUNGA_L4/Munga_L4.vcxproj.filters +++ b/MUNGA_L4/Munga_L4.vcxproj.filters @@ -561,6 +561,12 @@ Source Files\MUNGA_L4 + + Source Files\MUNGA_L4 + + + Source Files\MUNGA_L4 + Source Files\MUNGA_L4 @@ -1118,6 +1124,12 @@ Header Files\MUNGA_L4 + + 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 23cd36d..d2cf4b7 100644 --- a/docs/RP412-ROADMAP.md +++ b/docs/RP412-ROADMAP.md @@ -40,16 +40,20 @@ geometry reference — it is already the faithful software copy of the physical panel. On-screen buttons light with the same lamp states the game commands, so press-feedback works like the real button field. -- Replace the RIO serial path in the L4 layer with a native input layer - (keyboard / mouse / XInput, later Steam Input). Port vRIO's binding model — - its axis-travel semantics (throttle ratchet, spring-back pedals, ±80 stick) - are exactly what the game expects. **[investigate]** where the L4 layer reads - RIO input, and whether an abstraction seam already exists. -- Render the plasma display in-game (HUD overlay) instead of streaming - `ESC P` graphics to COM2 — `L4PLASMA.CPP` already composes the frame into a - local 1bpp buffer before serializing, so the seam is likely right there. -- Lamp feedback (button lighting) maps to on-screen highlight / controller - rumble / RGB later; low priority. +- ✅ **Prototype landed (2026-07-12).** The seam existed: the controls manager + consumes a small RIO surface (8 methods + 5 analog scalars), now split out + as `RIOBase`. **PadRIO** (`L4CONTROLS=PAD`) implements it from XInput + the + PC keyboard with vRIO's default profile — the stock `VTVRIOMapper`, lamps, + and button field run unchanged, no serial, hot-plug supported. Verified: + boots and plays with vRIO off and no COM ports. +- ✅ Plasma display: **PlasmaScreen** (`L4PLASMA=SCREEN`) renders the same + `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`. +- 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. - vRIO itself stays useful as a dev harness against unmodified builds. ## Workstream B — In-game sessions (from TeslaConsole)