Open Dinhero21 opened 1 year ago
A real-time-Function-replacement-system could be made but it would require custom transformers/plugins and might create a lot of overhead since, the way I'm currently thinking about it, is like this:
module
export function test () {
return 'a'
}
plugin/custom
import Mirror from 'mirror' // haha, get it? Reflection, Mirror (sorry)
Mirror.Function('module$test').replace(original => () => 'b')
module
+ import Mirror from 'mirror'
export function test () {
- return 'a'
+ return Mirror.Function.get('module$test').apply(this, arguments)
}
Function Identifier = Scope Identifier $ Function Name
Scope Identifier (global) =
Example:
// test
const test = () => {}
// object
const object = {
// object$method
method () {}
}
// a
function a () {
// a$b
function b () {
// a$b$(anonymous)
return () => {}
}
}
Mirror.Function
would allow you to Reflect Functions. It's called with the Function Identifier and returns a Mirror Function Helper.
The Mirror Function Helper has many methods to allow you to modify the function, such as:
My current ideas are to instead of replacing the function in the way shown above it would look something like this:
module
+ import Mirror from 'mirror'
- export function test () {
- return 'a'
- }
+ export const test = Mirror.Function.get('module$test')
This should have a very positive impact on performance since instead of calling Mirror.Function.get
every time the function is called, it will get called only once and stored in the place the "real" function would get stored anyway, leading to only a minor impact on initialization.
The biggest flaw I see in this is that if you want to override some function you need to do so prior to module initialization which happens when you import it.
So if you need to use the module and also override some function within it you would need to do it like so:
plugin/custom
import 'custom/pre'
import 'custom/index'
// Not needed
// import 'custom/post'
plugin/pre
// Gets called before index
import Mirror from 'mirror'
Mirror.Function('module$test').replace(original => () => 'b')
plugin/index
// module now gets initialized and test gets its value **permanently assigned** as Mirror.Function.get('module$test')
import { test } from 'module'
test() // b
// no-op since module has already initialized
Mirror.Function('module$test').replace(original => () => 'c')
test() // b
I could also override module imports which would look something like this:
example
- import { test } from 'module'
+ import Mirror from 'mirror'
+ const { test } = Mirror.Module.get('module')
test() // 'b'
This could be implemented in a building plugin that would always prefix all code with import Mirror from 'mirror'
and replace all import ... from '...'
with const ... = Mirror.Module.get('...')
The only problem I see is that we are not actually overriding the function itself, only its importing, which means that the module's reference to the function is the original's which might lead to this:
module
export function test () {
return 'a'
}
export function isTest (f) {
return f === test
}
plugin/custom
import Mirror from 'mirror'
Mirror.Function('module$test').replace(original => () => 'b')
example
import { test, isTest } from 'module'
test() // 'b'
isTest(test) // false
// since
// example's test is () => 'b'
// while module's test is function test () { return 'a' }
// which are not the same
I attempted to override the module using the Namespace Import and the function-like dynamic import but they both seal the output, making it impossible to mutate.
Hook-ing would also allow for a non-mutating-ly override of the module, which can be done in CommonJS (CJS) by simply overriding Module._load
(see node:module
), in ECMAScript Modules (ESM) it isen't as easy, but seems to be doable via Customization Hooks.
Note that this is only for the server side as the client side is being bundled (via Rollup) which has a Plugin API that allows you to do basically whatever you want with the source code. There is also a transpilation step (currently done by Babel, probably going to migrate to esbuild soon:tm:) which also has Plugin APIs.
I belive the Plugin/Transformer APIs are the only viable ways of doing it server-side as of now.
The way Vencord does it (see How Plugins Work In Vencord) is by replacing the source code at runtime (pre-execution) using regex.
I really like the idea but I feel like it could be better with scope-ing. Since we have access to the non-minified non-transpiled original source code we could traverse the AST and have a much cleaner and more compatible/dynamic approach than simple replacing.
For example, instead of:
replace(
'getAcceleration(){...}',
'getAcceleration(){...if (keyboard.isKeyDown('j')) $1 -= 1024;}'
)
it could be something much more verbose like:
import project from 'quilt' // haha, get it? Patching, Quilt (sorry)
const public = project.in('public')
const game = public.in('game')
const entity = game.in('entity') // File
const player = entity.in('server-entity/type/player.ts') // Context
const ServerPlayerEntity =
player
.in.class('ServerPlayerEntity')
const PlayerClass =
ServerPlayerEntity
.in.body()
// Context
const PlayerMove =
PlayerClass
.in(node => node.key.name === 'move')
// Example
PlayerMove
.run.instead('// runs this instead')
.run.before('// runs this before')
.run.after('// runs this after')
const PlayerMoveBody =
PlayerMove
.in.body()
PlayerMove
.in.return()
.run.before('if (keyboard.isKeyDown("j")) y -= 1024')
and because it "adapts" to code changes, updates and other mods shouldn't break as often with the latter as they would with the former making it so less maintenance is required and mod compatibility is better.
Projects like jscodeshift and recast seem very promising but are overly-verbose.
Take this code from recast's README for example:
// Grab a reference to the function declaration we just parsed.
const add = ast.program.body[0];
// Make sure it's a FunctionDeclaration (optional).
const n = recast.types.namedTypes;
n.FunctionDeclaration.assert(add);
// If you choose to use recast.builders to construct new AST nodes, all builder
// arguments will be dynamically type-checked against the Mozilla Parser API.
const b = recast.types.builders;
// This kind of manipulation should seem familiar if you've used Esprima or the
// Mozilla Parser API before.
ast.program.body[0] = b.variableDeclaration("var", [
b.variableDeclarator(add.id, b.functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);
// Just for fun, because addition is commutative:
add.params.push(add.params.shift());
Can you guess what it does?
It transforms
function add(a, b) {
return a +
// Weird formatting, huh?
b;
}
into
var add = function(b, a) {
return a +
// Weird formatting, huh?
b;
}
Taking slight inspiration from Minecraft Mods I had the idea of reflectively overriding class methods.
For example:
src/util/example
export class Example {
public hello (): void {
console.log('Hello World!')
}
}
export default Example
src/mod/example
import Example from '../util/example'
// You don't really need to use void
// or name your class _ but that is
// just so when other developers see
// your code they know that you are
// simply modding Example and will
// not use this class anywhere else
void class _ extends Example {
// ? Should I use override or overwrite
// overwrite - Destroy (data) or the data in (a file) by entering new data in its place.
// override - Use one's authority to reject or cancel (a decision, view, etc.)
// I guess this is more "overwrite" than "override" since you are replacing
// the method but the original is still accessible via "super" so you could
// also simply "remix" it and not "overwrite".
// Forge and Fabric (probably Java) use override.
@Override
public hello (): void {
super.hello() // Hello World!
console.log('... now remixed!')
}
}
The only problem I see with this (and also the problem before) is that it only works with objects.
For example:
src/util/example
export const CONFIG = {
modded: false
}
export let variable = 3.14
export const CONSTANT = 3.14
export function f (): void {
console.log('Hello World!')
}
src/mod/example
import { CONFIG, variable, CONSTANT, f } from '../util/example'
// This will work, since CONFIG is an Object
CONFIG.modded = true
// This will NOT work since imported values
// are constant
variable = 1.618
// This will NOT work for the same reason
CONSTANT = 3.14
// This will NOT work for the same reason
f = () => {}
// This will NOT work since even if f is
// an Object, most of its properties are
// not writable/configurable so you cant
// change them
Object.assign(f, () => {})
I could also make it possible to add code to the start or end of files, but then you could also not change constants.
I guess I could add a pre-processing step or an eslint-rule (but then the could would look so ugly full of lets).
Then it would be possible to do this:
// originally const but now let
// because of pre-processing
let CONSTANT = 3.14
function hello (): void {
console.log('Hello World!')
}
// --- mod code added to the end ---
CONSTANT = 1.618
const originalHello = hello
// you can't override functions
// "normally"
hello = function () {
originalHello()
console.log('... now remixed!')
}
I don't really see any uses for code added before since there isn't really much you can do outside of creating global variables that would be undefined otherwise (but then why would they modify the code if the original isn't even expecting them?) but I will probably add that as an option.
Modding (modifying) the game is currently possible using the dynamic plugin system. Simply use Reflection to inject your custom code into the game.
There are some limitations, for example, it is impossible to override module functions. (warning: pseudo-code below)
The DX is also not great since all your code needs to be files inside the src[/public]/plugin (for ease of sharing and avoiding conflicts).
You also can not modify any non-code files without doing Reflection on the loaders themselves or using the
node:fs
module.All that makes for a horrible, horrendous, impractical way of doing anything even slightly complex.