341 lines
8.6 KiB
Markdown
341 lines
8.6 KiB
Markdown
# @revenge-mod/patcher
|
|
|
|
This is a JavaScript library that allows to monkey patch functions in a way that is easy to use and understand.
|
|
|
|
It is made by [ryan-0324](https://github.com/ryan-0324) [(source uploaded on Discord)](https://discord.com/channels/1205207689832038522/1284131216156655646/1316076586981789709) and licensed under [CC0](./LICENSE).
|
|
|
|
This source code is polished and adapted to work with Revenge. We do not claim any rights to this library. You may use it granted you follow the license.
|
|
|
|
## How does this work?
|
|
|
|
Think of hooks as separate functions that are called `before`, `after`, or `instead` of the original function.
|
|
You can use them to modify the behavior of the original function, or to add new functionality.
|
|
|
|
### Before hooks
|
|
|
|
Before hooks allow you to modify the arguments passed to the original function, or to perform some action before the original function is called.
|
|
|
|
```js
|
|
import { before } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => console.log('Original method called with:', a)
|
|
}
|
|
|
|
before(obj, 'method', ([a]) => {
|
|
console.log('Before method called with:', a)
|
|
// Modify arguments by returning new array
|
|
return [a + 1]
|
|
})
|
|
|
|
obj.method(2)
|
|
// CONSOLE OUTPUT:
|
|
// Before method called with: 2
|
|
// Original method called with: 3
|
|
```
|
|
|
|
### After hooks
|
|
|
|
After hooks allow you to modify the return value of the original function, or to perform some action after the original function is called.
|
|
|
|
```js
|
|
import { after } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => a * 2
|
|
}
|
|
|
|
after(obj, 'method', (result) => {
|
|
console.log('After method called with result:', result)
|
|
// Modify return value
|
|
return result + 1
|
|
})
|
|
|
|
console.log(obj.method(2)) // 5
|
|
// CONSOLE OUTPUT:
|
|
// After method called with result: 4
|
|
// 5
|
|
```
|
|
|
|
### Instead hooks
|
|
|
|
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
|
|
```
|
|
|
|
### Combining hooks
|
|
|
|
You can combine multiple hooks on the same function.
|
|
|
|
```js
|
|
import { before, after, instead } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => a * 2
|
|
}
|
|
|
|
before(obj, 'method', ([a]) => {
|
|
console.log('Before method called with:', a)
|
|
return [a + 1] // 2 becomes 3
|
|
})
|
|
|
|
instead(obj, 'method', ([a], original) => {
|
|
console.log('Instead method called with:', a)
|
|
const result = original(a) // Call original with modified arg (3)
|
|
console.log('Original method returned:', result)
|
|
return result + 1 // 6 becomes 7
|
|
})
|
|
|
|
after(obj, 'method', (result) => {
|
|
console.log('After method called with result:', result)
|
|
return result + 1 // 7 becomes 8
|
|
})
|
|
|
|
console.log(obj.method(2)) // 8
|
|
// CONSOLE OUTPUT:
|
|
// Before method called with: 2
|
|
// Instead method called with: 3
|
|
// Original method returned: 6
|
|
// After method called with result: 7
|
|
// 8
|
|
```
|
|
|
|
### Stacking hooks
|
|
|
|
Stacking multiple hooks of same type will execute them in a specific order:
|
|
|
|
1. **Before hooks** (execute in reverse order - last added runs first)
|
|
2. **Instead hooks** (only the last added executes, unless calling original)
|
|
3. **After hooks** (execute in order - first added runs first)
|
|
|
|
Using multiple `before` hooks will execute them in **reverse order** (LIFO - Last In, First Out):
|
|
|
|
```js
|
|
import { before } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => console.log('Original method called with:', a)
|
|
}
|
|
|
|
before(obj, 'method', ([a]) => {
|
|
console.log('First before hook called with:', a)
|
|
return [a + 1]
|
|
})
|
|
|
|
before(obj, 'method', ([a]) => {
|
|
console.log('Second before hook called with:', a)
|
|
return [a + 2]
|
|
})
|
|
|
|
obj.method(2)
|
|
// CONSOLE OUTPUT:
|
|
// Second before hook called with: 2
|
|
// First before hook called with: 4
|
|
// Original method called with: 5
|
|
```
|
|
|
|
Multiple `after` hooks execute in **normal order** (FIFO - First In, First Out):
|
|
|
|
```js
|
|
import { after } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => a * 2
|
|
}
|
|
|
|
after(obj, 'method', (result) => {
|
|
console.log('First after hook called with result:', result)
|
|
return result + 1
|
|
})
|
|
|
|
after(obj, 'method', (result) => {
|
|
console.log('Second after hook called with result:', result)
|
|
return result + 2
|
|
})
|
|
|
|
console.log(obj.method(2)) // 7
|
|
// CONSOLE OUTPUT:
|
|
// First after hook called with result: 4
|
|
// Second after hook called with result: 5
|
|
// 7
|
|
```
|
|
|
|
Multiple `instead` hooks will only execute the **last one added**. To chain them, you must call the original function:
|
|
|
|
```js
|
|
import { instead } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => `original-${a}`
|
|
}
|
|
|
|
instead(obj, 'method', ([a], original) => {
|
|
console.log('First instead hook called with:', a)
|
|
return `first-${original(a)}`
|
|
})
|
|
|
|
instead(obj, 'method', ([a], original) => {
|
|
console.log('Second instead hook called with:', a)
|
|
// Calling original will execute the previous instead hook
|
|
return `second-${original(a)}`
|
|
})
|
|
|
|
console.log(obj.method('test')) // 'second-first-original-test'
|
|
// CONSOLE OUTPUT:
|
|
// Second instead hook called with: test
|
|
// First instead hook called with: test
|
|
// second-first-original-test
|
|
```
|
|
|
|
### Removing hooks
|
|
|
|
Each hook returns an unpatch function that removes the hook:
|
|
|
|
```js
|
|
import { before } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => console.log('Original method called with:', a)
|
|
}
|
|
|
|
const unpatch = before(obj, 'method', ([a]) => {
|
|
console.log('Before method called with:', a)
|
|
return [a + 1]
|
|
})
|
|
|
|
obj.method(2)
|
|
// CONSOLE OUTPUT:
|
|
// Before method called with: 2
|
|
// Original method called with: 3
|
|
|
|
unpatch() // Remove the hook
|
|
|
|
obj.method(2)
|
|
// CONSOLE OUTPUT:
|
|
// Original method called with: 2
|
|
```
|
|
|
|
Unpatch functions are safe to call multiple times and won't throw errors.
|
|
|
|
### Hook priority
|
|
|
|
All hooks support an optional priority parameter to control execution order. Higher priority hooks execute before lower priority hooks.
|
|
|
|
```js
|
|
import { before, after, instead, HookPriority } from '@revenge-mod/patcher'
|
|
|
|
const obj = {
|
|
method: (a) => a
|
|
}
|
|
|
|
// Before hooks execute in ascending priority order (lowest to highest)
|
|
before(obj, 'method', (args) => {
|
|
console.log('Low priority before hook')
|
|
return args
|
|
}, { priority: HookPriority.LOW })
|
|
|
|
before(obj, 'method', (args) => {
|
|
console.log('High priority before hook')
|
|
return args
|
|
}, { priority: HookPriority.HIGH })
|
|
|
|
obj.method(1)
|
|
// CONSOLE OUTPUT:
|
|
// Low priority before hook
|
|
// High priority before hook
|
|
|
|
// After hooks execute in descending priority order (highest to lowest)
|
|
after(obj, 'method', (result) => {
|
|
console.log('High priority after hook')
|
|
return result
|
|
}, { priority: HookPriority.HIGH })
|
|
|
|
after(obj, 'method', (result) => {
|
|
console.log('Low priority after hook')
|
|
return result
|
|
}, { priority: HookPriority.LOW })
|
|
|
|
obj.method(1)
|
|
// CONSOLE OUTPUT:
|
|
// High priority after hook
|
|
// Low priority after hook
|
|
|
|
// Instead hooks execute in descending priority order (highest to lowest)
|
|
instead(obj, 'method', function (args, original) {
|
|
console.log('High priority instead hook')
|
|
return original.apply(this, args)
|
|
}, { priority: HookPriority.HIGH })
|
|
|
|
instead(obj, 'method', function (args, original) {
|
|
console.log('Low priority instead hook')
|
|
return original.apply(this, args)
|
|
}, { priority: HookPriority.LOW })
|
|
|
|
obj.method(1)
|
|
// CONSOLE OUTPUT:
|
|
// High priority instead hook
|
|
// Low priority instead hook
|
|
```
|
|
|
|
Priority constants:
|
|
|
|
- `HookPriority.HIGHEST` (1000)
|
|
- `HookPriority.HIGH` (500)
|
|
- `HookPriority.NORMAL` (0) - default
|
|
- `HookPriority.LOW` (-500)
|
|
- `HookPriority.LOWEST` (-1000)
|
|
|
|
You can also use custom numeric values for fine-grained control.
|
|
|
|
### Constructor patching
|
|
|
|
You can also patch constructor functions:
|
|
|
|
```js
|
|
import { before } from '@revenge-mod/patcher'
|
|
|
|
class MyClass {
|
|
constructor(value) {
|
|
this.value = value
|
|
}
|
|
}
|
|
|
|
const exports = { MyClass }
|
|
|
|
before(exports, 'MyClass', ([value]) => {
|
|
console.log('Constructor called with:', value)
|
|
return [`modified-${value}`]
|
|
})
|
|
|
|
const instance = new exports.MyClass('test')
|
|
console.log(instance.value) // 'modified-test'
|
|
// CONSOLE OUTPUT:
|
|
// Constructor called with: test
|
|
// modified-test
|
|
```
|