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:
co-authored by
Claude Opus 5
parent
150961176c
commit
8edce41f88
@@ -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
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user