From a1db2dbb2cc74cfdfa0609f7f5ac35c6d087c59c Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Mon, 2 Jun 2025 15:56:07 +0700 Subject: [PATCH] feat(lib/assets)!: modify cache struct, change how overrides work, cleanup APIs --- lib/assets/src/_internal.ts | 4 +- lib/assets/src/caches.ts | 60 +++++++ lib/assets/src/index.ts | 164 ++++++++++-------- lib/assets/src/preinit.ts | 22 +-- .../screens/AssetBrowserSettingScreen.tsx | 6 +- .../screens/RevengeDeveloperSettingScreen.tsx | 2 +- .../screens/RevengePluginsSettingScreen.tsx | 4 +- 7 files changed, 171 insertions(+), 91 deletions(-) create mode 100644 lib/assets/src/caches.ts diff --git a/lib/assets/src/_internal.ts b/lib/assets/src/_internal.ts index f4fdaaf..f1a29e4 100644 --- a/lib/assets/src/_internal.ts +++ b/lib/assets/src/_internal.ts @@ -1,6 +1,6 @@ import type { Metro } from '@revenge-mod/modules/types' import type { Asset, AssetId } from './types' -export const _assets = new Map]>() -export const _overrides = new Map() +export const _customs = new Set() +export const _overrides = new WeakMap() export const _metas = new WeakMap() diff --git a/lib/assets/src/caches.ts b/lib/assets/src/caches.ts new file mode 100644 index 0000000..9e62747 --- /dev/null +++ b/lib/assets/src/caches.ts @@ -0,0 +1,60 @@ +// 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 { debounce } from '@revenge-mod/utils/callbacks' +import { createLogger } from '@revenge-mod/utils/logger' +import { mergeDeep } from '@revenge-mod/utils/objects' +import { proxify } from '@revenge-mod/utils/proxy' + +import type { Metro } from '@revenge-mod/modules/types' +import type { Asset } from './types' + +const logger = createLogger('revenge.assets.caches') + +const Version = 1 +const CurrentKey = `${Version}.${ClientInfoModule.Build}` + +const CacheStorage = getStorage('revenge/assets', { + default: { k: CurrentKey, c: {} }, + directory: 'cache', +}) + +export const cache: Cache['c'] = proxify( + () => { + if (CacheStorage.cache) return CacheStorage.cache.c + }, + { hint: 'object' }, +)! + +// biome-ignore lint/complexity/useArrowFunction: Arrow functions are not supported +CacheStorage.get().then(async function (cache) { + // TODO(lib/assets/caches): This loads way too late and requires native interop to load earlier + logger.log(`Loaded cache version: ${cache.k}`) + if (cache.k !== CurrentKey) { + await CacheStorage.delete() + await CacheStorage.set({ k: CurrentKey, c: {} }) + } +}) + +export interface Cache { + k: string + c: { + [key: Asset['name']]: { + [key: Asset['type']]: Metro.ModuleID + } + } +} + +// const save = debounce(() => CacheStorage.set({}), 1000) + +export async function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) { + // Merge directly into the cache, only debouncing actual writes + mergeDeep(CacheStorage.cache!.c, { + [asset.name]: { + [asset.type]: moduleId, + }, + }) + + // Commented out because caching currently does not work properly because it is loaded too late + // await save() +} diff --git a/lib/assets/src/index.ts b/lib/assets/src/index.ts index 0c4fc3d..4d5986b 100644 --- a/lib/assets/src/index.ts +++ b/lib/assets/src/index.ts @@ -1,8 +1,10 @@ import { ReactNative } from '@revenge-mod/react' -import { _assets, _metas, _overrides } from './_internal' +import { _customs, _metas, _overrides } from './_internal' +import { cache } from './caches' import { AssetRegistry } from './preinit' +import type { Metro } from '@revenge-mod/modules/types' import type { Asset, AssetId, RegisterableAsset } from './types' export { AssetRegistry, AssetRegistryModuleId } from './preinit' @@ -10,63 +12,6 @@ export { AssetRegistry, AssetRegistryModuleId } from './preinit' // iOS cannot display SVGs let _preferredType: Asset['type'] = ReactNative.Platform.OS === 'ios' ? 'png' : 'svg' -/** - * Yields all registered assets, including ones with same name but different types, custom assets, and overriden assets. - */ -export function* getAllAssets() { - for (const name of _assets.keys()) for (const asset of Object.values(_assets.get(name)![1])) yield asset -} - -/** - * Get an asset by its ID. **This may throw an error if the asset-registry has not been initialized.** - * - * @param id The asset ID. - */ -export function getAssetById(id: AssetId): Asset | undefined { - return AssetRegistry.getAssetByID(id) -} - -/** - * Get the ID of an asset. - * - * @param asset The asset to get the ID for. - */ -export function getAssetId(asset: Asset) { - return _metas.get(asset)?.[0] -} - -/** - * Get the module ID that registered the asset. Returns `-1` if the asset is a custom asset. - * - * @param asset The asset to get the module ID for. - */ -export function getAssetModuleId(asset: Asset) { - return _metas.get(asset)?.[1] -} - -/** - * Get an asset by its name. - * If more than one asset is registered with the same name, this will return the one with the preferred type, or the first registered one. - * - * @param name The asset name. - */ -export function getAssetByName(name: string): Asset | undefined { - const reg = _assets.get(name) - if (!reg) return - - return _overrides.get(name) ?? reg[1][_preferredType] ?? reg[0] -} - -/** - * Gets all assets matching the name. - * - * @param name The asset name. - * @returns A record keyed by the type of the asset, with the value being the asset itself. - */ -export function getAssetsByName(name: string): Record | undefined { - return _assets.get(name)?.[1] -} - /** * Set the preferred asset type. This is used to determine which asset to use when multiple types are available. * @@ -76,31 +21,110 @@ export function setPreferredAssetType(type: Asset['type']) { _preferredType = type } +/** + * Yields all registered assets, including ones with same name but different types, custom assets, and overriden assets. + */ +export function* getAssets(): Generator { + for (const name of Object.keys(cache!)) + for (const mid of Object.values(cache[name])) yield AssetRegistry.getAssetByID(__r(mid)) as Asset + + for (const asset of _customs) yield asset +} + +/** + * Get an asset by its name. + * If more than one asset is registered with the same name, this will return the one with the preferred type, or the first registered one. + * + * @param name The asset name. + */ +export function getAssetByName(name: string): Asset | undefined { + const id = getAssetIdByName(name) + if (id !== undefined) return AssetRegistry.getAssetByID(id) +} + +/** + * Gets all assets matching the name. + * + * @param name The asset name. + * @returns A record keyed by the type of the asset, with the value being the asset itself. + */ +export function getAssetsByName(name: string): Record | undefined { + const reg = cache[name] + if (!reg) return + + return Object.entries(reg).reduce( + (acc, [type, mid]) => { + acc[type as Asset['type']] = AssetRegistry.getAssetByID(__r(mid))! + return acc + }, + {} as Record, + ) +} + +/** + * Get the ID of an asset. + * + * @param asset The asset to get the ID for. + */ +export function getAssetId(asset: Asset): AssetId | undefined { + return _metas.get(asset)?.[0] +} + +/** + * Get an asset ID by its name. + * If more than one asset is registered with the same name, this will return the one with the preferred type, or the first registered one. + * + * @param name The asset name. + */ +export function getAssetIdByName(name: string): AssetId | undefined { + const reg = cache[name] + if (!reg) return + + const mid = reg[_preferredType] ?? reg[Object.keys(reg)[0]!] + if (mid !== undefined) return __r(mid) +} + +/** + * Get the module ID that registered the asset. Returns `-1` if the asset is a custom asset. + * + * @param asset The asset to get the module ID for. + */ +export function getAssetModuleId(asset: Asset): Metro.ModuleID | -1 | undefined { + return _metas.get(asset)?.[1] +} + /** * Register an asset with the given name. - * If an asset with the same name already exists, it will be overridden if `override` is set to true. * * @param asset The asset to register. - * @param override Whether to override the existing asset if one exists. `false` by default. - * @throws An error if an asset with the same name and type already exists and `override` is not set to true. + * @returns The asset ID. */ -export function registerAsset(asset: RegisterableAsset, override?: boolean) { - const { name, type } = asset +export function registerAsset(asset: RegisterableAsset): AssetId { + if (cache[asset.name]?.[asset.type] !== undefined) + throw new Error(`Asset with name ${asset.name} and type ${asset.type} already exists!`) - if (override) _overrides.set(name, asset) - // Make sure to not override the asset if it already exists, unless explicitly set to override - else if (_assets.get(name)?.[1]?.[type]) throw new Error(`Asset "${name}" with type "${type}" already exists`) + _customs.add(asset) // @ts-expect-error return AssetRegistry.registerAsset(asset) } +/** + * Override an asset with a custom asset. + * + * @param asset The asset to override. + * @param override The custom asset to override with. + */ +export function addAssetOverride(asset: Asset, override: Asset) { + _overrides.set(asset, override) +} + /** * Remove an asset override. * - * @param name The name of the asset to remove the override for. - * @returns Whether the override was removed. + * @param asset The asset to remove the override for. + * @returns The asset that was removed. */ -export function removeAssetOverride(name: string) { - return _overrides.delete(name) +export function removeAssetOverride(asset: Asset) { + return _overrides.delete(asset) } diff --git a/lib/assets/src/preinit.ts b/lib/assets/src/preinit.ts index 231862d..8bed941 100644 --- a/lib/assets/src/preinit.ts +++ b/lib/assets/src/preinit.ts @@ -3,7 +3,8 @@ import { byName, byProps } from '@revenge-mod/modules/finders/filters' import { waitForModules } from '@revenge-mod/modules/finders/wait' import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' -import { _assets, _metas, _overrides } from './_internal' +import { _metas, _overrides } from './_internal' +import { cacheAsset } from './caches' import type { ReactNative } from '@revenge-mod/react/types' import type { Asset, ReactNativeAsset } from './types' @@ -43,18 +44,13 @@ const unsubAR = waitForModules(byProps('registerAsse exports.registerAsset = (asset: Asset) => { const result = orig(asset as ReactNativeAsset) - const { name, type } = asset - - let reg = _assets.get(name) - if (!reg) { - reg = [asset, {}] - _assets.set(name, reg) + // Cache packager assets only + if ((asset as ReactNativeAsset).__packager_asset) { + cacheAsset(asset, _initing) + // In-memory cache + _metas.set(asset, [result, _initing]) } - // In-memory cache - reg[1][type] = asset - _metas.set(asset, [result, _initing]) - return result } }) @@ -68,7 +64,7 @@ const unsubRAS = waitForModules( unsubRAS() // Custom assets - // Why do we need to do this? Because RN/Dicord (unsure which) will attempt to resolve the asset via its path, which we don't provide via custom assets. + // Why do we need to do this? Because RN/Discord (unsure which) will attempt to resolve the asset via its path, which we don't provide via custom assets. // This will result in a crash, because it tries to read the path and run checks on it, but the path is undefined. // @ts-expect-error rAS.addCustomSourceTransformer(({ asset }) => { @@ -78,6 +74,6 @@ const unsubRAS = waitForModules( // Asset overrides // @ts-expect-error - rAS.addCustomSourceTransformer(({ asset }) => _overrides.get(asset.name)) + rAS.addCustomSourceTransformer(({ asset }) => _overrides.get(asset)) }, ) diff --git a/src/plugins/init/developer-settings/screens/AssetBrowserSettingScreen.tsx b/src/plugins/init/developer-settings/screens/AssetBrowserSettingScreen.tsx index 9bf4dec..e485e2e 100644 --- a/src/plugins/init/developer-settings/screens/AssetBrowserSettingScreen.tsx +++ b/src/plugins/init/developer-settings/screens/AssetBrowserSettingScreen.tsx @@ -8,7 +8,7 @@ import Page from '~/components/Page' import SearchInput from '~/components/SearchInput' import TableRowAssetIcon from '~/components/TableRowAssetIcon' -import { getAllAssets, getAssetId, getAssetModuleId } from '@revenge-mod/assets' +import { getAssetId, getAssetModuleId, getAssets } from '@revenge-mod/assets' import type { Asset, AssetId } from '@revenge-mod/assets/types' import type { Metro } from '@revenge-mod/modules/types' @@ -28,8 +28,8 @@ const UndisplayableFallback = { export default function AssetBrowserSettingScreen() { const [search, setSearch] = React.useState('') const assets = React.useMemo( - () => [...getAllAssets()].map(asset => [getAssetId(asset)!, getAssetModuleId(asset)!, asset] as const), - [getAllAssets], + () => [...getAssets()].map(asset => [getAssetId(asset)!, getAssetModuleId(asset)!, asset] as const), + [getAssets], ) const filteredAssets = React.useMemo( diff --git a/src/plugins/init/developer-settings/screens/RevengeDeveloperSettingScreen.tsx b/src/plugins/init/developer-settings/screens/RevengeDeveloperSettingScreen.tsx index 32f7795..9412c0c 100644 --- a/src/plugins/init/developer-settings/screens/RevengeDeveloperSettingScreen.tsx +++ b/src/plugins/init/developer-settings/screens/RevengeDeveloperSettingScreen.tsx @@ -1,7 +1,7 @@ +import { ToastActionCreators } from '@revenge-mod/discord/actions' import { Design } from '@revenge-mod/discord/design' import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer' import { React, ReactNative } from '@revenge-mod/react' -import { ToastActionCreators } from '@revenge-mod/discord/actions' import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord' import { api } from '..' diff --git a/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx b/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx index 859e36f..a002264 100644 --- a/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx @@ -16,7 +16,7 @@ import FormSwitch from '~/components/FormSwitch' import Page from '~/components/Page' import SearchInput from '~/components/SearchInput' -import { getAssetByName, getAssetId } from '@revenge-mod/assets' +import { getAssetIdByName } from '@revenge-mod/assets' import { Tokens } from '@revenge-mod/discord/common' import { PluginFlags } from '@revenge-mod/plugins/constants' import { useReRender } from '@revenge-mod/utils/react' @@ -118,7 +118,7 @@ function InstalledPluginCard({ - + {name}