Applied on top of the glass-cockpit-refit merge, all from the pre-merge review: 1. README CONTROLS TABLE REWRITTEN for the new default board. It still taught "W/S throttle, A/D turn" while the defaults moved flight to the numpad and handed the letter rows to the MFD banks -- a fresh install would read controls that do not work. The new table teaches the numpad flight block, the panel-on-your-keys groups, F7 for control mode (was M), backtick for the camera (was V), the coolant valves on 1-3/QWE with the detent warning kept, and PgUp/PgDn volume. UPGRADERS ARE TOLD EXPLICITLY that their preserved bindings.txt keeps their old keys, and that deleting it opts into the board. 2. BACKTICK VIEW-TOGGLE GUARDED. The bindings-row-wins rule (KeyHasBinding) covered V and J/K/L but not backtick itself, which polled unconditionally -- and BACKTICK is a nameable, unbound key, so binding it to a button double-dispatched (button + camera). Both halves of the view poll are now guarded. 3. ENVIRON.INI REFUSES ONE-SHOTS. The loader applied ANY key=value line, and BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the wizard DELETES the process var after running, so in every relaunched child there is no real env var left to win over the file -- capture wizard at every mission start. BT_JOYCONFIG is now skipped with the reason in the template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG and friends) do not belong in a file that silently persists forever. 4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING. CONTROLS.md gains the PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never mentioned volume) and the note that the keys moved so they no longer share a key with anything in any layout; the bindings template header reserves PgUp/PgDn informally. 5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine). The C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 -- its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for'). CMake now detects an SDK older than 10.0.22000 and builds a dormant stub with the same five-function scalar interface (logs once at Start, identical behaviour otherwise) -- extending the feature's own runtime philosophy ("no Dynamic Lighting -> log once, stay dormant") to build time. Dynamic Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older build machine, and machines with a newer SDK build the real mirror unchanged. VERIFIED on the merged tree (4.11.572): * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved externals are byte-identical to every pre-merge build (20 CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40 were CSS was shorthand -- corrected here for the record). * solo boot: environ.ini template written + loaded, mode resolver announces the surround, historical bands L276/R276/T223/B336 confirmed, keylight stub logs its dormant line, BT_SHOT capture shows the under-glass button treatment and the corrected hat labels, 0 faults. * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved to volume.cfg. (Side catch: content\volume.cfg had been sitting at 0.00 from an old test -- anyone using this tree had a silent game; reset.) * full relay cycle on the merged exe: 2 pods staged, launch pair, mission, StopMission, round RESET -- all clean. * all three console suites still pass; checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
443 lines
11 KiB
C++
443 lines
11 KiB
C++
//===========================================================================//
|
|
// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror.
|
|
//
|
|
// Ported from RP412's L4KEYLIGHT (itself a port of vRIO's
|
|
// KeyboardLampMirror). Keys bound to a lamp address in bindings.txt glow
|
|
// with the panel palette, so the keyboard reads as the pod's button field.
|
|
//
|
|
// COMPILED APART FROM THE REST: /std:c++17 + conformance mode, per-file in
|
|
// CMakeLists (C++/WinRT needs both; the project builds C++14 /permissive).
|
|
// NOTE: RP412 also had to force DEFAULT struct packing here because its
|
|
// engine compiles /Zp1, which would break the WinRT ABI -- BT411 does NOT
|
|
// set /Zp (checked 2026-07-26: BT_OPTS is /permissive /W0 /wd4996 /EHsc
|
|
// /bigobj /MP), so that hazard does not apply. The scalars-only interface
|
|
// in l4keylight.h is kept anyway: it costs nothing and keeps this TU
|
|
// isolated if packing is ever added.
|
|
//
|
|
// Everything WinRT runs on a private worker thread: device watcher,
|
|
// keyboard claiming, and a 100 ms paint loop that mirrors the lamp bytes
|
|
// (the same flash formula as BTLampBrightnessOf, so the board and the
|
|
// on-screen cockpit buttons blink in step).
|
|
//===========================================================================//
|
|
|
|
//
|
|
// BUILD-TIME STUB GATE (merge 2026-07-26). The C++/WinRT body below needs a
|
|
// Windows SDK whose cppwinrt headers are new enough for Dynamic Lighting --
|
|
// SDK 10.0.19041's fail inside winrt/base itself (C2039 'wait_for'). On a
|
|
// build machine with only an older SDK, CMake defines BT_KEYLIGHT_STUB and
|
|
// this TU compiles the dormant stub instead: same five-function scalar
|
|
// interface, logs once at Start(), the game runs identically otherwise.
|
|
// This extends the feature's own runtime philosophy (no Dynamic Lighting ->
|
|
// log once, stay dormant) to build time, and keeps the link closed on every
|
|
// machine without demanding an SDK upgrade.
|
|
//
|
|
#if defined(BT_KEYLIGHT_STUB)
|
|
|
|
#include "l4keylight.h"
|
|
|
|
static void (*s_stubLogger)(const char *line) = 0;
|
|
|
|
void KeyLight_SetLogger(void (*logger)(const char *line))
|
|
{
|
|
s_stubLogger = logger;
|
|
}
|
|
void KeyLight_SetMap(const int *, const int *, const unsigned char *, int) {}
|
|
void KeyLight_UpdateLamps(const unsigned char *, int) {}
|
|
void KeyLight_Start()
|
|
{
|
|
if (s_stubLogger)
|
|
s_stubLogger("[keylight] built without Dynamic Lighting support "
|
|
"(Windows SDK too old at COMPILE time) -- RGB mirror "
|
|
"dormant on this build");
|
|
}
|
|
void KeyLight_Stop() {}
|
|
|
|
#else // the real C++/WinRT mirror
|
|
|
|
#include <windows.h>
|
|
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <vector>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
|
|
#include <winrt/base.h>
|
|
#include <winrt/Windows.Foundation.h>
|
|
#include <winrt/Windows.Foundation.Collections.h>
|
|
#include <winrt/Windows.Devices.Enumeration.h>
|
|
#include <winrt/Windows.Devices.Lights.h>
|
|
#include <winrt/Windows.System.h>
|
|
#include <winrt/Windows.UI.h>
|
|
|
|
#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<KeyEntry> gMap;
|
|
unsigned char gLamps[64] = {};
|
|
void (*gLogger)(const char *) = nullptr;
|
|
|
|
std::atomic<bool> gRunning{ false };
|
|
std::thread gWorker;
|
|
|
|
void Log(const char *line)
|
|
{
|
|
void (*logger)(const char *);
|
|
{
|
|
std::lock_guard<std::mutex> 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, animating the flash modes.
|
|
// MUST match BTLampBrightnessOf (l4vb16.h) exactly, so the board and
|
|
// the on-screen buttons blink together: solid shows state 1, flashing
|
|
// ALTERNATES state 1 and state 2 (RIO::LampState, L4RIO.h). Copied
|
|
// rather than included because this TU takes no engine headers.
|
|
//---------------------------------------------------------------
|
|
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<ClaimedArray> 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<std::mutex> 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<std::mutex> 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<Color> lastColors;
|
|
int waited = 0;
|
|
while (gRunning.load())
|
|
{
|
|
Sleep(50);
|
|
waited += 50;
|
|
if (waited < 100)
|
|
{
|
|
continue;
|
|
}
|
|
waited = 0;
|
|
|
|
std::vector<KeyEntry> map;
|
|
unsigned char lamps[64];
|
|
{
|
|
std::lock_guard<std::mutex> hold(gLock);
|
|
map = gMap;
|
|
memcpy(lamps, gLamps, sizeof(lamps));
|
|
}
|
|
|
|
std::vector<Color> colors(map.size());
|
|
std::vector<VirtualKey> 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<std::mutex> 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<Color const>(colors.data(), colors.data() + colors.size()),
|
|
array_view<VirtualKey const>(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<std::mutex> 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<std::mutex> hold(gLock);
|
|
gLogger = logger;
|
|
}
|
|
|
|
void
|
|
KeyLight_SetMap(
|
|
const int *virtual_keys,
|
|
const int *addresses,
|
|
const unsigned char *yellow,
|
|
int count
|
|
)
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::mutex> 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();
|
|
}
|
|
}
|
|
|
|
#endif // BT_KEYLIGHT_STUB
|