bindings: one-time migration to the board-2 layout -- the two-population fork is gone

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>
This commit is contained in:
arcattack
2026-07-26 14:19:13 -05:00
co-authored by Claude Opus 5
parent 150961176c
commit 8edce41f88
5 changed files with 328 additions and 4 deletions
+13
View File
@@ -536,6 +536,19 @@ copied from them.
build + solo un-regressed. The `[glasswin] CLICK` forensics log (names the button before
dispatch) landed with this. Detail: `engine/MUNGA/RECEIVER.cpp` Build.
## Bindings migration (board-2, 2026-07-26)
`bindings.txt` files without the `# bindings-board 2` marker are migrated ONCE at load
(`PadBindingProfile::MigrateBindingsFile`, L4PADBINDINGS.cpp): rows matching the union of every
HISTORICAL default set (85 canonical rows, c2ee729..1cda880) are dropped as "the old board, not a
choice"; player-authored rows are carried and win over the new defaults (the losing default is
emitted commented `# (yours wins)`); the joystick wizard's marker-delimited section is preserved
verbatim; the original is saved as `bindings.old.txt`. Idempotent after (marker check). Operator
decision: eliminate the two-population fork the preserved-file convention had created -- there is
no "old layout" world anymore, only per-row customization. Restoring bindings.old.txt just
re-migrates: deliberate. Verified end-to-end with the real exe: carry/stand-down/wizard/backup on
an old-world file, byte-identical idempotency on reboot, marker-headed fresh generation.
## Key Relationships
- Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes ·
Feeds: [[multiplayer]] (miniconsole hosting, Steam transport) · Build: [[build-and-run]]
+4 -1
View File
@@ -4,7 +4,10 @@ Two ways to fly: the **keyboard/pad** (the ~25 core actions) and the **cockpit p
every one of the pod's 72 illuminated buttons is mouse-clickable exactly where it sat in the
cabinet. Left-click presses; **right-click latches** a button held.
Rebind keys in `content\bindings.txt` (delete it to regenerate the defaults).
Rebind keys in `content\bindings.txt` (delete it to regenerate the defaults). Upgrading from a
pre-board build migrates your file automatically: rows you authored and the joystick wizard's
section are kept and beat the new defaults; the old stock layout is replaced; the previous file
is saved as `bindings.old.txt`.
Cockpit layout and display sizes live in `content\environ.ini` (likewise self-documenting).
---
+299
View File
@@ -227,6 +227,11 @@ static int
//###########################################################################
static const char *defaultProfileText =
"# bindings-board 2 -- KEEP THIS LINE. It marks the keyboard-layout generation;\n"
"# files without it are pre-board layouts and are migrated automatically\n"
"# (your own rows and the joystick wizard section are kept, the obsolete old\n"
"# defaults are replaced, and the previous file is saved as bindings.old.txt).\n"
"#\n"
"# bindings.txt -- PadRIO input bindings (glass cockpit). Auto-written with\n"
"# defaults on first run; edit freely, delete to regenerate. Grammar:\n"
"# key <KEYNAME> button <addr> RIO button (hex 0x.. ok)\n"
@@ -430,6 +435,299 @@ PadBindingProfile::~PadBindingProfile()
{
}
//############################################################################
// ONE-TIME BINDINGS MIGRATION (operator decision 2026-07-26: "eliminate
// competing code paths").
//
// Before this, an upgrader's preserved bindings.txt froze them on whatever
// keyboard layout it was written under -- two player populations, two sets of
// muscle-memory documentation, forever. Now a file without the
// "# bindings-board 2" marker is migrated in place, exactly once:
//
// * every row matching a HISTORICAL DEFAULT (the union of every default set
// ever shipped, canonicalized below) is dropped -- those rows were never a
// player's choice, they were just the old board;
// * every row the player actually authored (anything else) is CARRIED, and
// wins over the new default for that key/element, whose default row is
// emitted commented-out with a note;
// * the joystick wizard's marker-delimited section is preserved verbatim
// (same markers L4JOY.cpp rewrites between);
// * the previous file is saved as bindings.old.txt first.
//
// Restoring bindings.old.txt over bindings.txt just re-runs the migration next
// boot -- deliberate: the old WORLD is not restorable, only the player's own
// rows persist. That is the whole point.
//############################################################################
// Union of every default row ever shipped (c2ee729..1cda880), canonicalized:
// comments stripped, whitespace collapsed, uppercased. A row matching one of
// these is "the old board", not a customization.
static const char *kHistoricalDefaultRows[] =
{
"KEY 0 BUTTON 0X1B", "KEY 1 BUTTON 0X10", "KEY 1 BUTTON 0X40",
"KEY 2 BUTTON 0X11", "KEY 2 BUTTON 0X46", "KEY 3 BUTTON 0X12",
"KEY 3 BUTTON 0X47", "KEY 4 BUTTON 0X13", "KEY 4 BUTTON 0X45",
"KEY 5 BUTTON 0X14", "KEY 6 BUTTON 0X15", "KEY 7 BUTTON 0X18",
"KEY 8 BUTTON 0X19", "KEY 9 BUTTON 0X1A",
"KEY A AXIS LEFTPEDAL DEFLECT + 2.5", "KEY B BUTTON 0X41",
"KEY B BUTTON 0X45", "KEY C BUTTON 0X2F", "KEY C BUTTON 0X46",
"KEY D AXIS RIGHTPEDAL DEFLECT + 2.5",
"KEY DOWN AXIS THROTTLE SLEW - 0.7", "KEY DOWN BUTTON 0X41",
"KEY E AXIS JOYSTICKX DEFLECT + 2.5", "KEY E KEYPAD 0 3",
"KEY EQUALS BUTTON 0X1D", "KEY F AXIS JOYSTICKY DEFLECT - 2.5",
"KEY F5 BUTTON 0X27", "KEY F6 BUTTON 0X26", "KEY F7 BUTTON 0X25",
"KEY F8 BUTTON 0X24", "KEY F9 BUTTON 0X22", "KEY G BUTTON 0X0E",
"KEY H BUTTON 0X2C", "KEY I KEYPAD 0 8", "KEY LALT BUTTON 0X3F",
"KEY LCTRL AXIS THROTTLE SLEW - 0.7", "KEY LCTRL BUTTON 0X47",
"KEY LEFT AXIS LEFTPEDAL DEFLECT + 2.5", "KEY LEFT BUTTON 0X44",
"KEY LSHIFT AXIS THROTTLE SLEW + 0.7", "KEY M BUTTON 0X18",
"KEY MINUS BUTTON 0X1C", "KEY N BUTTON 0X15",
"KEY NUMPAD2 AXIS JOYSTICKY DEFLECT - 2.5",
"KEY NUMPAD4 AXIS JOYSTICKX DEFLECT - 2.5",
"KEY NUMPAD5 AXIS THROTTLE SET 0",
"KEY NUMPAD6 AXIS JOYSTICKX DEFLECT + 2.5",
"KEY NUMPAD7 AXIS LEFTPEDAL DEFLECT + 2.5",
"KEY NUMPAD8 AXIS JOYSTICKY DEFLECT + 2.5",
"KEY NUMPAD9 AXIS RIGHTPEDAL DEFLECT + 2.5",
"KEY O KEYPAD 0 9", "KEY P KEYPAD 0 0",
"KEY Q AXIS JOYSTICKX DEFLECT - 2.5", "KEY Q KEYPAD 0 1",
"KEY R AXIS JOYSTICKY DEFLECT + 2.5", "KEY R KEYPAD 0 4",
"KEY RCTRL BUTTON 0X47", "KEY RIGHT AXIS RIGHTPEDAL DEFLECT + 2.5",
"KEY RIGHT BUTTON 0X43", "KEY S AXIS THROTTLE SLEW - 0.7",
"KEY SPACE BUTTON 0X40", "KEY T KEYPAD 0 5", "KEY U KEYPAD 0 7",
"KEY UP AXIS THROTTLE SLEW + 0.7", "KEY UP BUTTON 0X42",
"KEY V BUTTON 0X47", "KEY W AXIS THROTTLE SLEW + 0.7",
"KEY W KEYPAD 0 2", "KEY X AXIS THROTTLE SET 0", "KEY Y KEYPAD 0 6",
"PAD A BUTTON 0X40", "PAD B BUTTON 0X3F", "PAD DPAD_DOWN BUTTON 0X41",
"PAD DPAD_LEFT BUTTON 0X44", "PAD DPAD_RIGHT BUTTON 0X43",
"PAD DPAD_UP BUTTON 0X42", "PAD LB BUTTON 0X44", "PAD RB BUTTON 0X43",
"PAD X BUTTON 0X47", "PAD Y BUTTON 0X42",
"PADAXIS LT AXIS LEFTPEDAL", "PADAXIS LX AXIS JOYSTICKX",
"PADAXIS LY AXIS JOYSTICKY INVERT", "PADAXIS RT AXIS RIGHTPEDAL",
"PADAXIS RY AXIS THROTTLE SLEW 0.7",
};
// Canonicalize one bindings line for comparison: strip the comment, collapse
// runs of whitespace to single spaces, trim, uppercase. Returns the length
// (0 = the line is blank/comment-only).
static int
CanonicalizeBindingLine(const char *in, char *out, int out_size)
{
int n = 0;
int pending_space = 0;
for (const char *p = in; *p && *p != '#' && *p != '\n' && *p != '\r'; ++p)
{
if (*p == ' ' || *p == '\t')
{
if (n > 0)
pending_space = 1;
continue;
}
if (pending_space && n < out_size - 1)
{
out[n++] = ' ';
pending_space = 0;
}
if (n < out_size - 1)
out[n++] = (char)toupper((unsigned char)*p);
}
out[n] = 0;
return n;
}
static int
IsHistoricalDefaultRow(const char *canonical)
{
for (int i = 0;
i < (int)(sizeof(kHistoricalDefaultRows)
/ sizeof(kHistoricalDefaultRows[0])); ++i)
{
if (strcmp(canonical, kHistoricalDefaultRows[i]) == 0)
return 1;
}
return 0;
}
// The first two tokens of a canonical row ("KEY W", "PAD A", "PADAXIS LX")
// identify the physical control it claims; a carried player row disables the
// new default for the same control.
static void
BindingRowControlOf(const char *canonical, char *out, int out_size)
{
int n = 0, spaces = 0;
for (const char *p = canonical; *p && n < out_size - 1; ++p)
{
if (*p == ' ' && ++spaces >= 2)
break;
out[n++] = *p;
}
out[n] = 0;
}
void
PadBindingProfile::MigrateBindingsFile(const char *path)
{
FILE *f = fopen(path, "rt");
if (f == NULL)
return; // fresh install: no migration
static char old_text[65536];
size_t got = fread(old_text, 1, sizeof(old_text) - 1, f);
old_text[got] = 0;
fclose(f);
if (strstr(old_text, "# bindings-board 2") != NULL)
return; // already current
//
// Save the original first -- the one honest escape hatch.
//
{
FILE *bak = fopen("bindings.old.txt", "wt");
if (bak != NULL)
{
fputs(old_text, bak);
fclose(bak);
}
}
//
// Walk the old file: collect carried player rows (with their original
// text) and the wizard section verbatim; drop old defaults and old
// commentary.
//
static const char *beginMarker =
"# >>> BT_JOYCONFIG generated -- do not edit between markers";
static const char *endMarker = "# <<< BT_JOYCONFIG end";
static char carried[16384]; carried[0] = 0;
static char wizard[32768]; wizard[0] = 0;
static char carried_controls[4096]; carried_controls[0] = 0;
int carried_count = 0, dropped_count = 0;
int in_wizard = 0;
const char *cursor = old_text;
while (*cursor)
{
char line[512];
int n = 0;
while (cursor[n] && cursor[n] != '\n' && n < (int)sizeof(line) - 1)
++n;
memcpy(line, cursor, n);
line[n] = 0;
cursor += n;
if (*cursor == '\n')
++cursor;
if (strstr(line, beginMarker) != NULL)
in_wizard = 1;
if (in_wizard)
{
if (strlen(wizard) + strlen(line) + 2 < sizeof(wizard))
{
strcat(wizard, line);
strcat(wizard, "\n");
}
if (strstr(line, endMarker) != NULL)
in_wizard = 0;
continue;
}
char canonical[256];
if (CanonicalizeBindingLine(line, canonical, sizeof(canonical)) == 0)
continue; // blank / old commentary
if (IsHistoricalDefaultRow(canonical))
{
++dropped_count; // the old board, not a choice
continue;
}
// A row the player authored: carry it, and remember which control it
// claims so the new default for that control stands down.
if (strlen(carried) + strlen(line) + 2 < sizeof(carried))
{
strcat(carried, line);
strcat(carried, "\n");
++carried_count;
}
char control[64];
BindingRowControlOf(canonical, control, sizeof(control));
if (strlen(carried_controls) + strlen(control) + 2
< sizeof(carried_controls))
{
strcat(carried_controls, "|");
strcat(carried_controls, control);
}
}
//
// Re-emit: the new defaults (minus any row whose control the player owns),
// then the carried rows, then the wizard section.
//
FILE *out = fopen(path, "wt");
if (out == NULL)
{
DEBUG_STREAM << "[padrio] bindings migration could not rewrite "
<< path << " -- keeping the old file\n" << std::flush;
return;
}
int disabled_count = 0;
cursor = defaultProfileText;
while (*cursor)
{
char line[512];
int n = 0;
while (cursor[n] && cursor[n] != '\n' && n < (int)sizeof(line) - 1)
++n;
memcpy(line, cursor, n);
line[n] = 0;
cursor += n;
if (*cursor == '\n')
++cursor;
char canonical[256];
int is_row = CanonicalizeBindingLine(line, canonical,
sizeof(canonical)) > 0;
if (is_row && carried_count > 0)
{
char control[64], needle[68];
BindingRowControlOf(canonical, control, sizeof(control));
sprintf(needle, "|%s", control);
const char *hit = strstr(carried_controls, needle);
// exact control match (the next char must be '|' or NUL)
if (hit != NULL)
{
const char *tail = hit + strlen(needle);
if (*tail == 0 || *tail == '|')
{
fprintf(out, "# (yours wins) %s\n", line);
++disabled_count;
continue;
}
}
}
fprintf(out, "%s\n", line);
}
if (carried_count > 0)
{
fputs("\n# --- carried over from your previous bindings.txt "
"(these beat the defaults above) ---\n", out);
fputs(carried, out);
}
if (wizard[0])
{
fputs("\n", out);
fputs(wizard, out);
}
fclose(out);
DEBUG_STREAM << "[padrio] bindings.txt MIGRATED to the board-2 layout: "
<< carried_count << " of your row(s) carried ("
<< disabled_count << " default(s) stood down for them), "
<< dropped_count << " old-default row(s) replaced, wizard section "
<< (wizard[0] ? "preserved" : "absent")
<< "; previous file saved as bindings.old.txt\n" << std::flush;
}
void
PadBindingProfile::WriteDefaultFile(const char *path)
{
@@ -820,6 +1118,7 @@ void
void
PadBindingProfile::Load()
{
MigrateBindingsFile(bindingsFileName); // one-time board-2 migration (no-op after)
FILE *f = fopen(bindingsFileName, "rt");
if (f == NULL)
{
+6
View File
@@ -178,6 +178,12 @@ protected:
LoadDefaults();
void
WriteDefaultFile(const char *path);
// One-time migration of a pre-board-2 bindings.txt: player-authored rows
// and the wizard section are kept (and win over the new defaults), the old
// default rows are replaced, the original is saved as bindings.old.txt.
// No-op once the file carries the "# bindings-board 2" marker.
void
MigrateBindingsFile(const char *path);
int
ParseLine(char *line, int line_number);
};
+6 -3
View File
@@ -51,9 +51,12 @@ CONTROLS (keyboard) -- NEW DEFAULT LAYOUT THIS BUILD:
rows are the MFD button banks laid out where they sit on the panel,
and FLIGHT LIVES ON THE NUMBER PAD. Full map: CONTROLS.txt.
UPGRADING? Your saved content\bindings.txt is kept, so YOUR KEYS DO
NOT CHANGE (W/S throttle etc. still works for you). Delete
bindings.txt to switch to the new board described below.
UPGRADING? Your bindings are MIGRATED to the new board on first
launch: any keys YOU added or changed are kept (they beat the new
defaults), and your joystick/HOTAS wizard setup is kept exactly --
but the old stock keys (W/S throttle, A/D turn, V camera, M mode...)
are replaced by the layout below. Your previous file is saved as
content\bindings.old.txt if you want to copy a favourite line back.
FLIGHT (number pad -- NUMLOCK ON):
NumPad 8/2 ........... torso aim up/down NumPad 7/9 .... left/right pedal