2025-03-30 00:28:06 +07:00
|
|
|
import {
|
2025-06-03 16:08:37 +07:00
|
|
|
createPatchedFunctionProxy,
|
2025-03-30 00:28:06 +07:00
|
|
|
patchedFunctionProxyHandler,
|
|
|
|
|
patchedFunctionProxyStates,
|
|
|
|
|
unproxy,
|
2025-03-30 01:39:44 +07:00
|
|
|
} from '../_internal'
|
2025-06-03 16:24:58 +07:00
|
|
|
import type {
|
|
|
|
|
FunctionProxyState,
|
|
|
|
|
InsteadHookNode,
|
|
|
|
|
PatchedFunctionProxyState,
|
|
|
|
|
} from '../_internal'
|
2025-06-03 16:08:37 +07:00
|
|
|
import type {
|
|
|
|
|
AbstractNewable,
|
|
|
|
|
Callable,
|
|
|
|
|
FiniteDomain,
|
2025-10-24 21:27:07 +07:00
|
|
|
HookOptions,
|
2025-06-03 16:08:37 +07:00
|
|
|
InsteadHook,
|
|
|
|
|
UnknownFunction,
|
2025-07-13 15:47:45 +07:00
|
|
|
UnpatchFunction,
|
2025-06-03 16:08:37 +07:00
|
|
|
} from '../types'
|
|
|
|
|
|
2025-03-30 00:28:06 +07:00
|
|
|
const insteadHookProxyHandler = {
|
|
|
|
|
...patchedFunctionProxyHandler,
|
2025-06-03 16:08:37 +07:00
|
|
|
apply<T extends Callable>(
|
|
|
|
|
hookNode: InsteadHookNode<T>,
|
|
|
|
|
receiver: ThisParameterType<T>,
|
|
|
|
|
args: Parameters<T>,
|
|
|
|
|
) {
|
2025-06-09 22:46:17 +07:00
|
|
|
const { next, hook } = hookNode
|
|
|
|
|
return Reflect.apply(hook!, receiver, [
|
2025-06-03 16:08:37 +07:00
|
|
|
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,
|
|
|
|
|
) {
|
2025-06-09 22:46:17 +07:00
|
|
|
const { next, hook } = hookNode
|
2025-06-03 16:08:37 +07:00
|
|
|
return Reflect.construct(
|
2025-06-09 22:46:17 +07:00
|
|
|
hook!,
|
2025-06-03 16:08:37 +07:00
|
|
|
[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
|
2025-06-09 22:46:17 +07:00
|
|
|
hookNode.hook = undefined
|
2025-03-30 00:28:06 +07:00
|
|
|
|
|
|
|
|
const { prev, next } = hookNode
|
|
|
|
|
if (prev === undefined) {
|
|
|
|
|
state.instead = next
|
|
|
|
|
if (next === undefined) {
|
2025-06-03 16:08:37 +07:00
|
|
|
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.
|
|
|
|
|
*/
|
2025-06-03 16:08:37 +07:00
|
|
|
export function instead<
|
|
|
|
|
Parent extends Record<Key, UnknownFunction>,
|
2025-06-10 11:05:28 +07:00
|
|
|
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,
|
2025-07-13 15:47:45 +07:00
|
|
|
): 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
|
|
|
|
|
|
2025-06-09 23:28:25 +07:00
|
|
|
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
|
2025-06-03 16:08:37 +07:00
|
|
|
state = createPatchedFunctionProxy(
|
|
|
|
|
target,
|
|
|
|
|
parent,
|
|
|
|
|
key,
|
|
|
|
|
undefined,
|
|
|
|
|
hookNode,
|
|
|
|
|
undefined,
|
|
|
|
|
)
|
2025-03-30 00:28:06 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return unpatchInstead.bind(undefined, state, hookNode)
|
|
|
|
|
}
|