diff --git a/.gitignore b/.gitignore index 7d9d31d..32c37c8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ cdb_*.txt .vs/ *.user Thumbs.db + +# per-user input profile, written by the game on first PAD run (cwd=content) +/content/bindings.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index ca607a3..8d32a85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,9 @@ add_library(munga_engine STATIC "engine/MUNGA_L4/L4NET.CPP" "engine/MUNGA_L4/L4PARTICLES.cpp" "engine/MUNGA_L4/L4PCSPAK.cpp" + "engine/MUNGA_L4/L4PADBINDINGS.cpp" + "engine/MUNGA_L4/L4PADRIO.cpp" + "engine/MUNGA_L4/L4KEYLIGHT.cpp" "engine/MUNGA_L4/L4PLASMA.cpp" "engine/MUNGA_L4/L4RIO.cpp" "engine/MUNGA_L4/L4SERIAL.cpp" @@ -225,6 +228,13 @@ target_include_directories(munga_engine BEFORE PRIVATE target_compile_definitions(munga_engine PRIVATE ${BT_DEFS}) target_compile_options(munga_engine PRIVATE ${BT_OPTS}) +# L4KEYLIGHT (RGB keyboard lamp mirror) is C++/WinRT: it needs C++17 and +# conformance mode, and must not include engine headers (see its header +# comment). The engine's /std:c++14 is overridden per-file; MSVC takes the +# last /std on the command line. +set_source_files_properties("engine/MUNGA_L4/L4KEYLIGHT.cpp" PROPERTIES + COMPILE_OPTIONS "/std:c++17;/permissive-") + #===========================================================================# # Game logic: reconstructed BT modules + surviving-original BT source. # Headers come from game/original/BT{,_L4} and the game/fwd shims diff --git a/content/LAST.EGG b/content/LAST.EGG index 72e7c5a..61255bd 100644 --- a/content/LAST.EGG +++ b/content/LAST.EGG @@ -1,8 +1,8 @@ [mission] adventure=BattleTech -map=cavern +map=grass scenario=freeforall -time=night +time=day weather=clear temperature=27 length=600 @@ -156,9 +156,8 @@ x=128 y=32 width=8 [pilots] -pilot=127.0.0.1:1502 -pilot=127.0.0.1:1602 -[127.0.0.1:1502] +pilot=200.0.0.96 +[200.0.0.96] hostType=0 advancedDamage=1 loadzones=1 @@ -172,20 +171,6 @@ dropzone=one vehicle=bhk1 vehicleValue=1000 color=White -[127.0.0.1:1602] -hostType=0 -advancedDamage=1 -loadzones=1 -name=Boreas -bitmapindex=2 -experience=expert -badge=VGL -patch=Yellow -role=Role::Default -dropzone=one -vehicle=ava1 -vehicleValue=1000 -color=Red [largebitmap] bitmap=BitMap::Large::Aeolus [BitMap::Large::Aeolus] @@ -242,4 +227,3 @@ width=4 model=dfltrole [Role::NoReturn] model=noretun - diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 100345c..8a8dc68 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -206,6 +206,19 @@ as `AttributeIndexSet::Find` crashing in `WeaponCluster::WeaponCluster("PercentD (mechweap.cpp now static_asserts its pad base against `PoweredSubsystem::NextAttributeID`). [T2] +**The inverse trap — SHORT parent chain (BT412, 2026-07-14):** `Find(ID)` is positional +(`attributeIndex[ID-1]`), so when the WinTesla parent chain carries FEWER attributes than the +DOS binary's did, every child attribute resolves one slot EARLY relative to a 1995 stream's +numeric IDs — no crash, values silently land in the WRONG MEMBER. Proven live the first time +a real device drove `MechControlsMapper`: the binary's `.CTL` control-mapping stream uses +stick=3/throttle=4 (the DOS Subsystem chain had 2 base attrs), the WinTesla chain has 1 +(`SimulationState`), so the stick mapping wrote `throttlePosition` and the throttle mapping +wrote `pedalsPosition`. Only the numeric-ID path (CreateStreamedMappings / `.CTL`) is +affected — gauge databinding resolves by NAME and never saw it. Fix: a named pad slot at the +front of `mechmppr.cpp AttributePointers[]` + the enum locked to the binary's values. +Diagnostic: `BT_CTRLMAP_LOG=1` dumps each streamed mapping's resolved pointer — compare against +`&mapper->member` (the mech4 `[mppr]` log prints `&stick`/`&thr`). [T2] + ## 12. Frame-pacing trap — the binary assumes a LOCKED 60 fps (task #11) The 1995 pod ran frame-locked; reconstructed per-frame logic can carry HIDDEN frame-rate diff --git a/context/steamification.md b/context/steamification.md index e4b1a14..c41706b 100644 --- a/context/steamification.md +++ b/context/steamification.md @@ -41,7 +41,30 @@ pattern). Forked at BT411 `4e72f0c` (2026-07-14). 31-subsystem ticks); two-instance loopback MP via btconsole.py — mesh forms (console + 1502↔1602 game ports, "All connections completed!"), mission runs both sides, each world ticks its master + the remote replicant. [T2] -- Phases 2–7: not started — see the roadmap. +- **Phase 2 (PadRIO input layer) — DONE 2026-07-14.** Ported from RP412: + `L4RIO.h` (RIOBase split; serial RIO re-parented), `L4CTRL.h` (`rioPointer` is + `RIOBase*`), `L4CTRL.cpp` PAD-token hook (→ `primaryControlType=PrimaryRIO`, so the + stock `MechRIOMapper` engages — confirmed), new `L4PADRIO.*` / `L4PADBINDINGS.*` / + `L4KEYLIGHT.*` (gates renamed `BT412KEYLIGHT`; KeyLight TU builds `/std:c++17 + /permissive-` per-file). Default `bindings.txt` = the shared Tesla board layout + (numpad flight, letter rows = MFD banks, F-keys = Secondary columns, joystick head + 0x40-0x47 on Space/arrows/pad). `content/bindings.txt` is gitignored (written on + first PAD run). + **BT-side changes PadRIO forced:** (1) both keyboard input bridges (mech4.cpp write + of mapper attrs; mechmppr.cpp `BT_KEY_BRIDGE`) now stand down when a RIO device + exists (`bridge_controls->rioPointer != 0`) — they clobbered the engine push every + frame; M mode-cycle / X recenter conveniences stay live. (2) **the mapper attribute + chain was off by one** — see [[reconstruction-gotchas]] §11 (short-parent-chain + positional trap): the `.CTL` stick mapping wrote `throttlePosition`. Fixed with a + pad slot + binary-locked enum. This was a latent REAL-POD bug (Phase 8 would have + hit it). + Verified live: `L4CONTROLS=PAD` → throttle lever ramps (rate axis, sticky), stick + deflects (turnDemand=1 + the authentic speed-vs-turn clamp: 61.5→22.0 u/s while + turning), mech drives across the map, XInput pad detected + 3 s re-probe, bindings + 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. ## The seams (what plugs in where) [T0 unless noted] diff --git a/engine/MUNGA_L4/L4CTRL.cpp b/engine/MUNGA_L4/L4CTRL.cpp index f93b0a0..9d2ca81 100644 --- a/engine/MUNGA_L4/L4CTRL.cpp +++ b/engine/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'); } @@ -1469,6 +1484,16 @@ void stick.x = rioPointer->JoystickX; stick.y = rioPointer->JoystickY; + { + // BT412 diagnostic (BT_STICK_LOG=1): trace the RIO stick push. + static const int s_stickLog = (getenv("BT_STICK_LOG") != 0); + if (s_stickLog && (stick.x != (Scalar)0 || stick.y != (Scalar)0)) + { + DEBUG_STREAM << "[stick-push] x=" << (float)stick.x + << " y=" << (float)stick.y << "\n" << std::flush; + } + } + Check(&joystickGroup[LBE4ControlsManager::JoystickPhysical]); joystickGroup[LBE4ControlsManager::JoystickPhysical].Update( &stick, mode_mask @@ -2124,6 +2149,19 @@ void if (mapping->mappingType == ControlsMapping::DirectMapping) { attribute = subsystem->GetAttributePointer(mapping->attributeID); + { + // BT412 diagnostic (BT_CTRLMAP_LOG=1): dump each streamed + // mapping as it installs -- group/element/attribute/mask. + static const int s_mapLog = (getenv("BT_CTRLMAP_LOG") != 0); + if (s_mapLog) + { + DEBUG_STREAM << "[ctrlmap] direct grp=" << (int)mapping->controlsGroup + << " elem=" << element + << " attrID=" << (int)mapping->attributeID + << " mask=" << (unsigned)mode_mask + << " ptr=" << attribute << "\n" << std::flush; + } + } switch (mapping->controlsGroup) { case ScalarGroup: diff --git a/engine/MUNGA_L4/L4CTRL.h b/engine/MUNGA_L4/L4CTRL.h index 9743d37..c396706 100644 --- a/engine/MUNGA_L4/L4CTRL.h +++ b/engine/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/engine/MUNGA_L4/L4KEYLIGHT.cpp b/engine/MUNGA_L4/L4KEYLIGHT.cpp new file mode 100644 index 0000000..197d6cc --- /dev/null +++ b/engine/MUNGA_L4/L4KEYLIGHT.cpp @@ -0,0 +1,397 @@ +//===========================================================================// +// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror. +// +// COMPILED APART FROM THE ENGINE: /std:c++17, DEFAULT struct packing, +// conformance mode (see the per-file settings in Munga_L4.vcxproj). +// No engine headers may be included here - the engine compiles /Zp1 +// and its types would take a different layout in this translation +// unit. The l4keylight.h interface is scalars only for that reason. +// +// Everything WinRT runs on a private worker thread: device watcher, +// keyboard claiming, and a 100 ms paint loop that mirrors the lamp +// bytes (same flash formula as the on-screen cockpit buttons in +// L4MFDVIEW, so the board and the screen blink in step). +//===========================================================================// + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "windowsapp.lib") + +#include "l4keylight.h" + +namespace +{ + using namespace winrt; + using namespace winrt::Windows::Devices::Enumeration; + using namespace winrt::Windows::Devices::Lights; + using winrt::Windows::UI::Color; + using winrt::Windows::System::VirtualKey; + + struct KeyEntry + { + int virtualKey; + int address; + bool yellow; + }; + + struct ClaimedArray + { + hstring id; + LampArray array{ nullptr }; + bool perKey = false; + bool baseCoated = false; + }; + + std::mutex gLock; + std::vector gMap; + unsigned char gLamps[64] = {}; + void (*gLogger)(const char *) = nullptr; + + std::atomic gRunning{ false }; + std::thread gWorker; + + void Log(const char *line) + { + void (*logger)(const char *); + { + std::lock_guard hold(gLock); + logger = gLogger; + } + if (logger != nullptr) + { + logger(line); + } + } + + void Logf(const char *format, ...) + { + char line[256]; + va_list args; + va_start(args, format); + _vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args); + va_end(args); + Log(line); + } + + //--------------------------------------------------------------- + // Lamp byte -> brightness level 0-3, animating the flash modes. + // Identical to LampLevel in L4MFDVIEW.cpp so the keyboard and the + // on-screen buttons blink together. + //--------------------------------------------------------------- + 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; + } + + //--------------------------------------------------------------- + // The panel palette (vRIO's KeyboardLampMirror shades): red for + // the banks, yellow for Secondary/Screen; the off shade keeps the + // bound keys faintly visible so the board reads as a button field. + //--------------------------------------------------------------- + Color Shade(int level, bool yellow) + { + Color color; + color.A = 255; + if (yellow) + { + if (level >= 3) { color.R = 245; color.G = 210; color.B = 60; } + else if (level >= 1) { color.R = 140; color.G = 118; color.B = 38; } + else { color.R = 70; color.G = 60; color.B = 24; } + } + else + { + if (level >= 3) { color.R = 230; color.G = 70; color.B = 70; } + else if (level >= 1) { color.R = 120; color.G = 50; color.B = 50; } + else { color.R = 64; color.G = 40; color.B = 40; } + } + return color; + } + + bool SameColor(const Color &a, const Color &b) + { + return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B; + } + + //--------------------------------------------------------------- + // The worker: watcher + claim + paint loop + //--------------------------------------------------------------- + void Worker() + { + try + { + init_apartment(); + } + catch (...) + { + // apartment already set on this thread; carry on + } + + std::mutex claimedLock; + std::vector claimed; + bool anySeen = false; + + DeviceWatcher watcher{ nullptr }; + try + { + watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector()); + + watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info) + { + try + { + LampArray array = LampArray::FromIdAsync(info.Id()).get(); + if (array == nullptr || + array.LampArrayKind() != LampArrayKind::Keyboard) + { + return; // mice / strips / cases stay untouched + } + anySeen = true; + ClaimedArray entry; + entry.id = info.Id(); + entry.array = array; + entry.perKey = array.SupportsVirtualKeys(); + { + std::lock_guard hold(claimedLock); + claimed.push_back(entry); + } + Logf(entry.perKey + ? "KeyLight: + %ls (%d LEDs, per-key)" + : "KeyLight: + %ls (%d zones - board-wide mirror)", + info.Name().c_str(), (int) array.LampCount()); + } + catch (...) + { + Log("KeyLight: could not open a lamp array"); + } + }); + watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update) + { + std::lock_guard hold(claimedLock); + for (size_t i = 0; i < claimed.size(); ++i) + { + if (claimed[i].id == update.Id()) + { + claimed.erase(claimed.begin() + i); + Log("KeyLight: keyboard disconnected"); + break; + } + } + }); + watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &) + { + // required for the watcher to progress + }); + watcher.Start(); + } + catch (...) + { + Log("KeyLight: Dynamic Lighting unavailable on this system"); + return; + } + + // + // Paint loop: 100 ms cadence, repaint only on change + // + std::vector lastColors; + int waited = 0; + while (gRunning.load()) + { + Sleep(50); + waited += 50; + if (waited < 100) + { + continue; + } + waited = 0; + + std::vector map; + unsigned char lamps[64]; + { + std::lock_guard hold(gLock); + map = gMap; + memcpy(lamps, gLamps, sizeof(lamps)); + } + + std::vector colors(map.size()); + std::vector keys(map.size()); + int bestLevel = 0; + bool bestYellow = false; + bool changed = (lastColors.size() != map.size()); + for (size_t i = 0; i < map.size(); ++i) + { + int address = map[i].address; + int level = (address >= 0 && address < 64) + ? LampLevel(lamps[address]) : 0; + if (level > bestLevel) + { + bestLevel = level; + bestYellow = map[i].yellow; + } + colors[i] = Shade(level, map[i].yellow); + keys[i] = (VirtualKey) map[i].virtualKey; + if (!changed && !SameColor(colors[i], lastColors[i])) + { + changed = true; + } + } + + std::lock_guard hold(claimedLock); + bool freshClaim = false; + for (ClaimedArray &entry : claimed) + { + if (!entry.baseCoated) + { + freshClaim = true; + } + } + if (!changed && !freshClaim) + { + continue; + } + + Color aggregate = Shade(bestLevel, bestYellow); + for (ClaimedArray &entry : claimed) + { + try + { + if (entry.perKey) + { + if (!entry.baseCoated) + { + Color black; + black.A = 255; black.R = 0; black.G = 0; black.B = 0; + entry.array.SetColor(black); + entry.baseCoated = true; + } + if (!map.empty()) + { + entry.array.SetColorsForKeys( + array_view(colors.data(), colors.data() + colors.size()), + array_view(keys.data(), keys.data() + keys.size())); + } + } + else + { + entry.baseCoated = true; + entry.array.SetColor(aggregate); + } + } + catch (...) + { + // device wobble; the watcher handles removal + } + } + lastColors = colors; + } + + // + // Releasing the arrays hands the LEDs back to Windows + // + try + { + watcher.Stop(); + } + catch (...) + { + } + { + std::lock_guard hold(claimedLock); + claimed.clear(); + } + if (!anySeen) + { + Log("KeyLight: no Dynamic Lighting keyboard was found this session"); + } + } +} + +//######################################################################## +// The scalar interface (safe across the packing boundary) +//######################################################################## + +void + KeyLight_SetLogger(void (*logger)(const char *line)) +{ + std::lock_guard hold(gLock); + gLogger = logger; +} + +void + KeyLight_SetMap( + const int *virtual_keys, + const int *addresses, + const unsigned char *yellow, + int count + ) +{ + std::lock_guard hold(gLock); + gMap.clear(); + gMap.reserve(count); + for (int i = 0; i < count; ++i) + { + KeyEntry entry; + entry.virtualKey = virtual_keys[i]; + entry.address = addresses[i]; + entry.yellow = (yellow[i] != 0); + gMap.push_back(entry); + } +} + +void + KeyLight_UpdateLamps(const unsigned char *lamp_state, int count) +{ + if (count > 64) + { + count = 64; + } + std::lock_guard hold(gLock); + memcpy(gLamps, lamp_state, count); +} + +void + KeyLight_Start() +{ + if (gRunning.exchange(true)) + { + return; + } + gWorker = std::thread(Worker); +} + +void + KeyLight_Stop() +{ + if (!gRunning.exchange(false)) + { + return; + } + if (gWorker.joinable()) + { + gWorker.join(); + } +} diff --git a/engine/MUNGA_L4/L4KEYLIGHT.h b/engine/MUNGA_L4/L4KEYLIGHT.h new file mode 100644 index 0000000..2476d0d --- /dev/null +++ b/engine/MUNGA_L4/L4KEYLIGHT.h @@ -0,0 +1,53 @@ +//===========================================================================// +// File: l4keylight.h // +// Project: MUNGA Brick: RGB keyboard lamp mirror // +// Contents: Windows Dynamic Lighting bridge (vRIO's KeyboardLampMirror) // +//---------------------------------------------------------------------------// +// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. // +// PROPRIETARY AND CONFIDENTIAL // +//===========================================================================// + +#pragma once + +//######################################################################## +// Mirrors the game-commanded RIO lamp states onto per-key RGB keyboards +// through Windows Dynamic Lighting (the WinRT LampArray API), following +// vRIO's KeyboardLampMirror: keys bound to lamp addresses glow with the +// panel palette (red for the banks, yellow for the Secondary/Screen +// columns), flash modes blink at the panel's rates, other keys are +// blacked out so the keyboard reads as the button field. Zone-lit +// keyboards mirror the strongest lamp board-wide. Dynamic Lighting +// grants LED control to the FOREGROUND app - which is the game itself, +// so no Windows settings dance is needed. +// +// The implementation is C++/WinRT on a private worker thread, compiled +// with default struct packing (the engine builds /Zp1, which would +// break the WinRT ABI) - hence this interface is scalars only. All +// functions are safe to call when Dynamic Lighting is unavailable; +// failures log once and the mirror stays dormant. +//######################################################################## + +// Where the mirror's status lines go (the caller owns the sink). +void + KeyLight_SetLogger(void (*logger)(const char *line)); + +// The key map: parallel arrays of virtual keys, their RIO lamp +// addresses (0x00-0x47), and whether each paints yellow (Secondary / +// Screen columns) instead of red. +void + KeyLight_SetMap( + const int *virtual_keys, + const int *addresses, + const unsigned char *yellow, + int count + ); + +// Latest game-commanded lamp bytes (indexed by RIO lamp number). +void + KeyLight_UpdateLamps(const unsigned char *lamp_state, int count); + +// Claim keyboards and start painting / release the LEDs to Windows. +void + KeyLight_Start(); +void + KeyLight_Stop(); diff --git a/engine/MUNGA_L4/L4PADBINDINGS.cpp b/engine/MUNGA_L4/L4PADBINDINGS.cpp new file mode 100644 index 0000000..4c51aba --- /dev/null +++ b/engine/MUNGA_L4/L4PADBINDINGS.cpp @@ -0,0 +1,542 @@ +#include "mungal4.h" +#pragma hdrstop + +#include "l4padbindings.h" + +#include +#include + +//######################################################################## +// Name tables: .NET Keys names (what vRIO uses) to virtual keys, pad +// names to XInput masks. Matching is case-insensitive. +//######################################################################## + +namespace +{ + struct NameValue + { + const char *name; + int value; + }; + + const NameValue kKeyNames[] = + { + // letters and digits are generated in LookupKey + { "F1", VK_F1 }, { "F2", VK_F2 }, { "F3", VK_F3 }, { "F4", VK_F4 }, + { "F5", VK_F5 }, { "F6", VK_F6 }, { "F7", VK_F7 }, { "F8", VK_F8 }, + { "F9", VK_F9 }, { "F10", VK_F10 }, { "F11", VK_F11 }, { "F12", VK_F12 }, + { "NumPad0", VK_NUMPAD0 }, { "NumPad1", VK_NUMPAD1 }, + { "NumPad2", VK_NUMPAD2 }, { "NumPad3", VK_NUMPAD3 }, + { "NumPad4", VK_NUMPAD4 }, { "NumPad5", VK_NUMPAD5 }, + { "NumPad6", VK_NUMPAD6 }, { "NumPad7", VK_NUMPAD7 }, + { "NumPad8", VK_NUMPAD8 }, { "NumPad9", VK_NUMPAD9 }, + { "Divide", VK_DIVIDE }, { "Multiply", VK_MULTIPLY }, + { "Subtract", VK_SUBTRACT }, { "Add", VK_ADD }, + { "Decimal", VK_DECIMAL }, + { "Up", VK_UP }, { "Down", VK_DOWN }, + { "Left", VK_LEFT }, { "Right", VK_RIGHT }, + { "Space", VK_SPACE }, { "Return", VK_RETURN }, { "Enter", VK_RETURN }, + { "Escape", VK_ESCAPE }, { "Tab", VK_TAB }, { "Back", VK_BACK }, + { "Home", VK_HOME }, { "End", VK_END }, + { "PageUp", VK_PRIOR }, { "PageDown", VK_NEXT }, + { "Prior", VK_PRIOR }, { "Next", VK_NEXT }, + { "Insert", VK_INSERT }, { "Delete", VK_DELETE }, + { "ShiftKey", VK_SHIFT }, { "Shift", VK_SHIFT }, + { "ControlKey", VK_CONTROL }, { "Ctrl", VK_CONTROL }, { "Control", VK_CONTROL }, + { "Menu", VK_MENU }, { "Alt", VK_MENU }, + { "OemMinus", VK_OEM_MINUS }, { "Oemplus", VK_OEM_PLUS }, + { "Oemcomma", VK_OEM_COMMA }, { "OemPeriod", VK_OEM_PERIOD }, + { "OemOpenBrackets", VK_OEM_4 }, { "OemCloseBrackets", VK_OEM_6 }, + { "OemSemicolon", VK_OEM_1 }, { "OemQuotes", VK_OEM_7 }, + { "OemQuestion", VK_OEM_2 }, { "OemPipe", VK_OEM_5 }, + { "Oemtilde", VK_OEM_3 }, + }; + + const NameValue kPadButtonNames[] = + { + { "A", XINPUT_GAMEPAD_A }, { "B", XINPUT_GAMEPAD_B }, + { "X", XINPUT_GAMEPAD_X }, { "Y", XINPUT_GAMEPAD_Y }, + { "DPadUp", XINPUT_GAMEPAD_DPAD_UP }, + { "DPadDown", XINPUT_GAMEPAD_DPAD_DOWN }, + { "DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT }, + { "DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT }, + { "Start", XINPUT_GAMEPAD_START }, { "Back", XINPUT_GAMEPAD_BACK }, + { "LeftShoulder", XINPUT_GAMEPAD_LEFT_SHOULDER }, + { "RightShoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER }, + { "LeftThumb", XINPUT_GAMEPAD_LEFT_THUMB }, + { "RightThumb", XINPUT_GAMEPAD_RIGHT_THUMB }, + }; + + const NameValue kPadAxisNames[] = + { + { "LeftStickX", BindPadLeftStickX }, { "LeftStickY", BindPadLeftStickY }, + { "RightStickX", BindPadRightStickX }, { "RightStickY", BindPadRightStickY }, + { "LeftTrigger", BindPadLeftTrigger }, { "RightTrigger", BindPadRightTrigger }, + }; + + const NameValue kRioAxisNames[] = + { + { "Throttle", BindAxisThrottle }, + { "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal }, + { "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX }, + }; + + Logical NameEquals(const char *a, const char *b) + { + return _stricmp(a, b) == 0; + } + + int LookupTable(const NameValue *table, int count, const char *name) + { + for (int i = 0; i < count; ++i) + { + if (NameEquals(table[i].name, name)) + { + return table[i].value; + } + } + return -1; + } + + int LookupKey(const char *name) + { + // single letter A-Z (VK == uppercase ASCII) + if (name[1] == '\0' && + ((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z'))) + { + return toupper(name[0]); + } + // digit row: D0-D9 (VK == '0'-'9') + if ((name[0] == 'D' || name[0] == 'd') && + name[1] >= '0' && name[1] <= '9' && name[2] == '\0') + { + return name[1]; + } + return LookupTable(kKeyNames, sizeof(kKeyNames) / sizeof(kKeyNames[0]), name); + } + + Logical ParseAddress(const char *text, int *address) + { + int value = -1; + if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) + { + if (sscanf(text + 2, "%x", &value) != 1) + { + return False; + } + } + else if (sscanf(text, "%d", &value) != 1) + { + return False; + } + if ((value >= 0x00 && value <= 0x47) || (value >= 0x50 && value <= 0x6F)) + { + *address = value; + return True; + } + return False; + } + + Logical ParseNumber(const char *text, Scalar *value) + { + float parsed; + if (sscanf(text, "%f", &parsed) != 1) + { + return False; + } + *value = (Scalar) parsed; + return True; + } + + //--------------------------------------------------------------- + // Line tokenizer: whitespace separated, '#' starts a comment + //--------------------------------------------------------------- + int Tokenize(char *line, char *tokens[], int max_tokens) + { + char *hash = strchr(line, '#'); + if (hash != NULL) + { + *hash = '\0'; + } + int count = 0; + char *cursor = line; + while (count < max_tokens) + { + while (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n') + { + ++cursor; + } + if (*cursor == '\0') + { + break; + } + tokens[count++] = cursor; + while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && + *cursor != '\r' && *cursor != '\n') + { + ++cursor; + } + if (*cursor != '\0') + { + *cursor++ = '\0'; + } + } + return count; + } + + //--------------------------------------------------------------- + // One line of the profile grammar + //--------------------------------------------------------------- + Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile) + { + if (token_count < 4) + { + return False; + } + + if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "button")) + { + int key = LookupKey(tokens[1]); + int address; + if (key < 0 || !ParseAddress(tokens[3], &address) || + profile->keyButtonCount >= PadBindingProfile::maxKeyButtons) + { + return False; + } + Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle")); + PadKeyButtonBinding *binding = &profile->keyButtons[profile->keyButtonCount++]; + memset(binding, 0, sizeof(*binding)); + binding->virtualKey = key; + binding->address = address; + binding->toggle = toggle; + return True; + } + + if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "axis")) + { + if (token_count < 6) + { + return False; + } + int key = LookupKey(tokens[1]); + int axis = LookupTable(kRioAxisNames, + sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]); + int mode = NameEquals(tokens[4], "deflect") ? BindKeyDeflect + : NameEquals(tokens[4], "rate") ? BindKeyRate : -1; + Scalar value; + if (key < 0 || axis < 0 || mode < 0 || !ParseNumber(tokens[5], &value) || + profile->keyAxisCount >= PadBindingProfile::maxKeyAxes) + { + return False; + } + PadKeyAxisBinding *binding = &profile->keyAxes[profile->keyAxisCount++]; + binding->virtualKey = key; + binding->axis = axis; + binding->mode = mode; + binding->value = value; + return True; + } + + if (NameEquals(tokens[0], "pad") && NameEquals(tokens[2], "button")) + { + int mask = LookupTable(kPadButtonNames, + sizeof(kPadButtonNames) / sizeof(kPadButtonNames[0]), tokens[1]); + int address; + if (mask < 0 || !ParseAddress(tokens[3], &address) || + profile->padButtonCount >= PadBindingProfile::maxPadButtons) + { + return False; + } + Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle")); + PadPadButtonBinding *binding = &profile->padButtons[profile->padButtonCount++]; + memset(binding, 0, sizeof(*binding)); + binding->padMask = (unsigned short) mask; + binding->address = address; + binding->toggle = toggle; + return True; + } + + if (NameEquals(tokens[0], "padaxis") && NameEquals(tokens[2], "axis")) + { + int source = LookupTable(kPadAxisNames, + sizeof(kPadAxisNames) / sizeof(kPadAxisNames[0]), tokens[1]); + int axis = LookupTable(kRioAxisNames, + sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]); + if (source < 0 || axis < 0 || + profile->padAxisCount >= PadBindingProfile::maxPadAxes) + { + return False; + } + PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++]; + memset(binding, 0, sizeof(*binding)); + binding->source = source; + binding->axis = axis; + for (int i = 4; i < token_count; ++i) + { + if (NameEquals(tokens[i], "invert")) + { + binding->invert = True; + } + else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count) + { + if (!ParseNumber(tokens[++i], &binding->deadzone)) + { + return False; + } + } + else if (NameEquals(tokens[i], "rate") && i + 1 < token_count) + { + if (!ParseNumber(tokens[++i], &binding->rate)) + { + return False; + } + } + else + { + return False; + } + } + return True; + } + + return False; + } + + //--------------------------------------------------------------- + // The default profile: vRIO's board-complete layout, adapted for + // the desktop game - the driving keys (WASD/QE/PgUp/PgDn) stay + // axes, so six bank buttons lose their keyboard keys (they are + // still clickable on the on-screen cockpit, and rebindable here). + //--------------------------------------------------------------- + const char kDefaultProfile[] = +"# BT412 input bindings - keyboard and Xbox (XInput) controller.\n" +"#\n" +"# One binding per line, '#' starts a comment, keywords are case-\n" +"# insensitive. Loaded at game start; delete this file to restore\n" +"# the defaults.\n" +"#\n" +"# key button [toggle]\n" +"# key axis deflect \n" +"# key axis rate \n" +"# pad