作者声明:文章创作过程中使用了AI辅助
本文的正经风格版本:https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility
第一章:为什么要当邪修?
因为正道太累了啊!
你看那些名门正派的Fabric弟子,每天对着Mixin注解磕头拜佛,AccessWidener写得比情书还长,Gradle配置调得比炼丹炉还复杂。结果呢?MC一更新,全部白给!1.21写的模组到26.1直接原地飞升,连个全尸都不留。
而我们邪修不一样。我们没有信仰,没有底线,没有import net.minecraft.*。我们只有一样东西——
反射。
这玩意儿就像修仙界的"万能钥匙",虽然脏、虽然慢、虽然随时可能炸膛,但它能用。编译期看不到MC的类?没关系,运行时硬抢!方法名改了?没关系,挨个试!参数类型不对?没关系,动态代理糊脸!
邪修的核心理念就八个字:活着就行,管他姿势。
第二章:邪修三件套
2.1 常量层:你的"通缉令"
把所有版本里变来变去的类名、方法名、字段名,全部写进一个巨大的常量表里。这张表就是你的"仇人名单",上面记满了MC每次更新欠下的债。
// 邪修の仇人录
static final String C_LEVEL = "net.minecraft.world.level.Level";
static final String C_COMPONENT = "net.minecraft.network.chat.Component";
static final String M_GET_UUID = im("Entity", "getUUID", "()Ljava/util/UUID;", "method_5667");
static final String M_LITERAL = im("Component", "literal", "(Ljava/lang/String;)LComponent;", "method_43470");
记住:业务代码里绝对不能出现任何硬编码的MC类名。一旦出现,你就破了戒,离走火入魔不远了。
2.2 反射层:你的"作案工具"
这一层封装所有脏活累活。Class.forName、Method.invoke、Field.get……这些见不得光的操作全部藏在这里,对外只提供人话接口。
关键技巧:
缓存一切:反射调用一次就够了,第二次还用就是浪费生命。用ConcurrentHashMap存起来,找不到的也要存个NOT_FOUND哨兵,不然每次都重新扫描,JVM会骂你。
描述符必须带:找方法不带描述符等于裸奔。getUUID()混淆后可能是method5667也可能是method5845,一个返回UUID一个返回String,猜错了当场社死。
重载方法要遍历:别傻乎乎只缓存第一个匹配到的方法。把所有同名方法都捞出来,调用时用isInstance逐个验身,谁对得上就用谁。
2.3 业务层:你的"伪装身份"
这一层看起来和正常模组一模一样。getBlockState()、sendMessage()、checkPermission()……全是人话,干干净净。
但只有你自己知道,这些人话底下藏着多少反射。
第三章:邪修实战骚操作
3.1 环境探测:先搞清楚自己在哪
Fabric生产环境用intermediary名,MC 26+又改回named名。你得先知道自己身处哪个平行宇宙:
// 使用神识探查一下当前是不是 named 运行时
static final boolean IS_NAMED = tryLoad("net.minecraft.world.level.Level") != null;
然后查表回退:先试named名,不行就翻"仇人录"找intermediary名。intermediary名跨版本稳定,是你最后的保命符:
3.2 事件监听:动态代理捏脸术
Fabric的事件回调接口里全是MC原生类,编译期根本import不了。怎么办?不implements了,直接捏!
用JDK动态代理在运行时现场造一个假对象塞进去。代理对象收到回调后,把参数打包成Object[]扔给业务层慢慢拆:
这招的精髓在于:只要我不承认依赖存在,依赖就不存在。我们把它叫做薛定谔的类型安全。
3.3 多策略兜底:一条路走到黑不如多条路走到亮
获取方块ID为例:
先试新API BuiltInRegistries,抛异常了?换老API Registries,又抛了?用toString()硬抠字符串(Block{minecraft:diorite} → minecraft:diorite),还不行?返回minecraft:air,假装什么都没发生。
邪修的信条:优雅地失败 > 粗暴地崩溃。
3.4 文本组件兼容:新旧两开花
老版本new TextComponent("hello"),新版本Component.literal("hello")。写个兼容方法:
注意:这里Componentliteral和newTextComponent都是反射拿到的,编译期依然干干净净。邪修的代码,永远不能被编译器抓到把柄。
第四章:邪修禁忌
忌到处散落反射调用:不封装的反射就像没穿衣服上街,迟早出事。
忌信任单一API:MC的承诺比渣男的誓言还不靠谱,永远准备Plan BCD。
忌忽略性能:不做缓存的反射修士,活不过筑基期。
忌忘记哨兵:ConcurrentHashMap不能存null,不设NOT_FOUND占位符,你会陷入无限重试的地狱轮回。
忌跟正道修士争论:他们不懂你的苦,你也不必解释。默默写完兼容代码,看他们的模组在下个版本集体暴毙,然后微微一笑。
终章:邪修的归宿
这套体系搭完,你就能实现:
✅ 编译期零MC依赖
✅ 单JAR通吃1.18到26+
✅ 不用Mixin、不用AW、不看官方脸色
✅ 在别人模组集体阵亡时独自存活
代价是什么?
❌ 代码可读性约等于天书
❌ 调试时堆栈信息像乱码
❌ 新人接手时会诅咒你十八代祖宗
❌ 你永远无法理直气壮地说"我写的是正经模组"
但没关系。
邪修不需要认可,只需要运行。
当你看到自己的模组在数个大版本更新后依然顽强地加载成功,而隔壁正道修士还在Discord里哭诉Mixin冲突时——
那一刻,你就知道,这条路,走对了。