diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index ae4a6e2..59edf58 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -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]] diff --git a/docs/CONTROLS.md b/docs/CONTROLS.md index 437e693..558ee02 100644 --- a/docs/CONTROLS.md +++ b/docs/CONTROLS.md @@ -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). --- diff --git a/engine/MUNGA_L4/L4PADBINDINGS.cpp b/engine/MUNGA_L4/L4PADBINDINGS.cpp index 38849f1..7f87667 100644 --- a/engine/MUNGA_L4/L4PADBINDINGS.cpp +++ b/engine/MUNGA_L4/L4PADBINDINGS.cpp @@ -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 button 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) { diff --git a/engine/MUNGA_L4/L4PADBINDINGS.h b/engine/MUNGA_L4/L4PADBINDINGS.h index a0f5d78..f6dc066 100644 --- a/engine/MUNGA_L4/L4PADBINDINGS.h +++ b/engine/MUNGA_L4/L4PADBINDINGS.h @@ -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); }; diff --git a/players/README.txt b/players/README.txt index 2198994..2f2ed83 100644 --- a/players/README.txt +++ b/players/README.txt @@ -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