mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 10:00:11 +02:00
Gaze first: a filter for Steam's UI that drops gamepad input in laser mode
Steam reads the Frame controllers as its own virtual gamepad, so no SteamVR binding keeps stray bumper and thumbstick presses away from its UI. input/steam-gamepad-filter.js, evaluated in Steam's SharedJSContext through its CEF debugger, wraps the gamepad source's OnButtonDown, OnButtonUp, and OnAnalogPad and drops everything but the Steam button while the dashboard is in laser mode (mode "auto"), and logs presses and mode changes. Tested blocking in the headset: nothing reached the UI; the Steam button and the grips never go through it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87a63932c0
commit
537fdac419
1 file changed
+103
@@ -0,0 +1,103 @@
|
||||
// Frametop gaze first: a filter in Steam's UI (SharedJSContext, through Steam's CEF debugger on
|
||||
// 127.0.0.1:8080) that keeps the Frame controllers' gamepad input away from Steam's UI while
|
||||
// gaze mode has the dashboard (docs/gaze-first.md). Steam reads the controllers as its own
|
||||
// virtual gamepad (Steam Input), so no SteamVR binding can do this.
|
||||
//
|
||||
// Steam's gamepad input source (webpack module 17900, class E) turns each controller message
|
||||
// into this.OnButtonDown / OnButtonUp / OnAnalogPad calls, which live on its base class. The
|
||||
// filter defines wrappers on E's prototype, so both live instances go through them. They look
|
||||
// up window.__frametopGaze at every call, so evaluating this file again updates the logic in
|
||||
// place; a wrapper from an older version is replaced. It returns the state as JSON.
|
||||
//
|
||||
// The dashboard's mode (laser or gamepad) is SteamVR's, not the UI's: the UI only mirrors it
|
||||
// (VRFocus, module 84114: J.Instance.ShowGamepadFocusMode, from SteamVR's
|
||||
// system_panel_interaction_mode).
|
||||
//
|
||||
// window.__frametopGaze:
|
||||
// mode "off" (pass everything), "log" (pass, and log), "block" (drop all but `allow`),
|
||||
// "auto" (block while the dashboard is in laser mode, pass in gamepad mode)
|
||||
// allow buttons that always pass: the Steam button's guide and quick menu, and Steam's
|
||||
// own dummy input
|
||||
// log the last 256 events: [ms, "down"|"up"|"analog", button, controller, passed]
|
||||
// modes the dashboard's mode changes: [ms, "gamepad"|"laser"] (sampled every 50 ms)
|
||||
// A release passes if its press did, so nothing stays held when blocking starts.
|
||||
// Buttons (Steam's enum): 1 OK 2 CANCEL 3 SECONDARY 4 OPTIONS 5/6 bumpers 7/8 triggers 9-12 dpad
|
||||
// 13 SELECT 14 START 15/16 stick clicks 17/18 stick touch 23-26 rear buttons (24 left grip,
|
||||
// 26 right grip) 27 STEAM_GUIDE 28 STEAM_QUICK_MENU 29 DUMMY_INPUT.
|
||||
(() => {
|
||||
const VERSION = 2;
|
||||
if (!window.__ftreq) {
|
||||
const name = Object.keys(window).find(k => k.startsWith("webpackChunk"));
|
||||
window[name].push([[Symbol("frametop")], {}, r => { window.__ftreq = r; }]);
|
||||
}
|
||||
const E = window.__ftreq(17900).E;
|
||||
const proto = E.prototype;
|
||||
if (!("HandleControllerInputMessages" in proto)) throw new Error("Steam's gamepad source moved (module 17900)");
|
||||
const base = Object.getPrototypeOf(proto);
|
||||
const G = window.__frametopGaze || (window.__frametopGaze = {mode: "log", allow: [27, 28, 29], log: [], held: {}, dropped: 0});
|
||||
G.modes = G.modes || [];
|
||||
G.gamepadMode = () => {
|
||||
try { return !!window.__ftreq(84114).J.Instance?.ShowGamepadFocusMode; } catch (e) { return false; }
|
||||
};
|
||||
G.note = (ev, button, controller, passed) => {
|
||||
G.log.push([Math.round(performance.now()), ev, button, controller, passed]);
|
||||
if (G.log.length > 256) G.log.shift();
|
||||
};
|
||||
G.blocks = button => {
|
||||
if (G.allow.includes(button)) return false;
|
||||
return G.mode === "block" || (G.mode === "auto" && !G.gamepadMode());
|
||||
};
|
||||
if (proto.OnButtonDown && proto.OnButtonDown.__frametop !== VERSION) {
|
||||
delete proto.OnButtonDown;
|
||||
delete proto.OnButtonUp;
|
||||
delete proto.OnAnalogPad;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(proto, "OnButtonDown")) {
|
||||
proto.OnButtonDown = function (button, controller, ...rest) {
|
||||
let pass = true;
|
||||
try {
|
||||
const g = window.__frametopGaze;
|
||||
g.inst = this;
|
||||
pass = !g.blocks(button);
|
||||
if (g.mode !== "off") g.note("down", button, controller, pass);
|
||||
if (pass) g.held[controller + ":" + button] = true;
|
||||
else g.dropped++;
|
||||
} catch (e) { pass = true; }
|
||||
if (pass) return base.OnButtonDown.call(this, button, controller, ...rest);
|
||||
};
|
||||
proto.OnButtonUp = function (button, controller, ...rest) {
|
||||
let pass = true;
|
||||
try {
|
||||
const g = window.__frametopGaze, key = controller + ":" + button;
|
||||
pass = !!g.held[key] || !g.blocks(button);
|
||||
delete g.held[key];
|
||||
if (g.mode !== "off") g.note("up", button, controller, pass);
|
||||
} catch (e) { pass = true; }
|
||||
if (pass) return base.OnButtonUp.call(this, button, controller, ...rest);
|
||||
};
|
||||
proto.OnAnalogPad = function (button, x, y, controller, ...rest) {
|
||||
let pass = true;
|
||||
try {
|
||||
const g = window.__frametopGaze;
|
||||
pass = !g.blocks(button);
|
||||
if (g.mode !== "off" && (x || y)) g.note("analog", button, controller, pass);
|
||||
} catch (e) { pass = true; }
|
||||
if (pass) return base.OnAnalogPad.call(this, button, x, y, controller, ...rest);
|
||||
};
|
||||
for (const f of [proto.OnButtonDown, proto.OnButtonUp, proto.OnAnalogPad]) f.__frametop = VERSION;
|
||||
G.installed = Date.now();
|
||||
}
|
||||
if (!G.sampler) {
|
||||
let last = null;
|
||||
G.sampler = setInterval(() => {
|
||||
const m = G.gamepadMode() ? "gamepad" : "laser";
|
||||
if (m !== last) {
|
||||
last = m;
|
||||
G.modes.push([Math.round(performance.now()), m]);
|
||||
if (G.modes.length > 128) G.modes.shift();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
return JSON.stringify({version: VERSION, mode: G.mode, gamepad: G.gamepadMode(), installed: G.installed,
|
||||
dropped: G.dropped, modes: G.modes.slice(-10), log: G.log.slice(-10)});
|
||||
})()
|
||||
Reference in new issue
Block a user