diff --git a/plugins/server-drawer/src/index.ts b/plugins/server-drawer/src/index.ts index 8855336..35208a6 100644 --- a/plugins/server-drawer/src/index.ts +++ b/plugins/server-drawer/src/index.ts @@ -2,6 +2,7 @@ import { logger } from "@vendetta"; import { storage } from "@vendetta/plugin"; import { patchQuestDockRender } from "./patches/questDockRender"; import { patchQuestDockBase } from "./patches/questDockBase"; +import { patchQuestDockVisible } from "./patches/questDockVisible"; import { patchMobileQuestDock } from "./patches/mobileQuestDock"; import { patchGetQuestAsset } from "./patches/getQuestAsset"; import { patchExpanded, patchEmpty } from "./patches/contentPatch"; @@ -37,6 +38,7 @@ function applyAll() { const patchers: Record boolean> = { questDockRender: () => patchQuestDockRender(cleanups), questDockBase: () => patchQuestDockBase(cleanups), + questDockVisible: () => patchQuestDockVisible(cleanups), mobileQuestDock: () => patchMobileQuestDock(cleanups), getQuestAsset: () => patchGetQuestAsset(cleanups), expanded: () => patchExpanded(cleanups), diff --git a/plugins/server-drawer/src/patches/questDockVisible.ts b/plugins/server-drawer/src/patches/questDockVisible.ts new file mode 100644 index 0000000..a6c232a --- /dev/null +++ b/plugins/server-drawer/src/patches/questDockVisible.ts @@ -0,0 +1,20 @@ +import { rawFindByProps } from "@shared/lib/rawFind"; + +// A third gate alongside useIsMobileQuestDockRendered/useIsMobileQuestDockRenderedBase, found by +// reading the component that actually consumes all three together +// (QuestDockWithQuestContext, confirmed against decompiled current-build Discord source): even +// with the quest data faked and the "rendered" hooks forced true, this one still decides whether +// the dock is visible right now based on navigation route and channel-focus state - so without +// forcing it too, the drawer could still never show up depending on what screen you're on. +export function patchQuestDockVisible(cleanups: (() => void)[]): boolean { + const mod = rawFindByProps("useIsMobileQuestDockVisibleToUser"); + if (!mod?.useIsMobileQuestDockVisibleToUser) return false; + + const orig = mod.useIsMobileQuestDockVisibleToUser; + mod.useIsMobileQuestDockVisibleToUser = function (...args: any[]) { + orig.apply(this, args); + return true; + }; + cleanups.push(() => { mod.useIsMobileQuestDockVisibleToUser = orig; }); + return true; +} diff --git a/shared/lib/rawFind.ts b/shared/lib/rawFind.ts index 7dea442..c84c9a9 100644 --- a/shared/lib/rawFind.ts +++ b/shared/lib/rawFind.ts @@ -3,47 +3,35 @@ // comes up empty, it's marked "not found" and every later call with the same search never // rescans window.modules again, even after the real module registers (confirmed by reading // Revenge's own source: metro/internals/modules.ts's getModules() returns early on a cached -// NOT_FOUND flag, before ever reaching the loop that walks currently-registered modules). That -// makes retry loops (this repo's waitFor/lazy helpers) pointless for any search whose first -// attempt happens to run before the target is registered - every retry after that first miss -// hits the same poisoned cache instead of actually looking again. This walks window.modules -// directly, bypassing that cache entirely, for use specifically inside retry loops. +// NOT_FOUND flag). That makes retry loops pointless for any search whose first attempt happens +// to run before the target is registered - every retry after that first miss hits the same +// poisoned cache instead of actually looking again. // -// The module-exports cache below is shared across every call (not per-predicate) and only ever -// grows - each call only evaluates modules it hasn't seen before, reusing everything already -// pulled. Requiring every not-yet-loaded module in the bundle is inherently a one-time cost (the -// exact same cost Revenge's own finders pay on their first, uncached search), but paying it once -// and reusing the result is very different from paying it on every 200ms retry tick across -// several different patchers, which is what calling window.__r fresh every time would do. +// This walks window.modules directly to bypass that cache - but critically, it never forces a +// module to initialize (never calls window.__r on something that hasn't run yet). An earlier +// version of this file did force-require everything on every scan, which is genuinely dangerous: +// Metro only ever runs a module's factory once and caches whatever it produced forever, so +// force-evaluating a module before whatever it depends on on is actually ready can permanently +// wedge it into a broken state for the rest of the session - no amount of retrying fixes that, +// since the factory never runs again. This only inspects modules Metro's own isInitialized flag +// says have ALREADY been required by something else, which is entirely passive and safe to poll +// repeatedly - it just waits for Discord's own code to naturally reach the module we want. declare const window: any; -const exportsCache = new Map(); - -function scanNewModules(): void { - const modules = window?.modules; - if (!modules) return; - - for (const key in modules) { - const id = Number(key); - if (exportsCache.has(id)) continue; - - try { - const exports = window.__r(id); - if (exports) exportsCache.set(id, exports); - } catch { - // Some modules throw when required this early/out of order - skip and try again - // next call, once whatever they depend on has loaded. - } - } -} - export function rawFind(predicate: (exports: any) => boolean): T | undefined { - scanNewModules(); + const modules = window?.modules; + if (!modules) return undefined; + + for (const id in modules) { + const def = modules[id]; + if (!def?.isInitialized) continue; + + const exports = def.publicModule?.exports; + if (!exports) continue; - for (const exports of exportsCache.values()) { try { if (predicate(exports)) return exports; - if (exports?.default != null && predicate(exports.default)) return exports.default; + if (exports.default != null && predicate(exports.default)) return exports.default; } catch { // A predicate throwing on one module's shape shouldn't stop the scan. }