revenge-bundle-next/lib/patcher/src/hooks/instead.ts

178 lines
4.8 KiB
TypeScript
Raw Normal View History

2025-03-30 00:28:06 +07:00
import {
createPatchedFunctionProxy,
2025-03-30 00:28:06 +07:00
patchedFunctionProxyHandler,
patchedFunctionProxyStates,
unproxy,
} from '../_internal'
2025-06-03 16:24:58 +07:00
import type {
FunctionProxyState,
InsteadHookNode,
PatchedFunctionProxyState,
} from '../_internal'
import type {
AbstractNewable,
Callable,
FiniteDomain,
2025-10-24 21:27:07 +07:00
HookOptions,
InsteadHook,
UnknownFunction,
UnpatchFunction,
} from '../types'
2025-03-30 00:28:06 +07:00
const insteadHookProxyHandler = {
...patchedFunctionProxyHandler,
apply<T extends Callable>(
hookNode: InsteadHookNode<T>,
receiver: ThisParameterType<T>,
args: Parameters<T>,
) {
const { next, hook } = hookNode
return Reflect.apply(hook!, receiver, [
args,
next === undefined ? hookNode.target : next.proxy,
])
2025-03-30 00:28:06 +07:00
},
construct<T extends AbstractNewable<never, object>>(
hookNode: InsteadHookNode<T>,
args: ConstructorParameters<T>,
ctor: AbstractNewable,
) {
const { next, hook } = hookNode
return Reflect.construct(
hook!,
[args, next === undefined ? hookNode.target : next.proxy],
ctor,
)
2025-03-30 00:28:06 +07:00
},
} as const satisfies Required<ProxyHandler<FunctionProxyState>>
function unpatchInstead<T extends UnknownFunction>(
state: PatchedFunctionProxyState<PropertyKey, T>,
hookNode: InsteadHookNode<T>,
) {
if (hookNode.unpatched) return
hookNode.unpatched = true
hookNode.hook = undefined
2025-03-30 00:28:06 +07:00
const { prev, next } = hookNode
if (prev === undefined) {
state.instead = next
if (next === undefined) {
if (state.before === undefined && state.after === undefined)
unproxy(state)
2025-03-30 00:28:06 +07:00
return
}
} else {
prev.next = next
hookNode.prev = undefined
if (next === undefined) return
}
next.prev = prev
hookNode.next = undefined
}
2025-07-10 21:24:04 +07:00
/**
* Instead hooks allow you to completely replace the original function with a new one, while still being able to call the original function if needed.
*
* ```js
* import { instead } from '@revenge-mod/patcher'
*
* const obj = {
* method: (a) => {
* console.log('Original method called with:', a)
* return 'original result'
* }
* }
*
* instead(obj, 'method', ([a], original) => {
* console.log('Instead method called with:', a)
* // Call the original function if needed
* const originalResult = original('modified')
* console.log('Original method was called')
*
* // Return a new value
* return 'new value'
* })
*
* console.log(obj.method('test')) // 'new value'
* // CONSOLE OUTPUT:
* // Instead method called with: test
* // Original method called with: modified
* // Original method was called
* // new value
* ```
*
* @param parent The parent object containing the method to patch.
* @param key The key of the method to patch.
* @param hook The hook function to execute instead of the original method.
2025-10-24 21:27:07 +07:00
* @param options Optional configuration including priority.
2025-07-10 21:24:04 +07:00
*
* @return A function to unpatch.
*/
export function instead<
Parent extends Record<Key, UnknownFunction>,
Key extends keyof Parent,
2025-10-24 21:27:07 +07:00
>(
parent: Parent,
key: Key,
hook: InsteadHook<Parent[Key]>,
options?: HookOptions,
): UnpatchFunction
2025-03-30 00:28:06 +07:00
export function instead<Key extends PropertyKey, Value extends UnknownFunction>(
parent: Record<Key, Value>,
key: FiniteDomain<Key>,
hook: InsteadHook<Value>,
2025-10-24 21:27:07 +07:00
options?: HookOptions,
): UnpatchFunction {
2025-03-30 00:28:06 +07:00
const target = parent[key]
2025-10-24 21:27:07 +07:00
const priority = options?.priority ?? 0
2025-03-30 00:28:06 +07:00
let state = patchedFunctionProxyStates.get(target)
// biome-ignore lint/complexity/useArrowFunction: We need a function that has a constructor
const hookNode = function () {}
// The handler makes the proxy behave as target.
hookNode.proxy = new Proxy<any>(hookNode, insteadHookProxyHandler) as Value
hookNode.target = target
hookNode.unpatched = false
hookNode.hook = hook
2025-10-24 21:27:07 +07:00
hookNode.priority = priority
2025-03-30 00:28:06 +07:00
hookNode.prev = undefined
if (state?.parent === parent && state.key === key) {
2025-10-24 21:27:07 +07:00
let current = state.instead
let prev: InsteadHookNode<Value> | undefined
while (current && current.priority > priority) {
prev = current
current = current.next
}
hookNode.next = current
if (prev) {
prev.next = hookNode
hookNode.prev = prev
} else {
state.instead = hookNode
}
if (current) {
current.prev = hookNode
}
2025-03-30 00:28:06 +07:00
} else {
hookNode.next = undefined
state = createPatchedFunctionProxy(
target,
parent,
key,
undefined,
hookNode,
undefined,
)
2025-03-30 00:28:06 +07:00
}
return unpatchInstead.bind(undefined, state, hookNode)
}