mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 06:00:09 +02:00
The last state of the experiment, kept on this branch only: - input/gazefirst.py, input/steamui.py: the relay reading the controllers from vrserver's web socket, switching the compositor binding, and driving the Steam UI filter (whose block now expires unless renewed). - The helper's trigger and bumper holds (steered by the controller's position), the laser keeper, and the click gate that waits for the laser. - pointer/probe/focustest (overlay flag 1 << 4, as an input client too), vrsetting (SteamVR settings through vrserver), lasertest --reclaim. Every press and release Steam sees takes SteamVR out of laser mode, and taking the laser back breaks clicks and drags. See docs/gaze-controllers.md on experimental. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
5.7 KiB
JavaScript
107 lines
5.7 KiB
JavaScript
// 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)
|
|
// until with "block" or "auto": blocking stops at this time (Date.now(), ms) unless it's
|
|
// moved on. The input relay (input/gazefirst.py) sets it 15 s ahead every 10 s or so,
|
|
// so if the relay dies, the controllers come back to Steam's UI by themselves.
|
|
// 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) || (G.until && Date.now() > G.until)) 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)});
|
|
})()
|