diff --git a/lib/assets/src/caches.ts b/lib/assets/src/caches.ts index eedcb75..926e693 100644 --- a/lib/assets/src/caches.ts +++ b/lib/assets/src/caches.ts @@ -1,51 +1,50 @@ -// TODO(lib/assets): do not depend on Discord's ClientInfoModule for versioning, use native interop -import { ClientInfoModule } from '@revenge-mod/discord/native' -import { getStorage } from '@revenge-mod/storage' +import { + callBridgeMethod, + callBridgeMethodSync, +} from '@revenge-mod/modules/native' import { debounce } from '@revenge-mod/utils/callback' -import { mergeDeep } from '@revenge-mod/utils/object' import type { Metro } from '@revenge-mod/modules/types' import type { Asset } from './types' -const Version = 1 -const Key = `${Version}.${ClientInfoModule.getConstants().Build}` +const ExpectedCacheVersion = 2 + +export const Uncached: Cache = { + data: {}, + version: ExpectedCacheVersion, +} // In-memory cache -export const cache: Cache = {} +export let cache: Cache = + callBridgeMethodSync('revenge.caches.assets.read', []) ?? Uncached -const CacheStorage = getStorage(`revenge/assets.${Key}`, { - default: cache, - directory: 'cache', -}) - -// TODO(lib/assets/caches): This loads way too late and requires native interop to load earlier -export const cached = CacheStorage.get().then(cache_ => { - const cached = cache_ !== cache - // If we are not using the default value, merge it into the in-memory cache, then point the storage cache to the in-memory cache. - if (cached) { - Object.assign(cache, cache_) - CacheStorage.cache = cache - } - - return cached -}) +if (cache.version !== ExpectedCacheVersion) { + // TODO: Alert to user to update build + cache = Uncached +} export interface Cache { - [key: Asset['name']]: { - [key: Asset['type']]: Metro.ModuleID + data: { + [key: Asset['name']]: { + [key: Asset['type']]: Metro.ModuleID + } } + version: number } const save = debounce(() => { - CacheStorage.set({}) + callBridgeMethod('revenge.caches.assets.write', [cache.data]) }, 1000) export function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) { - // Merge directly into the cache, only debouncing actual writes - mergeDeep(cache, { - [asset.name]: { - [asset.type]: moduleId, - }, - }) + const reg = (cache.data[asset.name] ??= {}) + reg[asset.type] = moduleId save() } + +declare module '@revenge-mod/modules/native' { + interface Methods { + 'revenge.caches.assets.read': [[], Cache | null] + 'revenge.caches.assets.write': [[data: Cache['data']], void] + } +} diff --git a/lib/assets/src/index.ts b/lib/assets/src/index.ts index a4b9adb..4f0bef7 100644 --- a/lib/assets/src/index.ts +++ b/lib/assets/src/index.ts @@ -48,7 +48,7 @@ export function* getCustomAssets(): Generator { * Yields all registered packager assets, including ones with same name but different types. */ export function* getPackagerAssets(): Generator { - for (const reg of Object.values(cache)) + for (const reg of Object.values(cache.data)) for (const moduleId of Object.values(reg)) yield AssetsRegistry.getAssetByID(metroRequire(moduleId)) } @@ -77,7 +77,7 @@ export function getAssetByName( export function getAssetsByName( name: string, ): Record | undefined { - const reg = cache[name] + const reg = cache.data[name] if (!reg) return return Object.entries(reg).reduce( @@ -106,7 +106,7 @@ export function getAssetIdByName( name: string, type?: Asset['type'], ): AssetId | undefined { - const reg = cache[name] + const reg = cache.data[name] if (!reg) return if (type !== undefined) { @@ -127,7 +127,7 @@ export function getAssetIdByName( * @returns The asset ID. */ export function registerAsset(asset: RegisterableAsset): AssetId { - if (cache[asset.name]?.[asset.type] !== undefined) + if (cache.data[asset.name]?.[asset.type] !== undefined) throw new Error( `Asset with name ${asset.name} and type ${asset.type} already exists!`, ) diff --git a/lib/assets/src/preinit.ts b/lib/assets/src/preinit.ts index 21b4ec0..8d2e585 100644 --- a/lib/assets/src/preinit.ts +++ b/lib/assets/src/preinit.ts @@ -15,7 +15,7 @@ import { import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' import { proxify } from '@revenge-mod/utils/proxy' import { aOverrides } from './_internal' -import { cacheAsset, cached } from './caches' +import { cache, cacheAsset, Uncached } from './caches' import type { Metro } from '@revenge-mod/modules/types' import type { ReactNative } from '@revenge-mod/react/types' import type { Asset, PackagerAsset } from './types' @@ -52,24 +52,20 @@ const unsubAR = waitForModules( if (getModuleDependencies(id)!.length) { unsubAR() - // TODO(lib/assets/caches): Ideally we should not wait for the cache promise here, see the promise impl for more info. - cached.then(cached => { - if (!cached) { - // Resolve reference once and keep in closure - const metroRequire = __r + if (cache === Uncached) { + // Resolve reference once and keep in closure + const metroRequire = __r - // More fragile way, but also more performant: - // There is exactly one asset before the reexported asset registry :/ - const firstAssetModuleId = id - 1 - for (const mId of mUninitialized) { - if (mId < firstAssetModuleId) continue + // More fragile way, but also more performant: + // There is exactly one asset before the reexported asset registry :/ + const firstAssetModuleId = id - 1 + for (const mId of mUninitialized) { + if (mId < firstAssetModuleId) continue - const deps = getModuleDependencies(mId)! - if (deps.length === 1 && deps[0] === id) - metroRequire(mId) - } + const deps = getModuleDependencies(mId)! + if (deps.length === 1 && deps[0] === id) metroRequire(mId) } - }) + } // We already patched the original asset registry, so we don't need to patch again. return diff --git a/lib/discord/src/flux/stores.ts b/lib/discord/src/flux/stores.ts index 9b89d07..3bbd727 100644 --- a/lib/discord/src/flux/stores.ts +++ b/lib/discord/src/flux/stores.ts @@ -9,7 +9,7 @@ import { } from '@revenge-mod/modules/finders/filters' import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' import { asap, noop } from '@revenge-mod/utils/callback' -import { cached, cacheFilterResultForId } from '#modules/src/caches' +import { cache, cacheFilterResultForId, Uncached } from '#modules/src/caches' import { FilterResultFlags } from '#modules/src/finders/_internal' import { FilterFlag } from '#modules/src/finders/filters/constants' import type { @@ -140,13 +140,11 @@ waitForModules(withStore(), (store, id) => { Stores[name] = store }) -cached.then(cached => { - if (!cached) - asap(() => { - const lookup = lookupModules(withStore(), { - uninitialized: true, - }) - - while (lookup.next().done); +if (cache === Uncached) + asap(() => { + const lookup = lookupModules(withStore(), { + uninitialized: true, }) -}) + + while (lookup.next().done); + }) diff --git a/lib/modules/package.json b/lib/modules/package.json index 007769b..5a74d85 100644 --- a/lib/modules/package.json +++ b/lib/modules/package.json @@ -19,7 +19,10 @@ "default": "./src/metro/utils.ts" }, "./native": { - "default": "./src/native.ts" + "default": "./src/native/index.ts" + }, + "./native/fs": { + "default": "./src/native/fs.ts" }, "./types": { "types": "./src/types.ts" diff --git a/lib/modules/src/caches.ts b/lib/modules/src/caches.ts index 84d2625..314db95 100644 --- a/lib/modules/src/caches.ts +++ b/lib/modules/src/caches.ts @@ -1,59 +1,61 @@ -// TODO(lib/modules/caches): do not depend on Discord's ClientInfoModule for versioning, use native interop -import { ClientInfoModule } from '@revenge-mod/discord/native' -import { getStorage } from '@revenge-mod/storage' +import { + callBridgeMethod, + callBridgeMethodSync, +} from '@revenge-mod/modules/native' import { debounce } from '@revenge-mod/utils/callback' import type { Metro } from '@revenge-mod/modules/types' import type { FilterResultFlag } from './finders/_internal' import type { Filter } from './finders/filters' -const Version = 2 -const Key = `${Version}.${ClientInfoModule.getConstants().Build}` +const ExpectedCacheVersion = 3 + +export const Uncached: Cache = { + blacklist: [], + finds: {}, + version: ExpectedCacheVersion, +} // In-memory cache -const cache: Cache = { b: [], f: {} } +export let cache: Cache = + callBridgeMethodSync('revenge.caches.modules.read', []) ?? Uncached -const CacheStorage = getStorage(`revenge/modules.${Key}`, { - default: cache, - directory: 'cache', -}) +if (cache.version !== ExpectedCacheVersion) { + // TODO: Alert to user to update build + cache = Uncached +} -// TODO(lib/modules/caches): This loads way too late and requires native interop to load earlier -export const cached = CacheStorage.get().then(cache_ => { - const cached = cache_ !== cache - // If we are not using the default value, merge it into the in-memory cache, then point the storage cache to the in-memory cache. - if (cached) { - Object.assign(cache, cache_) - CacheStorage.cache = cache - } - - return cached -}) +export type Blacklist = Metro.ModuleID[] +export type Finds = Record< + Filter['key'], + Record | null +> export interface Cache { - b: Metro.ModuleID[] - f: Record< - Filter['key'], - Record | null | undefined - > + blacklist: Blacklist + finds: Finds + version: number } const save = debounce(() => { - CacheStorage.set({}) + callBridgeMethod('revenge.caches.modules.write', [ + cache.blacklist, + cache.finds, + ]) }, 1000) export function cacheBlacklistedModule(id: Metro.ModuleID): boolean { - cache.b.push(id) + cache.blacklist.push(id) save() return true } export function cacheFilterResultForId( - key: Filter['key'], + key: keyof Finds, id: Metro.ModuleID, flag: FilterResultFlag, ): FilterResultFlag { - const reg = (cache.f[key] ??= {}) + const reg = (cache.finds[key] ??= {}) reg[id] = flag save() @@ -61,12 +63,22 @@ export function cacheFilterResultForId( return flag } -export function cacheFilterNotFound(key: Filter['key']) { - cache.f[key] = null +export function cacheFilterNotFound(key: keyof Finds) { + cache.finds[key] = null } -export const getCachedFilterRegistry = ( - key: Filter['key'], -): Cache['f'][Filter['key']] | undefined => cache.f[key] +export const getFilterMatches = ( + key: keyof Finds, +): Finds[keyof Finds] | undefined => cache.finds[key] -export const getCachedBlacklistedModules = () => cache.b +export const getBlacklistedModules = () => cache.blacklist + +declare module '@revenge-mod/modules/native' { + interface Methods { + 'revenge.caches.modules.read': [[], Cache | null] + 'revenge.caches.modules.write': [ + [blacklist: Blacklist, finds: Finds], + void, + ] + } +} diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 700d124..365e424 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -1,7 +1,7 @@ import { getCurrentStack } from '@revenge-mod/utils/error' import { cacheFilterResultForId } from '../caches' -import { metroRequire } from '../metro/custom' import { mInitialized } from '../metro/patches' +import { metroRequire } from '../metro/runtime' import { isModuleExportBad } from '../metro/utils' import { FilterFlag } from './filters' import type { If } from '@revenge-mod/utils/types' diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 34938b3..03ed9ce 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -1,13 +1,13 @@ import { getCurrentStack } from '@revenge-mod/utils/error' import { proxify } from '@revenge-mod/utils/proxy' -import { cacheFilterNotFound, getCachedFilterRegistry } from '../caches' -import { metroRequire } from '../metro/custom' +import { cacheFilterNotFound, getFilterMatches } from '../caches' import { mImportedPaths, mInitialized, mList, mUninitialized, } from '../metro/patches' +import { metroRequire } from '../metro/runtime' import { getInitializedModuleExports, isModuleInitialized, @@ -163,7 +163,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { if (options?.cached ?? true) { const notInit = !(options?.initialize ?? true) - const reg = getCachedFilterRegistry(filter.key) + const reg = getFilterMatches(filter.key) // Return early if previous lookup was a full lookup and no modules were found if (reg === null) return @@ -307,7 +307,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { if (options?.cached ?? true) { const notInit = !(options?.initialize ?? true) - const reg = getCachedFilterRegistry(filter.key) + const reg = getFilterMatches(filter.key) // Return early if previous lookup was a full lookup and no modules were found if (reg === null) return NotFoundResult diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index ca072a1..9b675e2 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -1,5 +1,5 @@ import { noop } from '@revenge-mod/utils/callback' -import { getCachedFilterRegistry } from '../caches' +import { getFilterMatches } from '../caches' import { mInitialized } from '../metro/patches' import { onAnyModuleInitialized, @@ -97,13 +97,13 @@ export function waitForModules( options?: WaitForModulesOptions, ): WaitForModulesUnsubscribeFunction { if (options?.cached) { - const reg = getCachedFilterRegistry(filter.key) + const reg = getFilterMatches(filter.key) if (reg === null) return noop if (reg) { if (__BUILD_FLAG_DEBUG_MODULE_WAITS__) nativeLoggingHook( - `\u001b[32mUsing cached results for wait \u001b[33m${filter.key}\u001b[0m`, + `\u001b[32mUsing cached results for wait: \u001b[33m${filter.key}\u001b[0m`, 1, ) diff --git a/lib/modules/src/metro/patches.ts b/lib/modules/src/metro/patches.ts index 29bd422..736e158 100644 --- a/lib/modules/src/metro/patches.ts +++ b/lib/modules/src/metro/patches.ts @@ -1,14 +1,10 @@ -import { - cacheBlacklistedModule, - cached, - getCachedBlacklistedModules, -} from '../caches' +import { cache, cacheBlacklistedModule, Uncached } from '../caches' import { global, metroImportAll, metroImportDefault, metroRequire, -} from './custom' +} from './runtime' import { onModuleInitialized } from './subscriptions' import { executeImportedPathSubscriptions, @@ -144,11 +140,8 @@ function handleFactoryCall( /// MODULE PATCHES AND BLACKLISTS // Restore blacklists -cached.then(cached => { - if (cached) - for (const id of getCachedBlacklistedModules()) - mUninitialized.delete(id) -}) +if (cache !== Uncached) + for (const id of cache.blacklist) mUninitialized.delete(id) const ImportTrackerModuleId = 2 diff --git a/lib/modules/src/metro/custom.ts b/lib/modules/src/metro/runtime.ts similarity index 80% rename from lib/modules/src/metro/custom.ts rename to lib/modules/src/metro/runtime.ts index 8f98e49..b838d53 100644 --- a/lib/modules/src/metro/custom.ts +++ b/lib/modules/src/metro/runtime.ts @@ -1,6 +1,8 @@ /** - * A more performant implementation of Metro's core functions. + * A minimal implementation of Metro's runtime with little overhead. * Making initialization faster and use less resources. + * + * Also avoids cloning exports, allowing for patches to be applied directly without checking for clones. */ import { mList } from './patches' @@ -58,6 +60,9 @@ export const metroImportDefault: Metro.RequireFn = moduleId => { if (mod.flags & HasImportedDefault) return mod.importedDefault const exports = metroRequire(moduleId) + + mod.flags |= HasImportedDefault + return (mod.importedDefault = exports?.__esModule ? exports.default : exports) @@ -68,7 +73,11 @@ export const metroImportAll: Metro.RequireFn = moduleId => { if (mod.flags & HasImportedAll) return mod.importedAll const exports = metroRequire(moduleId) + // Our implementation doesn't match Metro's because we modify the exports directly instead of cloning + // But this is why ours is superior, it allows patching the exports without needing to do it more than a single time if (!exports?.__esModule) exports.default = exports + mod.flags |= HasImportedAll + return (mod.importedAll = exports) } diff --git a/lib/modules/src/metro/utils.ts b/lib/modules/src/metro/utils.ts index 91abd31..66c7501 100644 --- a/lib/modules/src/metro/utils.ts +++ b/lib/modules/src/metro/utils.ts @@ -1,6 +1,6 @@ import { isProxy } from '@revenge-mod/utils/proxy' -import { Initialized } from './custom' import { mDeps, mList } from './patches' +import { Initialized } from './runtime' import type { Metro } from '../types' /** diff --git a/lib/modules/src/native/fs.ts b/lib/modules/src/native/fs.ts new file mode 100644 index 0000000..29feb8b --- /dev/null +++ b/lib/modules/src/native/fs.ts @@ -0,0 +1,45 @@ +import { + callBridgeMethod, + callBridgeMethodSync, +} from '@revenge-mod/modules/native' + +export function readFile(path: string) { + return callBridgeMethod('revenge.fs.read', [path]) +} + +export function writeFile(path: string, data: string) { + return callBridgeMethod('revenge.fs.write', [path, data]) +} + +export function exists(path: string) { + return callBridgeMethod('revenge.fs.exists', [path]) +} + +export function rm(path: string) { + return callBridgeMethod('revenge.fs.delete', [path]) +} + +export function readFileSync(path: string) { + return callBridgeMethodSync('revenge.fs.read', [path]) +} + +export function writeFileSync(path: string, data: string) { + return callBridgeMethodSync('revenge.fs.write', [path, data]) +} + +export function rmSync(path: string) { + return callBridgeMethodSync('revenge.fs.exists', [path]) +} + +export function deleteFileSync(path: string) { + return callBridgeMethodSync('revenge.fs.delete', [path]) +} + +declare module '@revenge-mod/modules/native' { + export interface Methods { + 'revenge.fs.read': [[path: string], string] + 'revenge.fs.write': [[path: string, data: string], void] + 'revenge.fs.exists': [[path: string], boolean] + 'revenge.fs.delete': [[path: string], boolean] + } +} diff --git a/lib/modules/src/native.ts b/lib/modules/src/native/index.ts similarity index 59% rename from lib/modules/src/native.ts rename to lib/modules/src/native/index.ts index 72c669e..1034384 100644 --- a/lib/modules/src/native.ts +++ b/lib/modules/src/native/index.ts @@ -45,16 +45,22 @@ function makePayload(name: string, args: any[]): object { * @param args The arguments to pass to the native method. * @returns A promise that resolves with the result of the native method call. */ -export async function callMethod(name: string, args: any[]): Promise { - const result = await BridgePromise.readAsDataURL(makePayload(name, args)) +export async function callBridgeMethod( + name: N, + args: MethodArgs, +): Promise> { + try { + const result = await BridgePromise.readAsDataURL( + makePayload(name, args), + ) - if ('error' in result) - throw new Error(`Call failed: ${result.error as string}`) - if ('result' in result) return result.result as T + if ('error' in result) throw result.error + if ('result' in result) return result.result as MethodResult - throw new Error( - 'Call failed: The module did not return a valid result. The native hook must have failed.', - ) + throw 'The module did not return a valid result. The native hook must have failed.' + } catch (error) { + throw new Error(`Call failed: ${error}`) + } } /** @@ -66,17 +72,46 @@ export async function callMethod(name: string, args: any[]): Promise { * @param args The arguments to pass to the native method. * @returns The result of the native method call. */ -export function callMethodSync(name: string, args: any[]): T { +export function callBridgeMethodSync( + name: N, + args: MethodArgs, +): MethodResult { try { const result = Bridge.getBBox(0, makePayload(name, args)) if ('error' in result) throw result.error - if ('result' in result) return result.result as T + if ('result' in result) return result.result as MethodResult throw 'The module did not return a valid result. The native hook must have failed.' } catch (error) { - throw new Error( - `Call failed: ${error instanceof Error ? error.message : String(error)}`, - ) + throw new Error(`Call failed: ${error}`) } } + +/** + * Get the bridge information. + */ +export function getBridgeInfo(): BridgeInfo | null { + try { + return callBridgeMethodSync('revenge.info', []) + } catch (e) { + nativeLoggingHook( + `\u001b[31mFailed to get native bridge info: ${e}\u001b[0m`, + 2, + ) + return null + } +} + +export interface BridgeInfo { + name: string + version: number +} + +export type MethodName = Extract +export type MethodArgs = Methods[T][0] +export type MethodResult = Methods[T][1] + +export interface Methods { + 'revenge.info': [[], BridgeInfo] +} diff --git a/lib/plugins/src/_internal/index.ts b/lib/plugins/src/_internal/index.ts index f09b4a3..4acb5e0 100644 --- a/lib/plugins/src/_internal/index.ts +++ b/lib/plugins/src/_internal/index.ts @@ -1,8 +1,16 @@ import { TypedEventEmitter } from '@revenge-mod/discord/common/utils' +import { + callBridgeMethod, + callBridgeMethodSync, +} from '@revenge-mod/modules/native' import { getErrorStack } from '@revenge-mod/utils/error' import { sleepReject } from '@revenge-mod/utils/promise' import { pUnscopedApi as uapi } from '../apis' -import { PluginFlags as Flag, PluginStatus as Status } from '../constants' +import { + PluginFlags as Flag, + PersistentPluginFlags, + PluginStatus as Status, +} from '../constants' import { addPluginApiDecorator, decoratePluginApi, @@ -83,13 +91,30 @@ export const pEmitter = new TypedEventEmitter<{ export const pList = new Map() export const pMetadata = new WeakMap() +const { flags: PersistedFlags }: PersistedPluginStates = callBridgeMethodSync( + 'revenge.plugins.states.read', + [], +) ?? { + flags: {}, +} + +pEmitter.on('flagUpdate', plugin => { + PersistedFlags[plugin.manifest.id] = plugin.flags & PersistentPluginFlags + callBridgeMethod('revenge.plugins.states.write', [PersistedFlags]) +}) + /** * Registers a new plugin with the system. + * + * @param manifest The manifest of the plugin. + * @param options The options for the plugin. + * @param defflags The default flags for the plugin. + * @param iflags The internal flags for the plugin. */ export function registerPlugin( manifest: PluginManifest, options: PluginOptions, - flags: number, + defflags: number, iflags: number, ) { // TODO(plugins): verify plugin manifest @@ -127,7 +152,7 @@ export function registerPlugin( dependents: [], handleError: e => handlePluginError(e, plugin), options, - flags, + flags: PersistedFlags[manifest.id] ?? defflags, } pMetadata.set(plugin, meta) @@ -299,7 +324,6 @@ export async function disablePlugin(plugin: AnyPlugin) { if (plugin.status && !(plugin.status & Status.Stopping)) await stopPlugin(plugin) - // TODO(plugins): write to storage plugin.flags &= ~Flag.Enabled pEmitter.emit('disabled', plugin) } @@ -317,9 +341,7 @@ export async function enablePlugin(plugin: AnyPlugin) { }), ) - // TODO(plugins): write to storage plugin.flags |= Flag.Enabled - pEmitter.emit('enabled', plugin) } @@ -557,3 +579,18 @@ async function cleanupPlugin(plugin: AnyPlugin, meta: InternalPluginMeta) { await Promise.all(proms) } + +declare module '@revenge-mod/modules/native' { + interface Methods { + 'revenge.plugins.states.read': [[], PersistedPluginStates | null] + 'revenge.plugins.states.write': [[statuses: PersistedPluginFlags], void] + } +} + +interface PersistedPluginStates { + flags: PersistedPluginFlags +} + +interface PersistedPluginFlags { + [id: PluginManifest['id']]: number +} diff --git a/lib/plugins/src/apis/modules.ts b/lib/plugins/src/apis/modules.ts index 5a64ee2..9d9763c 100644 --- a/lib/plugins/src/apis/modules.ts +++ b/lib/plugins/src/apis/modules.ts @@ -3,6 +3,7 @@ import * as PluginApiModulesFindersFilters from '@revenge-mod/modules/finders/fi import * as PluginApiModulesMetroSubscriptions from '@revenge-mod/modules/metro/subscriptions' import * as PluginApiModulesMetroUtils from '@revenge-mod/modules/metro/utils' import * as PluginApiModulesNative_ from '@revenge-mod/modules/native' +import * as PluginApiModulesNativeFileSystem from '@revenge-mod/modules/native/fs' import { spreadDescriptors } from '.' export interface PluginApiModules { @@ -11,7 +12,9 @@ export interface PluginApiModules { native: PluginApiModulesNative } -export type PluginApiModulesNative = typeof PluginApiModulesNative_ +export type PluginApiModulesNative = typeof PluginApiModulesNative_ & { + fs: typeof PluginApiModulesNativeFileSystem +} export type PluginApiModulesMetro = // biome-ignore format: Don't @@ -30,5 +33,7 @@ export const modules: PluginApiModules = { PluginApiModulesMetroUtils, spreadDescriptors(PluginApiModulesMetroSubscriptions, {}), ), - native: PluginApiModulesNative_, + native: spreadDescriptors(PluginApiModulesNative_, { + fs: PluginApiModulesNativeFileSystem, + }), } diff --git a/lib/plugins/src/constants.ts b/lib/plugins/src/constants.ts index 7a2129c..104e107 100644 --- a/lib/plugins/src/constants.ts +++ b/lib/plugins/src/constants.ts @@ -21,6 +21,11 @@ export const PluginFlags = { EnabledLate: 1 << 3, } +/** + * A bitmask of {@link PluginFlags} that are persisted to storage. + */ +export const PersistentPluginFlags = PluginFlags.Enabled + /** * The plugin status. */ diff --git a/src/init.ts b/src/init.ts index 6af55e1..99aa28a 100644 --- a/src/init.ts +++ b/src/init.ts @@ -3,7 +3,7 @@ import '@revenge-mod/react/init' import '@revenge-mod/storage/init' import { onRunApplication } from '@revenge-mod/react/native' -import { onError } from './preinit' +import { onError } from '~preinit' const unsub = onRunApplication(() => { unsub() diff --git a/src/plugins/start/settings/constants.ts b/src/plugins/start/settings/constants.ts index 944c1cb..dfc8c10 100644 --- a/src/plugins/start/settings/constants.ts +++ b/src/plugins/start/settings/constants.ts @@ -15,6 +15,7 @@ export const Setting = { ReactVersion: 'ReactVersion', ReactNativeVersion: 'ReactNativeVersion', HermesVersion: 'HermesVersion', + LoaderVersion: 'LoaderVersion', } as const export const RouteNames = { diff --git a/src/plugins/start/settings/definitions/LoaderVersionSetting.tsx b/src/plugins/start/settings/definitions/LoaderVersionSetting.tsx new file mode 100644 index 0000000..7f9d23e --- /dev/null +++ b/src/plugins/start/settings/definitions/LoaderVersionSetting.tsx @@ -0,0 +1,19 @@ +import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' +import { getBridgeInfo } from '@revenge-mod/modules/native' +import { Setting } from '../constants' +import { CopyableSetting } from './shared' +import type { SettingsItem } from '@revenge-mod/discord/modules/settings' + +const bridgeInfo = getBridgeInfo() + +const LoaderVersionSetting: SettingsItem = CopyableSetting( + { + parent: Setting.Revenge, + IconComponent: () => , + title: () => 'Loader', + usePredicate: () => Boolean(bridgeInfo), + }, + () => `${bridgeInfo!.name} (${bridgeInfo!.version})`, +) + +export default LoaderVersionSetting diff --git a/src/plugins/start/settings/register.tsx b/src/plugins/start/settings/register.tsx index bc48476..61e023d 100644 --- a/src/plugins/start/settings/register.tsx +++ b/src/plugins/start/settings/register.tsx @@ -4,6 +4,7 @@ import { } from '@revenge-mod/discord/modules/settings' import { Setting } from './constants' import HermesVersionSetting from './definitions/HermesVersionSetting' +import LoaderVersionSetting from './definitions/LoaderVersionSetting' import ReactNativeVersionSetting from './definitions/ReactNativeVersionSetting' import ReactVersionSetting from './definitions/ReactVersionSetting' import ReloadSetting from './definitions/ReloadSetting' @@ -25,6 +26,7 @@ registerSettingsItems({ [Setting.ReactVersion]: ReactVersionSetting, [Setting.ReactNativeVersion]: ReactNativeVersionSetting, [Setting.HermesVersion]: HermesVersionSetting, + [Setting.LoaderVersion]: LoaderVersionSetting, }) registerSettingsSection('REVENGE', { diff --git a/src/plugins/start/settings/screens/RevengeSettingScreen.tsx b/src/plugins/start/settings/screens/RevengeSettingScreen.tsx index 26f9425..44dc0f9 100644 --- a/src/plugins/start/settings/screens/RevengeSettingScreen.tsx +++ b/src/plugins/start/settings/screens/RevengeSettingScreen.tsx @@ -1,4 +1,3 @@ -import { Design } from '@revenge-mod/discord/design' import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer' import { Setting } from '../constants' @@ -10,24 +9,11 @@ export default function RevengeSettingScreen() { label: 'Revenge', settings: [ Setting.RevengeVersion, + Setting.LoaderVersion, Setting.RevengeDiscord, Setting.RevengeSourceRepository, Setting.RevengeLicense, ], - subLabel: ( - <> - - You are using the next version of Revenge! - - - This version is experimental and may be - unstable. - - - ), }, { label: 'Versions', diff --git a/src/preinit.ts b/src/preinit.ts index 3dcff95..b11daa4 100644 --- a/src/preinit.ts +++ b/src/preinit.ts @@ -5,8 +5,9 @@ import { onModuleFirstRequired, onModuleInitialized, } from '@revenge-mod/modules/metro/subscriptions' +import { callBridgeMethod } from '@revenge-mod/modules/native' import { getErrorStack } from '@revenge-mod/utils/error' -import { BuildEnvironment, FullVersion } from './constants' +import { BuildEnvironment, FullVersion } from '~constants' const IndexModuleId = 0 @@ -49,19 +50,19 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() { } }) -export function onError(e: unknown) { - // TODO(init): Move to use native provided alert function, which will accept a string stack trace - // Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further - const { ClientInfoModule, DeviceModule } = - require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native') +export function onError(error: unknown) { + const stack = getErrorStack(error) ?? String(error) - const Client = ClientInfoModule.getConstants() - const Device = DeviceModule.getConstants() + callBridgeMethod('revenge.alertError', [ + stack, + `${FullVersion} (${BuildEnvironment})`, + ]) - alert( - `Failed to load Revenge (${FullVersion} (${BuildEnvironment}))\n` + - `Discord: ${Client.Version} (${Client.Build})\n` + - `Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` + - getErrorStack(e), - ) + nativeLoggingHook(`\u001b[31m${stack}\u001b[0m`, 2) +} + +declare module '@revenge-mod/modules/native' { + export interface Methods { + 'revenge.alertError': [[error: string, version: string], void] + } }