// Glass Shell runtime module "react" (P2): Steam React framework (tier T3). // Contract: docs/phase2/contracts/react.md. Mechanisms: docs/phase2/capabilities/steam-react.md (SR). // // What it gives other modules (const react = rt.use('react')): // - Steam's own React, jsx runtime, router and components, found by source text (SR §3.2); // - routes.add(path, Component) under /library/lgs/... and routes.override(path, fn(steamChildren)) // by patching the main window's route switch fiber (SR §3.4, §3.5), self-healing on navigation; // - patch.byProps(id, predicate, wrap) for memo / observer / forwardRef / class components found by // the shape of their props (SR §3.2); // - ui helpers (Page, ErrorBoundary, menu, modal, confirm, loc), data helpers, and // - actions (launchNonSteam, primary, desktopWindow, navigate) that are LOGGED, never run, in test mode. // // Fail closed: the first call that needs Steam's modules runs ready(); a missing required finder or route // switch throws before anything is patched. install() itself is cheap and patches nothing. remove() // restores every fiber, holder and listener and reports patchedLeft. Nothing persists: everything lives // in SharedJSContext's heap. (() => { 'use strict'; const RT = window.__LGS_RT || window.__LGS_RT_TEST; if (!RT || typeof RT.define !== 'function') return; const VERSION = 3; // contract react.md v3 (§11) const PREFIX = '/library/lgs/'; const REQUIRED = ['React', 'jsx', 'RoutePaths', 'Focusable', 'DialogButton', 'GamepadPage']; const MARK = '__lgsP2'; // set on every function this module installs // Steam's library-grid launch source (the value the grid's tile menu passes; module 94601 `Zw7` = 1000 // in build 11094443). It only tags the launch for Steam's own statistics. const LAUNCH_SOURCE_LIBRARY = 1000; const CLIENT = 'mostavailable'; // what the library grid's tile menu passes (SR, 43394 callers) const ACTION_LOG_MAX = 200; const WALK_MAX = 400000; let rt = null; // P1's scoped runtime for this module let S = null; // state of the current install let aliased = null; // the api we exposed as RT.react let jsxMod = null, reactMod = null; // Steam's jsx runtime and React exports (patchedLeft checks) // ------------------------------------------------------------ small utilities // Our trampolines (patch.byProps, the route switch) keep the function they replace as __lgsOrig: source // checks read through them, so a predicate that reads the source still matches a patched component // (a second layer, review R1 m5). function unwrapFn(f) { for (let i = 0; i < 8 && typeof f === 'function' && typeof f.__lgsOrig === 'function'; i++) f = f.__lgsOrig; return f; } function fnSrc(v) { try { if (v == null) return ''; const f = unwrapFn(typeof v === 'function' ? v : (v.render || (v.type && (v.type.render || v.type)))); if (typeof f !== 'function') return ''; let s = Function.prototype.toString.call(f); if (f.prototype && typeof f.prototype.render === 'function') s += '\n' + Function.prototype.toString.call(unwrapFn(f.prototype.render)); return s; } catch (_) { return ''; } } function pick(ex, test) { if (!ex || (typeof ex !== 'object' && typeof ex !== 'function')) return null; for (const k of Object.keys(ex)) { let v; try { v = ex[k]; } catch (_) { continue; } try { if (v && test(v, fnSrc(v), k)) return v; } catch (_) { /* next */ } } return null; } const has = (s, ...n) => n.every((x) => s.includes(x)); const util = { fnSrc, pick, has }; const errMsg = (e) => String((e && e.message) || e); function logE(msg, data) { try { rt.error(msg, data); } catch (_) { /* no runtime log */ } } function logI(msg, data) { try { rt.log(msg, data); } catch (_) { /* no runtime log */ } } // ------------------------------------------------------------ finders (SR §3.2) // name -> [needles every candidate module's SOURCE must contain, pick(exports, util)]. // Needles are literal strings Steam keeps across builds; never webpack ids or minified names. const FINDERS = { React: [['react.production'], (ex) => (ex.createElement && ex.useState && ex.version ? ex : null)], jsx: [['react-jsx-runtime.production'], (ex) => (ex.jsx && ex.jsxs ? ex : null)], ReactDOM: [['react-dom.production'], (ex) => (ex.createPortal && ex.flushSync ? ex : null)], Router: [['computeRootMatch', 'Router-History'], (ex) => { const proto = (v) => (v && v.prototype && typeof v.prototype.render === 'function' ? Function.prototype.toString.call(v.prototype.render) : ''); const Route = pick(ex, (v) => has(proto(v), 'computedMatch', '.component')); const Switch = pick(ex, (v) => has(proto(v), 'Children.forEach', 'computedMatch')); const matchPath = pick(ex, (v, s) => typeof v === 'function' && !proto(v) && has(s, 'isExact', 'sensitive')); return Route && Switch && matchPath ? { Route, Switch, matchPath } : null; }], RoutePaths: [['"/zoo"', 'GamepadUI'], (ex) => pick(ex, (v) => typeof v === 'object' && v.GamepadUI && v.GamepadUI.Zoo && v.Library && typeof v.Library.Home === 'function')], Focusable: [['"flow-children":', 'focusWithinClassName', '"Panel"'], (ex) => pick(ex, (v, s) => typeof v === 'function' && has(s, '"flow-children"', 'focusWithinClassName'))], DialogButton: [['"DialogButton","_DialogLayout","Secondary"'], (ex) => pick(ex, (v, s) => has(s, '"DialogButton","_DialogLayout","Secondary"'))], DialogButtonPrimary: [['"DialogButton","_DialogLayout","Primary"'], (ex) => pick(ex, (v, s) => has(s, '"DialogButton","_DialogLayout","Primary"'))], Field: [['spacingBetweenLabelAndChild', '"keep-inline"'], (ex) => pick(ex, (v, s) => has(s, 'spacingBetweenLabelAndChild', '"keep-inline"', 'childrenLayout'))], ToggleField: [['"ToggleField"'], (ex) => pick(ex, (v, s) => v.render && has(s, '"ToggleField"'))], SliderField: [['"SliderField"'], (ex) => pick(ex, (v, s) => typeof v === 'function' && s.length < 400 && has(s, '"SliderField"'))], DropdownField: [['"DropDownField"'], (ex) => pick(ex, (v, s) => typeof v === 'function' && s.length < 400 && has(s, '"DropDownField"'))], GamepadPage: [['useHeaderOpacitiesForGamepadPage', 'padForHeader'], (ex) => pick(ex, (v, s) => v.render && has(s, 'padForHeader', 'scrollable', 'headerVisibility'))], ScrollPanel: [['scrollPaddingLeft', 'ScrollBoth'], (ex) => pick(ex, (v, s) => v.render && has(s, 'scrollPaddingTop', 'scrollDirection'))], ScrollPanelGroup: [['scrollPaddingLeft', 'ScrollBoth', 'scrollStepPercent'], (ex) => pick(ex, (v, s) => v.render && has(s, 'scrollStepPercent', 'onGamepadDirection'))], ConfirmModal: [['bAlertDialog', 'strMiddleButtonText', 'bProgressDialog'], (ex) => pick(ex, (v, s) => typeof v === 'function' && has(s, 'bAlertDialog', 'strMiddleButtonText', 'closeModal'))], showModal: [['bHideMainWindowForPopouts', 'bForcePopOut'], (ex) => pick(ex, (v, s) => typeof v === 'function' && s.length < 200 && has(s, 'bHideMainWindowForPopouts'))], showContextMenu: [['GetContextMenuManagerFromWindow', 'CreateContextMenuInstance'], (ex) => pick(ex, (v, s) => typeof v === 'function' && has(s, 'CreateContextMenuInstance', '.Show()'))], Menu: [['contextMenuCheckMark', 'bInteractableItem'], (ex) => { const MenuItem = pick(ex, (v, s) => has(s, 'bInteractableItem', 'OnOKButton')); const Menu = pick(ex, (v, s) => typeof v === 'function' && s.length < 160 && has(s, 'labelId:', 'useId()')); const MenuSeparator = pick(ex, (v, s) => typeof v === 'function' && has(s, 'ContextMenuSeparator', '"separator"')); return MenuItem && Menu ? { Menu, MenuItem, MenuSeparator } : null; }], useNonSteamApps: [['ScanForInstalledNonSteamApps', 'useEffect'], (ex) => pick(ex, (v, s) => typeof v === 'function' && has(s, 'ScanForInstalledNonSteamApps'))], LaunchFilters: [['"vrurlhandler"', '"Install Chromium"'], (ex) => { const arrs = Object.keys(ex).map((k) => { try { return ex[k]; } catch (_) { return null; } }).filter(Array.isArray); const always = arrs.find((a) => a.includes('vrurlhandler') && !a.includes('konsole')); const devOnly = arrs.find((a) => a.includes('konsole')); const allow = arrs.find((a) => a.includes('Install Chromium')); const useDevMode = pick(ex, (v, s) => typeof v === 'function' && s.length < 200 && has(s, '"developer_mode_enabled"')); return always && devOnly && allow ? { always, devOnly, allow, useDevMode } : null; }], // The library tile menu (module 43394 in 11094443): GetPrimaryActionMenuItem runs // runAction(primaryAction(instance, overview, client), overview, client, launchSource, window)(). AppActions: [['GetPrimaryActionMenuItem', 'ContextMenuAction'], (ex) => { const primaryAction = pick(ex, (v, s) => typeof v === 'function' && has(s, 'BIsAppBlocked()', 'display_status') && !s.includes('Local-only')); const runAction = pick(ex, (v, s) => typeof v === 'function' && has(s, 'Local-only app action for non-local client data', 'CancelLaunch')); return primaryAction && runAction ? { primaryAction, runAction } : null; }], // "#GameAction_" label (module 18488): label(action, count) ActionLabel: [['"#GameActionPlural_"', '"#GameAction_"'], (ex) => pick(ex, (v, s) => typeof v === 'function' && has(s, '"#GameActionPlural_"', '"#GameAction_"'))], // The bar's CSS module (12031 in 11094443): its hashed class names, so the statusPill target does // not depend on the theme's class index. BarClasses: [['QuickAccessButton:"'], (ex) => (ex && typeof ex.QuickAccessButton === 'string' ? ex : null)], // The VR gamepad UI message service (module 92102): p.SteamVR.DashboardDesktopWindowClicked VRMessages: [['"VRGamepadUIMessages"', 'm_SteamVR_ClientMethods'], (ex) => pick(ex, (v) => typeof v === 'object' && v.SteamVR && typeof v.SteamVR.DashboardDesktopWindowClicked === 'function')], }; function newState() { return { req: null, src: null, srcTimer: null, factories: 0, M: null, where: {}, counts: {}, ready: false, error: null, scanMs: null, scan: null, attempts: 0, retryAt: 0, noSwitch: false, broken: new Set(), RouteType: null, ui: null, switchPatches: [], routes: new Map(), overrides: new Map(), unlisten: null, winOff: null, lateOffs: [], targets: new Map(), handles: new Map(), touched: new Set(), typeMap: new Map(), jsxHook: null, menus: new Set(), modals: new Set(), actionLog: [], actionSubs: new Set(), forceTest: false, }; } // ------------------------------------------------------------ webpack and source scan function getReq() { if (S.req) return S.req; const chunk = window.webpackChunksteamui; if (!chunk || typeof chunk.push !== 'function') throw transientError('lgs-react: webpackChunksteamui not found (Steam UI not loaded yet?)'); const sym = Symbol('lgs-react'); let req = null; chunk.push([[sym], {}, (r) => { req = r; }]); // webpack also stores the pushed entry in the array; take ours out again. for (let i = chunk.length - 1; i >= 0; i--) { const c = chunk[i]; if (c && c[0] && c[0][0] === sym) { chunk.splice(i, 1); break; } } if (!req || !req.m) throw transientError('lgs-react: webpack require not available'); S.req = req; return req; } function sources() { if (S.src) return S.src; const req = getReq(); const out = []; for (const id of Object.keys(req.m)) { let s; try { s = Function.prototype.toString.call(req.m[id]); } catch (_) { continue; } out.push([id, s]); } S.src = out; S.factories = out.length; return out; } // One pass over the cached sources; require only modules whose source has every needle, smallest // first (a module nobody loaded yet would run its top-level code on require). // Each finder is searched by ONE anchor needle (its longest: string search skips ahead by the pattern's // length), source-major so each source is scanned while it is hot; only the anchor's modules are then // checked for the other needles. Measured on the Frame (build 11094443, a busy device): 143-165 ms // against 466-494 ms for one alternation regex of every needle, which V8 runs alternative by // alternative over 21 MB of two-byte source. function findIn(spec) { const src = sources(); const req = getReq(); const cand = {}, size = {}; const anchorOf = {}; for (const k in spec) { cand[k] = []; anchorOf[k] = spec[k][0].reduce((x, y) => (y.length > x.length ? y : x)); } const anchors = [...new Set(Object.values(anchorOf))]; const byAnchor = new Map(anchors.map((x) => [x, []])); for (let i = 0; i < src.length; i++) { const s = src[i][1]; for (const x of anchors) if (s.includes(x)) byAnchor.get(x).push(i); } for (const k in spec) { const nd = spec[k][0]; for (const i of byAnchor.get(anchorOf[k])) { const [id, s] = src[i]; if (nd.every((n) => s.includes(n))) { cand[k].push(id); size[id] = s.length; } } } const mods = {}, where = {}, counts = {}; for (const k in spec) { cand[k].sort((a, b) => size[a] - size[b]); counts[k] = cand[k].length; for (const id of cand[k]) { let ex; try { ex = req(id); } catch (_) { continue; } let v = null; try { v = spec[k][1](ex, util); } catch (_) { v = null; } if (v) { mods[k] = v; where[k] = id; break; } } } return { mods, where, counts }; } // ------------------------------------------------------------ Steam glue const inst = () => window.SteamUIStore.WindowStore.VRGamepadUIMainWindowInstance; const mainWin = () => { const i = inst(); return i.BrowserWindow || i.m_BrowserWindow; }; const route = () => inst().m_history.location.pathname; const nav = { inst, win: mainWin, route, location: () => inst().m_history.location, go(path, replace) { inst().Navigate(path, !!replace); return path; }, back() { inst().NavigateBack(); }, focusRoot() { inst().FocusApplicationRoot(); }, }; // ------------------------------------------------------------ fibers function fiberOf(el) { if (!el) return null; for (const k of Object.keys(el)) if (k.startsWith('__reactFiber$')) return el[k]; return null; } function hostRoot() { const doc = mainWin().document; const tw = doc.createTreeWalker(doc.body, 1 /* SHOW_ELEMENT */); for (let n = tw.nextNode(), i = 0; n && i < 5000; n = tw.nextNode(), i++) { let f = fiberOf(n); if (!f) continue; while (f.return) f = f.return; // A DOM node's fiber pointer can be the alternate; walk the committed tree (FiberRoot.current). if (f.tag === 3 && f.stateNode && f.stateNode.current) f = f.stateNode.current; return f; } throw new Error('lgs-react: no React fiber in the main window'); } // Depth-first over the mounted tree (main window and every popup: popups are portals of SharedJSContext's // single root). visit(f) returning true stops the walk. function walk(visit, from) { const stack = [from || hostRoot()]; let n = 0; while (stack.length && n++ < WALK_MAX) { const f = stack.pop(); if (visit(f) === true) return n; if (f.sibling) stack.push(f.sibling); if (f.child) stack.push(f.child); } return n; } function findFibers(pred, max) { const out = []; walk((f) => { const p = f.memoizedProps; if (p && typeof p === 'object') { let ok = false; try { ok = pred(p, f); } catch (_) { ok = false; } if (ok) out.push(f); } return max && out.length >= max; }); return out; } // The first DOM node a fiber renders (depth-first through its children). function firstHost(f) { for (let c = f && f.child, i = 0; c && i < 200; c = c.child, i++) if (c.tag === 5) return c.stateNode; return null; } function closestFiber(el, pred) { for (let f = fiberOf(el); f; f = f.return) { const p = f.memoizedProps; if (f.tag !== 5 && f.tag !== 6 && p && typeof p === 'object') { let ok = false; try { ok = pred(p, f); } catch (_) { ok = false; } if (ok) return f; } } return null; } const fiber = { of: fiberOf, firstHost, root: () => hostRoot(), walk: (visit) => walk(visit), findAll: (pred, opts) => findFibers(pred, opts && opts.max), closest: closestFiber, props(el) { const f = closestFiber(el, () => true); return f ? f.memoizedProps : null; }, }; // The main route switch: a function component whose children prop is the list of top-level // elements (it contains both /settings and /zoo). SR §3.3. function routeSwitchFibers(P) { const settings = P.Settings.Root(), zoo = P.GamepadUI.Zoo.Root(); const out = []; walk((f) => { const ch = f.memoizedProps && typeof f.memoizedProps === 'object' ? f.memoizedProps.children : null; if (f.tag === 0 && Array.isArray(ch) && ch.some((c) => c && c.props && c.props.path === settings) && ch.some((c) => c && c.props && c.props.path === zoo)) out.push(f); }); return out; } // ------------------------------------------------------------ ready(): the one scan, fail closed // Kill switch: the flag `react` set to an off value turns every T3 feature off (callers fail to // install and fall back to T1). Unset means on. Not sticky: it is read on every ready(). function killed() { try { if (!rt || !rt.flags || typeof rt.flags.get !== 'function') return false; const v = rt.flags.get('react'); return v !== undefined && !rt.flags.enabled('react'); } catch (_) { return false; } } // A failure that can clear by itself (Steam's UI still mounting, as on the re-injection after a // SharedJSContext restart): the route switch or the main window's fiber is not there yet, or webpack is // not loaded. ready() retries those after a backoff (1, 2, 4, 8, 16, then every 30 s) instead of keeping // T3 off until the next lgs on. A missing required finder stays sticky (review R1 m7). function transientError(msg) { const e = new Error(msg); e.transient = true; return e; } const RETRY_BASE_MS = 1000, RETRY_MAX_MS = 30000; function ready() { if (!S) throw new Error('lgs-react: module not installed'); if (killed()) throw new Error('lgs-react: flag react is off (T3 off)'); if (S.ready) return true; if (S.error && (!S.error.transient || Date.now() < S.retryAt)) throw S.error; const t0 = performance.now(); try { // The finders' result is kept across transient retries (no second scan). const r = S.scan || findIn(FINDERS); for (const k of S.broken) { delete r.mods[k]; delete r.where[k]; } const missing = REQUIRED.filter((k) => !r.mods[k]); if (missing.length) throw new Error('lgs-react: cannot find ' + missing.join(', ') + ' (fail closed; T3 off)'); S.scan = r; let fibers; try { fibers = S.noSwitch ? [] : routeSwitchFibers(r.mods.RoutePaths); } catch (e) { throw transientError(errMsg(e) + ' (fail closed; T3 off until a retry finds it)'); } if (!fibers.length) throw transientError('lgs-react: main route switch not found (fail closed; T3 off until a retry finds it)'); const zoo = r.mods.RoutePaths.GamepadUI.Zoo.Root(); const sib = fibers[0].memoizedProps.children.find((c) => c && c.props && c.props.path === zoo); S.RouteType = sib ? sib.type : (r.mods.Router && r.mods.Router.Route); if (!S.RouteType) throw new Error('lgs-react: no element type (fail closed; T3 off)'); S.M = r.mods; S.where = r.where; S.counts = r.counts; jsxMod = r.mods.jsx; reactMod = r.mods.React; S.ui = makeUI(r.mods); S.ready = true; S.error = null; S.attempts = 0; S.retryAt = 0; S.scan = null; S.scanMs = Math.round(performance.now() - t0); logI(`ready in ${S.scanMs} ms (${S.factories} factories)`); dropSourcesLater(); return true; } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); S.scanMs = Math.round(performance.now() - t0); S.src = null; if (err.transient) { S.attempts++; err.retryAfterMs = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * Math.pow(2, S.attempts - 1)); err.attempt = S.attempts; S.retryAt = Date.now() + err.retryAfterMs; logE(`ready() failed (transient, attempt ${S.attempts}; retried by the next ready() after ${err.retryAfterMs} ms)`, err.message); } else { S.scan = null; logE('ready() failed', err.message); } S.error = err; throw err; } } const need = () => { ready(); return S.M; }; // The source cache serves callers' own finders (find()) during their install; it is dropped 60 s // after the last use (a later find() scans again). const SRC_KEEP_MS = 60000; function dropSourcesLater() { if (!S) return; if (S.srcTimer) { try { S.srcTimer(); } catch (_) { /* cleared */ } S.srcTimer = null; } const st = S; try { if (rt && typeof rt.setTimeout === 'function') st.srcTimer = rt.setTimeout(() => { st.src = null; st.srcTimer = null; }, SRC_KEEP_MS); } catch (_) { st.srcTimer = null; } } // ------------------------------------------------------------ route matching function compile(path) { const keys = []; const re = path.replace(/[.+*?^${}()[\]\\]/g, '\\$&').replace(/\/:([A-Za-z0-9_]+)/g, (_, k) => { keys.push(k); return '/([^/]+)'; }); return { re: new RegExp('^' + re + '(?:/(?=.)|/?$)', 'i'), reExact: new RegExp('^' + re + '/?$', 'i'), keys }; } function match(pathname, path, exact) { if (S && S.M && S.M.Router && S.M.Router.matchPath) { try { return S.M.Router.matchPath(pathname, { path, exact: !!exact }); } catch (_) { /* fall through */ } } const c = compile(path); const m = (exact ? c.reExact : c.re).exec(pathname); if (!m) return null; const params = {}; c.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1]); }); return { path, url: m[0].replace(/\/$/, '') || '/', isExact: pathname === m[0] || pathname === m[0] + '/', params }; } // ------------------------------------------------------------ the route switch patch (SR §3.4, §3.5) function switchRender(orig, self, props, second) { const st = S; if (!st || !st.ready || (!st.routes.size && !st.overrides.size) || !props || typeof props !== 'object') return orig.call(self, props, second); let children; try { children = buildChildren(st, props.children); } catch (e) { logE('route switch: our children failed, rendering Steam\'s', String((e && e.stack) || e).slice(0, 700)); return orig.call(self, props, second); } return orig.call(self, Object.assign({}, props, { children }), second); } function buildChildren(st, ch) { const { React } = st.M; const { jsx } = st.M.jsx; const loc = nav.location(); let list = [].concat(ch); if (st.overrides.size) { list = list.map((c) => { const p = c && c.props && c.props.path; const o = typeof p === 'string' ? st.overrides.get(p) : null; if (!o) return c; const orig = c.props.children; const host = (steam, routeProps) => jsx(st.ui.ErrorBoundary, { name: 'override ' + p, fallback: steam === undefined ? null : steam, children: jsx(OverrideHost, { o, steam, loc, routeProps }), }); // Steam's route wrappers (Pc, Jh) call a function child with the route props (achievements); // keep that form so Steam's own children get them too. if (typeof orig === 'function') return React.cloneElement(c, { children: (rp) => host(orig(rp), rp) }); return React.cloneElement(c, { children: host(orig, null) }); }); } if (st.routes.size) { const ours = [...st.routes.values()].sort((a, b) => b.path.length - a.path.length).map((r) => jsx(st.RouteType, { path: r.path, exact: !!r.opts.exact, children: jsx(st.ui.ErrorBoundary, { name: 'route ' + r.path, children: jsx(RouteHost, { r, loc }) }), }, 'lgs-route:' + r.path)); list = ours.concat(list); } return list; } function OverrideHost(props) { const { o, steam, loc, routeProps } = props; const m = (routeProps && routeProps.match) || match(loc.pathname, o.path, false); const out = o.fn(steam === undefined ? null : steam, { match: m, location: loc }); return out === undefined ? null : out; } function RouteHost(props) { const { r, loc } = props; return S.M.jsx.jsx(r.Component, { match: match(loc.pathname, r.path, r.opts.exact), location: loc }); } function patchSwitch() { let n = 0; for (const f of routeSwitchFibers(S.M.RoutePaths)) { if (f.type && f.type[MARK]) continue; const orig = f.type; const patched = function LgsRouteSwitch(props, second) { return switchRender(orig, this, props, second); }; patched[MARK] = 'switch'; patched.__lgsOrig = orig; f.type = patched; if (f.alternate) f.alternate.type = patched; S.switchPatches.push({ fiber: f, alt: f.alternate, orig, patched }); n++; } return n; } function unpatchSwitch() { let n = 0; for (const p of S.switchPatches) { for (const f of [p.fiber, p.alt, p.fiber.alternate]) if (f && f.type === p.patched) { f.type = p.orig; n++; } } S.switchPatches = []; return n; } function switchChildren() { const f = routeSwitchFibers(S.M.RoutePaths)[0]; return f ? [].concat(f.memoizedProps.children) : []; } function onOurPath(pathname) { for (const r of S.routes.values()) if (match(pathname, r.path, r.opts.exact)) return true; for (const o of S.overrides.values()) if (match(pathname, o.path, false)) return true; return false; } // One history listener and one window listener while anything is registered: re-patch the switch // before the router renders into our paths (SR §3.4 self-healing), re-apply byProps patches on // bare function components that remounted, and attach optional patches still waiting for their target // (a pending handle counts as a registration by itself: review R1 M2). const pendingAny = () => { for (const h of S.handles.values()) if (h._pending) return true; return false; }; function syncWatchers() { const any = S.routes.size || S.overrides.size || [...S.targets.values()].some((T) => T.needsRefresh) || pendingAny(); if (any && !S.unlisten) { S.unlisten = inst().m_history.listen((loc) => { try { if ((S.routes.size || S.overrides.size) && onOurPath(loc.pathname)) patchSwitch(); } catch (e) { logE('re-patch failed', errMsg(e)); } refreshTargets('navigation'); scheduleLateAttach('navigation'); }); // New popup windows, and pooled popups shown again (P1: "+" list, bar menus), may mount patched // components afresh. const offs = []; const onWin = (why) => () => { refreshTargets(why); scheduleLateAttach(why); }; try { if (rt.windows && typeof rt.windows.onAdd === 'function') offs.push(rt.windows.onAdd(onWin('window'))); } catch (_) { /* older runtime */ } try { if (rt.windows && typeof rt.windows.onShow === 'function') offs.push(rt.windows.onShow(onWin('window shown'))); } catch (_) { /* older runtime */ } S.winOff = offs.length ? () => { for (const off of offs) { try { off(); } catch (_) { /* gone */ } } } : null; } else if (!any && S.unlisten) { try { S.unlisten(); } catch (_) { /* gone */ } S.unlisten = null; if (S.winOff) { try { S.winOff(); } catch (_) { /* gone */ } S.winOff = null; } } if (!pendingAny()) cancelLateAttach(); if (!S.routes.size && !S.overrides.size && S.switchPatches.length) unpatchSwitch(); } // The history event comes before React commits the new route, so a target that mounts with that route is // not there yet when the listener runs. While an optional patch is pending, it is looked for again a few // times after each navigation (or window shown, or its registration): a bounded series of scoped timers // (P1's rt.setTimeout, cleared on removal), restarted by the next event and stopped once nothing is pending. const LATE_ATTACH_MS = [120, 400, 1000, 2500]; function cancelLateAttach() { if (!S || !S.lateOffs.length) return; for (const off of S.lateOffs) { try { off(); } catch (_) { /* fired */ } } S.lateOffs = []; } function scheduleLateAttach(why) { if (!S || !S.ready || !pendingAny()) return; cancelLateAttach(); const st = S; for (const ms of LATE_ATTACH_MS) { let off = null; const fire = () => { const i = st.lateOffs.indexOf(off); if (i >= 0) st.lateOffs.splice(i, 1); // a fired timer is no longer scheduled (status().lateTimers) if (S !== st) return; attachPending(why + ' +' + ms + ' ms'); if (!pendingAny()) cancelLateAttach(); }; try { off = rt && typeof rt.setTimeout === 'function' ? rt.setTimeout(fire, ms) : null; } catch (_) { off = null; } if (typeof off === 'function') st.lateOffs.push(off); } } function attachPending(why) { if (!S || !S.ready) return 0; let n = 0; for (const h of [...S.handles.values()]) { if (!h._pending) continue; try { if (h._attach(why)) n++; } catch (e) { logE(`patch ${h.id}: late attach failed (${why})`, errMsg(e)); } } return n; } function rerenderIfOn(test) { let cur; try { cur = route(); } catch (_) { return; } if (test(cur)) { try { inst().Navigate(cur, true); } catch (e) { logE('re-render navigation failed', errMsg(e)); } } } const routes = { add(path, Component, opts) { need(); opts = Object.assign({}, opts); if (typeof path !== 'string' || !path.startsWith(PREFIX) || path.length <= PREFIX.length) throw new Error(`lgs-react: routes.add: ${path} must be under ${PREFIX} (SR §3.4)`); if (typeof Component !== 'function' && !(Component && typeof Component === 'object')) throw new Error('lgs-react: routes.add: Component must be a component'); if (S.routes.has(path)) throw new Error(`lgs-react: routes.add: ${path} is already added`); if (!S.switchPatches.length && !patchSwitch()) throw new Error('lgs-react: route switch not found; nothing added'); const r = { path, Component, opts, owner: opts.owner || null }; S.routes.set(path, r); syncWatchers(); rerenderIfOn((cur) => !!match(cur, path, opts.exact)); const st = S; return { path, remove() { if (S !== st || !S.routes.has(path) || S.routes.get(path) !== r) return false; let on = false; try { on = !!match(route(), path, opts.exact); } catch (_) { /* no window */ } S.routes.delete(path); if (on) { try { inst().Navigate('/library/home', true); } catch (_) { /* gone */ } } syncWatchers(); return true; }, }; }, override(path, fn, opts) { need(); opts = Object.assign({}, opts); if (typeof path !== 'string' || !path) throw new Error('lgs-react: routes.override: path must be a string'); if (typeof fn !== 'function') throw new Error('lgs-react: routes.override: fn must be a function'); if (S.overrides.has(path)) throw new Error(`lgs-react: routes.override: ${path} is already overridden`); if (!switchChildren().some((c) => c && c.props && c.props.path === path)) throw new Error(`lgs-react: routes.override: Steam has no top-level route ${path}`); if (!S.switchPatches.length && !patchSwitch()) throw new Error('lgs-react: route switch not found; nothing overridden'); const o = { path, fn, opts, owner: opts.owner || null }; S.overrides.set(path, o); syncWatchers(); rerenderIfOn((cur) => !!match(cur, path, false)); const st = S; return { path, remove() { if (S !== st || S.overrides.get(path) !== o) return false; S.overrides.delete(path); syncWatchers(); rerenderIfOn((cur) => !!match(cur, path, false)); return true; }, }; }, list() { return { added: S ? [...S.routes.keys()] : [], overridden: S ? [...S.overrides.keys()] : [] }; }, get paths() { return need().RoutePaths; }, }; // ------------------------------------------------------------ patches by props shape (SR §3.2) // Our trampolines keep __lgsOrig for life (an inert one, with no layers left, has an empty MARK). const baseFn = (f) => (f && f.__lgsOrig ? f.__lgsOrig : f); // What to patch for a fiber: a shared holder (memo .type, forwardRef .render, class prototype.render) // and/or the live fibers' own type. function describe(f) { const t = f.type; switch (f.tag) { case 0: { if (typeof t !== 'function') return null; const p = f.return; if (p && p.tag === 14 && p.type && baseFn(p.type.type) === baseFn(t)) return { kind: 'memo', holder: p.type, key: 'type', orig: baseFn(t), fiberType: true }; return { kind: 'fn', holder: null, key: null, orig: baseFn(t), fiberType: true }; } case 15: { if (typeof t !== 'function') return null; const h = f.elementType && typeof f.elementType === 'object' ? f.elementType : null; return { kind: 'simplememo', holder: h, key: h ? 'type' : null, orig: baseFn(t), fiberType: true }; } case 14: { const inner = t && t.type; if (typeof inner === 'function') return { kind: 'memo', holder: t, key: 'type', orig: baseFn(inner), fiberType: true }; if (inner && typeof inner.render === 'function') return { kind: 'fwd', holder: inner, key: 'render', orig: baseFn(inner.render), fiberType: false }; return null; } case 11: if (t && typeof t.render === 'function') return { kind: 'fwd', holder: t, key: 'render', orig: baseFn(t.render), fiberType: false }; return null; case 1: if (typeof t === 'function' && t.prototype && typeof t.prototype.render === 'function') return { kind: 'class', holder: t.prototype, key: 'render', orig: baseFn(t.prototype.render), fiberType: false }; return null; default: return null; } } const PATCH_TAGS = new Set([0, 1, 11, 14, 15]); function matchTargets(pred, tags) { const found = new Map(); // orig -> desc walk((f) => { if (!(tags || PATCH_TAGS).has(f.tag)) return; const p = f.memoizedProps; if (!p || typeof p !== 'object') return; let ok = false; try { ok = !!pred(p, f); } catch (_) { ok = false; } if (!ok) return; const d = describe(f); if (d && !found.has(d.orig)) found.set(d.orig, d); }); return [...found.values()]; } function swapLiveFibers(T) { let n = 0; const fix = (f) => { if (!f || (f.tag !== 0 && f.tag !== 15)) return false; let changed = false; if (typeof f.type === 'function' && baseFn(f.type) === T.orig && f.type !== T.installed) { f.type = T.installed; changed = true; } // A bare function mounted while patched has our trampoline as elementType (jsx substitution); // give it the original back on removal so Steam's next element reuses the fiber. if (!T.layers.length && T.tramp && f.elementType === T.tramp) { f.elementType = T.orig; changed = true; } return changed; }; walk((f) => { if (fix(f)) n++; fix(f.alternate); }); T.live = n; return n; } // One stable trampoline per target for its whole life, so the type React sees never changes while // layers come and go (no remounts); it calls the current composition of the layers. function installTarget(T) { let fn = T.orig; for (const L of T.layers) { const w = L.wrap(fn); if (typeof w !== 'function') throw new Error(`lgs-react: patch ${L.id}: wrap() must return a function`); fn = w; } T.composed = fn; if (!T.tramp) { const tramp = function LgsPatched() { return T.composed.apply(this, arguments); }; tramp.__lgsOrig = T.orig; if (T.orig.displayName) tramp.displayName = T.orig.displayName; T.tramp = tramp; } T.tramp[MARK] = T.layers.map((L) => L.id).join(','); T.installed = T.layers.length ? T.tramp : T.orig; if (T.holder) { T.holder[T.key] = T.installed; S.touched.add(T); } if (T.kind === 'fn') { if (T.layers.length) S.typeMap.set(T.orig, T.tramp); else S.typeMap.delete(T.orig); syncJsxHook(); } if (T.fiberType) swapLiveFibers(T); } // A bare function component has no shared holder, and a remount would bring its original type back. // While one is patched, Steam's element factories (jsx, jsxs, createElement) substitute our trampoline // for it: one Map lookup per element, nothing else. Removed when the last such patch goes. function syncJsxHook() { const want = S.typeMap.size > 0; if (want && !S.jsxHook) { const J = S.M.jsx, R = S.M.React, map = S.typeMap; const oj = J.jsx, ojs = J.jsxs, oce = R.createElement; const sub = (t) => (typeof t === 'function' ? (map.get(t) || t) : t); const nj = function (t, p, k) { return oj(sub(t), p, k); }; const njs = function (t, p, k) { return ojs(sub(t), p, k); }; const nce = function (t) { if (typeof t === 'function' && map.has(t)) { const a = Array.prototype.slice.call(arguments); a[0] = map.get(t); return oce.apply(this, a); } return oce.apply(this, arguments); }; for (const f of [nj, njs, nce]) f[MARK] = 'jsx'; J.jsx = nj; J.jsxs = njs; R.createElement = nce; S.jsxHook = { J, R, oj, ojs, oce, nj, njs, nce }; } else if (!want && S.jsxHook) { unhookJsx(); } } function unhookJsx() { const h = S && S.jsxHook; if (!h) return 0; let n = 0; if (h.J.jsx === h.nj) { h.J.jsx = h.oj; n++; } if (h.J.jsxs === h.njs) { h.J.jsxs = h.ojs; n++; } if (h.R.createElement === h.nce) { h.R.createElement = h.oce; n++; } S.jsxHook = null; return n; } function refreshTargets(why) { if (!S || !S.ready) return; for (const T of S.targets.values()) { if (T.needsRefresh && T.layers.length) { try { swapLiveFibers(T); } catch (e) { logE('patch refresh failed (' + why + ')', errMsg(e)); } } } attachPending(why); } const patch = { byProps(id, predicate, wrap, opts) { need(); opts = Object.assign({ max: 1 }, opts); if (typeof id !== 'string' || !id) throw new Error('lgs-react: patch.byProps: id must be a string'); if (S.handles.has(id)) throw new Error(`lgs-react: patch.byProps: ${id} already exists`); if (typeof predicate !== 'function') throw new Error(`lgs-react: patch.byProps ${id}: predicate must be a function (is it one of patch.targets that is still null?)`); if (typeof wrap !== 'function') throw new Error(`lgs-react: patch.byProps ${id}: wrap must be a function`); const tags = opts.tags ? new Set(opts.tags) : null; const layer = { id, wrap }; const h = { id, count: 0, live: 0, kinds: [], _pending: false, _targets: [], _attach(late) { const found = matchTargets(predicate, tags); if (found.length > opts.max) throw new Error(`lgs-react: patch.byProps ${id}: ${found.length} components match (max ${opts.max}); tighten the predicate`); if (!found.length) { if (opts.optional) { h._pending = true; syncWatchers(); // The target may be mounting right now (registered during a navigation): look again shortly. if (!late) scheduleLateAttach('registration'); return 0; } throw new Error(`lgs-react: patch.byProps ${id}: nothing matches (fail closed)`); } // Validate every wrap before touching anything. for (const d of found) { const w = wrap(d.orig); if (typeof w !== 'function') throw new Error(`lgs-react: patch.byProps ${id}: wrap() must return a function`); } for (const d of found) { let T = S.targets.get(d.orig); if (!T) { T = Object.assign({ layers: [], installed: d.orig, live: 0 }, d); T.needsRefresh = !T.holder && T.fiberType; S.targets.set(d.orig, T); } T.layers.push(layer); installTarget(T); h._targets.push(T); } h._pending = false; h.count = h._targets.length; h.kinds = h._targets.map((T) => T.kind); h.live = h._targets.reduce((a, T) => a + (T.live || 0), 0); if (late) { h.attachedLate = String(late); logI(`patch ${id}: attached late (${late})`, { count: h.count }); } syncWatchers(); return h.count; }, refresh() { refreshTargets('manual'); h.live = h._targets.reduce((a, T) => a + (T.live || 0), 0); return h.live; }, remove() { if (!S || S.handles.get(id) !== h) return false; for (const T of h._targets) { T.layers = T.layers.filter((L) => L !== layer); try { installTarget(T); } catch (e) { logE(`patch ${id}: restore failed`, errMsg(e)); } if (!T.layers.length) S.targets.delete(T.orig); } h._targets = []; h.count = 0; S.handles.delete(id); syncWatchers(); return true; }, }; S.handles.set(id, h); try { h._attach(false); } catch (e) { S.handles.delete(id); throw e; } return h; }, // Named predicates for known targets (contract §5.2). null = not mapped yet. targets: { plusButton: (p) => { const k = Object.keys(p); return k.length === 1 && k[0] === 'allowLaunchProgram'; }, pagedSettings: (p) => { if (!Array.isArray(p.pages)) return false; let sys = null; try { sys = S.M.RoutePaths.Settings.System(); } catch (_) { return false; } return p.pages.some((pg) => pg && pg.route === sys); }, // The bar's status pill (Quick Access button; CC3): a function component with no props whose first // DOM node is %{QuickAccessButton}. It renders Steam's bar popup button ({refBarPopupHandle, // popupContents, onPopupVisibilityChange, tooltip, ...}); a wrap may change that element's props. statusPill: (p, f) => { if (!f || f.tag !== 0 || Object.keys(p).length !== 0) return false; const el = firstHost(f); if (!el || !el.classList) return false; const cls = S && S.M && S.M.BarClasses ? S.M.BarClasses.QuickAccessButton : null; if (cls) return el.classList.contains(cls); let sel = null; try { sel = rt.sel('%{QuickAccessButton}'); } catch (_) { return false; } return !!el.matches(sel); }, // The dashboard bar (the glass tuner, rt 33-tune.js): the memo with no props that renders // %{PopupBody>DashboardBar} around the Home circle (Bookend) and the main %{BarSurface}; its source // names the bar's tab lists (probed on build 11094443: one match). dashboardBar: (p, f) => !!f && (f.tag === 15 || f.tag === 0) && Object.keys(p).length === 0 && fnSrc(f.type).includes('rgBarTabsForMainSurface'), // The game page's action row (GP §4.12): the forwardRef that renders %{AppButtons} (Play, Steam // Input, the gear) as a Focusable row inside its output. Its parent PlaySection has the same props // plus onNav; the row's own source names its CSS module class ActionRow. appButtons: (p, f) => !!f && f.tag === 11 && 'overview' in p && 'details' in p && 'onGameInfoToggle' in p && 'bShowingLaunchDetails' in p && !('onNav' in p) && fnSrc(f.type).includes('.ActionRow'), }, list() { return S ? [...S.handles.values()].map((h) => ({ id: h.id, count: h.count, live: h.live, kinds: h.kinds, pending: h._pending, late: h.attachedLate || null })) : []; }, // Force one render of every live instance a handle patched, so the patch shows at once. // class: forceUpdate(); mobx observer: the observer's own path (new stateVersion + onStoreChange, // what mobx does when an observable it reads changes). A plain function component cannot be forced // from outside; it is counted as skipped and shows the patch at its next own render. rerender(h) { need(); if (!h || !Array.isArray(h._targets)) throw new Error('lgs-react: patch.rerender: not a patch handle'); const out = { forced: 0, skipped: 0, how: {} }; const mine = (f, T) => { switch (T.kind) { case 'fwd': return f.tag === 11 && f.type === T.holder; case 'class': return f.tag === 1 && f.type && f.type.prototype === T.holder; default: return (f.tag === 0 || f.tag === 15) && typeof f.type === 'function' && baseFn(f.type) === T.orig; } }; const fibers = []; walk((f) => { for (const T of h._targets) if (mine(f, T)) { fibers.push(f); break; } }); for (const f of fibers) { const how = forceFiber(f); if (how) { out.forced++; out.how[how] = (out.how[how] || 0) + 1; } else out.skipped++; } return out; }, }; function forceFiber(f) { try { if (f.tag === 1 && f.stateNode && typeof f.stateNode.forceUpdate === 'function') { f.stateNode.forceUpdate(); return 'class'; } for (let hk = f.memoizedState, i = 0; hk && i < 80; hk = hk.next, i++) { const s = hk.memoizedState; const adm = s && typeof s === 'object' ? s.current : null; if (adm && typeof adm === 'object' && typeof adm.onStoreChange === 'function' && 'stateVersion' in adm && adm.reaction) { adm.stateVersion = Symbol('lgs-rerender'); adm.onStoreChange(); return 'observer'; } } } catch (e) { logE('rerender failed', errMsg(e)); } return null; } // ------------------------------------------------------------ localization (PLAN §1.15) // Steam's LocalizeString returns the raw string ("%1$s mins played all time") and ignores extra // arguments; loc() fills %1$s … %9$s from args itself (REQ C2a->P2 #11). A placeholder without an // argument is left as it is. function loc(token, ...args) { try { const LM = window.LocalizationManager; if (!LM || typeof token !== 'string') return null; let s = LM.LocalizeString(token); if (typeof s !== 'string' || !s || s === token) return null; if (args.length) s = s.replace(/%(\d)\$s/g, (m, n) => (args[n - 1] != null ? String(args[n - 1]) : m)); return s; } catch (_) { return null; } } function lang() { try { const LM = window.LocalizationManager; const l = LM.GetPreferredLocales ? LM.GetPreferredLocales() : null; return String((Array.isArray(l) ? l[0] : l) || 'english'); } catch (_) { return 'english'; } } const isEnglish = () => /^en/i.test(lang()); // Steam's string for token, else the English text only when the UI language is English, else null. const text = (token, english) => (token && loc(token)) || (isEnglish() ? (english || null) : null); // ------------------------------------------------------------ UI helpers function makeUI(M) { const React = M.React; const { jsx, jsxs } = M.jsx; class ErrorBoundary extends React.Component { constructor(p) { super(p); this.state = { err: null }; } static getDerivedStateFromError(err) { return { err }; } componentDidCatch(err) { logE('render error in ' + (this.props.name || 'view'), String((err && err.stack) || err).slice(0, 600)); if (typeof this.props.onError === 'function') { try { this.props.onError(err); } catch (_) { /* caller's handler */ } } } render() { if (!this.state.err) return this.props.children === undefined ? null : this.props.children; if (this.props.fallback !== undefined) return this.props.fallback; return jsx(FailPage, { name: this.props.name, err: this.state.err }); } } // Our error page: Steam's header and footer stay; Back is focused (SR §7). The root spans the page // (width 100%, 48 px side insets) so the title and the capsule sit on the window's centre line on any // route (VP P-30, P-36; review R1 M3). Type, colour and sizes come from P4's tokens, with the token // values as fallbacks for the theme-off state: title = Title 2, Back = a 60 px capsule in Headline. const FAIL_ROOT = { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '32px', width: '100%', height: '100%', boxSizing: 'border-box', padding: '0 48px', textAlign: 'center', color: 'var(--lgs-text-1, rgb(255 255 255 / .96))', }; const FAIL_TITLE = { font: 'var(--lgs-type-title2, 700 30px/1.25 "Motiva Sans", sans-serif)', letterSpacing: 'var(--lgs-track-title, -.01em)', maxWidth: '100%' }; const FAIL_BACK = { width: 'auto', minWidth: '240px', minHeight: 'var(--lgs-btn, 60px)', padding: '0 48px', borderRadius: 'calc(var(--lgs-btn, 60px) / 2)', font: 'var(--lgs-type-headline, 600 24px/1.30 "Motiva Sans", sans-serif)', }; function FailPage() { const back = () => nav.back(); const title = text(null, 'This view could not be shown'); return jsx(M.GamepadPage, { scrollable: false, children: jsxs(M.Focusable, { className: 'lgs-react-fail', onCancel: back, onCancelActionDescription: text('#Button_Back', 'Back'), style: FAIL_ROOT, children: [ title ? jsx('div', { className: 'lgs-react-fail-title', style: FAIL_TITLE, children: title }, 't') : null, // A visionOS capsule sized to its label (60 tall, at least 240 wide), not a full-width bar. jsx(M.DialogButton, { className: 'lgs-react-fail-back', autoFocus: true, onClick: back, style: FAIL_BACK, children: text('#Button_Back', 'Back') }, 'b'), ] }) }); } function Page(props) { const { onCancel, className, pageProps, rootProps, children, name } = props; const cancel = onCancel || (() => nav.back()); return jsx(ErrorBoundary, { name: name || 'page', children: jsx(M.GamepadPage, Object.assign({ scrollable: false }, pageProps, { children: jsx(M.Focusable, Object.assign({}, rootProps, { className, onCancel: cancel, onCancelActionDescription: text('#Button_Back', 'Back'), children, })), })) }); } function Style(props) { return jsx('style', { children: props.css }); } return { ErrorBoundary, FailPage, Page, Style }; } // ------------------------------------------------------------ focus back after a modal (D-P2-7, RX-3) const NULL_TREE = /null-tree/i; const RESTORE_MS = [50, 200, 500, 1000]; function React_isElement(M, x) { try { return !!M.React.isValidElement(x); } catch (_) { return false; } } function activeNavTree() { try { const F = window.FocusNavController; if (typeof F.GetActiveNavTree === 'function') return F.GetActiveNavTree() || null; const c = F.GetActiveContext(); return (c && c.m_LastActiveNavTree) || null; } catch (_) { return null; } } const treeLive = (t) => { try { const el = t && t.m_Root && t.m_Root.m_element; return !!(el && el.isConnected); } catch (_) { return false; } }; const treeHasNode = (t) => { try { return !!(typeof t.GetLastFocusedNode === 'function' ? t.GetLastFocusedNode() : t.m_lastFocusNode); } catch (_) { return true; } }; // The nav tree that has focus when a modal opens, and how to give focus back to it when the modal is gone. function rememberFocus() { const tree = activeNavTree(); if (!tree || NULL_TREE.test(String(tree.id || '')) || typeof tree.TakeFocus !== 'function') return null; let r0 = null; try { r0 = route(); } catch (_) { return null; } const st = S; let started = false; return { restore() { if (started) return; // once per modal started = true; // The modal's own tree, still active (and its DOM still connected) while Steam closes it. const closing = activeNavTree(); let i = 0, acted = 0; const attempt = () => { if (S !== st || !st) return; try { if (route() !== r0 || !treeLive(tree)) return; // the modal navigated away, or the page is gone const cur = activeNavTree(); const lost = !cur || NULL_TREE.test(String(cur.id || '')) || (cur === tree && !treeHasNode(tree)); if (lost) { // TakeFocus() alone does nothing while the context's active tree is vr-null-tree: activate the // page's tree first (Steam's own Activate: OnGamepadNavigationTreeActivated), then focus its last // node. Checked again at the next attempt; at most two tries. if (acted < 2) { acted++; if (typeof tree.Activate === 'function') tree.Activate(); tree.TakeFocus(); logI('modal closed: focus given back to ' + String(tree.id || 'the page') + ' (try ' + acted + ')'); } } else if (cur === tree) { return; // focus is back (Steam's or ours) } else if (cur !== closing && treeLive(cur)) { return; // focus is somewhere else that is live (another modal) } } catch (e) { logE('modal focus restore failed', errMsg(e)); return; } if (i < RESTORE_MS.length) schedule(); }; const schedule = () => { const ms = RESTORE_MS[i++] - (i > 1 ? RESTORE_MS[i - 2] : 0); try { if (rt && typeof rt.setTimeout === 'function') rt.setTimeout(attempt, ms); } catch (_) { /* removed */ } }; schedule(); }, }; } // Wraps the modal's content: forwards every prop Steam passes to the caller's element, with Steam's // closeModal wrapped so the focus series starts when the modal closes (Steam keeps a closed modal mounted // for a while, so its unmount is too late: seen 25 s at 13:20). Steam's own modals (ConfirmModal's OK and // Cancel, B through onCancel) close through this prop. function ModalFocusHost(props) { const { inner, back, ...rest } = props; const R = S && S.M ? S.M.React : null; if (!R) return inner; if (typeof rest.closeModal === 'function') { const close = rest.closeModal; rest.closeModal = function lgsCloseModal() { try { back.restore(); } catch (_) { /* module gone */ } return close.apply(this, arguments); }; } return R.cloneElement(inner, rest); } function trackMenu(m) { if (!m) return m; S.menus.add(m); try { if (typeof m.SetOnHideCallback === 'function') { const st = S; m.SetOnHideCallback(() => { if (st.menus) st.menus.delete(m); }); } } catch (_) { /* older client */ } return m; } const ui = { get Page() { need(); return S.ui.Page; }, get ErrorBoundary() { need(); return S.ui.ErrorBoundary; }, get Style() { need(); return S.ui.Style; }, style(css) { need(); return S.M.jsx.jsx(S.ui.Style, { css }); }, loc, lang, text, menu(content, anchor, opts) { const M = need(); opts = opts || {}; if (!M.showContextMenu || !M.Menu) throw new Error('lgs-react: Steam context menu components not found'); const { jsx } = M.jsx; const { Menu, MenuItem, MenuSeparator } = M.Menu; let el = content; if (Array.isArray(content)) { el = jsx(Menu, { label: opts.label, children: content.map((it, i) => { if (!it) return null; if (it === 'separator' || it.separator) return MenuSeparator ? jsx(MenuSeparator, {}, 'sep' + i) : null; const p = { onSelected: it.onSelected, disabled: !!it.disabled, children: it.label }; if (it.tone) p.tone = it.tone; if (it.className) p.className = it.className; return jsx(MenuItem, p, it.key || 'item' + i); }) }); } return trackMenu(M.showContextMenu(el, anchor || mainWin().document.body, opts.position || {})); }, modal(element, opts) { const M = need(); opts = opts || {}; if (!M.showModal) throw new Error('lgs-react: Steam showModal not found'); const st = S; const win = opts.window || mainWin(); let el = element; // Focus goes back to the control that had it (D-P2-7): Steam clears the page tree's last focus node // when the modal opens and, in VR, parks focus in `vr-null-tree` when it closes, where // FocusApplicationRoot() reactivates the page tree with no node. When our modal's content unmounts and // focus is still lost on the same route, the page tree's own TakeFocus() restores it (live: the same // poster; RX-3). opts.restoreFocus: false opts out. if (opts.restoreFocus !== false && React_isElement(M, element)) { const back = rememberFocus(); if (back) el = M.jsx.jsx(ModalFocusHost, { inner: element, back }); } return Promise.resolve(M.showModal(el, win, opts.options)).then((m) => { if (m && st.modals) st.modals.add(m); return m; }); }, confirm(o) { const M = need(); if (!M.ConfirmModal) throw new Error('lgs-react: Steam ConfirmModal not found'); o = o || {}; const p = { strTitle: o.title, strDescription: o.description, onOK: o.onOK, onCancel: o.onCancel }; if (o.okText) p.strOKButtonText = o.okText; if (o.cancelText) p.strCancelButtonText = o.cancelText; if (o.middleText) { p.strMiddleButtonText = o.middleText; p.onMiddleButton = o.onMiddle; } if (o.alert) p.bAlertDialog = true; return ui.modal(M.jsx.jsx(M.ConfirmModal, p), o); }, }; // ------------------------------------------------------------ data (read-only, SR §3.7) function overview(x) { if (x && typeof x === 'object') return x; try { return window.appStore.GetAppOverviewByAppID(Number(x)) || null; } catch (_) { return null; } } function isLiquidGlass(a) { return !!a && ((typeof a.strExePath === 'string' && /\/glass-shell\/device\/lgs$/.test(a.strExePath)) || (a.strAppName === 'LiquidAss' || a.strAppName === 'Liquid Glass')); } function filterPrograms(list, devMode, opts) { if (!Array.isArray(list)) return []; const F = S && S.M && S.M.LaunchFilters; const ex = F ? [...F.always, ...(devMode ? [] : F.devOnly)] : ['steam', 'vrurlhandler']; const allow = F ? F.allow : []; const out = []; for (const a of list) { if (!a) continue; const keep = allow.includes(a.strAppName) || !ex.some((x) => a.strExePath === x || String(a.strExePath || '').endsWith('/' + x)); if (!keep) continue; const lg = isLiquidGlass(a); if (lg && !(opts && opts.includeLiquidGlass)) continue; out.push({ key: a.strCmdline, name: a.strAppName, exePath: a.strExePath, cmdline: a.strCmdline, args: a.strArguments, iconUrl: a.strIconDataBase64 ? 'data:image/png;base64,' + a.strIconDataBase64 : null, isApplication: !!a.bIsApplication, isLiquidGlass: lg, raw: a, }); } return out; } const data = { stores: () => ({ collectionStore: window.collectionStore, appStore: window.appStore, urlStore: window.urlStore }), app: (appid) => overview(appid), collection(id) { try { return window.collectionStore.GetCollection(id) || null; } catch (_) { return null; } }, installedGames(opts) { opts = opts || {}; const cs = window.collectionStore; const apps = (cs && cs.localGamesCollection ? cs.localGamesCollection.allApps : []).slice(); if (opts.sort === 'name') apps.sort((a, b) => String(a.display_name).localeCompare(String(b.display_name))); else apps.sort((a, b) => (b.rt_last_time_played || 0) - (a.rt_last_time_played || 0)); return (opts.limit ? apps.slice(0, opts.limit) : apps).map((ov) => ({ appid: ov.appid, name: ov.display_name, overview: ov })); }, recentGames(limit) { const c = window.collectionStore && window.collectionStore.recentAppsCollection; const apps = c ? (c.visibleApps || c.allApps || []).slice() : []; return (limit ? apps.slice(0, limit) : apps).map((ov) => ({ appid: ov.appid, name: ov.display_name, overview: ov })); }, art(x) { const ov = overview(x); const out = { portrait: [], hero: null, logo: null, header: null, icon: null, custom: { hero: [], logo: [], landscape: [], portrait: [] } }; if (!ov) return out; const as = window.appStore, us = window.urlStore; try { out.portrait.push(...[].concat(as.GetCachedVerticalCapsuleURL(ov) || [])); } catch (_) { /* older client */ } try { const u = as.GetVerticalCapsuleURLForApp(ov); if (u) out.portrait.push(u); } catch (_) { /* older client */ } out.portrait = out.portrait.filter(Boolean); const b = (file) => { try { return us.BuildCachedLibraryAssetURL(ov.appid, file, ov.local_cache_version) || null; } catch (_) { return null; } }; out.hero = b('library_hero.jpg'); out.logo = b('logo.png'); out.header = b('header.jpg'); try { out.icon = as.GetIconURLForApp(ov) || null; } catch (_) { /* none */ } const c = (fn) => { try { const v = as[fn] && as[fn](ov); return Array.isArray(v) ? v.filter(Boolean) : (v ? [v] : []); } catch (_) { return []; } }; out.custom = { hero: c('GetCustomHeroImageURLs'), logo: c('GetCustomLogoImageURLs'), landscape: c('GetCustomLandcapeImageURLs'), portrait: c('GetCustomVerticalCapsuleImageURLs') }; return out; }, // Hooks: call only inside a component rendered by a route, override or patch of this module. useDevMode() { const M = need(); const v = M.LaunchFilters && M.LaunchFilters.useDevMode ? M.LaunchFilters.useDevMode() : [true]; return !!(Array.isArray(v) ? v[0] : v); }, useNonSteamApps(opts) { const M = need(); opts = opts || {}; const dev = data.useDevMode(); const raw = M.useNonSteamApps ? M.useNonSteamApps(true, opts.enabled !== false, opts.refresh || 0) : undefined; const lg = !!opts.includeLiquidGlass; return M.React.useMemo(() => (Array.isArray(raw) ? filterPrograms(raw, dev, { includeLiquidGlass: lg }) : null), [raw, dev, lg]); }, filterPrograms: (list, devMode, opts) => filterPrograms(list, devMode, opts), isLiquidGlass, }; // ------------------------------------------------------------ actions (HA §14, PLAN §2.3 P2, RX-7) function eventWindow(ev) { try { const t = ev && (ev.currentTarget || (ev.nativeEvent && ev.nativeEvent.currentTarget)); return (t && t.ownerDocument && t.ownerDocument.defaultView) || null; } catch (_) { return null; } } // A synthetic (lab) mouse or pointer event puts the action in test mode by itself. One untrusted click is // NOT a lab click: Steam's own controls turn the gamepad's A into HTMLElement.click(), so a DialogButton's // onClick and a MenuItem's onSelected get an untrusted PointerEvent 'click' with pointerType '' (pointerId // -1). That is the user's A press, the gamepad path, and must reach the action (review R1 M1). Lab clicks // are MouseEvents (P10's L.click, the selftest) or pointer events with a pointerType; a locked lab step // stays in test mode anyway through P10's action logger (reason 1), whatever its clicks look like. function steamProgrammaticClick(ne) { return ne.type === 'click' && ne.pointerType === '' && (ne.pointerId === undefined || ne.pointerId === -1 || ne.pointerId === 0); } function untrustedPointer(ev) { const ne = ev && (ev.nativeEvent || ev); if (!ne || typeof ne !== 'object' || typeof ne.isTrusted !== 'boolean' || ne.isTrusted) return false; if (!/^(click|dblclick|auxclick|contextmenu|mouse|pointer)/.test(String(ne.type || ''))) return false; return !steamProgrammaticClick(ne); } function testReasons(ev) { const r = []; let loggerOn = null; try { const t = rt.test && rt.test.actions; loggerOn = t && typeof t.enabled === 'function' ? !!t.enabled() : null; } catch (_) { loggerOn = null; } if (loggerOn === null) r.push('test state unreadable (fail closed)'); else if (loggerOn) r.push('runtime action logger on'); if (S && S.forceTest) r.push('actions.test(true)'); let live; try { live = rt.flags.get('actionsLive'); } catch (_) { live = undefined; } if (live !== true) r.push('flag actionsLive is not true'); if (untrustedPointer(ev)) r.push('synthetic pointer event'); return r; } function recordAction(entry) { S.actionLog.push(entry); if (S.actionLog.length > ACTION_LOG_MAX) S.actionLog.splice(0, S.actionLog.length - ACTION_LOG_MAX); try { const t = rt.test && rt.test.actions; if (t && typeof t.record === 'function' && (entry.mode !== 'executed')) t.record({ fn: entry.fn, arg: entry.arg, mode: entry.mode, reason: entry.reason, detail: entry.detail }); } catch (_) { /* no runtime */ } for (const cb of S.actionSubs) { try { cb(entry); } catch (_) { /* subscriber gone */ } } } // plan() -> {run: fn | null, refuse: reason, detail} function act(fn, arg, ev, plan) { if (!S) throw new Error('lgs-react: module not installed'); const reasons = testReasons(ev); let p; try { p = plan() || {}; } catch (e) { p = { run: null, refuse: 'Steam handler lookup failed: ' + errMsg(e) }; } const entry = { t: Date.now(), fn, arg, mode: 'executed', reason: null, detail: p.detail || null }; if (typeof p.run !== 'function') { entry.mode = 'refused'; entry.reason = p.refuse || 'Steam handler not found'; } else if (reasons.length) { entry.mode = 'logged'; entry.reason = reasons.join('; '); } if (entry.mode === 'executed') { try { const r = p.run(); if (r && typeof r.catch === 'function') r.catch((e) => logE(`action ${fn} failed`, errMsg(e))); } catch (e) { entry.mode = 'error'; entry.reason = errMsg(e); } } recordAction(entry); return { fn, arg, mode: entry.mode, reason: entry.reason, detail: entry.detail }; } function primaryInfo(appid) { const M = need(); const ov = overview(appid); if (!ov) return { appid, action: null, label: null, reason: 'no such app' }; if (!M.AppActions) return { appid, action: null, label: null, reason: 'Steam app actions not found' }; const action = M.AppActions.primaryAction(inst(), ov, CLIENT); let label = null; if (action) { try { label = M.ActionLabel ? M.ActionLabel(action, 1) : null; } catch (_) { label = null; } } return { appid: ov.appid, name: ov.display_name, action: action || null, label, overview: ov }; } const actions = { launchNonSteam(cmdline, ev) { return act('launchNonSteam', cmdline, ev, () => { if (typeof cmdline !== 'string' || !cmdline) return { refuse: 'cmdline must be a non-empty string' }; const A = window.SteamClient && window.SteamClient.Apps; if (!A || typeof A.LaunchNonSteamApp !== 'function') return { refuse: 'SteamClient.Apps.LaunchNonSteamApp not found' }; return { run: () => A.LaunchNonSteamApp(cmdline) }; }); }, primary(appid, ev) { return act('primary', appid, ev, () => { const i = primaryInfo(appid); if (!i.action) return { refuse: i.reason || 'no primary action for this app', detail: { action: null } }; const M = S.M; const ov = i.overview; return { detail: { action: i.action, label: i.label, name: i.name }, // Steam's tile menu passes the window of the clicked element (event.currentTarget's // ownerDocument.defaultView); without an event, the main window. run: () => { const f = M.AppActions.runAction(i.action, ov, CLIENT, LAUNCH_SOURCE_LIBRARY, eventWindow(ev) || mainWin()); if (typeof f === 'function') f(); }, }; }); }, desktopWindow(windowId, ev) { return act('desktopWindow', windowId, ev, () => { const M = need(); const p = M.VRMessages; if (!p || !p.SteamVR || typeof p.SteamVR.DashboardDesktopWindowClicked !== 'function') return { refuse: 'DashboardDesktopWindowClicked not found' }; if (windowId == null) return { refuse: 'windowId missing' }; return { run: () => p.SteamVR.DashboardDesktopWindowClicked({ window_id: windowId }) }; }); }, navigate(path, opts, ev) { if (opts && typeof opts === 'object' && (opts.nativeEvent || typeof opts.isTrusted === 'boolean')) { ev = opts; opts = {}; } const replace = !!(opts && opts.replace); return act('navigate', path, ev, () => { if (typeof path !== 'string' || !path.startsWith('/')) return { refuse: 'path must start with /' }; return { detail: { replace }, run: () => inst().Navigate(path, replace) }; }); }, primaryInfo(appid) { const i = primaryInfo(appid); return { appid: i.appid, name: i.name || null, action: i.action, label: i.label, reason: i.reason || null }; }, mode(ev) { const r = testReasons(ev); return { mode: r.length ? 'test' : 'live', reasons: r }; }, get log() { return S ? S.actionLog.slice() : []; }, onLog(cb) { S.actionSubs.add(cb); const st = S; return () => { st.actionSubs.delete(cb); }; }, test(on) { S.forceTest = !!on; return actions.mode(); }, // The Steam function each action calls (identity checks, HA AT-4). handler(name) { switch (name) { case 'launchNonSteam': { const A = window.SteamClient && window.SteamClient.Apps; return A ? A.LaunchNonSteamApp : null; } case 'primary': { const M = need(); return M.AppActions ? { primaryAction: M.AppActions.primaryAction, runAction: M.AppActions.runAction, launchSource: LAUNCH_SOURCE_LIBRARY, client: CLIENT } : null; } case 'desktopWindow': { const M = need(); return M.VRMessages && M.VRMessages.SteamVR ? M.VRMessages.SteamVR.DashboardDesktopWindowClicked : null; } case 'navigate': return inst().Navigate; default: return null; } }, }; // ------------------------------------------------------------ status, removal function countPatchedLeft(touched) { let n = 0; const isOurs = (t) => !!t && ((typeof t === 'function' && t[MARK]) || (typeof t === 'object' && ((t.type && t.type[MARK]) || (t.render && t.render[MARK])))); try { walk((f) => { if (isOurs(f.type) || isOurs(f.elementType)) n++; if (f.alternate && (isOurs(f.alternate.type) || isOurs(f.alternate.elementType))) n++; }); } catch (_) { /* no window */ } for (const T of touched || []) { try { if (T.holder && T.holder[T.key] && T.holder[T.key][MARK]) n++; } catch (_) { /* gone */ } } // Steam's element factories (the jsx substitution for bare function components). try { const J = jsxMod, R = reactMod; if (J && ((J.jsx && J.jsx[MARK]) || (J.jsxs && J.jsxs[MARK]))) n++; if (R && R.createElement && R.createElement[MARK]) n++; } catch (_) { /* gone */ } return n; } function status() { const st = S; const out = { version: VERSION, installed: !!st, ready: !!(st && st.ready), error: st && st.error ? st.error.message : null, scanMs: st ? st.scanMs : null, factories: st ? st.factories : 0, finders: {}, switchFibers: null, patchedLive: null, routes: [], overrides: [], patches: [], actions: null }; if (!st) return out; for (const k in FINDERS) out.finders[k] = { module: st.where[k] || null, candidates: st.counts[k] == null ? null : st.counts[k], required: REQUIRED.includes(k), found: !!(st.M && st.M[k]) }; if (st.ready) { try { const live = routeSwitchFibers(st.M.RoutePaths); out.switchFibers = live.length; out.patchedLive = live.filter((f) => f.type && f.type[MARK]).length; } catch (e) { out.switchError = errMsg(e); } } out.routes = [...st.routes.keys()]; out.overrides = [...st.overrides.keys()]; out.patches = patch.list(); // Our functions live anywhere (fibers, holders, Steam's element factories); 0 when nothing is registered. out.patchedLeft = st.ready ? countPatchedLeft([...st.touched]) : 0; out.jsxHooked = !!st.jsxHook; // The history and window listeners (installed while anything is registered, pending patches included) // and the late-attach timers still scheduled. out.watching = !!st.unlisten; out.lateTimers = st.lateOffs.length; if (st.error) { out.errorTransient = !!st.error.transient; out.retryInMs = st.error.transient ? Math.max(0, st.retryAt - Date.now()) : null; out.attempts = st.attempts; } try { out.actions = Object.assign(actions.mode(), { logged: st.actionLog.length }); } catch (_) { /* no runtime */ } return out; } function removeAll() { const st = S; if (!st) return { patchedLeft: 0, restored: 0 }; const rep = { restored: 0, routeBefore: null, routeAfter: null }; for (const m of st.menus) { try { m.Hide(); } catch (_) { /* closed */ } } for (const m of st.modals) { try { m.Close(); } catch (_) { /* closed */ } } st.menus.clear(); st.modals.clear(); let cur = null; try { cur = route(); } catch (_) { /* no window */ } rep.routeBefore = cur; let onOurs = false, onOverride = false; if (cur && st.ready) { for (const r of st.routes.values()) if (match(cur, r.path, r.opts.exact)) onOurs = true; for (const o of st.overrides.values()) if (match(cur, o.path, false)) onOverride = true; } st.routes.clear(); st.overrides.clear(); const touched = [...st.touched]; for (const T of st.targets.values()) { T.layers = []; try { installTarget(T); rep.restored++; } catch (e) { logE('restore failed', errMsg(e)); } } st.targets.clear(); st.handles.clear(); rep.restored += unhookJsx(); rep.restored += unpatchSwitch(); if (st.unlisten) { try { st.unlisten(); } catch (_) { /* gone */ } st.unlisten = null; } if (st.winOff) { try { st.winOff(); } catch (_) { /* gone */ } st.winOff = null; } cancelLateAttach(); if (onOurs) { try { inst().Navigate('/library/home', true); } catch (_) { /* gone */ } } else if (onOverride) { try { inst().Navigate(cur, true); } catch (_) { /* gone */ } } rep.patchedLeft = st.ready ? countPatchedLeft(touched) : 0; try { rep.routeAfter = route(); } catch (_) { /* gone */ } st.actionSubs.clear(); st.src = null; st.req = null; st.M = null; st.ui = null; S = null; return rep; } // ------------------------------------------------------------ the API function makeApi() { const api = { version: VERSION, get isReady() { return !!(S && S.ready); }, ready, status, // Tests only: forget a failed scan (and any broken-finder hook) so ready() can run again. reset() { if (!S) return false; if (S.routes.size || S.overrides.size || S.handles.size || S.switchPatches.length) throw new Error('lgs-react: reset() with live patches; remove them first'); const keep = { actionLog: S.actionLog, actionSubs: S.actionSubs }; S = Object.assign(newState(), keep); return true; }, find(spec) { need(); const r = findIn(spec); dropSourcesLater(); return r; }, util, fiber, routes, patch, ui, data, actions, nav, get React() { return need().React; }, get jsx() { return need().jsx.jsx; }, get jsxs() { return need().jsx.jsxs; }, get Fragment() { return need().jsx.Fragment || need().React.Fragment; }, get ReactDOM() { return need().ReactDOM || null; }, get router() { return need().Router || null; }, get Routes() { return need().RoutePaths; }, get M() { return need(); }, get c() { const M = need(); const m = M.Menu || {}; return { Focusable: M.Focusable, DialogButton: M.DialogButton, DialogButtonPrimary: M.DialogButtonPrimary || null, Field: M.Field || null, ToggleField: M.ToggleField || null, SliderField: M.SliderField || null, DropdownField: M.DropdownField || null, GamepadPage: M.GamepadPage, ScrollPanel: M.ScrollPanel || null, ScrollPanelGroup: M.ScrollPanelGroup || null, Menu: m.Menu || null, MenuItem: m.MenuItem || null, MenuSeparator: m.MenuSeparator || null, ConfirmModal: M.ConfirmModal || null, showModal: M.showModal || null, showContextMenu: M.showContextMenu || null }; }, // Test hooks (RX-5): the next ready() treats these finders as missing. test: { breakFinder(name) { if (!S) return false; if (name) S.broken.add(name); else S.broken.clear(); return [...S.broken]; }, // RX-RETRY: while on, ready() sees no route switch, as while Steam's UI is still mounting (a // transient failure, retried after the backoff). noSwitch(on) { if (!S) return false; S.noSwitch = !!on; return S.noSwitch; }, countPatchedLeft: () => countPatchedLeft(S ? [...S.touched] : []), // The module's own removal (what lgs off runs), then a fresh, unscanned state as after lgs on. // Every handle anyone holds is dead afterwards: tests only, with no other T3 module enabled. cycle() { const keep = S ? { actionLog: S.actionLog } : {}; const rep = removeAll(); S = Object.assign(newState(), keep); return rep; }, // Simulates React remounting the route switch (the patch is lost): the original type goes back // on the live switch fiber; self-healing must re-patch it on the next navigation into our paths. dropSwitchPatch() { if (!S || !S.ready) return 0; let n = 0; for (const p of S.switchPatches) for (const f of [p.fiber, p.fiber.alternate]) if (f && f.type === p.patched) { f.type = p.orig; n++; } S.switchPatches = []; return n; }, }, }; return api; } RT.define({ name: 'react', deps: [], // The kill switch (contract §1): P1's BUILTIN_FLAGS has react: true; an off value removes this module, // so every module that depends on it is blocked and its area falls back to T1. flag: 'react', install(scope) { rt = scope || RT; S = newState(); const api = makeApi(); // `rt.react` (contract §0): an alias of rt.use('react') on the runtime object, so every module's // scoped rt (Object.create of it) and lab steps (__LGS_RT.react) see it. Removed with the module. // P1's rt.expose does this (tracked, deleted on removal); the getter is the fallback for a runtime // without it. aliased = null; let exposed = false; try { if (typeof rt.expose === 'function') { rt.expose('react', api); exposed = true; } } catch (e) { logE('rt.expose(react) failed', errMsg(e)); } if (!exposed) { try { if (!Object.prototype.hasOwnProperty.call(RT, 'react')) { Object.defineProperty(RT, 'react', { configurable: true, enumerable: false, get: () => api }); aliased = api; } } catch (_) { aliased = null; } } return api; }, remove() { if (aliased) { try { const d = Object.getOwnPropertyDescriptor(RT, 'react'); if (d && d.get && d.get() === aliased) delete RT.react; } catch (_) { /* runtime gone */ } aliased = null; } const rep = removeAll(); rt = null; return { patchedLeft: rep.patchedLeft || 0, restored: rep.restored || 0, routeBefore: rep.routeBefore || null, routeAfter: rep.routeAfter || null }; }, }); })();