Operator decision: "make this version devour their custom settings and re-output
the correct binding file with the updates -- I'd like to eliminate competing
code paths." Before this, the preserved-bindings.txt convention meant upgraders
stayed frozen on whatever layout their file was written under, forever: two
player populations, two sets of true documentation, and every future default
improvement reaching only fresh installs.
THE MIGRATION (PadBindingProfile::MigrateBindingsFile, run once at load):
* A file carrying the new "# bindings-board 2" marker is left alone -- the
check is the first thing that runs, so this is a no-op forever after.
* Otherwise: every row matching the UNION OF EVERY DEFAULT SET EVER SHIPPED
(85 canonical rows, harvested from c2ee729..1cda880 and embedded as a
table; comments stripped, whitespace collapsed, uppercased) is dropped --
those rows were never a player's choice, they were just the old board.
* Rows the player actually authored are CARRIED with their original text and
comments, and WIN over the new defaults: the losing default row is emitted
commented out as "# (yours wins) ...", keyed by the control ("KEY T",
"PAD A", "PADAXIS LX") so key rows only displace key rows.
* The joystick wizard's marker-delimited section is preserved VERBATIM (the
same markers L4JOY rewrites between), so HOTAS players migrate without
re-running the wizard.
* The previous file is saved as bindings.old.txt first. Restoring it over
bindings.txt just re-migrates next boot -- deliberate: the old WORLD is not
restorable, only the player's own rows persist. That is the point.
VERIFIED end-to-end with the real exe (three planted scenarios):
A. old-world file (8 old-default rows + 2 authored rows incl. a rate tweak +
a wizard section): both authored rows carried with comments, both
conflicting new defaults stood down with "(yours wins)", wizard section
byte-preserved, backup written, log line states all counts, and the
migrated file parses with ZERO malformed lines.
B. already-migrated file: byte-identical md5 after a full boot, no migration
line, no backup touched. (First attempt at this check clobbered its own
evidence by running case C first -- re-run properly.)
C. no file: fresh defaults now carry the marker as line 1, no migration.
The operator's real bindings.txt was stashed before testing and restored
untouched; it will migrate on their own next launch, as intended.
Docs flipped to the new reality: the README's "your keys DO NOT change"
upgrade promise is replaced by the migration story (customs + stick kept, old
stock keys replaced, bindings.old.txt escape hatch); CONTROLS.md gains the same
note; context/glass-cockpit.md records the mechanism, the 85-row table's
provenance, and why restoring the backup re-migrates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8):
enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling
with range normalization, reacquire-on-loss, 3s hot-plug re-enum.
XInput devices are excluded via the documented RawInput IG_ VID/PID
check (live-verified skipping a real XBOX360 pad -- no double-feed).
bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots
by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/
deadzone; hats as 4-direction buttons with diagonal chords). PadRIO
runs them through the existing channel/event machinery -- per-binding
edge arrays release automatically on detach (the #24 rule), and a
direct throttle axis maps full lever travel onto the 0..1 channel
while the gait detent still snaps a lever parked in the walk/run dead
band.
BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move
each control when prompted (stick/twist/throttle/fire), invert derived
from the move direction, hat look cluster auto-added, output written
between markers in bindings.txt with the rest of the file preserved.
Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched.
Verified: grammar accept/reject per line, XInput-exclusion live,
30s in-mission no-device polling clean. Awaiting a tester with real
stick hardware for axis verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gamepad players expect stick X = turn; the pod-faithful default maps it
to torso twist, and the pedal pair couldn't take a single signed axis.
New bindings channel "Turn": a signed composite that decomposes into
the pedal pair at the RIO publish (+ = right pedal, - = left), ADDING
to direct pedal bindings so mixed rigs compose, clamped 0..1 per pedal.
The default bindings.txt stays pod-faithful and documents the
twin-stick alternative inline:
padaxis LX axis Turn
padaxis RX axis JoystickX
Also covers the HOTAS ask (any signed axis can now drive turning).
Verified: regenerated defaults carry the doc lines; a twin-stick-edited
bindings.txt parses and boots clean. Awaiting live pad verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L4RIO.h splits the abstract cockpit-control surface (RIOBase: enums, the five
analog Scalars, GetNextEvent/SetLamp, no-op serial ops, NEW IsOperational) out
of the serial RIO (RIO : PCSerialPacket, RIOBase -- byte-for-byte behavior kept,
ctor assigns as before); LBE4ControlsManager holds a RIOBase* and gains the
gated L4CONTROLS=PAD factory arm (BT_GLASS; OFF build logs+ignores the token).
NEW gated TUs: L4PADRIO (XInput+keyboard synthesize the surface; 3s hot-plug
re-probe; focus-guarded keys; per-poll AnalogEvent heartbeat; lampState[] +
static SetScreenButton/GetLampState for the panel) and L4PADBINDINGS
(content\bindings.txt profile, self-documenting default written on first run;
deflect/slew/set axis model; addresses validated against ButtonCount).
Verified live (glass build, L4CONTROLS=PAD): bindings written+parsed 44/10/5,
XInput pad detected, 121 streamed mappings install via stock PrimaryRIO path,
2157 frames clean. Pod build (gates OFF) compiles the split with zero
behavioral delta.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>