CONTROLS.html: load your bindings.txt and the board becomes YOUR board
The interactive controls page was a static picture of the stock layout -- a
player's carried migration rows, rebinds and HOTAS setup were invisible to it,
and the mismatch grew with every customization. Now a "Load your bindings.txt"
box (file picker + drag-drop) parses the real bindings grammar client-side and
rewrites the interactive keyboard in place:
* every key shows the loaded file's truth; keys that differ from the stock
board get a hazard outline (the legend says so); hover readouts follow
because they were already attribute-driven;
* the game's bindings-row-wins rule is modelled: the PgUp/PgDn volume and
backtick view BUILT-INS keep their face only while their key is unbound;
* rows that have no keyboard geometry (pad / padaxis / wizard joyaxis-
joybutton-joyhat / MOUSE keys) land in an "also in your file" list;
* one click restores the stock view. Nothing is uploaded -- FileReader on a
user-chosen local file, works from the extracted zip on file://.
Key-name registry walks the three key blocks in DOM order (Shift/Ctrl/Alt
resolve left-then-right; the numpad's U+2212 minus and its ASCII twin both map
to NUMPADMINUS), address meanings are harvested from the stock board itself
plus a small table for the slots the default board leaves off (unwired columns,
the cabinet-only throttle bank, PANIC).
Two self-inflicted bugs found by the harness and fixed before landing: the
address harvest invented a "BTN" label for the many stock keys that have none,
which made EVERY default row read as a customization; and generated axis labels
("AIM +") differed from the page's hand-authored vocabulary ("AIM UP"), custom-
marking the whole stock numpad. Both now speak the board's own labels.
VERIFIED headlessly (no browser extension needed): scratchpad/
test_controls_page.py injects a self-test harness into a copy of the page,
feeds it scratchpad/fixture_migrated_bindings.txt -- a REAL board-2 migrated
file (full defaults + two authored rows + a wizard section) -- and runs it
under chrome --headless=new --dump-dom. 11/11: the two authored rows carried
and marked, zero stray custom marks across the rest of the board, both
built-ins survive, wizard rows listed, status line honest, and reset restores
the stock board exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6eaed84fad
commit
e46fc706a6
@@ -398,6 +398,41 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||||
/* ---- "load your bindings.txt" (the board becomes YOUR board) ---- */
|
||||
.loader {
|
||||
margin: 14px 0 4px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed var(--glass-edge);
|
||||
border-radius: 6px;
|
||||
background: var(--void-2);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 14px;
|
||||
}
|
||||
.loader.dragover { border-color: var(--phosphor); background: var(--glass); }
|
||||
.loader label.btn {
|
||||
cursor: pointer;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--dim);
|
||||
border-radius: 4px;
|
||||
color: var(--phosphor-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
.loader label.btn:hover { border-color: var(--phosphor); }
|
||||
.loader input[type="file"] { display: none; }
|
||||
.loader .status { color: var(--dim); font-size: 0.86em; }
|
||||
.loader .status b { color: var(--phosphor-2); }
|
||||
.loader .reset { color: var(--dim); font-size: 0.86em; cursor: pointer; text-decoration: underline; display: none; }
|
||||
.key.custom {
|
||||
outline: 1px solid var(--hazard-lit);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
.key.custom .k-sub { color: var(--hazard-lit); }
|
||||
.loader-extras { display: none; margin: 6px 0 0; font-size: 0.86em; color: var(--dim); }
|
||||
.loader-extras b { color: var(--phosphor-2); }
|
||||
.loader-legend { display: none; font-size: 0.82em; color: var(--dim); }
|
||||
.loader-legend i { color: var(--hazard-lit); font-style: normal; }
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
@@ -714,6 +749,16 @@
|
||||
saved as <code>bindings.old.txt</code>.
|
||||
</p>
|
||||
|
||||
<div class="loader" id="loader">
|
||||
<label class="btn" for="bindfile">Load your bindings.txt</label>
|
||||
<input type="file" id="bindfile" accept=".txt">
|
||||
<span class="status" id="load-status">…and the keyboard above becomes <b>your</b> keyboard
|
||||
(drop the file anywhere on this box). Nothing is uploaded — this page reads it locally.</span>
|
||||
<span class="reset" id="load-reset">show the stock layout again</span>
|
||||
<span class="loader-legend" id="load-legend"><i>outlined keys</i> differ from the stock board</span>
|
||||
</div>
|
||||
<p class="loader-extras" id="load-extras"></p>
|
||||
|
||||
<div class="two-col">
|
||||
<pre><i># grammar</i>
|
||||
<b>key</b> <KEY> <b>button</b> <addr>
|
||||
@@ -1060,5 +1105,296 @@
|
||||
|
||||
bind(kbEl, rdKb, ".key");
|
||||
bind(document.querySelector("svg.pad"), rdPad, ".ctl");
|
||||
//--------------------------------------------------------------------
|
||||
// LOAD YOUR bindings.txt (2026-07-26): the board above stops being a
|
||||
// picture of the stock layout and becomes the reader's actual keyboard.
|
||||
// Entirely local -- FileReader on a user-chosen file; nothing uploaded.
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
// Registry: bindings.txt KEYNAME -> the rendered key element. Caps on the
|
||||
// board are display glyphs ("`", "−", "PgUp"), and Shift/Ctrl/Alt appear
|
||||
// twice (left first, right second), so walk each block in DOM order.
|
||||
var keyEls = {};
|
||||
function reg(name, el) { if (name && el && !keyEls[name]) keyEls[name] = el; }
|
||||
(function () {
|
||||
var mainCap = {
|
||||
"`": "BACKTICK", "-": "MINUS", "=": "EQUALS", "[": "LBRACKET",
|
||||
"]": "RBRACKET", "\\": "BACKSLASH", ";": "SEMICOLON", "'": "QUOTE",
|
||||
",": "COMMA", ".": "PERIOD", "/": "SLASH", "Space": "SPACE",
|
||||
"Tab": "TAB", "Bksp": "BACKSPACE", "Enter": "ENTER"
|
||||
};
|
||||
var dupes = { "Shift": ["LSHIFT", "RSHIFT"], "Ctrl": ["LCTRL", "RCTRL"],
|
||||
"Alt": ["LALT", "RALT"] };
|
||||
var seen = {};
|
||||
Array.prototype.forEach.call(main.querySelectorAll(".key"), function (el) {
|
||||
var cap = el.firstChild ? el.firstChild.textContent : "";
|
||||
if (!cap) return;
|
||||
if (dupes[cap]) {
|
||||
seen[cap] = (seen[cap] || 0);
|
||||
reg(dupes[cap][Math.min(seen[cap], 1)], el);
|
||||
seen[cap] += 1;
|
||||
return;
|
||||
}
|
||||
if (mainCap[cap]) { reg(mainCap[cap], el); return; }
|
||||
if (/^[A-Z0-9]$/.test(cap) || /^F1?[0-9]$/.test(cap)) reg(cap, el);
|
||||
});
|
||||
var navCap = { "Ins": "INSERT", "Home": "HOME", "PgUp": "PAGEUP",
|
||||
"Del": "DELETE", "End": "END", "PgDn": "PAGEDOWN",
|
||||
"↑": "UP", "←": "LEFT", "↓": "DOWN",
|
||||
"→": "RIGHT" };
|
||||
Array.prototype.forEach.call(navWrap.querySelectorAll(".key"), function (el) {
|
||||
var cap = el.firstChild ? el.firstChild.textContent : "";
|
||||
if (navCap[cap]) reg(navCap[cap], el);
|
||||
});
|
||||
var padCap = { "/": "NUMPADSLASH", "*": "NUMPADSTAR",
|
||||
"−": "NUMPADMINUS", "+": "NUMPADPLUS",
|
||||
".": "NUMPADDOT" };
|
||||
Array.prototype.forEach.call(pad.querySelectorAll(".key"), function (el) {
|
||||
var cap = el.firstChild ? el.firstChild.textContent : "";
|
||||
if (/^[0-9]$/.test(cap)) reg("NUMPAD" + cap, el);
|
||||
else if (padCap[cap]) reg(padCap[cap], el);
|
||||
// the pad's minus renders as U+2212; accept the ASCII twin too
|
||||
else if (cap === "-") reg("NUMPADMINUS", el);
|
||||
});
|
||||
})();
|
||||
|
||||
// Remember every key's stock face, for reset and for the differs-mark.
|
||||
var stock = {};
|
||||
Object.keys(keyEls).forEach(function (name) {
|
||||
var el = keyEls[name], subEl = el.querySelector(".k-sub");
|
||||
stock[name] = {
|
||||
sub: subEl ? subEl.textContent : "",
|
||||
addr: el.getAttribute("data-addr") || "",
|
||||
desc: el.getAttribute("data-desc") || "",
|
||||
name: el.getAttribute("data-name") || ""
|
||||
};
|
||||
});
|
||||
|
||||
// What an address MEANS: harvested from the stock board itself, plus the
|
||||
// slots the default board leaves off (unwired columns, the cabinet-only
|
||||
// throttle bank) so a custom row still reads as something.
|
||||
var addrInfo = {};
|
||||
Object.keys(stock).forEach(function (name) {
|
||||
var s = stock[name];
|
||||
// Keep the sub EXACTLY as the stock board shows it -- most MFD bank keys
|
||||
// have no short label at all, and inventing one ("BTN") made every
|
||||
// default row read as a customization when a real file was loaded.
|
||||
if (/^0x/i.test(s.addr) && !addrInfo[s.addr.toLowerCase()])
|
||||
addrInfo[s.addr.toLowerCase()] = { sub: s.sub, desc: s.desc };
|
||||
});
|
||||
[["0x16", "—", "Column slot the pod never wired"],
|
||||
["0x17", "—", "Column slot the pod never wired"],
|
||||
["0x1e", "—", "Column slot the pod never wired"],
|
||||
["0x1f", "—", "Column slot the pod never wired"],
|
||||
["0x38", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
|
||||
["0x39", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
|
||||
["0x3a", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
|
||||
["0x3b", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
|
||||
["0x3c", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
|
||||
["0x3d", "PANIC", "The pod's panic button"],
|
||||
["0x3e", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"]
|
||||
].forEach(function (a) {
|
||||
if (!addrInfo[a[0]]) addrInfo[a[0]] = { sub: a[1], desc: a[2] };
|
||||
});
|
||||
|
||||
var chanShort = { "THROTTLE": "THR", "JOYSTICKX": "TWIST",
|
||||
"JOYSTICKY": "AIM", "LEFTPEDAL": "L PED",
|
||||
"RIGHTPEDAL": "R PED", "TURN": "TURN" };
|
||||
|
||||
// One parsed row -> a display face {sub, addr, desc}.
|
||||
function faceOf(tok) {
|
||||
var verb = (tok[2] || "").toUpperCase();
|
||||
if (verb === "BUTTON") {
|
||||
var addr = (tok[3] || "").toLowerCase();
|
||||
var info = addrInfo[addr] || { sub: "BTN", desc: "RIO button" };
|
||||
return { sub: info.sub, addr: tok[3] || "", desc: info.desc };
|
||||
}
|
||||
if (verb === "KEYPAD")
|
||||
return { sub: "KP " + tok[3] + "/" + tok[4], addr: "keypad",
|
||||
desc: "MFD keypad unit " + tok[3] + ", key " + tok[4] };
|
||||
if (verb === "AXIS") {
|
||||
var ch = (tok[3] || "").toUpperCase();
|
||||
var short_ = chanShort[ch] || tok[3] || "?";
|
||||
var how = (tok[4] || "").toUpperCase();
|
||||
if (how === "SET")
|
||||
return { sub: (ch === "THROTTLE" && tok[5] === "0")
|
||||
? "ALL STOP" : short_ + "=" + tok[5],
|
||||
addr: "axis", desc: tok[3] + " set to " + tok[5] };
|
||||
// Speak the board's own label vocabulary, or every stock axis row in
|
||||
// a loaded file reads as a "customization" purely by spelling
|
||||
// ("AIM +" vs the hand-authored "AIM UP").
|
||||
var axisLabel = {
|
||||
"JOYSTICKY+": "AIM UP", "JOYSTICKY-": "AIM DN",
|
||||
"JOYSTICKX+": "TWIST", "JOYSTICKX-": "TWIST",
|
||||
"LEFTPEDAL+": "L PED", "LEFTPEDAL-": "L PED",
|
||||
"RIGHTPEDAL+": "R PED", "RIGHTPEDAL-": "R PED",
|
||||
"THROTTLE+": "THR +", "THROTTLE-": "THR −",
|
||||
"TURN+": "TURN", "TURN-": "TURN"
|
||||
};
|
||||
var sign = tok[5] === "-" ? "-" : "+";
|
||||
var lbl = axisLabel[ch + sign]
|
||||
|| (short_ + (sign === "-" ? " −" : " +"));
|
||||
return { sub: lbl, addr: "axis",
|
||||
desc: tok[3] + " " + (how === "SLEW" ? "slew" : "deflect")
|
||||
+ " " + (tok[5] || "") + " " + (tok[6] || "") };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFace(el, face, isCustom) {
|
||||
var subEl = el.querySelector(".k-sub");
|
||||
if (face && face.sub) {
|
||||
if (!subEl) {
|
||||
subEl = document.createElement("span");
|
||||
subEl.className = "k-sub";
|
||||
el.appendChild(subEl);
|
||||
}
|
||||
subEl.textContent = face.sub;
|
||||
} else if (subEl) {
|
||||
subEl.remove();
|
||||
}
|
||||
el.setAttribute("data-name", el.firstChild.textContent);
|
||||
el.setAttribute("data-addr", face ? face.addr : "");
|
||||
el.setAttribute("data-desc", face ? face.desc : "");
|
||||
el.classList.toggle("custom", !!isCustom);
|
||||
}
|
||||
|
||||
var statusEl = document.getElementById("load-status");
|
||||
var resetEl = document.getElementById("load-reset");
|
||||
var legendEl = document.getElementById("load-legend");
|
||||
var extrasEl = document.getElementById("load-extras");
|
||||
|
||||
function resetBoard() {
|
||||
Object.keys(keyEls).forEach(function (name) {
|
||||
var el = keyEls[name], s = stock[name];
|
||||
var subEl = el.querySelector(".k-sub");
|
||||
if (s.sub) {
|
||||
if (!subEl) {
|
||||
subEl = document.createElement("span");
|
||||
subEl.className = "k-sub";
|
||||
el.appendChild(subEl);
|
||||
}
|
||||
subEl.textContent = s.sub;
|
||||
} else if (subEl) {
|
||||
subEl.remove();
|
||||
}
|
||||
if (s.name) el.setAttribute("data-name", s.name);
|
||||
else el.removeAttribute("data-name");
|
||||
el.setAttribute("data-addr", s.addr);
|
||||
el.setAttribute("data-desc", s.desc);
|
||||
el.classList.remove("custom");
|
||||
});
|
||||
statusEl.innerHTML = "Showing the <b>stock</b> layout.";
|
||||
resetEl.style.display = "none";
|
||||
legendEl.style.display = "none";
|
||||
extrasEl.style.display = "none";
|
||||
}
|
||||
|
||||
function applyFile(text, fname) {
|
||||
// Parse every row; group by keyboard / elsewhere.
|
||||
var perKey = {}, extras = [], rows = 0;
|
||||
text.split(/\r?\n/).forEach(function (raw) {
|
||||
var line = raw.replace(/#.*$/, "").trim();
|
||||
if (!line) return;
|
||||
var tok = line.split(/\s+/);
|
||||
var kind = tok[0].toUpperCase();
|
||||
if (kind === "KEY" && tok.length >= 3) {
|
||||
rows += 1;
|
||||
var name = tok[1].toUpperCase();
|
||||
if (/^MOUSE/.test(name)) { extras.push(line); return; }
|
||||
(perKey[name] = perKey[name] || []).push(tok);
|
||||
} else if (kind === "PAD" || kind === "PADAXIS"
|
||||
|| kind === "JOYAXIS" || kind === "JOYBUTTON"
|
||||
|| kind === "JOYHAT") {
|
||||
rows += 1;
|
||||
extras.push(line);
|
||||
}
|
||||
});
|
||||
if (rows === 0) {
|
||||
statusEl.innerHTML = "That file has no binding rows — is it really "
|
||||
+ "a bindings.txt?";
|
||||
return;
|
||||
}
|
||||
|
||||
// The board becomes the file: every registered key resets, then rows
|
||||
// land. Built-ins (backtick view, PgUp/PgDn volume) keep their face
|
||||
// unless the file binds that key -- the game's bindings-row-wins rule.
|
||||
var builtIns = { "BACKTICK": 1, "PAGEUP": 1, "PAGEDOWN": 1 };
|
||||
var customCount = 0;
|
||||
Object.keys(keyEls).forEach(function (name) {
|
||||
var el = keyEls[name];
|
||||
var rowsHere = perKey[name];
|
||||
if (!rowsHere || rowsHere.length === 0) {
|
||||
if (builtIns[name]) { // built-in survives an unbound key
|
||||
setFace(el, { sub: stock[name].sub, addr: stock[name].addr,
|
||||
desc: stock[name].desc }, false);
|
||||
return;
|
||||
}
|
||||
var wasBound = !!stock[name].sub;
|
||||
setFace(el, null, false);
|
||||
if (wasBound) el.setAttribute("data-desc", "Unbound in your file "
|
||||
+ "(stock: " + stock[name].sub + ")");
|
||||
return;
|
||||
}
|
||||
var face = faceOf(rowsHere[0]);
|
||||
if (!face) { setFace(el, null, false); return; }
|
||||
if (rowsHere.length > 1) {
|
||||
face.desc += " · also: " + rowsHere.slice(1).map(function (t) {
|
||||
return t.slice(2).join(" ");
|
||||
}).join(" / ");
|
||||
}
|
||||
var isCustom = (face.sub !== stock[name].sub)
|
||||
|| (face.addr !== stock[name].addr);
|
||||
if (isCustom) customCount += 1;
|
||||
setFace(el, face, isCustom);
|
||||
});
|
||||
|
||||
statusEl.innerHTML = "Showing <b>" + (fname || "your bindings")
|
||||
+ "</b> — " + rows + " row(s), <b>" + customCount
|
||||
+ "</b> key(s) differ from stock.";
|
||||
resetEl.style.display = "inline";
|
||||
legendEl.style.display = customCount ? "inline" : "none";
|
||||
if (extras.length) {
|
||||
extrasEl.innerHTML = "<b>Also in your file</b> (not on the keyboard): "
|
||||
+ extras.map(function (l) {
|
||||
return "<code>" + l.replace(/&/g, "&").replace(/</g, "<")
|
||||
+ "</code>";
|
||||
}).join(" · ");
|
||||
extrasEl.style.display = "block";
|
||||
} else {
|
||||
extrasEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
var loaderEl = document.getElementById("loader");
|
||||
var fileEl = document.getElementById("bindfile");
|
||||
fileEl.addEventListener("change", function () {
|
||||
var f = fileEl.files && fileEl.files[0];
|
||||
if (!f) return;
|
||||
var r = new FileReader();
|
||||
r.onload = function () { applyFile(String(r.result), f.name); };
|
||||
r.readAsText(f);
|
||||
});
|
||||
resetEl.addEventListener("click", resetBoard);
|
||||
["dragover", "dragenter"].forEach(function (ev) {
|
||||
loaderEl.addEventListener(ev, function (e) {
|
||||
e.preventDefault();
|
||||
loaderEl.classList.add("dragover");
|
||||
});
|
||||
});
|
||||
["dragleave", "drop"].forEach(function (ev) {
|
||||
loaderEl.addEventListener(ev, function (e) {
|
||||
e.preventDefault();
|
||||
loaderEl.classList.remove("dragover");
|
||||
});
|
||||
});
|
||||
loaderEl.addEventListener("drop", function (e) {
|
||||
var f = e.dataTransfer && e.dataTransfer.files
|
||||
&& e.dataTransfer.files[0];
|
||||
if (!f) return;
|
||||
var r = new FileReader();
|
||||
r.onload = function () { applyFile(String(r.result), f.name); };
|
||||
r.readAsText(f);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# bindings-board 2 -- KEEP THIS LINE. It marks the keyboard-layout generation;
|
||||
# files without it are pre-board layouts and are migrated automatically
|
||||
# (your own rows and the joystick wizard section are kept, the obsolete old
|
||||
# defaults are replaced, and the previous file is saved as bindings.old.txt).
|
||||
#
|
||||
# bindings.txt -- PadRIO input bindings (glass cockpit). Auto-written with
|
||||
# defaults on first run; edit freely, delete to regenerate. Grammar:
|
||||
# key <KEYNAME> button <addr> RIO button (hex 0x.. ok)
|
||||
# key <KEYNAME> keypad <unit> <key> MFD keypad unit 0-2, key 0-15
|
||||
# key <KEYNAME> axis <channel> deflect <+|-> <rate> spring stick
|
||||
# key <KEYNAME> axis <channel> slew <+|-> <rate> throttle lever
|
||||
# key <KEYNAME> axis <channel> set <value> jump/detent
|
||||
# pad <PADBTN> button <addr>
|
||||
# padaxis <PADAXIS> axis <channel> [invert] [slew <rate>]
|
||||
# <KEYNAME> may be a MOUSE BUTTON: MOUSEMIDDLE, MOUSE4, MOUSE5 (the two
|
||||
# side buttons; MOUSEX1/MOUSEX2 are aliases). MOUSELEFT and MOUSERIGHT
|
||||
# also work but are how you press cockpit buttons -- bind them and they
|
||||
# fire on every panel click too. Mouse MOVEMENT is not bindable.
|
||||
# Channels: Throttle JoystickX JoystickY LeftPedal RightPedal Turn
|
||||
# (Turn = signed composite: + drives the right pedal, - the left --
|
||||
# made for mapping a gamepad stick to turning)
|
||||
# Pad buttons: A B X Y LB RB BACK START LS RS DPAD_* Axes: LX LY RX RY LT RT
|
||||
#
|
||||
# LAYOUT: THE KEYBOARD IS THE POD'S BUTTON BOARD. The letter and number
|
||||
# rows are the MFD button banks laid out where they sit on the panel, and
|
||||
# flight lives on the numpad so the board stays free. Every one of the 72
|
||||
# pod buttons is also one mouse click away on the cockpit itself
|
||||
# (right-click = press-and-hold latch).
|
||||
#
|
||||
# A key bound here is REMOVED from the authentic 1995 typed-hotkey channel
|
||||
# so it cannot double-dispatch -- and this board binds nearly everything,
|
||||
# so those hotkeys (5 = MFD1 Quad, z = MFD3 Eng1, t/y/u/i/o = pilot select,
|
||||
# +/- = target zoom) are given up by design. Unbind a key here to get its
|
||||
# 1995 meaning back. Same rule for the built-ins: BACKTICK = view toggle
|
||||
# (V is a board button now), and J/K/L cycle the Mfd1/2/3 preset pages ONLY
|
||||
# while unbound.
|
||||
|
||||
# --- flight: the numpad ------------------------------------------------
|
||||
# 8/2/4/6 stick, 7/9 pedals, 5 all-stop detent, 0 main trigger.
|
||||
# SHIFT/CTRL walk the throttle lever and it STICKS where you leave it;
|
||||
# ALT is the throttle handle's reverse-thrust button.
|
||||
key NUMPAD8 axis JoystickY deflect + 2.5
|
||||
key NUMPAD2 axis JoystickY deflect - 2.5
|
||||
key NUMPAD4 axis JoystickX deflect - 2.5
|
||||
key NUMPAD6 axis JoystickX deflect + 2.5
|
||||
key NUMPAD7 axis LeftPedal deflect + 2.5
|
||||
key NUMPAD9 axis RightPedal deflect + 2.5
|
||||
key NUMPAD5 axis Throttle set 0
|
||||
key LSHIFT axis Throttle slew + 0.7
|
||||
key RSHIFT axis Throttle slew + 0.7
|
||||
key LCTRL axis Throttle slew - 0.7
|
||||
key RCTRL axis Throttle slew - 0.7
|
||||
key LALT button 0x3F
|
||||
key RALT button 0x3F
|
||||
|
||||
# PgUp/PgDn are the VOLUME keys (built in; they produce no typed character
|
||||
# so they can never collide with a hotkey) -- leave them unbound.
|
||||
|
||||
# --- the four mappable fire buttons ------------------------------------
|
||||
# The mouse's side buttons are free if you want them on the trigger:
|
||||
# key MOUSE4 button 0x40
|
||||
# key MOUSE5 button 0x46
|
||||
# key MOUSEMIDDLE button 0x41
|
||||
key SPACE button 0x40 # main trigger
|
||||
key NUMPAD0 button 0x40 # main trigger
|
||||
key NUMPAD1 button 0x45 # pinky
|
||||
key NUMPAD3 button 0x46 # middle thumb
|
||||
key NUMPADDOT button 0x47 # upper thumb
|
||||
|
||||
# --- the hat: look around, on the arrows -------------------------------
|
||||
key UP button 0x42 # torso CENTER (the shipped .RES name)
|
||||
key DOWN button 0x41 # look behind
|
||||
key LEFT button 0x44 # look left
|
||||
key RIGHT button 0x43 # look right
|
||||
|
||||
# --- UPPER MFD banks: number row on top, QWERTY beneath ----------------
|
||||
# Heat / coolant (0x28-0x2F) -- 1-4 and QWER ARE the coolant system:
|
||||
# 1/2/3 Condenser 1-3 valve, 4 coolant FLUSH (hold), Q/W/E Condenser
|
||||
# 4-6 valve, R balance coolant. The valve detents run 1-5-50-CLOSED.
|
||||
key 1 button 0x2F
|
||||
key 2 button 0x2E
|
||||
key 3 button 0x2D
|
||||
key 4 button 0x2C
|
||||
# (yours wins) key Q button 0x2B
|
||||
key W button 0x2A
|
||||
key E button 0x29
|
||||
key R button 0x28
|
||||
# Engineering / Mfd2 (0x20-0x27) -- page-gated: Quad page picks an Eng
|
||||
# page, Eng page drives generator select A-D / bus mode / coolant.
|
||||
key 5 button 0x27
|
||||
key 6 button 0x26
|
||||
key 7 button 0x25
|
||||
key 8 button 0x24
|
||||
# (yours wins) key T button 0x23
|
||||
key Y button 0x22
|
||||
key U button 0x21
|
||||
key I button 0x20
|
||||
# Comm / target hotbox (0x30-0x37) -- pilot select.
|
||||
key 9 button 0x37
|
||||
key 0 button 0x36
|
||||
key MINUS button 0x35
|
||||
key EQUALS button 0x34
|
||||
key O button 0x33
|
||||
key P button 0x32
|
||||
key LBRACKET button 0x31
|
||||
key RBRACKET button 0x30
|
||||
|
||||
# --- LOWER MFD banks: home row on top, the row below beneath -----------
|
||||
# G and B are the PANEL GAP between the two lower clusters -- left
|
||||
# unbound on purpose, so they keep their 1995 typed meaning.
|
||||
# Left Weapons / Mfd1 (0x08-0x0F).
|
||||
key A button 0x0F
|
||||
key S button 0x0E
|
||||
key D button 0x0D
|
||||
key F button 0x0C
|
||||
key Z button 0x0B
|
||||
key X button 0x0A
|
||||
key C button 0x09
|
||||
key V button 0x08
|
||||
# Right Weapons / Mfd3 (0x00-0x07).
|
||||
key H button 0x07
|
||||
key J button 0x06
|
||||
key K button 0x05
|
||||
key L button 0x04
|
||||
key N button 0x03
|
||||
key M button 0x02
|
||||
key COMMA button 0x01
|
||||
key PERIOD button 0x00
|
||||
|
||||
# --- the two columns flanking the map, top to bottom -------------------
|
||||
# Left (0x10-0x15): map zoom in/out, thermal IR, CROUCH, searchlight,
|
||||
# cycle secondary display. Right (0x18-0x1D): cycle control mode
|
||||
# (BAS/MID/ADV), unused, Generator A-D on/off.
|
||||
key F1 button 0x10
|
||||
key F2 button 0x11
|
||||
key F3 button 0x12
|
||||
key F4 button 0x13
|
||||
key F5 button 0x14
|
||||
key F6 button 0x15
|
||||
key F7 button 0x18
|
||||
key F8 button 0x19
|
||||
key F9 button 0x1A
|
||||
key F10 button 0x1B
|
||||
key F11 button 0x1C
|
||||
key F12 button 0x1D
|
||||
|
||||
# --- XInput pad ---
|
||||
# POD-FAITHFUL (default): stick X = torso twist, triggers = pedals.
|
||||
# TWIN-STICK alternative: replace the LX line with the two below --
|
||||
# padaxis LX axis Turn
|
||||
# padaxis RX axis JoystickX
|
||||
padaxis LX axis JoystickX
|
||||
padaxis LY axis JoystickY invert
|
||||
padaxis LT axis LeftPedal
|
||||
padaxis RT axis RightPedal
|
||||
padaxis RY axis Throttle slew 0.7
|
||||
pad A button 0x40
|
||||
pad B button 0x3F
|
||||
pad X button 0x47
|
||||
pad Y button 0x42
|
||||
pad LB button 0x44
|
||||
pad RB button 0x43
|
||||
pad DPAD_UP button 0x42
|
||||
pad DPAD_DOWN button 0x41
|
||||
pad DPAD_LEFT button 0x44
|
||||
pad DPAD_RIGHT button 0x43
|
||||
|
||||
# --- generic joysticks (flight sticks / HOTAS / pedals; DirectInput) ---
|
||||
# EASIEST: run the game once with BT_JOYCONFIG=1 (joyconfig.bat) -- an
|
||||
# interactive wizard detects your controls and writes this section for you.
|
||||
# Manual grammar (rows attach to the last joydev slot; slot 0 if none):
|
||||
# joydev <slot 0-3> [product-name substring]
|
||||
# joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis <channel> [invert] [slew <r>] [deadzone <f>]
|
||||
# joybutton <0-31> button <addr>
|
||||
# joyhat <0-3> <up|down|left|right> button <addr>
|
||||
# Typical flight stick (twist = RZ, throttle wheel = SL0):
|
||||
# joydev 0
|
||||
# joyaxis X axis JoystickX
|
||||
# joyaxis Y axis JoystickY invert
|
||||
# joyaxis RZ axis Turn
|
||||
# joyaxis SL0 axis Throttle invert deadzone 0
|
||||
# joybutton 0 button 0x40
|
||||
# joybutton 1 button 0x46
|
||||
# joyhat 0 up button 0x42
|
||||
# joyhat 0 down button 0x41
|
||||
# joyhat 0 left button 0x44
|
||||
# joyhat 0 right button 0x43
|
||||
|
||||
# --- carried over from your previous bindings.txt (these beat the defaults above) ---
|
||||
key T button 0x2B # my custom: T = eng page thing I like
|
||||
key Q axis Throttle slew + 0.9
|
||||
|
||||
# >>> BT_JOYCONFIG generated -- do not edit between markers
|
||||
joyaxis 0 X axis JoystickX
|
||||
joybutton 0 1 button 0x40
|
||||
# <<< BT_JOYCONFIG end
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Headless test for CONTROLS.html's bindings-loader.
|
||||
|
||||
Builds a copy of the page with a self-test harness injected inside its IIFE,
|
||||
feeds it the fixture (a REAL board-2 migrated bindings.txt: the full defaults
|
||||
plus two player-authored rows and a wizard section), and writes the result to
|
||||
%TEMP%\controls_test.html. Run it under headless Chrome and read the
|
||||
PASS/FAIL lines out of the dumped DOM's <pre id="test-out">:
|
||||
|
||||
python scratchpad/test_controls_page.py
|
||||
chrome --headless=new --disable-gpu --dump-dom file:///%TEMP%/controls_test.html
|
||||
|
||||
Asserts: player-authored rows are carried and marked custom, stock rows stay
|
||||
unmarked, the PgUp/PgDn and backtick built-ins survive unbound keys, wizard
|
||||
rows land in the extras list, and reset restores the stock board exactly.
|
||||
Verified 11/11 on 2026-07-26.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
SCRATCH = tempfile.gettempdir()
|
||||
|
||||
frag = io.open(r"C:\git\bt411\docs\CONTROLS.html", encoding="utf-8").read()
|
||||
sample = io.open(r"C:\git\bt411\scratchpad\fixture_migrated_bindings.txt",
|
||||
encoding="utf-8", errors="replace").read()
|
||||
|
||||
harness = r"""
|
||||
// ---------- INJECTED SELF-TEST (test copy only) ----------
|
||||
try {
|
||||
var SAMPLE = __SAMPLE__;
|
||||
applyFile(SAMPLE, "migrated-test");
|
||||
var out = [];
|
||||
function t(label, ok) { out.push((ok ? "PASS" : "FAIL") + " " + label); }
|
||||
function elOf(n) { return keyEls[n]; }
|
||||
function subOf(n) {
|
||||
var s = elOf(n) && elOf(n).querySelector(".k-sub");
|
||||
return s ? s.textContent : "";
|
||||
}
|
||||
t("T carries the custom button (VALVE face from 0x2B)",
|
||||
subOf("T") === "VALVE" && elOf("T").classList.contains("custom"));
|
||||
t("Q carries the tweaked throttle (THR +) and is custom",
|
||||
subOf("Q") === "THR +" && elOf("Q").classList.contains("custom"));
|
||||
t("W matches stock (VALVE, not custom)",
|
||||
subOf("W") === "VALVE" && !elOf("W").classList.contains("custom"));
|
||||
t("PgUp built-in volume survives an unbound key",
|
||||
subOf("PAGEUP") === "VOL +"
|
||||
&& !elOf("PAGEUP").classList.contains("custom"));
|
||||
t("backtick view built-in survives", subOf("BACKTICK") === "VIEW");
|
||||
t("extras list shows the wizard rows",
|
||||
document.getElementById("load-extras").style.display === "block"
|
||||
&& document.getElementById("load-extras")
|
||||
.textContent.indexOf("joyaxis") >= 0);
|
||||
t("status reports a load",
|
||||
document.getElementById("load-status")
|
||||
.textContent.indexOf("differ from stock") >= 0);
|
||||
var stray = 0, strayList = [];
|
||||
Object.keys(keyEls).forEach(function (n) {
|
||||
if (n !== "T" && n !== "Q"
|
||||
&& keyEls[n].classList.contains("custom")) {
|
||||
stray += 1;
|
||||
var sEl = keyEls[n].querySelector(".k-sub");
|
||||
strayList.push(n + "[sub='" + (sEl ? sEl.textContent : "")
|
||||
+ "' stockSub='" + stock[n].sub + "' addr='"
|
||||
+ keyEls[n].getAttribute("data-addr") + "' stockAddr='"
|
||||
+ stock[n].addr + "']");
|
||||
}
|
||||
});
|
||||
t("no stray custom marks (only T and Q); strays: "
|
||||
+ strayList.join(" "), stray === 0);
|
||||
resetBoard();
|
||||
t("reset restores W", subOf("W") === "VALVE");
|
||||
t("reset restores Q to stock VALVE", subOf("Q") === "VALVE");
|
||||
t("reset clears custom marks",
|
||||
document.querySelectorAll(".key.custom").length === 0);
|
||||
var pre = document.createElement("pre");
|
||||
pre.id = "test-out";
|
||||
pre.textContent = out.join("\n");
|
||||
document.body.appendChild(pre);
|
||||
} catch (err) {
|
||||
var pre2 = document.createElement("pre");
|
||||
pre2.id = "test-out";
|
||||
pre2.textContent = "HARNESS ERROR: " + err + " :: " + (err && err.stack);
|
||||
document.body.appendChild(pre2);
|
||||
}
|
||||
"""
|
||||
|
||||
harness = harness.replace("__SAMPLE__", json.dumps(sample))
|
||||
closer = " })();\n</script>"
|
||||
assert closer in frag, "closer not found"
|
||||
test = frag.replace(closer, harness + closer, 1)
|
||||
|
||||
page = ("<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n"
|
||||
"<title>test</title>\n</head>\n<body>\n" + test + "\n</body>\n</html>\n")
|
||||
out = os.path.join(SCRATCH, "controls_test.html")
|
||||
io.open(out, "w", encoding="utf-8").write(page)
|
||||
print("test page written:", len(page), "chars")
|
||||
Reference in New Issue
Block a user