资讯详情

资讯详情

Minecraft Forge模组开发:前置依赖配置与mods.toml实战指南

一说到给模组添加前置很多人第一反应是“我只要在 mods.toml 里写一行 dependencies 不就行了吗”说这话的人要么是只看了半截教程要么是已经被“前置模组”四个字吓退的半途而废者。坦白讲我当初跳进 1.20.1 Forge 模组开发时也被前置关系折腾得不轻——代码写得好好的一启动游戏就弹错误提示缺少某某 mod要么版本不对要么依赖写错最后崩溃报告根本看不懂。这篇教程不会扯高深的加载机制也不会让你去啃一堆英文 wiki。我会用最直白的话告诉你前置模组到底是什么、为什么要加、在 build.gradle 和 mods.toml 里怎么写以及我踩过的那些坑。不管你是第一次写模组的新手还是想要了解软依赖和版本控制的进阶开发者读完都应该能自己动手把依赖声明清楚。1. 前置模组到底是什么它解决的不只是“缺一个文件”1.1 一个生活化的比喻做饭要备好调料如果你做过红烧肉一定知道除了五花肉还得准备生抽、老抽、八角、冰糖。少了调料菜不是不能做但做出来完全是另一个味道缺了关键的调料甚至根本没法下锅。模组里的前置模组其实就是那些“调料”。你的模组可能要用别人的 API 来注册饰品槽、显示配方页面、播放动画或者读取另一个模组的数据。这些功能不是你从 Minecraft 底层一点点造的而是站在其他模组的肩膀上实现的。Forge 在加载模组时会先检查你的“调料清单”——也就是依赖声明——如果发现缺少了某个必须的模组或者版本对不上就直接不让你启动宁可不让你带病运行。1.2 硬依赖与软依赖mandatory 的区别前置模组在声明时有一个非常关键的属性mandatory。它决定这个依赖是“必须有”还是“可没有”。硬依赖mandatorytrue这个模组没有游戏直接崩溃启动时给你弹一个 “Missing or unsupported mandatory dependencies” 的错误连主菜单都进不去。适合那些你的模组核心功能完全依赖别人 API 的情况。软依赖mandatoryfalse没有它游戏照常启动你的模组也能加载只是相关功能会缺失或者降级。典型场景是“如果有 JEI我就加一个配方页如果没有 JEI我的模组照常运行”。很多新手只会写硬依赖结果把一个本可以很轻量的模组搞成了一堆强制要求。设计依赖关系时要想清楚你到底是真的离不开它还是只是想让体验更好1.3 什么时候你需要给自己的模组加前置常见的情况大概有四类你可以对照一下调用别人提供的 API比如你的模组想给 JEI 添加自定义配方的显示面板或者通过 Curios 给玩家加额外饰品槽。这类 API 通常只有对应模组存在时才可用。依赖其他模组的数据或内容比如你的模组要生成某个结构里面使用了另一个模组的方块和实体。需要统一的前置库比如用 GeckoLib 播动画用 Selene 做部分功能库。这些库本身也是一个模组需要玩家安装。需要控制加载顺序你的模组要在某个前置注册完东西之后再执行操作这种情况下即使没有实际调用它的 API前置关系也会确保 Forge 的加载顺序符合预期。2. 开发环境准备工具链正确才算入门2.1 JDK 17 与 Forge 1.20.1 MDK 的对应关系Minecraft 1.20.1 是基于 Java 17 构建的所以你的开发环境必须是 JDK 17 或者更高版本但不能高到不兼容。我见过不少人装了 JDK 8 就去跑 1.20.1 的 MDK结果 Gradle 直接报错连编译都过不去。这里推荐使用 Eclipse Adoptium 的 Temurin 17或者 Zulu 17 都行。安装完以后在命令行里输入java -version确认显示的是 17.x 开头而不是 1.8 或者 11。如果系统里有多个 JDK记得把 JAVA_HOME 指到 17 的那个目录。Forge 1.20.1 对应的版本号是 47.x比如 47.2.0、47.2.20 等。你到 Forge 官网下载 MDK 时选择 1.20.1 版本压缩包里的gradle.properties大约是这样minecraft_version1.20.1 forge_version47.2.20如果你的项目和教程里看到的版本号不一样不用太担心只要 Forge 版本在 47.x 的范围内基础流程完全一致。2.2 初始化 MDK 时最容易翻车的三个点从压缩包里解压 MDK 后我建议你不要自己手动建 Gradle 项目而是直接打开终端在项目根目录运行对应的命令。用 IDEA 的话直接gradlew idea然后导入用 Eclipse 就用gradlew eclipse。但这个环节有几个坑第一个坑是 Gradle 下载慢。首次运行gradlew会从国外仓库拉一大堆依赖如果你网络状况不好可能卡在Downloading半天不动。解决办法是修改build.gradle里的仓库配置加上阿里云或腾讯的镜像源比如把mavenCentral()和maven { url https://maven.minecraftforge.net }前面插入镜像地址。第二个坑是内存不足。Gradle 和 Minecraft 客户端同时跑的时候默认内存可能不够用。建议修改gradle.properties把org.gradle.jvmargs-Xmx4G这种参数调大一些至少 4GB。内存不够时典型症状是编译中途 Gradle 进程被杀或者启动游戏时黑屏闪退。第三个坑是报错看不懂。如果你看到类似于Unable to make field private int java.util.ArrayList.size accessible这种消息多半是 Gradle 版本和当前 JDK 不兼容。Forge 1.20.1 的 MDK 默认 Gradle 版本是 8.x一定要用 JDK 17 而不是 JDK 21 去跑否则容易踩模块访问的边界问题。2.3 第一次跑起带前置模组的开发客户端初始化完成后运行gradlew runClient就能启动一个开发环境客户端。但要注意默认情况下这个客户端只会加载你的模组前置模组不会自动出现。有两种方式把前置模组塞进开发环境在build.gradle的dependencies里加上runtimeOnly依赖。Gradle 会把这个 jar 放进运行时 classpath开发客户端启动时就能加载。把前置模组的 jar 手动复制到run/mods文件夹里和正常玩家安装模组一样。我个人的习惯是两种都做。runtimeOnly保证 Gradle 帮我在干净的开发环境里拉取正确版本run/mods适合临时测试某个从 CurseForge 下载的特殊版本。关于如何配置dependencies下一节详细展开。3. 用 Gradle 把前置模组请进项目依赖坐标与仓库3.1 去哪里找依赖坐标Maven、CurseMaven、Modrinth Maven这是新手最容易一头雾水的地方。你要在前置模组的官方页面找到一个可以用于 Gradle 的“坐标”也就是类似group:artifact:version的东西。第一种作者官方 Maven 仓库很多主流模组作者会主动提供 Maven 仓库。比如 JEI 的宿主 Jared 在https://maven.blamejared.com/上公开了 JEI 的依赖Curios 的作者也在https://maven.theillusivec4.top/上放了相关版本。这种方式最干净、最稳定只要能找到官方的文档或 GitHub 页面优先用这个。第二种Modrinth MavenModrinth 平台提供了一种通用 Maven 地址https://api.modrinth.com/maven。坐标格式是maven.modrinth:slug:version其中slug是模组在 Modrinth 上的链接后缀比如 JEI 就是jei。这种方式的优点是不需要去猜 CurseForge 的 fileId缺点是并非所有作者都允许自己的模组通过 Modrinth Maven 分发所以有些项目会提示 unavailable。第三种CurseMavenCurseForge 本身不提供官方 Maven但社区有 CurseForge Maven 的镜像服务地址是https://www.cursemaven.com。坐标格式是curse.maven:projectId:fileId。projectId是模组在 CurseForge 页面地址里的那串数字fileId是具体文件的编号需要到 Files 页面去找。这个方式非常万能几乎任何 CurseForge 模组都能拉但体验一般某些情况下下载会失败而且文件和页面上的更新状态可能不同步。我建议的优先级是官方 Maven Modrinth Maven CurseMaven。官方 Maven 最可靠CurseMaven 只作为兜底。3.2 build.gradle 完整配置示例JEI Curios假设你的模组需要使用 JEI 的配方接口同时想兼容 Curios 的饰品扩展。在build.gradle里大致是这样的配置repositories { maven { name BlameJared url https://maven.blamejared.com/ } maven { name KFFPG url https://maven.theillusivec4.top/ } maven { name Modrinth url https://api.modrinth.com/maven } } dependencies { minecraft net.minecraftforge:forge:1.20.1-47.2.20 compileOnly fg.deobf(mezz.jei:jei-1.20.1-forge:15.2.0.27) runtimeOnly fg.deobf(mezz.jei:jei-1.20.1-forge:15.2.0.27) implementation fg.deobf(top.theillusivec4.curios:curios-forge:1.20.1-1.3.1.0) }这里有几个细节需要解释。compileOnly表示编译时可用运行时不传递给玩家。JEI 通常在开发时用compileOnly因为玩家自己会装 JEI你不需要把 JEI 打包进你的 mod 里。但runtimeOnly又要加上是为了开发环境的测试方便否则你在 runClient 里看不到 JEI 的界面。implementation则是编译和运行时都用而且不会传递到下游项目。它适合 Curios 这种你希望开发环境里有、但最终发布时仍然让玩家自己装的模组。3.3 fg.deobf 到底帮你做了什么你可能注意到我写的每个模组依赖前面都套了一个fg.deobf(...)。这个函数是 ForgeGradle 提供的作用是反混淆。Minecraft 官方版 jar 里类的命名在不同环境下不一样。模组开发者拿到的 MDK 环境用的是官方命名或中间映射而第三方模组发布时可能使用 SRG 名称或者经过混淆的映射。如果你不加fg.deobf直接引用会发现编译能过但运行时频繁报NoSuchMethodError甚至类名都找不到。fg.deobf会把第三方模组的 jar 重新映射为当前开发环境能识别的名称。它耗时会多一点但避免了无数个深夜崩溃。所以请务必让每个前置模组依赖都套上fg.deobf不要偷懒。4. 向 Forge 宣告“我需要这个模组”mods.toml 与 Mod 注解4.1 mods.toml 里的 dependencies 写法光在build.gradle里配置依赖是不够的那只解决了开发阶段的 classpath 问题。当你的模组发布给玩家时Forge 需要知道你声明了哪些前置关系这时就要写META-INF/mods.toml。MDK 默认会生成一个mods.toml文件里面有modId、version、displayName等字段。你需要在文件的底部加上[[dependencies.你的modid]]这样的数组。假设你的 mod id 是mymod要声明对 JEI 的硬依赖和对 Curios 的软依赖写法如下[[dependencies.mymod]] modIdjei mandatorytrue versionRange[15.0.0,16) orderingNONE sideBOTH [[dependencies.mymod]] modIdcurios mandatoryfalse versionRange[1.20.1-1.3.0,) orderingAFTER sideBOTHmodId必须是前置模组声明在它自己mods.toml里的那个 ID不能是显示名也不能是文件名。大小写也要注意很多模组的 modId 是全小写写错了检查会直接失败。mandatory刚才说过true 表示强制。versionRange是 Maven 风格的范围表达式稍后专门展开讲。ordering控制加载顺序NONE表示不关心前后BEFORE表示你的模组要在它之前加载AFTER表示要在它之后加载。如果你的模组要消费前置模组注册好的数据一般用AFTER比较安全。side表示这个依赖在哪个运行端必须存在。BOTH表示客户端和服务端都必须有CLIENT表示只在客户端强制服务端可以不装SERVER则相反。4.2 Mod 注解一行流写法除了mods.tomlForge 也允许你在主类上通过Mod注解的dependencies参数声明依赖。写法是这样的Mod( value MyMod.MODID, dependencies required:after:jei[15.0.0,);optional:curios[1.20.1-1.3.0,) ) public class MyMod { public static final String MODID mymod; }这里的dependencies字符串由多个条目组成条目之间用英文分号;分隔。每个条目开头是required或optional表示硬依赖还是软依赖。optional后可以不跟版本范围表明任意版本都行。不过我的建议是核心依赖关系尽量写在mods.toml里因为它的格式更严谨、字段更丰富IDE 和社区工具对mods.toml的检查和提示也更好。Mod注解的字符串方式是真的方便但一旦写错很容易被忽略。4.3 Maven 版本范围语法[]、()、逗号的含义这是很多人会卡住的地方。Forge 的版本范围沿用了 Maven 的区间语法我给你列一份速查表。写法含义[1.0]精确等于 1.0[1.0,2.0)大于等于 1.0 且小于 2.0[1.0,)大于等于 1.0上界不限(,1.0]小于等于 1.0(1.0,2.0]大于 1.0 且小于等于 2.01.0只有软依赖时可能用表示任意版本或未指定其中的方括号[表示包含边界圆括号(表示不包含边界。版本号按语义化版本比较1.20.1 这种不是纯数字的点分结构也能比较。我建议大家在声明硬依赖时给一个相对开放的区间而不是精确版本。比如依赖 JEI与其写[15.2.0.27]锁死在某个内部构建号不如写[15.0.0,16)表示“15.x 的 JEI 都行”。这样玩家装了其他 15.x 小版本也能正常运行避免无意义的兼容问题。4.4 加载顺序 ordering 与运行端 side 的选择ordering看起来只是锦上添花但在某些场景下是致命的。比如你的模组在FMLCommonSetupEvent里要读取另一个模组注册的方块状态而这个前置模组在同一个事件里也在做注册谁先跑谁后跑就决定了你是否能拿到数据。如果你不确定就先用AFTER要求前置模组先加载。side的选择则决定了多人联机时服务器和客户端的依赖要求。如果你的前置模组是纯客户端模组比如某些光影前置、小地图美化库那么服务端根本不加载它。这种情况下如果你在mods.toml里写了sideBOTH服务端就会连带报错反过来玩家也进不了服务器。这也是不少整合包崩溃的头号原因。如果你是单人开发、不搞联机那BOTH通常不会出问题。但只要你想把模组发布到多人环境就要认真考虑这个字段客户端依赖写CLIENT服务端依赖写SERVER两头都要就写BOTH。5. 别只在配置里写死代码里的软依赖兼容写法5.1 运行时检查前置是否加载声明依赖只是让 Forge 在启动时检查但你的代码仍然需要在恰当的时候判断前置是否存在特别是在做软依赖时。Forge 提供了ModList.get().isLoaded(前置modid)这个方法。一个常见的误区是在模组构造函数里直接调用它。构造函数执行时模组列表还没完全准备好这时候查询可能拿到不完整的结果。更稳妥的做法是放在FMLCommonSetupEvent或者更晚的事件里Mod.EventBusSubscriber(modid MyMod.MODID, bus Mod.EventBusSubscriber.Bus.MOD) public class CommonSetupHandler { SubscribeEvent public static void onCommonSetup(FMLCommonSetupEvent event) { event.enqueueWork(() - { if (ModList.get().isLoaded(jei)) { // 执行依赖 JEI 的初始化 } if (ModList.get().isLoaded(curios)) { // 执行依赖 Curios 的初始化 } }); } }event.enqueueWork是 Forge 要求的线程调度操作它把任务放到主线程执行。如果你的初始化逻辑涉及注册类对象或访问大多数游戏 API请务必包一层enqueueWork。5.2 用反射调前置 API 的通用模板你可能会问如果前置是可选的但我在代码里直接import了它的类编译器不就报错了吗确实会。所以要实现真正的软依赖要么用OnlyIn之类的注解规避要么用反射。反射是更通用的思路模板大致如下if (ModList.get().isLoaded(jei)) { try { Class? runtimeClass Class.forName(mezz.jei.api.runtime.IJeiRuntime); // 这里通过你自己的入口获取 JEI 运行时对象 // 以下只是示意具体请参考该 API 的官方文档 Object runtime SomeBridge.getRuntime(); // 对 runtime 做业务逻辑 } catch (ClassNotFoundException e) { LOGGER.error(JEI 已加载但是找不到对应的类可能版本不匹配, e); } }用反射的好处是即使前置模组没装你的代码也不会在类加载阶段直接爆炸。缺点是你失去了编译期类型检查字段、方法名写错了只能在运行时报错。所以反射代码一定要加日志出问题后能立刻定位。5.3 InterModCommsmod 和 mod 之间怎么传话除了直接调用 APIForge 还提供了一种低耦合的跨模组机制InterModComms。它的思路很简单你不需要知道对方的类只需要往对方的 modId 发一条消息并附上一个回调对象。对方在某个时机接收这些消息并处理。示例代码大概是这样的InterModComms.sendTo(目标modid, 某个事件key, () - 你要传过去的对象);接收方在自己的初始化逻辑里通过InterModComms.getMessages(自己的modid)拿到消息流然后读取数据。这个模式非常适合那种“我不关心谁实现了只要有人提供给我就行”的扩展场景。不过我要提醒的是每种 IMC 消息的 key 和对象格式完全由接收方决定。你在使用前一定要查目标 mod 的官方文档不要凭空猜测。很多模组虽然有 IMC 接口但因为没有统一文档实际用起来处处是坑。6. 开发到上线会碰见的怪问题我的排查笔记6.1 编译通过但启动时提示“Missing or unsupported mandatory dependencies”这个错误几乎是每个模组开发者的第一个拦路虎。编译没问题说明 classpath 里的依赖是齐全的启动时却报缺依赖说明检查逻辑走的是mods.toml。我建议按如下顺序排查确认mods.toml里的modId跟前置模组实际注册的 modId 一致。大小写、下划线、连字符都会导致不匹配。确认versionRange包含了当前前置模组的实际版本。如果你写的是[15.0.0,16)但实际安装的是 16.0.0自然会判定为不支持。看看崩溃日志开头是否列出了“Detected mods”的表格。如果前置压根不在列表里说明它没有被加载不是你的范围写错。如果你同时在Mod注解和mods.toml里都写了 dependencies记得保持二者一致。某些情况下冲突的声明会把 Forge 绕晕。6.2 前置模组没在客户端/服务端同步导致崩溃这个坑很隐蔽。本地单人测试一切正常但一开服务器服务端就报缺少某个模组。原因多半是你把仅客户端依赖写成了sideBOTH。正确的做法是分析你的模组代码确认它到底在哪一端需要依赖。如果某个前置只用于渲染、客户端 UI 或客户端事件就在mods.toml里写sideCLIENT。如果服务端也需要处理逻辑比如饰品槽影响了战斗计算那就必须两边都有通常写BOTH。如果同一个模组在不同端有不同需求你可以考虑在代码里用OnlyIn(Dist.CLIENT)标记客户端专属方法同时在 mods.toml 里把依赖声明为CLIENT。这样服务端即使没有前置也不至于因为类加载问题直接崩溃。6.3 vcruntime140_1.dll 缺失跟 Java 无关但确实会挡住启动这个热词之所以会和“我的世界”绑定是因为很多玩家或开发者在启动游戏时突然弹出一个由于找不到vcruntime140_1.dll无法继续执行代码的提示第一反应就是去重装 Java。但实际上这个 dll 是Microsoft Visual C 运行库的一部分跟 Java 本身没有直接关系。Minecraft 启动器、部分显卡驱动组件、甚至 Gradle 调用某些原生工具时都可能间接依赖它。解决方法是到微软官网下载Microsoft Visual C 2015-2022 Redistributable x64安装后重启电脑。绝大多数情况下这个错误就消失了。如果你是开发者跑gradlew时偶尔遇到奇怪的进程崩溃检查一下系统是否装齐了运行库也是很好的排错思路。6.4 高清修复OptiFine与 Forge 前置共存的两个坑“高清修复 java版我的世界”也是经常和 Forge 模组开发扯到一起的话题。OptiFine 在 1.12.2 时代几乎万能但在 1.20.1 的 Forge 环境下它和很多模组前置的兼容性开始捉襟见肘。第一个坑是类加载冲突。OptiFine 会改变部分渲染渲染管线和类加载行为可能导致你的模组在调用某个前置 API 时出现奇怪的ClassCastException或NoClassDefFoundError。如果你在开了 OptiFine 的客户端上调试模组出了问题先关掉它再复现一次能省下大量无用功。第二个坑是前置和 OptiFine 的功能重叠。比如某些光影前置要求使用 Oculus或者某些优化模组和 OptiFine 的渲染钩子互相覆盖。在开发阶段建议你用 Embeddium 这类 Forge 向优化替代方案来做测试而不是默认 OptiFine。等正式发布时给玩家说明你测试过的环境也是一种负责任的做法。6.5 开发环境 Gradle 依赖导致的白屏与崩溃如果你在build.gradle里写了前置依赖但运行 runClient 时游戏直接白屏或退出先检查是不是漏了fg.deobf。没有反混淆时依赖里的方法名可能是func_123456_a这样的映射名编译期可能正好撞上旧的缓存运行时就炸了。另一个常见问题是缓存里的旧版本。Gradle 会把依赖缓存到本地如果你改了版本号但发现还是加载旧版本清理一下.gradle/caches里相应模块的缓存再重新运行即可。别问我怎么知道的这种事我干过至少三次。7. 我从 1.12 到 1.20.1 摸出来的几条经验最后分享一些和流程无关的个人体会希望能让你少走弯路。尽量用 mods.toml 而不是 Mod 注解来声明依赖。我早期写 1.12 模组时习惯了注解字符串到了 1.20.1 总觉得没什么差别。但后来多次遇到 IDE 不提示、大小写潜移默化写错、日志里报错位置不清等问题最后还是老老实实回到 mods.toml。它的字段更透明配合社区工具也更容易检查。版本范围宁宽松勿严苛。我见过有朋友把 JEI 版本锁死在某个快照版本上结果玩家只要装了稍微新一点的 JEI 就启动失败。与其这样不如给出一个合理的范围比如[15.0.0,16)让玩家有更大的兼容空间。当然如果你的确依赖了某个新 API那范围的下界还是要正确抬高。发布模组时不要忘了把前置关系写到页面上。很多新手只知道在 mods.toml 里写但发布到 CurseForge 或 Modrinth 时没有在页面的 “Relations / Dependencies” 里设置。玩家用整合包工具安装时工具是读页面关系的而不是读 mods.toml这样就会导致玩家根本不知道还需要装那些前置。把两者都维护好你的模组才能被顺畅地安装。不要在模组包内直接复制前置 jar 再分发。这个问题涉及许可证。虽然技术上可行但很多模组作者不允许自己的 jar 被重新打包进其他模组的下载文件里。最稳妥的做法是只声明依赖让玩家通过启动器自动下载前置。如果一定要发整合包请确认每个前置模组的许可证允许这样做并在发布说明里注明。跳进模组开发这几年我最大的感受是前置模组不是麻烦而是一种分工协作的方式。它让每个开发者可以专注做自己的亮点同时借用社区已经成熟的能力。把依赖关系管理好你的模组会更稳定用户的安装体验也会舒服得多。如果你在其他环节遇到奇怪的崩溃欢迎拿着日志慢慢比对绝大多数问题都在这些基础检查里能找到答案。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →