Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic mixins #12

Merged
merged 23 commits into from
Jun 22, 2023
Merged

Add dynamic mixins #12

merged 23 commits into from
Jun 22, 2023

Conversation

mattco98
Copy link
Member

@mattco98 mattco98 commented Jun 1, 2023

This PR removed the old scuffed mixin system and replaces it with a fully dynamic mixin system. The user can specify fully custom injections and subscribe to them in the main module code. Example:

metadata.json:

{
    "entry": "index.js",
    "mixinEntry": "mixins.js"
}

mixins.js:

const bowItemMixin = new Mixin('net.minecraft.item.BowItem');

export const BOW_ITEM_USE_MIXIN = bowItemMixin.inject({
    method: 'use',
    at: new At({
        value: 'INVOKE',
        target: 'Lnet/minecraft/entity/player/PlayerEntity;setCurrentHand(Lnet/minecraft/util/Hand;)V',
        shift: At.Shift.BEFORE,
    }),
    locals: new Locals(LocalCapture.CAPTURE_FAILHARD, ['Lnet/minecraft/item/ItemStack;']),
    cancellable: true,
});

index.js:

import { BOW_ITEM_USE_MIXIN } from "./mixins.js";

BOW_ITEM_USE_MIXIN.attach((receiver, world, user, hand, cir, itemStack) => {
    ChatLib.chat('used bow! item: ' + itemStack.toString());
})

Note that the user must specify the descriptors of any locals they wish to capture in addition to the LocalCapture enum value, as I don't think it is possible to determine this information automatically without copy pasting a lot of Mixin code

TODO:

  • More injectors (currently only support @Inject)
  • Handle multiple injection points (i.e. multiple @At annotations) Not worth the trouble
  • Can Mixin throw errors that don't crash the game? If so, buffer these (and any other early-phase errors) so we can print them to the console later New console buffers errors automatically
  • Indy???
  • More testing!
  • Better error messages
  • Can @Local capture method parameters as well? If so, remove auto-parameter capture from all the injectors and just let the user tell us about the parameters they care about.
  • Unify locals and parameter capture across all injectors

@mattco98 mattco98 marked this pull request as draft June 1, 2023 03:33
@mattco98 mattco98 force-pushed the dynamic-mixins branch 10 times, most recently from 9dce01a to 9bea427 Compare June 5, 2023 00:28
@mattco98 mattco98 force-pushed the dynamic-mixins branch 9 times, most recently from cf4d110 to 652f3ac Compare June 16, 2023 15:09
@mattco98
Copy link
Member Author

mattco98 commented Jun 16, 2023

Some typing info on the mixin lib, will probably use this as a base for a wiki entry:

Typings

declare const Java: {
    /**
     * Returns the Java class associated with [className]. During mixin application, does
     * not allow loading mapped class type (types in the `net.minecraft` or 
     * `com.mojang.blaze3d` packages).
     */
    type(className: string): any /* Class */;
};

declare const Opcodes: {
    [key: string]: number;
};

declare const Condition: {
    [key: string]: object;
};

////////////////////////
// Descriptor helpers //
////////////////////////

declare const void_: 'V';
declare const boolean: 'Z';
declare const char: 'C';
declare const byte: 'B';
declare const short: 'S';
declare const int: 'I';
declare const float: 'F';
declare const long: 'J';
declare const double: 'D';

declare function desc(options: {
    owner?: string;
    name?: string;
    /**
     * Used for field descriptors to specify the field type. Cannot be used with `args` or `ret`.
     */
    type?: string;
    args?: string[];
    ret?: string;
}): string;

/**
 * Helper for creating class descriptors. Instead of writing
 * "La/b/c;", this allows you to write "J`a.b.c`"
 */
declare function J(strings: string[], exprs: string[]): string;

////////////
// Mixins //
////////////

/**
 * The entry point to a mixin. Note that CT mixins for the most part do not support
 * single-target injections, hence why target is not an array.
 */
declare const Mixin: IMixin;

/**
 * See org.spongepowered.asm.mixin.Mixin for more information on each parameter.
 * The string type is treated as `{ target: <string value> }`.
 */
interface IMixin {
    new (options: string | {
        target: string;
        priority?: number;
        remap?: boolean;
    }): IMixin;

    /**
     * See org.spongepowered.asm.mixin.injection.Inject for more information on each parameter. 
     */
    inject(options: {
        method: string;
        id?: string;
        slices?: Slice | Slice[];
        at?: At | At[];
        cancellable?: boolean;
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallback;

    /**
     * See org.spongepowered.asm.mixin.injection.Redirect for more information on each parameter. 
     */
    redirect(options: {
        method: string;
        slice?: Slice;
        at?: At;
        args?: string[];
        returnType: string;
        locals?: Local | Local[];
        require?: number;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See org.spongepowered.asm.mixin.injection.ModifyArg for more information on each parameter. 
     */
    modifyArg(options: {
        method: string;
        slice?: Slice;
        at?: At;
        index?: number;
        /**
         * Will cause all parameters of the targeted method to be captured and passed to the callback
         * instead of just the parameter given by `index`.
         */
        captureAllParams?: boolean;
        locals?: Local | Local[];
        remap?: boolean;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See org.spongepowered.asm.mixin.injection.ModifyArgs for more information on each parameter. 
     * Note that because the argument types will be abstracted away at runtime behind the single
     * `Args` object, special care must be taken to provide the correct types. For example, all numbers in
     * Rhino are `double`s, so if you need to set a float argument, you would need to do something
     * along the lines of `args.set(0, new java.lang.Float(1))`.
     */
    modifyArgs(options: {
        method: string;
        slice?: Slice;
        at: At;
        locals?: Local | Local[];
        require?: number;
        remap?: boolean;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See org.spongepowered.asm.mixin.injection.ModifyConstant for more information on each parameter. 
     */
    modifyConstant(options: {
        method: string;
        slice?: Slice | Slice[];
        constant: Constant;
        locals?: Local | Local[];
        require?: number;
        remap?: boolean;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See com.llamalad7.mixinextras.injector.ModifyExpressionValue for more information on each parameter. 
     */
    modifyExpressionValue(options: {
        method: string;
        at: At;
        slices?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;

    /**
     * See com.llamalad7.mixinextras.injector.ModifyReceiver for more information on each parameter. 
     */
    modifyReceiver(options: {
        method: string;
        at: At;
        slices?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;

    /**
     * See com.llamalad7.mixinextras.injector.ModifyReturnValue for more information on each parameter. 
     */
    modifyReturnValue(options: {
        method: string;
        at: At;
        slice?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;

    /**
     * See org.spongepowered.asm.mixin.injection.ModifyVariable for more information on each parameter. 
     */
    modifyVariable(options: {
        method: string;
        at: At;
        slice?: Slice;
        /**
         * See [Local]
         */
        print?: boolean;
        /**
         * See [Local]
         */
        ordinal?: number;
        /**
         * See [Local]
         */
        index?: number;
        /**
         * See [Local]
         */
        type?: string;
        /**
         * See [Local]
         */
        parameterName?: string;
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable

    /**
     * See com.llamalad7.mixinextras.injector.WrapOperation for more information on each parameter. 
     */
    wrapOperation(options: {
        method: string;
        at?: At;
        constant?: Constant;
        slice?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;

    /**
     * See com.llamalad7.mixinextras.injector.WrapWithCondition for more information on each parameter. 
     */
    wrapWithCondition(options: {
        method: string;
        at: At;
        slice?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;
    
    /**
     * Makes the given field public. If [isMutable] is true, the field will have its `final` modifier removed.
     */
    widenField(name: string, isMutable?: boolean): void;
    
    /**
     * Makes the given method public. If [isMutable] is true, the method will have its `final` modifier removed.
     */
    widenMethod(name: string, isMutable?: boolean): void;
};

/**
 * Object returned from mixin injectors that allows registering (and re-registering)
 * the callback which will be called when the mixin injector is called. `args` will
 * always start with the receiver object (the `this` value of the class that the
 * mixin is applied to) unless the mixin is targeting a static context.
 */
interface IMixinCallback {
    attach(callback: (...args: any[]) => void): void;
    release(): void;
};

/**
 * Similar to [IMixinCallback], but requires the callback to return a value. The exact
 * type of that value depends on the mixin.
 */
interface IMixinCallbackReturnable {
    attach(callback: (...args: any[]) => any): void;
    release(): void;
};

/**
 * See org.spongepowered.asm.mixin.injection.At for more information on each
 * option. Note that the string type is treated as `{ value: <string value> }`.
 */
interface At {
    new (options: string | {
        value: string;
        id?: string;
        slice?: string;
        by?: number;
        args?: string[];
        target?: string;
        ordinal?: number;
        opcode?: number;
        remap?: boolean;
    }): At;  
};

/**
 * See org.spongepowered.asm.mixin.injection.Slice for more information on each option.
 */
interface Slice {
    new (options: {
        id?: string;
        from?: At;
        to?: At;
    }): Slice;
};

type LocalOptions =
    string | // treated as { parameterName: <string value> }
    number | // treated as { index: <number value> }
    {
        /**
         * Will cause the local variable table (LVT) of this method to be printed. Causes
         * this injection to not apply.
         * 
         * TODO: Is the entire injection aborted? Or just this local attaching 
         */
        print?: boolean;

        /**
         * The name of a parameter to capture. This cannot be used with non-parameter
         * locals, hence the name. Cannot be used with `index`, `ordinal`, or `type`. 
         */
        parameterName?: string;

        /**
         * The index of the variable in the LVT. Cannot be used with `ordinal`. If used,
         * `type` must also be included.
         */
        index?: number;
        
        /**
         * The ordinal of the variable of the given type in the method. Cannot be used
         * with `index`. If used, `type` must also be included.
         */
        ordinal?: number;

        /**
         * The types of variable being captured. Pairs with either `index` or `ordinal`.
         * Note that you do not need to specify mutable type wrappers manually; instead,
         * prefer setting the `mutable` flag below to `true`. 
         */
        type?: string;

        /**
         * Whether the local should be captured mutably. This will cause the local variable
         * type to be wrapped in the appropriate 
         * [mutable wrapper](https://github.com/LlamaLad7/MixinExtras/tree/master/src/main/java/com/llamalad7/mixinextras/sugar/ref).
         */
        mutable?: boolean;
    };

interface Local {
    new (options: LocalOptions): Local;
};

/**
 * See org.spongepowered.asm.mixin.injection.Constant for more information on each option.
 */
interface Constant {
    new (options: {
        nullValue?: boolean;
        intValue?: number;
        floatValue?: number;
        longValue?: number;
        doubleValue?: number;
        stringValue?:
/**
 * The entry point to a mixin. Note that CT mixins for the most part do not support
 * single-target injections, hence why target is not an array.
 */
declare const Mixin: IMixin;

/**
 * See org.spongepowered.asm.mixin.Mixin for more information on each parameter.
 * The string type is treated as `{ target: <string value> }`.
 */
interface IMixin {
    new (options: string | {
        target: string;
        priority?: number;
        remap?: boolean;
    }): IMixin;

    /**
     * See org.spongepowered.asm.mixin.injection.Inject for more information on each parameter. 
     */
    inject(options: {
        method: string;
        id?: string;
        slices?: Slice | Slice[];
        at?: At | At[];
        cancellable?: boolean;
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallback;

    /**
     * See org.spongepowered.asm.mixin.injection.Redirect for more information on each parameter. 
     */
    redirect(options: {
        method: string;
        slice?: Slice;
        at?: At;
        args?: string[];
        returnType: string;
        locals?: Local | Local[];
        require?: number;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See org.spongepowered.asm.mixin.injection.ModifyArg for more information on each parameter. 
     */
    modifyArg(options: {
        method: string;
        slice?: Slice;
        at?: At;
        index?: number;
        /**
         * Will cause all parameters of the targeted method to be captured and passed to the callback
         * instead of just the parameter given by `index`.
         */
        captureAllParams?: boolean;
        locals?: Local | Local[];
        remap?: boolean;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See org.spongepowered.asm.mixin.injection.ModifyArgs for more information on each parameter. 
     * Note that because the argument types will be abstracted away at runtime behind the single
     * `Args` object, special care must be taken to provide the correct types. For example, all numbers in
     * Rhino are `double`s, so if you need to set a float argument, you would need to do something
     * along the lines of `args.set(0, new java.lang.Float(1))`.
     */
    modifyArgs(options: {
        method: string;
        slice?: Slice;
        at: At;
        locals?: Local | Local[];
        require?: number;
        remap?: boolean;
        expect?: number;
        allow?: number;
        constraints?: string;
    }): IMixinCallbackReturnable;

    /**
     * See com.llamalad7.mixinextras.injector.ModifyExpressionValue for more information on each parameter. 
     */
    modifyExpressionValue(options: {
        method: string;
        at: At;
        slices?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;

    /**
     * See com.llamalad7.mixinextras.injector.ModifyReceiver for more information on each parameter. 
     */
    modifyReceiver(options: {
        method: string;
        at: At;
        slices?: Slice | Slice[];
        locals?: Local | Local[];
        remap?: boolean;
        require?: number;
        expect?: number;
        allow?: number;
    }): IMixinCallbackReturnable;
    
    /**
     * Makes the given field public. If [isMutable] is true, the field will have its `final` modifier removed.
     */
    widenField(name: string, isMutable?: boolean): void;
    
    /**
     * Makes the given method public. If [isMutable] is true, the method will have its `final` modifier removed.
     */
    widenMethod(name: string, isMutable?: boolean): void;
};

/**
 * Object returned from mixin injectors that allows registering (and re-registering)
 * the callback which will be called when the mixin injector is called. `args` will
 * always start with the receiver object (the `this` value of the class that the
 * mixin is applied to) unless the mixin is targeting a static context.
 */
interface IMixinCallback {
    attach(callback: (...args: any[]) => void): void;
    release(): void;
};

/**
 * Similar to [IMixinCallback], but requires the callback to return a value. The exact
 * type of that value depends on the mixin.
 */
interface IMixinCallbackReturnable {
    attach(callback: (...args: any[]) => any): void;
    release(): void;
};

/**
 * See org.spongepowered.asm.mixin.injection.At for more information on each
 * option. Note that the string type is treated as `{ value: <string value> }`.
 */
interface At {
    new (options: string | {
        value: string;
        id?: string;
        slice?: string;
        by?: number;
        args?: string[];
        target?: string;
        ordinal?: number;
        opcode?: number;
        remap?: boolean;
    }): At;  
};

/**
 * See org.spongepowered.asm.mixin.injection.Slice for more information on each option.
 */
interface Slice {
    new (options: {
        id?: string;
        from?: At;
        to?: At;
    }): Slice;
};

type LocalOptions =
    string | // treated as { parameterName: <string value> }
    number | // treated as { index: <number value> }
    {
        /**
         * Will cause the local variable table (LVT) of this method to be printed. Causes
         * this injection to not apply.
         * 
         * TODO: Is the entire injection aborted? Or just this local attaching 
         */
        print?: boolean;

        /**
         * The name of a parameter to capture. This cannot be used with non-parameter
         * locals, hence the name. Cannot be used with `index`, `ordinal`, or `type`. 
         */
        parameterName?: string;

        /**
         * The index of the variable in the LVT. Cannot be used with `ordinal`. If used,
         * `type` must also be included.
         */
        index?: number;
        
        /**
         * The ordinal of the variable of the given type in the method. Cannot be used
         * with `index`. If used, `type` must also be included.
         */
        ordinal?: number;

        /**
         * The types of variable being captured. Pairs with either `index` or `ordinal`.
         * Note that you do not need to specify mutable type wrappers manually; instead,
         * prefer setting the `mutable` flag below to `true`. 
         */
        type?: string;

        /**
         * Whether the local should be captured mutably. This will cause the local variable
         * type to be wrapped in the appropriate 
         * [mutable wrapper](https://github.com/LlamaLad7/MixinExtras/tree/master/src/main/java/com/llamalad7/mixinextras/sugar/ref).
         */
        mutable?: boolean;
    };

interface Local {
    new (options: LocalOptions): Local;
}; string;
        classValue?: string;
        ordinal?: number;
        slice?: string;
        expandZeroConditions?: boolean;
        log?: boolean;
    }): Constant;
};

@mattco98 mattco98 force-pushed the dynamic-mixins branch 9 times, most recently from 5c3b1ac to 9003e59 Compare June 17, 2023 20:18
@mattco98 mattco98 marked this pull request as ready for review June 18, 2023 19:34
@mattco98
Copy link
Member Author

Wrote a wiki for this: https://github.com/ChatTriggers/ctjs/wiki/Dynamic-Mixins

Fixes a few issues: proper .equals/.hashCode for arrays, fix for tagged
template literals not working, and allows undefined to convert to Java's
`null`.
Other things are interested in this
This is a bit cleaner, and keep the Mapping class nice and focused
This will be replaced by a more robust dynamic mixin system
MC does it on its own, but it happens in its main() function, which
doesn't get called until after mixin application. So we mimic its
behavior ourselves.
Also adds mixinProvidedLibs.js, which will contain the glue between the
JSLoader and the JS language.
@mattco98 mattco98 merged commit 42aaa53 into main Jun 22, 2023
@mattco98 mattco98 deleted the dynamic-mixins branch June 22, 2023 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant