Skip to content

Latest commit

 

History

History
315 lines (284 loc) · 16.8 KB

5.4.md

File metadata and controls

315 lines (284 loc) · 16.8 KB

修改

除了上一篇介绍的@Overwrite@Inject之外,Mixin还提供了其他的修改方式

我们还是以修改标题为例,继续考虑还有哪些可行的方案:

  • 可以把Display.setTitle替换成调用自己的方法

    private void createDisplay() throws LWJGLException {
        Display.setResizable(true);
        this.mixinMethod("Minecraft 1.12.2"); // <--注意看这里
        try {
            Display.create((new PixelFormat()).withDepthBits(24));
        } catch (LWJGLException lwjglexception) {
            LOGGER.error("Couldn't set pixel format", lwjglexception);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {}
            if (this.fullscreen) {
                this.updateDisplayMode();
            }
            Display.create();
        }
    }
    
    private void mixinMethod(String title) {
        Display.setTitle("MyCustomTitle");
    }

    针对这种修改方式,Mixin提供了:

    • @Redirect文档
      允许模组取消原先调用的方法,并重新指向自己的方法。
      用法和@Inject类似:
    package com.example.mixins;
    
    import net.minecraft.client.Minecraft;
    import net.minecraft.client.settings.KeyBinding;
    import org.apache.logging.log4j.Logger;
    import org.lwjgl.opengl.Display;
    import org.spongepowered.asm.lib.Opcodes;
    import org.spongepowered.asm.mixin.Mixin;
    import org.spongepowered.asm.mixin.injection.At;
    import org.spongepowered.asm.mixin.injection.Redirect;
    
    @Mixin(Minecraft.class)
    public abstract class MixinMinecraft {
        // @Redirect 要求 @At.value 只能是 INVOKE 或 FIELD
    
        // 对方法参数和返回值的要求详见下面的例子:
        // 如果定位的是调用静态方法或者构造方法(构造方法返回被构造的类型):
        //   private <重定向方法的返回类型> function(<重定向方法的所有参数>)
        // 特别地,当重定向的是构造方法内的父类构造方法传入参数中的方法或字段引用(不是重定向父类构造方法本身),那么注入方法必须是静态的
        // 重定向父类构造方法本身参见 @ModifyArgs 注解, @At 注解的用法参见下一章-定位
        // 和 @Inject 类似,下面所有情况,如果有必要,都可以在参数后追加目标方法的参数
        @Redirect(
            method = "createDisplay",
            at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setTitle(Ljava/lang/String;)V", remap = false)
        )
        private void redirect_createDisplay(String title) { Display.setTitle("MyCustomTitle"); }
    
        // 如果定位的是调用实例方法、接口方法或父类方法:
        //   private <重定向方法的返回类型> function(<重定向方法所在类实例类型>, <重定向方法的所有参数>)
        @Redirect(
            method = "createDisplay",
            at = @At(value = "INVOKE", target = "Lorg/apache/logging/log4j/Logger;error(Ljava/lang/String;Ljava/lang/Throwable;)V", remap = false)
        )
        private void redirect_createDisplay(Logger logger, String message, Throwable t) {}
    
        // 如果定位的是读取静态字段:
        //   private <字段类型> function()
        @Redirect(
            method = "createDisplay",
            at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;LOGGER:Lorg/apache/logging/log4j/Logger;", opcode = Opcodes.GETSTATIC)
        )
        private Logger redirect_createDisplay() { return null; }
    
        // 如果定位的是写入静态字段:
        //   private void function(<字段类型>)
        @Redirect(
            method = "freeMemory",
            at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;memoryReserve:[B", opcode = Opcodes.PUTSTATIC)
        )
        private void redirect_freeMemory(byte[] memoryReserve) {}
    
        // 如果定位的是读取实例字段:
        //   private <字段类型> function(<字段所在类实例类型>)
        @Redirect(
            method = "createDisplay",
            at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;fullscreen:Z", opcode = Opcodes.GETFIELD)
        )
        private boolean redirect_createDisplay(Minecraft mc) { return false; }
    
        // 如果定位的是写入实例字段:
        //   private void function(<字段所在类实例类型>, <字段类型>)
        @Redirect(
            method = "run",
            at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;running:Z", opcode = Opcodes.PUTFIELD)
        )
        private void redirect_run(Minecraft mc, boolean running) {}
    
        // 特别地,如果定位的是数组元素相关的读写的话
        //  读取数组元素:
        //   private <数组元素类型> function(<数组类型>, int index)
        @Redirect(
            method = "processKeyBinds",
            at = @At(value = "FIELD", args = "array=get", target = "Lnet/minecraft/client/settings/GameSettings;keyBindsHotbar:[Lnet/minecraft/client/settings/KeyBinding;")
        )
        private KeyBinding redirect_processKeyBinds(KeyBinding[] keyBindsHotbar, int index) { return null; }
    
        //  写入数组元素:(因为整个 Minecraft 类中没有写入数组元素的操作,所以没有可用的例子)
        //   private void function(<数组类型>, int index, <数组元素类型>)
        //@Redirect(
        //    method = "someMethod",
        //    at = @At(value = "FIELD", args = "array=set", target = "LSomeClass1;someArray:[LSomeClass2;")
        //)
        //private void redirect_someMethod(SomeClass2[] someArray, int index, SomeClass2 value) {}
    
        //  获取一维数组 length 属性:
        //   private int function(<数组类型>)
        //@Redirect(
        //    method = "someMethod",
        //    at = @At(value = "FIELD", args = "array=length", target = "LSomeClass1;someArray:[LSomeClass2;")
        //)
        //private int redirect_someMethod(SomeClass2[] someArray) { return 0; }
    
        //  获取多维数组中的某一维的 length 属性:
        //   private int function(<数组类型>, int baseDim1, int baseDim2, int baseDim3, ...) <-- 后面 int 参数的数量等于数组维度数量-1
        //@Redirect(
        //    method = "someMethod",
        //    at = @At(value = "FIELD", args = "array=length", target = "LSomeClass1;someArray:[[[LSomeClass2;")
        //)
        //private int redirect_someMethod(SomeClass2[][][] someArray, int baseDim1, int baseDim2) { return 0; }
    }
  • 我们还可以把Display.setTitle传入参数处理一下:

    private void createDisplay() throws LWJGLException {
        Display.setResizable(true);
        Display.setTitle(this.mixinMethod("Minecraft 1.12.2")); // <--注意看这里
        try {
            Display.create((new PixelFormat()).withDepthBits(24));
        } catch (LWJGLException lwjglexception) {
            LOGGER.error("Couldn't set pixel format", lwjglexception);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {}
            if (this.fullscreen) {
                this.updateDisplayMode();
            }
            Display.create();
        }
    }
    
    private String mixinMethod(String title) {
        return "MyCustomTitle";
    }

    我们并没有取消原来的Display.setTitle,只是把它的参数处理了一下。
    Mixin针对这种修改方式,提供了:

    • @ModifyArg文档
      允许模组修改方法传入参数,用法和@Inject类似:
    package com.example.mixins;
    
    import net.minecraft.client.Minecraft;
    import org.spongepowered.asm.mixin.Mixin;
    import org.spongepowered.asm.mixin.injection.At;
    import org.spongepowered.asm.mixin.injection.ModifyArg;
    
    @Mixin(Minecraft.class)
    public abstract class MixinMinecraft {
        // @ModifyArg 要求 @At.value 只能是 INVOKE
        // Mixin会根据注入方法的返回值类型自动判断模组修改的是哪个参数
        // 如果target指向的目标有不止一个和注入方法返回值类型相同类型的参数
        // 那么需要指定 index 属性,来确定修改的是哪个参数
        // index 从 0 开始,且为相同类型参数的索引
        // 例如某个方法参数是 int, int, String, int, String
        // 你想修改最后一个 String 类型的参数,那么应当写 index = 1
    
        // 注入方法的返回值类型需要和被修改的参数类型一致
        // 参数类型可以是:
        //   只传入被修改的参数
        //   传入target指向目标的所有参数
        //   传入target指向目标的所有参数和目标方法的所有参数
        // 其他具体内容请参考文档
        @ModifyArg(
            method = "createDisplay",
            at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setTitle(Ljava/lang/String;)V", remap = false)
        )
        private String modifyArg_createDisplay(String title) {
            return "MyCustomTitle";
        }
    }
  • 能修改单个方法参数,自然也能一次性修改多个方法参数:

    • @ModifyArgs文档
      不过Mixin建议尽量不要用它,一方面,大部分情况下,都可以用@Redirect代替,另一方面,用了这个注解,Mixin会执行一堆装箱和拆箱操作,以便于调用注入方法,这样影响代码运行效率。只有在@Redirect无法匹配的同时需要修改多个参数的情况下,才能考虑使用这个注解。
      比如需要修改构造方法一开始调用的父类构造方法传入的多个参数时:
    package com.example.mixins;
    
    import net.minecraft.client.gui.recipebook.GuiButtonRecipeTab;
    import net.minecraft.creativetab.CreativeTabs;
    import org.spongepowered.asm.mixin.Mixin;
    import org.spongepowered.asm.mixin.injection.At;
    import org.spongepowered.asm.mixin.injection.ModifyArgs;
    import org.spongepowered.asm.mixin.injection.invoke.arg.Args;
    
    @Mixin(GuiButtonRecipeTab.class)
    public abstract class MixinGuiButtonRecipeTab {
        // 与 @ModifyArg 一样,@ModifyArgs 也要求 @At.value 只能是 INVOKE
        // @ModifyArgs 还要求注入方法的返回类型必须是 void ,第一个参数必须是 Args 类型,里面保存了目标中所有的参数的值
        // 如果有必要,也可以在注入方法中追加目标方法的参数
        // 因为我们修改的是调用父类构造方法时的参数,所以注入方法必须是静态方法
        @ModifyArgs(
            method = "<init>",
            at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiButtonToggle;<init>(IIIIIZ)V", remap = false)
        )
        private static void modifyArgs_$init$(Args args, int id, CreativeTabs tabs) {
            args.set(3, 50);
            args.set(4, 100); // <-- 打开物品栏的合成配方界面将会变得很鬼畜
        }
    }
  • 继续我们的修改标题的话题,我们还可以这样:

    private void createDisplay() throws LWJGLException {
        Display.setResizable(true);
        String $str = this.mixinMethod("Minecraft 1.12.2"); // <--注意看这里
        Display.setTitle($str); 
        try {
            Display.create((new PixelFormat()).withDepthBits(24));
        } catch (LWJGLException lwjglexception) {
            LOGGER.error("Couldn't set pixel format", lwjglexception);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {}
            if (this.fullscreen) {
                this.updateDisplayMode();
            }
            Display.create();
        }
    }
    
    private String mixinMethod(String title) {
        return "MyCustomTitle";
    }

    虽然这样和前面那样修改可能看不出什么差别,但是我想更加强调这是修改字符串常量本身,而不是修改setTitle方法。Mixin提供了一个用于修改方法体内常量的注解:

    • @ModifyConstant文档
      这个注解可以用于修改使用LDC*CONST_**IPUSH等操作码引用的常量:
    package com.example.mixins;
    
    import net.minecraft.client.Minecraft;
    import org.spongepowered.asm.mixin.Mixin;
    import org.spongepowered.asm.mixin.injection.Constant;
    import org.spongepowered.asm.mixin.injection.ModifyConstant;
    
    @Mixin(Minecraft.class)
    public abstract class MixinMinecraft {
        // 和 @ModifyArg 类似,注入方法的参数类型和返回值类型必须和匹配的常量一致
        // 如果有必要,也可以在注入方法中追加目标方法的参数
        @ModifyConstant(
            method = "createDisplay",
            constant = @Constant(stringValue = "Minecraft 1.12.2")
        )
        private String modifyConstant_createDisplay(String title) {
            return "MyCustomTitle";
        }
    }
    • @Constant文档
      需要注意以下两个属性:
      • intValue: 可以匹配bytecharintshort类型的常量
      • expandZeroConditions: 参考定位一章的常量引用特殊情况:与0比较一节
  • 能修改常量,自然也能修改局部变量:

    • @ModifyVariable文档
      这个注解用于修改方法体内的局部变量,注入点必须在被修改的局部变量的作用域之中。

      • print: 用于指定是否打印指定类型的局部变量表(和@Inject.locals的打印功能类似)
      • ordinal: 用于指定在局部变量表中相同类型的变量的索引,从0开始
      • index: 用于指定在局部变量表中的索引,从0开始
      • name: 用于指定局部变量的名称
      • argsOnly: 是否只匹配目标方法的参数,不匹配方法体内的局部变量

      ordinalindexname三个属性只需要填一个即可。
      例如有一个方法的局部变量表参数分别是 intintStringintString,你想修改最后一个String类型的变量,那么只需要设置 oridinal = 1 或者 index = 4 即可。
      @Inject.locals一样,Mixin建议先设置print属性为true,打印局部变量表,然后再完成参数的确定。当printtrue时,注入方法并不会注入到目标方法中去。
      以修改窗体16x16的图标为例:

      // @ModifyVariable 没有限制 @At.value 的取值
      // 与其他修改方式不一样的是, @ModifyVariable 标记的注入方法不能附带目标方法的参数
      // 参数只能有一个,参数类型和返回类型必须与被修改的局部变量一致
      @ModifyVariable(
          method = "setWindowIcon",
          at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setIcon([Ljava/nio/ByteBuffer;)I", remap = false),
          ordinal = 0
      )
      private InputStream modifyVariable_setWindowIcon(InputStream inputStream) {
          // ...
          return inputStream;
      }        

有的时候我们为了模组的兼容性之类的原因,可能会修改一些在编译时不存在,运行时可能存在的方法,如果我们直接指定注解的method而不做其他操作,那么编译时会出错,提示目标方法不存在,这个时候,Mixin提供了下面这个注解:

  • @Dynamic文档
    用于标记那些要注入到编译时不存在的方法的注入方法。

有的时候我们的目标方法的参数存在非公有类型,显然我们不大可能在注入方法中引用它,因此Mixin提供了下面这个注解:

  • @Coerce文档
    这个注解标记在方法参数上。对于引用类型而言,用于指示这个参数的类型是目标方法对应参数的类型的超类(只能是超类,不能是接口,如果目标方法参数类型是非公有接口,那么注入方法对应参数类型只能是Object),这个注解对@Inject@ModifyVariable捕获的局部变量类型也是可用的,但是这个注解不能用于@Surrogate方法中的参数。