跳转至

21 发布

mod 在自己机器上跑通之后,下一步是让别的玩家也能用。

发布意味着什么

发布之后,使用环境和本地开发不同:

  • 其他玩家拿到的是固定的文件包,没有你的编辑器和调试习惯。
  • 出问题时他们看到的是日志里的错误消息,不是源码。
  • 你以后更新时,他们本地已经有旧版配置文件。

发布包需要让别人能安装、运行、更新和卸载。这个要求会反过来影响文件结构、版本号、配置和日志。


Mod 身份:元数据

附加到入口类的元数据是 BML+ 识别和管理 Mod 的依据:

[bml.mod id="complete.mod" name="Complete Mod" version="1.0.0" author="YourName" bml="0.3.13" description="A complete tutorial mod"]
class CompleteMod {
}

各字段含义

字段 作用 要求
id 机器标识。配置文件名、日志前缀、依赖声明全靠它 发布后永远不改
name 在 mod 列表里显示的名字 可以随版本调整
version 当前版本号 每次发布必须递增
author 作者名 可以写多个,用逗号分隔
bml 需要的最低 BML 版本 用了新 API 时必须更新
description 一句话功能描述 控制在 80 字符以内

id 的选择规则

id 相当于 mod 的身份证号。BML 用它决定配置文件路径(ModLoader/Configs/<id>.cfg)、日志标记、依赖声明。

好的 id 是小写字母加点号,例如 fps.displayball.spawnerlevel.timer。避免中文、空格或特殊符号。

一旦发布第一个版本,id 就固定了。改了 id,BML 会把它当作另一个 mod,旧配置变成孤儿,用户设置全部丢失。


版本号

使用三段式版本号:主版本.次版本.修订版本

什么时候改哪个数字

你做了什么 版本变化 示例
修 bug、改措辞、修正日志格式 修订 +1 1.0.0 -> 1.0.1
加新功能(命令、配置项),旧行为不变 次版本 +1,修订归零 1.0.0 -> 1.1.0
删除命令、改变配置含义、改变默认行为 主版本 +1,其余归零 1.0.0 -> 2.0.0

具体场景

假设你的 mod 当前是 1.2.3

  • 发现窗口位置计算有误,修好了 -> 1.2.4
  • 加了一个 /cmod reset 命令 -> 1.3.0
  • 把配置项 ShowWindow 从布尔改成了整数(0=隐藏、1=小窗、2=大窗)-> 2.0.0

主版本号变化意味着"用户可能需要重新配置"。这是对用户最重要的信号。


BML+ 版本要求

bml="0.3.13" 表示需要 BML+ 0.3.13 或更高版本。版本不满足时 Mod 不会被加载,日志会提示不匹配。

判断方法:检查用到的 API 是在哪个版本引入的。基础回调(OnLoadOnUnloadGetConfig)可以保持较低要求;用了新 API 就更新到对应版本。不确定时写你开发环境的 BML 版本,至少保证测试过的环境匹配。


文件结构

命名约定

BML 通过 .mod.as 后缀识别入口文件。一个 mod 有且只有一个 .mod.as 文件。文件名通常和 mod 名称对应:

CompleteMod.mod.as

其他辅助脚本文件使用 .as 后缀(不带 .mod),它们不会被 BML 当作独立 mod 加载。

单文件发布

最直接的方式。适合代码量小(几百行以内)的 mod:

CompleteMod.mod.as

用户把这个文件放入 ModLoader/Mods/ 就能用。

目录发布

代码量大或需要分模块时使用目录结构:

CompleteMod/
  CompleteMod.mod.as
  libs/
    Helper.as
  README.md

规则:目录里有且只能有一个 *.mod.as 入口文件。BML 扫描 ModLoader/Mods/ 时,如果遇到子目录,会进入其中寻找入口。

zip 包分发

正式发布目录形式的 Mod 时,使用 BML+ SDK 随附的打包脚本:

Set-Location "<Ballance>/ModLoader/Mods/CompleteMod"
& "<BML-SDK>/scripts/Pack-BMLScriptMod.ps1" -Force

默认产物是 dist/CompleteMod.zip。自动化流程仍可通过 -Source-Output 指定其他路径。源目录的顶层必须恰好有一个 *.mod.as 入口;打包脚本会保留 相对路径,并让入口位于 zip 根目录:

CompleteMod.zip
  CompleteMod.mod.as
  libs/
    Helper.as
  README.md

用户把 CompleteMod.zip 直接放入 ModLoader/Mods/ 即可,不需要手动解压。 .bmodp 只用于原生 DLL Mod,不能作为脚本 Mod 的扩展名。

打包器会自动排除 dist、编辑器设置、版本控制元数据、as.predefined 和 Python 缓存文件。下面的源码与运行行为检查仍需作者自己完成。


不应包含的内容

打包前检查一遍,确保没有把以下内容混入发布包:

不该出现的 原因
本机绝对路径 别人的电脑路径不同
调试专用代码(每帧日志输出或测试命令) 干扰用户体验和日志阅读
编辑器临时文件(.swp.bak~ 结尾文件) 与发布无关
你的 BML 配置文件夹 包含你的个人设置
编译产物或日志文件 无用且可能很大

如果你有仅开发时使用的测试命令,在发布前移除,或者用配置项默认关闭。不要让玩家在正常使用时看到开发入口。

发布前测试

在自己的开发环境跑通不够。你需要模拟"一个从未见过这个 mod 的用户"的安装体验。

步骤 1:清理现有状态

  • 删掉 ModLoader/Configs/ 下你的 mod 配置文件
  • 删掉游戏目录中任何你 mod 创建的临时文件
  • 目的:确认首次运行时,mod 不依赖遗留配置

步骤 2:使用打包后的文件安装

不要直接使用开发目录里的源码。在一个干净的 ModLoader/Mods/ 中只放入打包 得到的 CompleteMod.zip。先移走相同 id 的开发目录或单文件,避免 BML 同时 发现两个副本。zip 包默认不参与文件监视,最终验证时应重新启动 Player。

步骤 3:启动游戏并检查日志

打开 ModLoader/ModLoader.log,确认:

  • mod 被正确识别并加载(应该看到带 id 的加载消息)
  • 没有脚本编译错误
  • 没有 "null handle" 或 "undefined variable" 警告
  • 如果有命令,输入命令能正常响应

步骤 4:完整游戏流程测试

  • 进入关卡 -> 退出关卡 -> 重新进入 -> 退出游戏
  • 确认每个阶段 mod 的行为正常,没有残留对象或崩溃
  • 如果 mod 有 OnUnload 清理逻辑,确认卸载时不报错

如果你声明了 bml="0.3.13" 但手头只有更新的 BML 版本,至少在发布说明中注明实际测试的版本。


发布说明

每个版本都应该附带说明文档。发布说明写给"准备安装这个 mod 的玩家"看,内容要从玩家视角出发。

模板

Complete Mod v1.0.0

需要:BML+ 0.3.13 或更新版本。
安装:把 CompleteMod.zip 直接放入 ModLoader/Mods/,不要解压。
命令:cmod [spawn|despawn|push|status|window]
配置:ShowWindow, AutoSpawn
卸载:删除 mod 文件。如需清掉设置,删除 ModLoader/Configs/complete.mod.cfg。
已验证:Ballance Player + BML+ 0.3.13

每一条都是对用户的承诺:安装路径要写完整(不要假设用户知道 Mods 在哪);命令和配置要列全(用户不一定会看源码);卸载说明要包含配置文件路径。

更新说明

从第二个版本开始,还应包含变更内容:

Complete Mod v1.1.0

新增:/cmod reset 命令,重置所有生成的对象
修复:退出关卡时偶尔出现的空指针错误
配置:新增 MaxObjects 项(默认值 10)
注意:旧版配置自动兼容,无需手动修改

配置迁移

默认值由代码负责

配置的默认值必须在代码中设置:

showWindowProp.SetDefaultBoolean(true);

不要通过分发默认配置文件来提供初始值。原因:用户更新 mod 后,如果你的新配置文件覆盖了他的自定义设置,他会丢失个人配置。

改名时兼容旧 key

如果新版本需要修改配置项名称(例如从 OldKey 改为 NewKey),必须写迁移代码:

if (config.HasKey("MyMod", "OldKey") && !config.HasKey("MyMod", "NewKey")) {
    BML::ConfigProperty@ old = config.GetProperty("MyMod", "OldKey");
    BML::ConfigProperty@ new = config.GetProperty("MyMod", "NewKey");
    if (old !is null && new !is null) {
        new.SetBoolean(old.GetBoolean(true));
    }
}

这段代码的逻辑:

  1. 检查旧 key 是否存在且新 key 尚未创建
  2. 如果条件满足,读取旧值写入新 key
  3. 使用 @ 句柄和 !is null 检查防止空引用

这样做的好处:用户从旧版更新后,已有设置会自动迁移到新配置项,无需手动操作。

什么时候不需要迁移

  • 新增一个以前没有的配置项,直接设默认值即可
  • 删除一个配置项,旧值留在文件里不影响运行,无需特殊处理

风险分级与用户沟通

风险等级

发布前确认你的 mod 属于哪个等级:

等级 行为范围 举例
低风险 只读取游戏状态、显示额外信息 FPS 显示、日志观察窗口
中风险 创建或删除自己的对象 木球生成器、临时标记物
高风险 修改原版数据、移动玩家控制的球、改变行为图 重力修改、关卡数据编辑

如何向用户传达风险

低风险 mod 通常不需要特别说明。中、高风险 mod 应在发布说明中写清:

  • 影响范围:"本 mod 会修改当前关卡的重力参数"
  • 是否可逆:"退出关卡后恢复默认值" 或 "修改持久保存,需手动恢复"
  • 与其他 mod 的冲突可能:"如果同时使用了修改物理参数的 mod,可能产生冲突"

玩家宁可看到风险警告,也不想在崩溃后才发现问题。


分发渠道

Ballance 社区常见的发布平台:

渠道 适用场景 注意事项
BallanceBug 论坛 中文社区首选 帖子中贴安装说明,附件上传 zip
Discord(Ballance 服务器) 国际社区 发到对应频道,写英文说明
GitHub Release 需要版本管理、issue 跟踪 适合持续维护的 mod
直接发送文件 小范围测试 不适合正式发布,缺少版本追溯

无论哪个渠道,确保安装路径、BML 版本要求、mod 版本号、使用说明四项信息可见。


发布检查清单

按顺序逐项确认:

# 检查项 通过标准
1 入口文件 有且只有一个 *.mod.as 文件
2 元数据完整 id、name、version、author、bml、description 全部填写
3 版本号递增 比上一次发布的版本号大
4 无本机路径 代码中没有硬编码的绝对路径
5 无调试残留 移除了测试命令和过量的日志输出
6 资源引用 使用相对路径或通过 BML 接口获取目录
7 配置默认值 所有配置项在代码中设了默认值
8 配置迁移 改名的配置项有兼容代码
9 清洁安装测试 删掉旧配置后首次运行正常
10 流程测试 进关卡、退关卡、重启游戏、卸载 mod,各阶段无报错
11 日志检查 启动后日志无错误、无警告
12 发布说明 安装、命令、配置、卸载、版本要求全部写清
13 风险说明 中高风险 mod 写明影响范围
14 打包格式 使用 SDK 打包脚本生成 zip,并能直接从 ModLoader/Mods/ 加载

常见发布错误

忘记更新版本号

现象:用户更新后 BML 显示的版本号和上次一样。解决:改代码后紧跟着改 version 字段,形成习惯。

id 被意外修改

现象:更新后旧配置失效,ModLoader/Configs/ 下多出新配置文件。原因:id 拼写变了(如 ball.spawner 变成 ballspawner)。解决:发布后 id 视为不可变。

配置依赖本机环境

现象:别人下载后首次运行报错,找不到路径或缺少初始值。解决:所有默认值通过 SetDefaultXxx() 在代码中设置,不依赖外部文件。

zip 中的入口数量不正确

现象:zip 放入 Mods/ 后 BML 拒绝加载。原因:包内没有入口,或存在多个 *.mod.as 入口。解决:让源目录顶层恰好保留一个入口,并使用 SDK 的 Pack-BMLScriptMod.ps1 重新打包。

残留调试代码

现象:用户反馈日志刷屏,影响性能。原因OnProcess 里留下了每帧 ctx.LogInfo,或者发布包还保留测试命令。解决:发布前搜索 ctx.Log 和命令注册代码,移除调试专用输出。

bml 版本要求过低

现象:旧版 BML 用户加载后报"未定义的方法"。原因:用了新 API 但 bml 字段没更新。解决:不确定时就写当前安装的 BML 版本号。


完成状态

mod 经过完整的发布流程:元数据正确、版本号递增、清洁测试通过、打包格式正确、发布说明完整。用户可以通过 zip 文件安装并正常使用。

下一步:22 排错索引