Skip to content

工具配置、模组结构、Json语法和mod.json

"工欲善其事,必先利其器。"

欢迎来到 JSON 模组开发的第一步。在这一章里,我们将从零开始,先给你配好一套舒服的写作环境,再带你把一个模组从里到外拆开看一遍,最后亲手写下模组的"身份证"——mod.json。学完这一章,你手里会有一个能被游戏认出来的最小模组骨架,后面所有章节的内容都会往里填。

学习本章需要具备的基础:

  • 知道怎么对文件和文件夹进行压缩与解压;
  • 能完成基本的文件扩展名修改。

本教程以 Mindustry v8 主线的源码为唯一依据,凡引用的字段、默认值、行为,都直接来自 D:\Pyrachan\MindustryD:\Pyrachan\Arc 的源码,并标注出处。


工具配置:告别记事本

JSON(JavaScript Object Notation)是一种标记语言,它只负责存数据,不包含任何业务逻辑。所以写 JSON 模组不需要装 IDE,一个像样的文本编辑器就足够了。

注意

绝对不要用 Windows 自带的记事本(notepad.exe)!它会在文件开头悄悄塞进 BOM 头、又容易搞出编码问题,导致游戏读不懂你的文件而频频报错。

我们真正需要的是三个能力:代码高亮括号自动补全代码格式化。按平台推荐:

  • 桌面端(Windows/macOS/Linux):首推 Visual Studio Code (VSCode),免费、轻量、插件生态强。解压打包推荐 7-ZipBandizip
  • 安卓端MT 管理器Squircle CE,自带高亮,还能直接在手机上改 .zip 模组包。
  • iOS 端Spec Editor,衍生自 VSCode,支持查错与格式化。

模组文件结构:以《饱和火力》为例

一个 JSON 模组,本质上就是一个按约定组织好的文件夹,发布时通常打成 .zip。游戏启动时会按固定的路径约定去读这些文件夹,从而加载你的内容。这些约定写在源码 mindustry/mod/Mods.java 里,不是靠口耳相传。

标准 JSON 模组的目录结构如下,按需创建,不必一次建全:

  • mod.jsonmod.hjson必需):模组配置文件,身份信息都在这。
  • content/:存放所有 JSON 数据的核心文件夹,下面再按内容类型分目录。
  • sprites/:模组自己的贴图文件夹,放你画的 PNG。
  • sprites-override/:用来覆盖原版贴图的文件夹。
  • bundles/:多语言翻译文件,放 bundle*.properties
  • scripts/:JavaScript 脚本,实现 JSON 做不到的特殊逻辑。
  • icon.pngpreview.png:模组在游戏模组列表里的图标。

先分清"文件夹"和"文件"

上面这些名字里,凡是以 / 结尾的(content/sprites/bundles/……)都是文件夹(目录),不是文件——/ 就是"这是个装东西的容器"的标志,读到它就该明白"里面要放东西"。

而且 content/ 里面只能放内容类型的子文件夹items/blocks/liquids/……)。JSON 内容文件必须进到对应的子文件夹里;直接丢在 content/ 根目录下的 JSON 不会被加载(源码 Mods.loadContent 只扫描以 ContentType 命名的子文件夹)。

用《饱和火力 3.3.2.1》来对照。解压它的安装包,你会看到 SFcontent/sprites/bundles/mod.json,还有 classes.dexkotlin/ 这些 Java 模组才有的东西。它的 mod.json 长这样:

json
{
  "name": "饱和火力",
  "displayName": "Saturation firepower",
  "java": true,
  "main": "sf.SF",
  "author": "Cry0flu1d",
  "version": "3.3.2.1",
  "minGameVersion": "155"
}

进阶拓展:《饱和火力》的特殊结构

注意到两点:一是 "java": true"main": "sf.SF",说明它是个 Java 模组——classes.dexkotlin/ 就是编译出来的代码;二是它的方块、物品没有放在 content/,而是放在了自定义的 SFcontent/ 下。

这是因为它有 Java 代码撑腰,可以在代码里自定义内容读取路径。但对纯 JSON 模组而言,你没有这段代码,必须严格遵守原版的 content/ 约定。我们在后面的章节会专门回来看这个"自定义路径"是怎么做到的。

content/ 下的内容目录

content/ 里面只放内容类型的子文件夹。游戏会按 mindustry/ctype/ContentType.java 里登记的内容类型(ContentType),逐个扫描对应的子文件夹(源码 Mods.loadContent)。

但有一条更关键的规则要记住:"有文件夹"不等于"能写 JSON"。一个文件夹里的 JSON 到底能不能被解析,最终看的是 ContentParser.parsers 表(ContentType → 解析器)里有没有注册对应内容类型;没有注册的,放进 JSON 会直接报错 No parsers for content type '...'(源码 ContentParser.parse 第 1037 行)。v8 里能注册为独立 JSON 内容的类型如下:

内容类型文件夹名存什么
Item 物品items铜、铅之类
Block 方块blocks工厂、炮塔、钻头
Liquid 流体liquids水、石油、气体
StatusEffect 状态效果statuses(旧名 status 也行)燃烧、冰冻
UnitType 单位units机甲、飞机
Weather 天气weather(旧名 weathers 也行)下雨、下孢子
SectorPreset 区块sectors战役里的区块预设
Planet 星球planets自定义星球
TeamEntry 队伍teams自定义队伍

三个"看着能放、其实不能"的文件夹

ContentType 还定义了 bulletsunitCommandsunitStances 三个文件夹,但 ContentParser.parsers 里没有注册它们的解析器——往里面放 JSON 会直接报错:

  • 子弹(BulletType):不能单独成文件,只能内联在炮塔的 ammoTypes/shootType、单位武器的 bullet 字段里(后面的章节会看到);
  • 单位指令(UnitCommand)/ 单位姿态(UnitStance):是代码定义的内容,JSON 模组无法新增,只能引用现成的。

内容文件里能写哪些键?

一个内容 JSON 文件里能写什么键,同样由 ContentParser 说了算:一是该内容类型的解析器(上面那张表),二是 readFields 反射读到的目标 Java 类的公开字段;遇到 EffectSoundColorDrawPartInterp 这类特殊类型的字段,再由 classParsers 表(字段类型 → 解析器)里的专用解析器接手。换句话说:想查一个内容能写什么,就去翻它对应的 Java 类(第九章会教完整的反查方法)。

一个小坑

  • bundlesspritessprites-override.git 这几个文件夹被游戏标记为特殊文件夹(源码 Mods.specialFolders),不会被当成内容扫描,别把 JSON 塞进它们。

文件名 = 内容名,别撞原版!

ContentParser 会拿文件名(不含扩展名)去比对已存在的内容。如果你的文件名恰好和某个原版内容同名(比如 copper.jsonduo.json),游戏不会新建内容,而是把你的字段覆盖到原版内容上——这就是"补丁(patch)机制",《饱和火力》改原版方块靠的就是它。

对初学者最重要的提醒:新建内容时,文件名要避开所有原版内部名,否则你以为是"新建",实际是"改原版"。下一章讲内部名时会展开讲这条规则。


JSON 与 Hjson:Mindustry 的方言

嘴上说"写 JSON 模组",实际上 Mindustry 支持的是更宽松、更好写的方言——Hjson(Human JSON)。它省掉了一大堆标点,让人读起来舒服得多。看一组对比:

json
{
  "type": "GenericCrafter",
  "description": "Hello world!",
  "health": 100,
  "hasItems": true,
  "requirements": [
    { "item": "copper", "amount": 10 },
    { "item": "lead", "amount": 10 }
  ],
  "research": {
    "parent": "copper-wall"
  }
}
hjson
{
  // 单行注释:定义方块类型
  type: GenericCrafter
  description: Hello world!
  health: 100
  hasItems: true

  /*
    多行注释:定义建造资源需求
  */
  requirements: [
    copper/10
    lead/10
  ]

  research: {
    parent: copper-wall
  }
}

从对比里能看出三处不同:

  1. 省略引号与逗号:大多数情况下双引号可省,每行结尾的逗号可省。
  2. 支持注释:标准 JSON 不允许注释,但 Hjson 支持 // 单行注释和 /* ... */ 多行注释,方便留备忘。
  3. Mindustry 的缩写糖copper/10 这个写法,等价于 {"item": "copper", "amount": 10}。这不是 Hjson 语法,而是 Mindustry 自己的解析器 ContentParser 提供的(源码 ContentParser.java 里有专门的 item/amountliquid/amountpayloaditem/amount 分支)。它的完整原理我们放到讲 ContentParser 的章节再拆。

最佳实践:标点务必半角

Hjson 给了很大自由,但有一条铁律:所有标点(冒号 :、大括号 {}、方括号 [])必须是英文半角符号

根据无数前人的血泪,游戏里一半以上的解析报错,都来自误用了中文冒号 ,或者漏写了结尾的 }。编辑器的高亮和括号匹配,能帮你第一时间发现这些。


编写 mod.json:模组的身份证

理论到此为止,现在动手。在你喜欢的位置新建一个文件夹(比如 my-first-mod),在里面新建文本文件 mod.json

mod.json 会被 Mods.findMeta 读进一个 ModMeta 对象(源码 Mods.javaModMeta 类)。它支持的字段不少,但刚开始只需要六个。请打开编辑器,输入:

json
{
  "name": "my-first-mod",
  "displayName": "我的第一个模组",
  "author": "你的名字",
  "description": "这是我跟着教程制作的第一个 Mindustry 模组。",
  "version": "1.0",
  "minGameVersion": "155"
}

逐项解释:

  • name内部名称,最重要的一个字段。游戏会把它转成小写、空格转连字符——源码 ModMeta.cleanupinternalName = name.toLowerCase().replace(" ", "-")。后续引用你自己的贴图、物品时,到处都要用到这个名字,强烈建议只用小写英文字母和连字符,从源头避免麻烦。
  • displayName:玩家看到的显示名,随便中文。
  • authordescription:你的大名和模组介绍。
  • version模组版本号,一个普通字符串(不写默认 "0")。它有两个用途:一是展示——模组列表、崩溃报告、服务器列表里都会以 模组名:版本 的形式显示它(源码 Mods.modNames);二是更新检查——当你在 mod.json 里填了 repo(GitHub 仓库)时,游戏会用语义化版本比较(源码 Strings.checkNewerSemver)拿它和仓库最新 release 对比,发现新版本就提示更新。所以格式虽然随便写都能跑,但建议用 1.01.2.3 这类 主版本.次版本.修订号 的形式,更新检查才靠谱。另外注意它不能包含换行(源码 ModMeta 会把第一个换行之后的内容截掉)。
  • minGameVersion最低游戏版本。它的格式是"build 号"或"build.修订号"(源码 Version.isAtLeast. 拆成整数比较)。游戏里显示的版本号就是你该填的依据。

版本号从哪来?

  • v7 稳定期的 build 号大致是 136~146v8147 开始往后。源码里 Vars.minModGameVersion = 136Vars.minJavaModGameVersion = 154,即:JSON 模组最早支持到 136,Java 模组最早到 154。
  • 上面的例子填 155,正是《饱和火力》用的 v8 build 号。实践时填你自己实测过的最低版本,别照抄。

先别贪多,其余字段以后用到再说

mod.json 还有 javamainrepodependencieshidden 等十来个可选字段,初学阶段一个都用不上。它们的作用整理在下面的进阶小节里,等真需要了(比如做 Java 模组、上架 GitHub)再回来查。

进阶:mod.json 完整字段速查

ModMeta 支持的全部字段如下,标注了类型与默认值。其中带 java/main 的是 Java 模组专属,带 repo/dependencies 的是发布相关,初学阶段全部跳过:

字段类型默认值说明
name字符串无(必填)内部名称,模组的唯一标识。游戏会把它转小写、空格换成连字符,得到 internalName
displayName字符串等于 name玩家在模组列表看到的显示名,可任意用中文
description字符串详细介绍,支持换行与颜色标签
subtitle字符串一句话副标题
author字符串作者名
version字符串"0"模组版本号,建议语义化版本如 1.2.3
minGameVersion字符串"0"最低游戏版本(build 号)
java布尔false是否为 Java 模组
main字符串Java 模组的主类,如 sf.SF
iosCompatible布尔false纯脚本模组是否兼容 iOS
repo字符串GitHub 仓库 作者/仓库名,用于游戏内更新
dependencies字符串数组强依赖的其他模组名
softDependencies字符串数组软依赖(没有也能跑)
hidden布尔false隐藏模组,只在服务器端/客户端生效,不能加内容
texturescale浮点1.0贴图缩放,值为"1x1 方块贴图的像素边长"
pregenerated布尔false贴图已预处理,跳过渗色与图标生成
contentOrder字符串数组指定内容加载顺序
legacyCompatible布尔false来自旧大版本、但仍兼容新版

效果验证

写完保存。选中 mod.json(以后有了 content/ 等文件夹,就一并全选),直接压缩成 .zip

::: fatal 常见错误:多套了一层文件夹 压缩时请直接全选文件再压缩,保证打开压缩包第一眼看到的就是 mod.json

如果打开压缩包先看到一个文件夹(比如 my-first-mod/),点进去才有 mod.json,游戏会报 No mod.json found,拒绝加载。 :::

进阶:v8 对"多套一层"的宽容

v8 的源码在 Mods.resolveRoot 里做了个小优化:如果压缩包根目录下只有唯一一个文件夹,它会自动钻进去找 mod.json。所以"恰好只套一层"现在也能加载了。

但别因此养成坏习惯——一旦压缩包里除了那个文件夹之外还有别的文件,这个宽容就失效。永远直接全选文件打包,最省心。

.zip 放进游戏的 mods 目录(游戏内"模组"界面可以一键打开这个目录),在模组列表里启用它,看到显示名"我的第一个模组"亮起,就说明你的第一张身份证办下来了。


小结

这一章我们做了三件事:配好了写作工具,摸清了模组的目录骨架(content/ + 一堆特殊文件夹),并写下了能被游戏识别的 mod.json。其中 name 内部名和 minGameVersion 最低游戏版本是两个最容易踩坑、也最常被后来引用的概念。

思考题content/ 下放物品的文件夹叫 items,放状态效果的文件夹,正式名是什么?为什么 statusesstatus 两个名字都能用?

下一章,我们往 content/items/ 里写下第一个真正属于你自己的内容——物品,并让它有名字、有贴图、能被科技树研究出来。