Minecraft(我的世界)中文论坛

 找回密码
 注册(register)

!header_login!

只需一步,立刻登录

查看: 679|回复: 6

[Mod开发教程] 使用Mixin插入枚举

[复制链接]
洞穴夜莺 当前离线
积分
11091
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2019-8-18
查看详细资料
发表于 2021-3-20 23:39:24 | 显示全部楼层 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
本帖最后由 洞穴夜莺 于 2021-4-2 16:47 编辑

本教程基于Fabric Mixin 0.8.0 on Minecraft 1.16.5
我曾经在帖子https://www.mcbbs.net/thread-1118687-1-1.html中说Mixin插入Enum会出现问题(那时候是1.15.x)
但是我现在发现这种方式插入枚举已经没有问题了(1.16.x)
具体方法说一下
这种方法插入Enum能实现的目标
  • 在value()的返回值中出现
  • valueOf(String)可用
创建对应静态公开字段是不在讨论范围内的(虽然在正常的enum中会创建对应公开静态字段,但这种行为在Minecraft Modding没有什么意义,也很少有人这么干),常规Mixin不允许创建静态公开字段(实际上即使创建了你也不得不通过反射来访问),用plugin可以实现但本帖不作讨论

枚举的一般结构
随手写一个枚举
  1. package cavenightingale;

  2. public enum TestEnum {
  3.         AAAA("1111"),
  4.         BBBB("2222");
  5.         TestEnum(String str) {
  6.         }
  7. }
复制代码
编译之后javap一下得到
  1. Compiled from "TestEnum.java"
  2. public final class cavenightingale.TestEnum extends java.lang.Enum<cavenightingale.TestEnum> {
  3.   public static final cavenightingale.TestEnum AAAA;

  4.   public static final cavenightingale.TestEnum BBBB;

  5.   private static final cavenightingale.TestEnum[] $VALUES;

  6.   public static cavenightingale.TestEnum[] values();
  7.     Code:
  8.        0: getstatic     #1                  // Field $VALUES:[Lcavenightingale/TestEnum;
  9.        3: invokevirtual #2                  // Method "[Lcavenightingale/TestEnum;".clone:()Ljava/lang/Object;
  10.        6: checkcast     #3                  // class "[Lcavenightingale/TestEnum;"
  11.        9: areturn

  12.   public static cavenightingale.TestEnum valueOf(java.lang.String);
  13.     Code:
  14.        0: ldc           #4                  // class cavenightingale/TestEnum
  15.        2: aload_0
  16.        3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  17.        6: checkcast     #4                  // class cavenightingale/TestEnum
  18.        9: areturn

  19.   private cavenightingale.TestEnum(java.lang.String);
  20.     Code:
  21.        0: aload_0
  22.        1: aload_1
  23.        2: iload_2
  24.        3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
  25.        6: return

  26.   static {};
  27.     Code:
  28.        0: new           #4                  // class cavenightingale/TestEnum
  29.        3: dup
  30.        4: ldc           #7                  // String AAAA
  31.        6: iconst_0
  32.        7: ldc           #8                  // String 1111
  33.        9: invokespecial #9                  // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
  34.       12: putstatic     #10                 // Field AAAA:Lcavenightingale/TestEnum;
  35.       15: new           #4                  // class cavenightingale/TestEnum
  36.       18: dup
  37.       19: ldc           #11                 // String BBBB
  38.       21: iconst_1
  39.       22: ldc           #12                 // String 2222
  40.       24: invokespecial #9                  // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
  41.       27: putstatic     #13                 // Field BBBB:Lcavenightingale/TestEnum;
  42.       30: iconst_2
  43.       31: anewarray     #4                  // class cavenightingale/TestEnum
  44.       34: dup
  45.       35: iconst_0
  46.       36: getstatic     #10                 // Field AAAA:Lcavenightingale/TestEnum;
  47.       39: aastore
  48.       40: dup
  49.       41: iconst_1
  50.       42: getstatic     #13                 // Field BBBB:Lcavenightingale/TestEnum;
  51.       45: aastore
  52.       46: putstatic     #1                  // Field $VALUES:[Lcavenightingale/TestEnum;
  53.       49: return
  54. }
复制代码
从上面可以看出,这个类的初始化挨个调用构造函数构造各个枚举对象,然后塞进$VALUES字段中
values()函数则返回$VALUES.clone(),比较头疼的是这个valueOf(String)函数,它调用了一个Enum.valueOf(Class, String)函数,它会调用Class.enumConstantDirectory(),继续深入查看会发现这玩意会反射调用枚举类的values()函数并缓存一些中间结果(并且相关字段还private,例如Class.enumConstants)
所以最好在第一次调用valueOf函数之前将枚举编辑完毕,避免到头来又要反射
所以说要做两个事情
一是要构造对应的枚举对象,二是要把$VALUES字段赋值

Mixin编辑自己的枚举
于是可以得出如下代码,注意这里的构造函数要在声明的参数列表前面填上String name和int ordinal,用来传递枚举名和序号两个参数
  1. package cavenightingale.mixin;

  2. import cavenightingale.TestEnum;
  3. import org.spongepowered.asm.mixin.Mixin;
  4. import org.spongepowered.asm.mixin.Mutable;
  5. import org.spongepowered.asm.mixin.Shadow;

  6. import java.util.Arrays;

  7. @Mixin(TestEnum.class)
  8. public class TestEnumMixin {
  9.         TestEnumMixin(String name, int ordinal, String str) {
  10.                 throw new AssertionError("can not happen");
  11.         }

  12.         @Shadow
  13.         @Mutable
  14.         @SuppressWarnings({"target", "mapping"})
  15.         private static TestEnum[] $VALUES;

  16.         static {
  17.                 int ordinal = $VALUES.length;
  18.                 $VALUES = Arrays.copyOf($VALUES, ordinal + 1);
  19.                 $VALUES[ordinal] = (TestEnum) (Object)new TestEnumMixin("CCCC", ordinal, "3333");
  20.         }
  21. }
复制代码
在上述代码中对TestEnumMixin构造函数的调用在Mixin的时候会被自动映射到对TestEnum构造函数的调用上去,这里不用担心
同时Mixin类中的static块调用是在目标类的static块调用之后的,所以可以直接在$VALUES上面修改,如果你觉得不放心@Inject到<clinit>的@At("RETURN")上也是一样的
@Shadow那个地方会产生找不到目标类中对应字段的编译警告,但是在运行期它又是可以找到的,所以我们用用@SuppressWarnings("target")压制警告,如果你使用的是高版本的Mixin应该是用@SuppressWarnings("unresolvable-target") ,不过这个修改目前FabricMixin没有跟进。
而压制mapping警告的原因则是这是我们自己的枚举类,没有混淆映射表是正常的。
然后你可以在你的Mod主类里面(这里仅做演示,就不上Logger了)
  1. System.out.println(TestEnum.valueOf("CCCC"));
  2. System.out.println(TestEnum.values()[2]);
复制代码


Mixin编辑Minecraft的枚举
然而,如果你把这些东西往Minecraft的类(例如ActionResult)上一套,你会发现你的Minecraft喜闻乐见地崩溃了
问题出在哪?如果你阅读崩溃报告,会发现原因是这玩意没有 $VALUES 字段???
我们知道Minecraft是做过混淆的,像$VALUES这种不会被外部用到的字段自然是被混淆了,eclipse ecj编译出来的类甚至用ENUM$VALUES代替$VALUES,照样好好的
那就去mapping里找一下ActionResult的反混淆[1]吧,通过搜索,可以找到如下内容
  1. c        aou        net/minecraft/class_1269        net/minecraft/util/ActionResult
  2.         m        ()Z        a        method_23665        isAccepted
  3.                 c        Returns whether an action is performed.
  4.         m        (Z)Laou;        a        method_29236        success
  5.                 p        0                        swingHand
  6.         m        ()Z        b        method_23666        shouldSwingHand
  7.                 c        Returns whether an actor should have a hand-swinging animation on\naction performance.
  8.         m        (Ljava/lang/String;)Laou;        valueOf        valueOf        valueOf
  9.         m        ()[Laou;        values        values        values
  10.         f        Laou;        a        field_5812        SUCCESS
  11.                 c        Indicates an action is performed and the actor's hand should swing to\nindicate the performance.
  12.         f        Laou;        b        field_21466        CONSUME
  13.                 c        Indicates an action is performed but no animation should accompany the\nperformance.
  14.         f        Laou;        c        field_5811        PASS
  15.                 c        Indicates an action is not performed but allows other actions to\nperform.
  16.         f        Laou;        d        field_5814        FAIL
  17.                 c        Indicates that an action is not performed and prevents other actions\nfrom performing.
  18.         f        [Laou;        e        field_5813        field_5813
复制代码
$VALUES的的一个最简单的特点是对应枚举的数组,由此可见符合条件的只有field_5813
于是就有了
  1. @Shadow
  2.         @Mutable
  3.         private static ActionResult[] field_5813;
复制代码
比较难缠的像是Raid$Member这种
  1. c        bhb$b        net/minecraft/class_3765$class_3766        net/minecraft/village/raid/Raid$Member
  2.         m        (Ljava/lang/String;ILaqe;[I)V        <init>        <init>        <init>
  3.                 p        3                        type
  4.                 p        4                        countInWave
  5.         m        ()[Lbhb$b;        a        method_16529        method_16529
  6.         m        (Lbhb$b;)Laqe;        a        method_16526        method_16526
  7.         m        (Lbhb$b;)[I        b        method_16527        method_16527
  8.         m        (Ljava/lang/String;)Lbhb$b;        valueOf        valueOf        valueOf
  9.         m        ()[Lbhb$b;        values        values        values
  10.         f        Lbhb$b;        a        field_16631        VINDICATOR
  11.         f        Lbhb$b;        b        field_16634        EVOKER
  12.         f        Lbhb$b;        c        field_16633        PILLAGER
  13.         f        Lbhb$b;        d        field_16635        WITCH
  14.         f        Lbhb$b;        e        field_16630        RAVAGER
  15.         f        [Lbhb$b;        f        field_16636        VALUES
  16.         f        Laqe;        g        field_16629        type
  17.         f        [I        h        field_16628        countInWave
  18.         f        [Lbhb$b;        i        field_16632        field_16632
复制代码
这玩意居然有两个[Lbhb$b;签名的字段,哪个才是真的呢?
我们知道,$VALUES字段从来不出现在反编译源码中,所以来看一下反编译源码就OK
  1. static enum Member {
  2.       VINDICATOR(EntityType.VINDICATOR, new int[]{0, 0, 2, 0, 1, 4, 2, 5}),
  3.       EVOKER(EntityType.EVOKER, new int[]{0, 0, 0, 0, 0, 1, 1, 2}),
  4.       PILLAGER(EntityType.PILLAGER, new int[]{0, 4, 3, 3, 4, 4, 4, 2}),
  5.       WITCH(EntityType.WITCH, new int[]{0, 0, 0, 0, 3, 0, 0, 1}),
  6.       RAVAGER(EntityType.RAVAGER, new int[]{0, 0, 0, 1, 0, 1, 0, 2});

  7.       private static final Raid.Member[] VALUES = values();
  8.       private final EntityType<? extends RaiderEntity> type;
  9.       private final int[] countInWave;

  10.       private Member(EntityType<? extends RaiderEntity> type, int[] countInWave) {
  11.          this.type = type;
  12.          this.countInWave = countInWave;
  13.       }
  14.    }
复制代码
好啦,那个VALUES是冒牌货,field_16632才是真的,但是鉴于VALUES是在目标类静态块中用values()赋值的,所以也要一同改掉
更加简单的方法,这个不用考虑有没有其他地方出现values()的问题
  1. @Inject(
  2.    method = "<clinit>()V",
  3.    at = @At(
  4.     value = "FIELD",
  5.     shift = At.Shift.AFTER,
  6.     target = "Lnet/minecraft/village/raid/Raid$Member;field_16632:[Lnet/minecraft/village/raid/Raid$Member;"
  7.    )
  8. )
复制代码

上面这段内容的意义是在field_16632填充完枚举之后马上调用,这样接下来values()获取到的就是修改后的副本。

Raid$Member是package-private?使用accessWidener即可https://fabricmc.net/wiki/tutorial:accesswideners(参考翻译https://www.mcbbs.net/forum.php? ... 112082&pid=19803736

完整代码
下面这段代码编辑Raid$Member给袭击的每一轮都添加一个幻术师。
  1. package cavenightingale.mixin;

  2. import net.minecraft.entity.EntityType;
  3. import net.minecraft.entity.raid.RaiderEntity;
  4. import net.minecraft.village.raid.Raid;
  5. import org.spongepowered.asm.mixin.Mixin;
  6. import org.spongepowered.asm.mixin.Mutable;
  7. import org.spongepowered.asm.mixin.Shadow;
  8. import org.spongepowered.asm.mixin.injection.At;
  9. import org.spongepowered.asm.mixin.injection.Inject;
  10. import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

  11. import java.util.Arrays;

  12. @Mixin(Raid.Member.class)
  13. public class RaidMemberMixin {
  14.         RaidMemberMixin(String name, int id, EntityType<? extends RaiderEntity> type, int[] countInWave) {
  15.                 throw new AssertionError("can not happen");
  16.         }

  17.         @Shadow
  18.         @Mutable
  19.         @SuppressWarnings("target")
  20.         private static Raid.Member[] field_16632;

  21.         @Inject(method = "<clinit>()V", at = @At(value = "FIELD", shift = At.Shift.AFTER,
  22.                         target = "Lnet/minecraft/village/raid/Raid$Member;field_16632:[Lnet/minecraft/village/raid/Raid$Member;"))
  23.         private static void injectEnum(CallbackInfo ci) {
  24.                 int ordinal = field_16632.length;
  25.                 field_16632 = Arrays.copyOf(field_16632, ordinal + 1);
  26.                 field_16632[ordinal] = (Raid.Member) (Object)
  27.                                 new RaidMemberMixin("ILLUSIONER", ordinal, EntityType.ILLUSIONER, new int[]{0, 1, 1, 1, 1, 1, 1, 1});
  28.         }
  29. }
复制代码

以及accessWidener
  1. accessWidener        v1        named
  2. accessible        class        net/minecraft/village/raid/Raid$Member
复制代码

效果:幻术师在袭击中生成啦!


既然有accessWidener为啥还要用Mixin改?
accessWidener可以把枚举的构造方法和$VALUES字段改成公开且可变的,然而这样参数列表中没有头两个参数String name, int id,故无法正常调用。你可以自行去试试。

那这个方法相比sun.misc.Unsafe编辑枚举有什么好处?
首先少了又长又臭的一大堆反射代码,其次不会让用户看到编辑final变量时的IllegalAccess的警告。

相比ASM编辑枚举又少写了不少代码,并且ASM编辑枚举最终还是要走Mixin Plugin,因为Fabric不支持纯ASM。

至于ConstructorAccessor,个人认为是最不值得考虑的一种方式,sun.misc.Unsafe虽然在java.unsupported模块里(sun.misc.Unsafe是对jdk.internal.misc.Unsafe封装,后者是没有导出的内部API),好歹还export了,ConstructorAccessor压根没export,除非你能够让用户启动时在JVM参数里加上--add-export ...,否则这个方法基本没得玩(用反射一通乱搞其实也可以,但没必要找虐)



  • 你idea的左侧的Extennal Libraries列表中会有一个yarn-xxx的jar文件,里面的mappings.tiny就是反混淆表




水怪席:@⚡️👮 @🥶❄️☠️ @bleake @伟大的小安 @诡灬稽 @enderman_JC @🐦💕🌸🌸 @RarityEG @800805 @水怪诗人 @小孩孩 @小灬望 @北极仙光 @离⁢⁢⁢⁢ @🌱⛄🐏 @Arrosin @板砖w
来自群组: Cloud Studio 云工坊

评分

参与人数 5人气 +11 金粒 +75 收起 理由
enderman_JC + 2 + 20 神乎其技!6的飞起!
780121zm + 2 + 20 很棒的教程
800805 + 2 + 10 MCBBS有你更精彩~
Eicy + 2 MCBBS有你更精彩~
dengyu + 3 + 25 MCBBS有你更精彩~原创奖励

查看全部评分

火车撞鸟 当前离线
积分
1720
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2016-8-11
查看详细资料
发表于 2021-3-21 08:36:23 | 显示全部楼层
洞穴夜莺 发表于 2021-3-21 08:20
@SuppressWarnings("unresolvable-target")试过了,木有用


正是因为实际上没有多少人这么干所以才说 ...
@SuppressWarnings("unresolvable-target")试过了,木有用


又去看了一眼,unresolvable-target 是原生 Mixin 0.8.3 新加的,Fabric 还没跟进,Fabric Mixin 用 0.8 就有的@SuppressWarnings("target") 应该可以,参见 SuppressedBy

评分

参与人数 1人气 +1 收起 理由
洞穴夜莺 + 1 OK,已补充

查看全部评分

回复

使用道具 举报

火车撞鸟 当前离线
积分
1720
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2016-8-11
查看详细资料
发表于 2021-3-21 07:04:48 | 显示全部楼层
本帖最后由 火车撞鸟 于 2021-3-21 07:14 编辑
这个代码有个问题就是@Shadow那个地方会产生找不到目标类中对应字段的编译警告,但是在运行期它又是可以找到的,我实在没有找到有什么方法可以压制这个警告,官方Mixin的文档和Fabric Mixin文档都对此只字未提,暂时不管它

Mixin 0.8 之后有对 @SuppressWarnings 做过增强,参见 SuppressedBy 。在 @Shadow 字段上使用 @SuppressWarnings("unresolvable-target") 即可。




创建对应静态公开字段是不在讨论范围内的,常规Mixin不允许创建静态公开字段(实际上即使创建了你也不得不通过反射来访问),用plugin可以实现但本帖不作讨论

不少 Mod 添加枚举的常规做法也并不是在目标类中添加新的公开静态字段,而是给在外面自己的类中的新创建的此枚举类型的字段赋值。




同时Mixin类中的static块调用是在目标类的static块调用之后的,所以可以直接在$VALUES上面修改,如果你觉得不放心@Inject到<clinit>的@At("RETURN")上也是一样的

鉴于你下面举的第二个例子,说明 @At("RETURN") 也不是最优的,因为你需要留意枚举类中是否有新的字段使用了 values(),从而需要额外修改,所以我觉得用

  1. @Inject(
  2.     method = "<clinit>()V",
  3.     at = @At(
  4.         value = "FIELD",
  5.         shift = At.Shift.AFTER,
  6.         target = "Lcom/example/ExampleEnum;$VALUES:Lcom/example/ExampleEnum;"
  7.     )
  8. )
复制代码

可能会好一些


评分

参与人数 1人气 +2 收起 理由
洞穴夜莺 + 2 Ssssssssssssssssssss

查看全部评分

回复

使用道具 举报

🥶❄️☠️ 当前离线
积分
9868
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2019-7-8
查看详细资料
头像被屏蔽
发表于 2021-3-21 07:47:08 | 显示全部楼层
这。。。类似c++for嵌套循环?

评分

参与人数 1金粒 -10 收起 理由
dengyu -10 与主题无关

查看全部评分

回复

使用道具 举报

洞穴夜莺 当前离线
积分
11091
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2019-8-18
查看详细资料
 楼主| 发表于 2021-3-21 08:20:45 | 显示全部楼层
火车撞鸟 发表于 2021-3-21 07:04
Mixin 0.8 之后有对 @SuppressWarnings 做过增强,参见 SuppressedBy 。在 @Shadow 字段上使用 @SuppressW ...

@SuppressWarnings("unresolvable-target")试过了,木有用


不少 Mod 添加枚举的常规做法也并不是在目标类中添加新的公开静态字段
正是因为实际上没有多少人这么干所以才说本帖不讨论


好建议
回复

使用道具 举报

海螺螺 当前离线
积分
19628
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2015-2-24
查看详细资料
发表于 2021-3-21 10:42:05 | 显示全部楼层
本帖最后由 海螺螺 于 2021-3-21 10:47 编辑

https://github.com/IzzelAliz/Arc ... htEnumExtender.java

这个代码可以生成没有额外构造方法参数的枚举

当然我没用

https://github.com/IzzelAliz/Arc ... api/EnumHelper.java

反射加,没有警告,反射自己的类是不会有警告的,那几个 jep 只管 jdk 的类

回复

使用道具 举报

洞穴夜莺 当前离线
积分
11091
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2019-8-18
查看详细资料
 楼主| 发表于 2021-3-21 11:55:16 | 显示全部楼层
本帖最后由 洞穴夜莺 于 2021-3-21 13:15 编辑
海螺螺 发表于 2021-3-21 10:42
https://github.com/IzzelAliz/Arc ... htEnumExtender.java

这个代码可以生成没有额外构造方法参数的枚举 ...

主要就是去改final修饰符的时候会有警告
并且Unsafe也是准备要移除的API了

↓Java语言标准压根没有规定java.unsupported模块,把sun.misc.Unsafe塞进一个这样的模块里,就算一段时间内不删除也不支持使用啊

评分

参与人数 1金粒 +1 收起 理由
海螺螺 + 1 CITATION NEEDED

查看全部评分

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 京公网安备 11010502037624号 | 手机版

GMT+8, 2021-6-18 14:34 , Processed in 0.073708 second(s), Total 32, Slave 26 queries, Release: Build.2021.06.13 1634, Gzip On, Redis On.

"Minecraft"以及"我的世界"为Mojang Synergies AB的商标 本站与Mojang以及微软公司没有从属关系

© 2010-2020 我的世界中文论坛 版权所有 本站原创图文内容版权属于原创作者,未经许可不得转载

快速回复 返回顶部 返回列表