feat(lib/assets)!: set metadata on asset objects, remove redundant APIs, add new APIs and parameters

This commit is contained in:
PalmDevs 2025-06-07 15:14:28 +07:00
parent 3903fe0a4b
commit e510eea36e
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
7 changed files with 119 additions and 108 deletions

View file

@ -1,12 +1,4 @@
import { createLogger } from '@revenge-mod/utils/logger'
import type { Metro } from '@revenge-mod/modules/types'
import type { Asset, AssetId } from './types'
import type { Asset, CustomAsset } from './types'
export const _logger = createLogger('revenge.assets')
export const _customs = new Set<Asset>()
export const _customs = new Set<CustomAsset>()
export const _overrides = new WeakMap<Asset, Asset>()
export const _metas = new WeakMap<
Asset,
[assetId: AssetId, moduleId: Metro.ModuleID]
>()

View file

@ -3,7 +3,6 @@ import { ClientInfoModule } from '@revenge-mod/discord/native'
import { getStorage } from '@revenge-mod/storage'
import { debounce } from '@revenge-mod/utils/callbacks'
import { mergeDeep } from '@revenge-mod/utils/objects'
import { _logger } from './_internal'
import type { Metro } from '@revenge-mod/modules/types'
import type { Asset } from './types'
@ -18,15 +17,12 @@ const CacheStorage = getStorage<Cache>(`revenge/assets.${Key}`, {
directory: 'cache',
})
_logger.log(`[${performance.now()}] CacheStorage instance created`)
// 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
// 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 (cache_ !== cache) {
_logger.log(`[${performance.now()}] CacheStorage loaded`)
Object.assign(cache, cache_)
CacheStorage.cache = cache
}

View file

@ -1,11 +1,19 @@
import { ReactNative } from '@revenge-mod/react'
import { _customs, _metas, _overrides } from './_internal'
import { _customs, _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'
import { AssetsRegistry } from './preinit'
import type {
Asset,
AssetId,
CustomAsset,
PackagerAsset,
RegisterableAsset,
} from './types'
export { AssetRegistry, AssetRegistryModuleId } from './preinit'
export {
AssetsRegistry as AssetRegistry,
AssetsRegistryModuleId as AssetRegistryModuleId,
} from './preinit'
// iOS cannot display SVGs
let _preferredType: Asset['type'] =
@ -21,25 +29,44 @@ export function setPreferredAssetType(type: Asset['type']) {
}
/**
* Yields all registered assets, including ones with same name but different types, custom assets, and overriden assets.
* Yields all assets, both packager and custom.
*/
export function* getAssets(): Generator<Asset> {
for (const name of Object.keys(cache!))
for (const mid of Object.values(cache[name]))
yield AssetRegistry.getAssetByID(__r(mid)) as Asset
yield* getPackagerAssets()
yield* getCustomAssets()
}
/**
* Yields all registered custom assets.
*/
export function* getCustomAssets(): Generator<CustomAsset> {
for (const asset of _customs) yield asset
}
/**
* Yields all registered packager assets, including ones with same name but different types.
*/
export function* getPackagerAssets(): Generator<PackagerAsset> {
for (const name in cache) {
const reg = cache[name]
for (const type in reg)
yield AssetsRegistry.getAssetByID(__r(reg[type]))
}
}
/**
* 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.
* @param type The preferred asset type, defaults to the current preferred type.
*/
export function getAssetByName(name: string): Asset | undefined {
const id = getAssetIdByName(name)
if (id !== undefined) return AssetRegistry.getAssetByID(id)
export function getAssetByName(
name: string,
type = _preferredType,
): Asset | undefined {
const id = getAssetIdByName(name, type)
if (id !== undefined) return AssetsRegistry.getAssetByID(id)
}
/**
@ -56,47 +83,31 @@ export function getAssetsByName(
return Object.entries(reg).reduce(
(acc, [type, mid]) => {
acc[type as Asset['type']] = AssetRegistry.getAssetByID(__r(mid))!
acc[type as Asset['type']] = AssetsRegistry.getAssetByID(__r(mid))!
return acc
},
{} as Record<Asset['type'], Asset>,
)
}
/**
* 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.
* @param type The preferred asset type, defaults to the current preferred type.
*/
export function getAssetIdByName(name: string): AssetId | undefined {
export function getAssetIdByName(
name: string,
type = _preferredType,
): AssetId | undefined {
const reg = cache[name]
if (!reg) return
const mid = reg[_preferredType] ?? reg[Object.keys(reg)[0]!]
const mid = reg[type] ?? 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.
*
@ -109,10 +120,10 @@ export function registerAsset(asset: RegisterableAsset): AssetId {
`Asset with name ${asset.name} and type ${asset.type} already exists!`,
)
_customs.add(asset)
_customs.add(asset as CustomAsset)
// @ts-expect-error
return AssetRegistry.registerAsset(asset)
return AssetsRegistry.registerAsset(asset)
}
/**

View file

@ -2,20 +2,20 @@ import { _initing, _uninits } from '@revenge-mod/modules/_/metro'
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 { _logger, _metas, _overrides } from './_internal'
import { _overrides } from './_internal'
import { cache, cacheAsset } from './caches'
import type { ReactNative } from '@revenge-mod/react/types'
import type { Asset, ReactNativeAsset } from './types'
import type { Asset, PackagerAsset } from './types'
export let AssetRegistry: ReactNative.AssetsRegistry
export let AssetRegistryModuleId: number
export let AssetsRegistry: ReactNative.AssetsRegistry
export let AssetsRegistryModuleId: number
// Tracking/caching assets
const unsubAR = waitForModules(
byProps<ReactNative.AssetsRegistry>('registerAsset'),
(exports, id) => {
AssetRegistryModuleId = id
AssetRegistry = exports as ReactNative.AssetsRegistry
AssetsRegistryModuleId = id
AssetsRegistry = exports as ReactNative.AssetsRegistry
// There are two matching exports. One is the original, and one is a re-export.
// The original asset-registry is simply required by the re-exported one with no changes.
@ -25,8 +25,6 @@ const unsubAR = waitForModules(
if (getModuleDependencies(id)!.length) {
unsubAR()
_logger.log(`[${performance.now()}] Registry found: ${id}`)
// Make sure we don't try to recache assets if it is already cached.
let uncached = true
for (const _ in cache) {
@ -35,8 +33,6 @@ const unsubAR = waitForModules(
}
if (uncached) {
_logger.log('Caching assets...')
// More fragile way, but also more performant:
// There is exactly one asset before the reexported asset registry :/
const firstAssetModuleId = id - 1
@ -54,16 +50,15 @@ const unsubAR = waitForModules(
const orig = exports.registerAsset
exports.registerAsset = (asset: Asset) => {
const result = orig(asset as ReactNativeAsset)
const result = orig(asset as PackagerAsset)
// Cache packager assets only
if ((asset as ReactNativeAsset).__packager_asset) {
cacheAsset(asset, _initing)
// In-memory cache
_metas.set(asset, [result, _initing])
// Cache and set moduleId for packager assets only
if ((asset as PackagerAsset).__packager_asset) {
asset.moduleId = _initing!
cacheAsset(asset, _initing!)
}
return result
return (asset.id = result)
}
},
)
@ -72,7 +67,7 @@ const unsubAR = waitForModules(
const unsubRAS = waitForModules(
byName<{
addCustomSourceTransformer: (
transformer: (arg: { asset: Asset }) => ReactNativeAsset,
transformer: (arg: { asset: Asset }) => PackagerAsset,
) => void
}>('resolveAssetSource'),
rAS => {

View file

@ -1,12 +1,25 @@
import type { Metro } from '@revenge-mod/modules/types'
import type { ReactNative } from '@revenge-mod/react/types'
export type Asset = ReactNativeAsset | RevengeAsset
export type Asset = PackagerAsset | CustomAsset
export type AssetId = number
export type ReactNativeAsset = ReactNative.AssetsRegistry.PackagerAsset
export interface RevengeAsset
extends Pick<ReactNativeAsset, 'name' | 'width' | 'height' | 'type'> {
export type PackagerAsset = ReactNative.AssetsRegistry.PackagerAsset
export interface CustomAsset
extends Pick<PackagerAsset, 'name' | 'width' | 'height' | 'type' | 'id'> {
uri: string
moduleId?: undefined
}
export type RegisterableAsset = Omit<RevengeAsset, 'id'>
export type RegisterableAsset = Omit<CustomAsset, 'id'>
declare module '@revenge-mod/react/types' {
export namespace ReactNative {
export namespace AssetsRegistry {
export interface PackagerAsset {
id: AssetId
moduleId: Metro.ModuleID
}
}
}
}

View file

@ -1,4 +1,4 @@
import { getAssetByName, getAssetModuleId } from '@revenge-mod/assets'
import { getAssetByName } from '@revenge-mod/assets'
import { TokensModuleId } from '@revenge-mod/discord/common'
import { byDependencies, byProps } from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
@ -45,7 +45,7 @@ export function lookupGeneratedIconComponent<N extends string>(
const mids = []
for (const name of assetNames) {
const mid = getAssetModuleId(getAssetByName(name)!)
const mid = getAssetByName(name)?.moduleId
if (!mid) return
mids.push(mid)
@ -53,7 +53,7 @@ export function lookupGeneratedIconComponent<N extends string>(
filter = [...MultiIconComponentFilterBase, ...mids, 2]
} else {
const mid = getAssetModuleId(getAssetByName(compName)!)
const mid = getAssetByName(compName)?.moduleId
if (!mid) return
IconComponentFilter[4] = mid

View file

@ -1,4 +1,4 @@
import { getAssetId, getAssetModuleId, getAssets } from '@revenge-mod/assets'
import { getAssets } from '@revenge-mod/assets'
import { AlertActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
@ -24,24 +24,13 @@ const UndisplayableFallback = {
export default function AssetBrowserSettingScreen() {
const [search, setSearch] = React.useState('')
const assets = React.useMemo(
() =>
[...getAssets()].map(
asset =>
[
getAssetId(asset)!,
getAssetModuleId(asset)!,
asset,
] as const,
),
[getAssets],
)
const assets = React.useMemo(() => [...getAssets()], [getAssets])
const filteredAssets = React.useMemo(
() =>
!search.length
? assets
: assets.filter(([, , asset]) =>
: assets.filter(asset =>
asset.name.toLowerCase().includes(search.toLowerCase()),
),
[assets, search],
@ -53,13 +42,17 @@ export default function AssetBrowserSettingScreen() {
<FlashList.FlashList
data={filteredAssets}
estimatedItemSize={80}
keyExtractor={([id]) => id.toString()}
renderItem={({ item: [id, moduleId, asset], index }) => (
keyExtractor={asset =>
asset.id
? asset.id.toString()
: `${asset.name}.${asset.type}`
}
renderItem={({ item: asset, index }) => (
<AssetDisplay
asset={asset}
end={index === assets.length - 1}
id={id}
moduleId={moduleId}
end={index === filteredAssets.length - 1}
id={asset.id}
moduleId={asset.moduleId}
start={!index}
/>
)}
@ -76,7 +69,7 @@ function AssetDisplay({
end,
}: {
id: AssetId
moduleId: Metro.ModuleID
moduleId?: Metro.ModuleID
asset: Asset
start?: boolean
end?: boolean
@ -85,7 +78,7 @@ function AssetDisplay({
const metadata = [
['ID', id.toString()],
['Type', asset.type],
['Module ID', moduleId.toString()],
['Module ID', moduleId?.toString()],
] as const
return (
@ -111,7 +104,7 @@ function AssetDisplay({
onPress={() => openAssetDisplayAlert(asset, id, metadata)}
start={start}
subLabel={metadata
.map(([name, value]) => `${name}: ${value}`)
.map(([name, value]) => `${name}: ${value ?? 'N/A'}`)
.join(' • ')}
variant={isDisplayable ? 'default' : 'danger'}
/>
@ -121,7 +114,9 @@ function AssetDisplay({
function openAssetDisplayAlert(
asset: Asset,
id: AssetId,
metadata: Readonly<Readonly<[string, string]>[]> | [string, string][],
metadata:
| Readonly<Readonly<[string, string | undefined]>[]>
| [string, string | undefined][],
) {
const isDisplayable = Displayable.has(asset.type)
@ -137,14 +132,23 @@ function openAssetDisplayAlert(
<PreviewUnavailable type={asset.type} />
)}
<TableRowGroup>
{metadata.map(([name, value]) => (
<TableRow
key={id}
label={name}
onPress={() => Clipboard.setString(value)}
subLabel={value}
/>
))}
{metadata.map(([name, value]) =>
value ? (
<TableRow
key={id}
label={name}
onPress={() => Clipboard.setString(value)}
subLabel={value}
/>
) : (
<TableRow
key={id}
label={name}
subLabel="N/A"
disabled
/>
),
)}
</TableRowGroup>
<Text color="text-danger" variant="text-xs/semibold">
Note: Asset IDs and module IDs are not consistent