initial implementation of new router and qam hooks

This commit is contained in:
AAGaming
2024-07-09 02:35:24 -04:00
parent dcfaf11696
commit 28c7254ef6
4 changed files with 167 additions and 191 deletions
+9 -2
View File
@@ -22,9 +22,7 @@ class ErrorBoundaryHook extends Logger {
this.log('Initialized'); this.log('Initialized');
window.__ERRORBOUNDARY_HOOK_INSTANCE?.deinit?.(); window.__ERRORBOUNDARY_HOOK_INSTANCE?.deinit?.();
window.__ERRORBOUNDARY_HOOK_INSTANCE = this; window.__ERRORBOUNDARY_HOOK_INSTANCE = this;
}
init() {
// valve writes only the sanest of code // valve writes only the sanest of code
const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/; const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/;
const initErrorReportingStore = findModuleExport( const initErrorReportingStore = findModuleExport(
@@ -76,6 +74,11 @@ class ErrorBoundaryHook extends Logger {
} }
this.errorBoundaryPatch = replacePatch(ErrorBoundary.prototype, 'render', function (this: any) { this.errorBoundaryPatch = replacePatch(ErrorBoundary.prototype, 'render', function (this: any) {
if (this.state._deckyForceRerender) {
const stateClone = {...this.state, _deckyForceRerender: null};
this.setState(stateClone);
return null;
}
if (this.state.error) { if (this.state.error) {
const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore; const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore;
return ( return (
@@ -89,6 +92,10 @@ class ErrorBoundaryHook extends Logger {
} }
return callOriginal; return callOriginal;
}); });
// Small hack that gives us a lot more flexibility to force rerenders.
ValveErrorBoundary.prototype._deckyForceRerender = function (this: any) {
this.setState({...this.state, _deckyForceRerender: true});
}
} }
public temporarilyDisableReporting() { public temporarilyDisableReporting() {
+1 -2
View File
@@ -36,6 +36,7 @@ import Toaster from './toaster';
import { getVersionInfo } from './updater'; import { getVersionInfo } from './updater';
import { getSetting, setSetting } from './utils/settings'; import { getSetting, setSetting } from './utils/settings';
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper'; import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
import AppHook from './app-hook';
const StorePage = lazy(() => import('./components/store/Store')); const StorePage = lazy(() => import('./components/store/Store'));
const SettingsPage = lazy(() => import('./components/settings')); const SettingsPage = lazy(() => import('./components/settings'));
@@ -82,8 +83,6 @@ class PluginLoader extends Logger {
constructor() { constructor() {
super(PluginLoader.name); super(PluginLoader.name);
this.errorBoundaryHook.init();
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this)); DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this)); DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this)); DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
+89 -70
View File
@@ -1,4 +1,4 @@
import { ErrorBoundary, Focusable, Patch, afterPatch } from '@decky/ui'; import { ErrorBoundary, Focusable, Patch, afterPatch, beforePatch, findInReactTree, findModuleByExport, findModuleExport, getReactRoot, sleep } from '@decky/ui';
import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react'; import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
import type { Route } from 'react-router'; import type { Route } from 'react-router';
@@ -25,13 +25,13 @@ declare global {
const isPatched = Symbol('is patched'); const isPatched = Symbol('is patched');
class RouterHook extends Logger { class RouterHook extends Logger {
private router: any;
private memoizedRouter: any;
private gamepadWrapper: any;
private routerState: DeckyRouterState = new DeckyRouterState(); private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState(); private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
private wrapperPatch: Patch; private renderedComponents: ReactElement[] = [];
private routerPatch?: Patch; private Route: any;
private DeckyWrapper = this.routerWrapper.bind(this);
private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this);
private toReplace = new Map<string, ReactNode>();
public routes?: any[]; public routes?: any[];
constructor() { constructor() {
@@ -41,16 +41,90 @@ class RouterHook extends Logger {
window.__ROUTER_HOOK_INSTANCE?.deinit?.(); window.__ROUTER_HOOK_INSTANCE?.deinit?.();
window.__ROUTER_HOOK_INSTANCE = this; window.__ROUTER_HOOK_INSTANCE = this;
this.gamepadWrapper = Focusable; (async()=> {
const root = getReactRoot(document.getElementById('root') as any);
// TODO be more specific, this is horrible and very very slow
const findRouterNode = () =>findInReactTree(root, node => typeof node?.pendingProps?.loggedIn == "undefined" && node?.type?.toString().includes("Settings.Root()"));
let routerNode = findRouterNode();
while (!routerNode) {
this.warn(
'Failed to find Router node, reattempting in 5 seconds.',
);
await sleep(5000);
routerNode = findRouterNode();
}
if (routerNode) {
this.debug("routerNode", routerNode);
// Patch the component globally
afterPatch(routerNode.elementType, "type", this.handleRouterRender.bind(this));
// Swap out the current instance
routerNode.type = routerNode.elementType.type;
if (routerNode?.alternate) {
routerNode.alternate.type = routerNode.type;
}
// Force a full rerender via our custom error boundary
routerNode?.return?.stateNode?._deckyForceRerender?.();
}
})();
}
let Route: new () => Route; public handleRouterRender(_: any, ret: any) {
const DeckyWrapper = this.DeckyWrapper;
const DeckyGlobalComponentsWrapper = this.DeckyGlobalComponentsWrapper;
if (!this.Route)
// TODO make more redundant
this.Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
if (ret._decky) {
return ret;
}
const returnVal = (
<>
<DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
<DeckyWrapper>{ret}</DeckyWrapper>
</DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
<DeckyGlobalComponentsWrapper />
</DeckyGlobalComponentsStateContextProvider>
</>
);
(returnVal as any)._decky = true;
return returnVal;
}
private globalComponentsWrapper () {
const { components } = useDeckyGlobalComponentsState();
if (this.renderedComponents.length != components.size) {
this.debug('Rerendering global components');
this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
}
return <>{this.renderedComponents}</>;
};
private routerWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched. // Used to store the new replicated routes we create to allow routes to be unpatched.
const processList = (
const { routes, routePatches } = useDeckyRouterState();
// TODO make more redundant
if (!children?.props?.children?.[0]?.props?.children) {
console.log("routerWrapper wrong component?", children)
return children;
}
const mainRouteList = children.props.children[0].props.children;
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
this.processList(mainRouteList, routes, routePatches, true);
this.processList(ingameRouteList, null, routePatches, false);
this.debug('Rerendered routes list');
return children;
};
private processList(
routeList: any[], routeList: any[],
routes: Map<string, RouterEntry> | null, routes: Map<string, RouterEntry> | null,
routePatches: Map<string, Set<RoutePatch>>, routePatches: Map<string, Set<RoutePatch>>,
save: boolean, save: boolean,
) => { ) {
const Route = this.Route;
this.debug('Route list: ', routeList); this.debug('Route list: ', routeList);
if (save) this.routes = routeList; if (save) this.routes = routeList;
let routerIndex = routeList.length; let routerIndex = routeList.length;
@@ -69,13 +143,13 @@ class RouterHook extends Logger {
} }
} }
routeList.forEach((route: Route, index: number) => { routeList.forEach((route: Route, index: number) => {
const replaced = toReplace.get(route?.props?.path as string); const replaced = this.toReplace.get(route?.props?.path as string);
if (replaced) { if (replaced) {
routeList[index].props.children = replaced; routeList[index].props.children = replaced;
toReplace.delete(route?.props?.path as string); this.toReplace.delete(route?.props?.path as string);
} }
if (route?.props?.path && routePatches.has(route.props.path as string)) { if (route?.props?.path && routePatches.has(route.props.path as string)) {
toReplace.set( this.toReplace.set(
route?.props?.path as string, route?.props?.path as string,
// @ts-ignore // @ts-ignore
routeList[index].props.children, routeList[index].props.children,
@@ -94,61 +168,6 @@ class RouterHook extends Logger {
} }
}); });
}; };
let toReplace = new Map<string, ReactNode>();
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
const { routes, routePatches } = useDeckyRouterState();
const mainRouteList = children.props.children[0].props.children;
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
processList(mainRouteList, routes, routePatches, true);
processList(ingameRouteList, null, routePatches, false);
this.debug('Rerendered routes list');
return children;
};
let renderedComponents: ReactElement[] = [];
const DeckyGlobalComponentsWrapper = () => {
const { components } = useDeckyGlobalComponentsState();
if (renderedComponents.length != components.size) {
this.debug('Rerendering global components');
renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
}
return <>{renderedComponents}</>;
};
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
if (ret?.props?.children?.props?.children?.length == 5 || ret?.props?.children?.props?.children?.length == 4) {
const idx = ret?.props?.children?.props?.children?.length == 4 ? 1 : 2;
const potentialSettingsRootString =
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type?.toString() || '';
if (potentialSettingsRootString?.includes('Settings.Root()')) {
if (!this.router) {
this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
if (!Route)
Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
const returnVal = (
<DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
<DeckyWrapper>{ret}</DeckyWrapper>
</DeckyRouterStateContextProvider>
);
return returnVal;
});
this.memoizedRouter = memo(this.router.type);
this.memoizedRouter.isDeckyRouter = true;
}
ret.props.children.props.children.push(
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
<DeckyGlobalComponentsWrapper />
</DeckyGlobalComponentsStateContextProvider>,
);
ret.props.children.props.children[idx].props.children[0].type = this.memoizedRouter;
}
}
return ret;
});
}
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) { addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
this.routerState.addRoute(path, component, props); this.routerState.addRoute(path, component, props);
@@ -175,8 +194,8 @@ class RouterHook extends Logger {
} }
deinit() { deinit() {
this.wrapperPatch.unpatch(); // this.wrapperPatch.unpatch();
this.routerPatch?.unpatch(); // this.routerPatch?.unpatch();
} }
} }
+24 -73
View File
@@ -1,5 +1,5 @@
// TabsHook for versions after the Desktop merge // TabsHook for versions after the Desktop merge
import { ErrorBoundary, Patch, QuickAccessTab, afterPatch, findInReactTree, getReactRoot, sleep } from '@decky/ui'; import { ErrorBoundary, Patch, QuickAccessTab, afterPatch, createReactTreePatcher, findInReactTree, findModuleByExport, getReactRoot, setReactPatcherLoggingEnabled, sleep } from '@decky/ui';
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState'; import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
import Logger from './logger'; import Logger from './logger';
@@ -22,6 +22,7 @@ class TabsHook extends Logger {
tabs: Tab[] = []; tabs: Tab[] = [];
private qAMRoot?: any; private qAMRoot?: any;
private qamPatch?: Patch; private qamPatch?: Patch;
private cachedTabs: any;
constructor() { constructor() {
super('TabsHook'); super('TabsHook');
@@ -32,87 +33,37 @@ class TabsHook extends Logger {
} }
init() { init() {
const tree = getReactRoot(document.getElementById('root') as any); // TODO patch the "embedded" renderer in this module too (seems to be for VR? unsure)
let qAMRoot: any; const qamModule = findModuleByExport(e => e?.type?.toString()?.includes("QuickAccessMenuBrowserView"));
const findQAMRoot = (currentNode: any, iters: number): any => { const qamRenderer = Object.values(qamModule).find((e: any) => e?.type?.toString()?.includes("QuickAccessMenuBrowserView"))
if (iters >= 80) {
// currently 67 const patchHandler = createReactTreePatcher([
return null; tree => findInReactTree(tree, node => node?.props?.onFocusNavDeactivated)
} ], (args, ret) => {
if ( this.log("qam render", args, ret);
(typeof currentNode?.memoizedProps?.visible == 'boolean' ||
typeof currentNode?.memoizedProps?.active == 'boolean') &&
currentNode?.type?.toString()?.includes('QuickAccessMenuBrowserView')
) {
this.log(`QAM root was found in ${iters} recursion cycles`);
return currentNode;
}
if (currentNode.child) {
let node = findQAMRoot(currentNode.child, iters + 1);
if (node !== null) return node;
}
if (currentNode.sibling) {
let node = findQAMRoot(currentNode.sibling, iters + 1);
if (node !== null) return node;
}
return null;
};
(async () => {
qAMRoot = findQAMRoot(tree, 0);
while (!qAMRoot) {
this.error(
'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
);
await sleep(5000);
qAMRoot = findQAMRoot(tree, 0);
}
this.qAMRoot = qAMRoot;
let patchedInnerQAM: any;
this.qamPatch = afterPatch(qAMRoot.return, 'type', (_: any, ret: any) => {
try {
if (!qAMRoot?.child) {
qAMRoot = findQAMRoot(tree, 0);
this.qAMRoot = qAMRoot;
}
if (qAMRoot?.child && !qAMRoot?.child?.type?.decky) {
afterPatch(qAMRoot.child, 'type', (_: any, ret: any) => {
try {
const qamTabsRenderer = findInReactTree(ret, (x) => x?.props?.onFocusNavDeactivated);
if (patchedInnerQAM) {
qamTabsRenderer.type = patchedInnerQAM;
} else {
afterPatch(qamTabsRenderer, 'type', (innerArgs: any, ret: any) => {
const tabs = findInReactTree(ret, (x) => x?.props?.tabs); const tabs = findInReactTree(ret, (x) => x?.props?.tabs);
this.render(tabs.props.tabs, innerArgs[0].visible); this.render(tabs.props.tabs, args[0].visible);
return ret; return ret;
}); }, "TabsHook");
patchedInnerQAM = qamTabsRenderer.type;
}
} catch (e) {
this.error('Error patching QAM inner', e);
}
return ret;
});
qAMRoot.child.type.decky = true;
qAMRoot.child.alternate.type = qAMRoot.child.type;
}
} catch (e) {
this.error('Error patching QAM', e);
}
return ret; this.qamPatch = afterPatch(qamRenderer, "type", patchHandler);
});
if (qAMRoot.return.alternate) { // Patch already rendered qam
qAMRoot.return.alternate.type = qAMRoot.return.type; const root = getReactRoot(document.getElementById('root') as any);
const qamNode = root && findInReactTree(root, (n: any) => n.elementType == qamRenderer); // need elementType, because type is actually mobx wrapper
if (qamNode) {
this.debug("qamNode", qamNode);
// Only affects this fiber node so we don't need to unpatch here
qamNode.type = qamNode.elementType.type;
if (qamNode?.alternate) {
qamNode.alternate.type = qamNode.type;
}
} }
this.log('Finished initial injection');
})();
} }
deinit() { deinit() {
this.qamPatch?.unpatch(); this.qamPatch?.unpatch();
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type; // this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
} }
add(tab: Tab) { add(tab: Tab) {