BML 原生 Mod API 总览¶
本文按用途说明 BML SDK 安装后提供的公开头文件。原生公开接口以安装目录中的
include/BML 为准。
最小入口¶
原生 Mod 是导出 BMLEntry 的动态库,通常使用 .bmodp 扩展名:
#include <BML/IMod.h>
class MyMod final : public IMod {
public:
explicit MyMod(IBML *bml) : IMod(bml) {}
const char *GetID() override { return "MyMod"; }
const char *GetVersion() override { return "1.0.0"; }
const char *GetName() override { return "My Mod"; }
const char *GetAuthor() override { return "Author"; }
const char *GetDescription() override { return "Example"; }
DECLARE_BML_VERSION;
};
MOD_EXPORT IMod *BMLEntry(IBML *bml) { return new MyMod(bml); }
MOD_EXPORT void BMLExit(IMod *mod) { delete mod; }
BMLEntry 返回的对象由 Mod DLL 分配。Mod 应导出 BMLExit,并在其中销毁
同一个对象,确保分配与释放使用相同的 C++ 运行库。对象创建后若注册失败,或
已加载的原生 Mod 被卸载,BML 都会调用 BMLExit。为兼容旧 Mod,缺少
BMLExit 的 DLL 仍可加载,但 BML 会记录警告,且无法安全销毁该 Mod 实例。
推荐通过安装包提供的 CMake 函数创建目标:
bml_add_mod 链接 BML::BML、启用 C++20、关闭编译器扩展,并直接生成
MyMod.bmodp。它要求使用兼容 MSVC ABI 的 32 位目标,并让链接器校验精确的
C 符号 BMLEntry 和 BMLExit。入口缺失或被 C++ 名称修饰时,构建会直接
失败,不会生成 Loader 无法安全使用的 Mod。
bml_install_mod 添加标准安装规则。将 CMAKE_INSTALL_PREFIX 指向 Ballance
的 ModLoader 目录,再使用 CMake 的 install 目标构建并部署到
ModLoader/Mods。
公开头文件¶
| 头文件 | 用途 |
|---|---|
Version.h, Defines.h |
版本宏、导出宏、状态码和基础定义 |
BML.h |
C ABI:版本、Loader 与 Mod 目录、命令注销、内存、字符串/编码、路径、文件与 Zip 工具 |
BMLAll.h |
一次包含全部原生 SDK 接口的便捷聚合头 |
IMod.h, IMessageReceiver.h |
Mod 元数据、生命周期、玩法和引擎回调 |
IBML.h |
Loader 服务、CK 管理器、对象查找、命令、定时器和依赖管理 |
ICommand.h |
命令执行、补全和基础参数解析 |
IConfig.h |
类型化配置属性 |
ILogger.h |
Info、Warn、Error 日志 |
DataShare.h |
低层、同进程的命名字节数据共享 |
Types.h, TypeConvert.h |
对象引用、向量与矩阵,以及与 Virtools 类型之间的互转 |
Interface.h |
Loader 交出的带版本接口结构体,以及取用它的方式 |
Runtime.h, Scene.h, Gameplay.h, Speedrun.h, UI.h |
通过接口结构体取用的 Loader 能力,附带内联 C++ 包装 |
Imc.h, ImcWire.hpp, ImcCpp.hpp |
IMC C/C++ 运行时与线格式 |
Bui.h |
Ballance 风格 ImGui 控件 |
Gui.h, Gui/*.h |
BGui Virtools 实体/行为 UI 封装 |
InputHook.h |
键盘、鼠标、手柄状态与可配对的输入屏蔽令牌 |
ExecuteBB.h |
用于执行或创建常用 Building Block 的 v0.3 兼容接口 |
ScriptHelper.h |
查找、连接、插入和删除行为图节点与参数 |
Guids.h, Guids/*.h |
Virtools 与 Ballance Building Block GUID 集合 |
Mod 生命周期与事件¶
IMod 继承 IMessageReceiver。实现类必须提供 ID、版本、名称、作者、说明和
BML 版本要求,并可按需重写以下回调:
- 生命周期:
OnLoad、OnUnload、OnProcess、OnRender。 - 配置和命令:
OnModifyConfig、OnPreCommandExecute、OnPostCommandExecute、OnCheatEnabled。 - 引擎对象:
OnLoadObject、OnLoadScript、OnPhysicalize、OnUnphysicalize。 - 游戏流程:菜单、加载/开始/重置/暂停/退出/下一关、死亡、结算、检查点、
生命和导航状态等
IMessageReceiver回调。
OnProcess 是唯一运行在 ImGui 帧内部的回调。所有 ImGui 和 Bui 控件都必须
在它里面绘制,不能放在 OnRender 中。参见三种 UI 接口。
OnRender 每次收到一个 CK_RENDER_FLAGS;原生 API 没有分别命名的
“渲染前/渲染后”回调。Loader 通知通过上面列出的 IMod 和
IMessageReceiver 虚函数同步到达。需要延后处理时,Mod 应在回调中复制所需
数据并放入自己拥有的队列。
IBML 服务¶
IBML 是 Loader 传给 Mod 的主服务入口,功能分为:
- CK 上下文和 Attribute、Behavior、Collision、Input、Message、Path、 Parameter、Render、Sound、Time 等管理器访问。
AddTimer/AddTimerLoop:按帧数或毫秒安排回调。- 游戏状态、作弊开关、游戏内消息、命令注册/查找/执行。
- 按名称查找 DataArray、Group、Material、Mesh、2D/3D Entity、Camera、 Light、Sound、Texture 和 Behavior。
- 设置 Initial Condition、显示状态和跳过下一 Tick 渲染。
- 注册球体、地面、模块和变换类型,读取 SR/HS 分数。
- 枚举/查找 Mod,并注册、检查、读取或清空依赖。
定时器必须通过 IBML 创建。SDK 不发布独立 Timer.h;Loader 负责调度和
处理这些回调,Mod 不应维护另一套隐式的静态定时器状态。
AddTimer 和 AddTimerLoop 各有 CKDWORD 与 float 两个重载:CKDWORD
按帧计数,float 按毫秒计数。两种单位共用同一个函数名,因此不带后缀的整型
字面量是二义的,无法编译。调用时必须写明后缀:
bml->AddTimer(1ul, [] { /* 下一帧 */ });
bml->AddTimer(1000.0f, [] { /* 一秒后 */ });
bml->AddTimerLoop(1.0f, [] { return KeepRunning(); });
循环回调返回 true 时继续运行。两个重载都不返回句柄,已安排的定时器无法取消,
需要停止时让循环回调返回 false。
原生 Mod 依赖¶
依赖必须在 BML 初始化 Mod 之前注册。构造函数是通常的注册位置,因为它在
BMLEntry 创建 Mod 时执行,早于任何 OnLoad 回调:
explicit MyMod(IBML *bml) : IMod(bml) {
AddDependency("RequiredMod", BMLVersion(1, 2, 0));
AddOptionalDependency("OptionalMod", BMLVersion(1, 0, 0));
}
BML 会调整初始化顺序,使已安装的依赖先于依赖方收到 OnLoad。缺失的可选
依赖会被忽略。缺失必需依赖或出现依赖循环时,整个 Mod 初始化阶段不会开始;
日志会指出发起依赖的 Mod、所需 ID 和版本,或受循环影响的 Mod。若依赖已经
安装但版本过低,BML 会跳过依赖方的 OnLoad,在日志中同时给出实际版本和
所需版本,并继续初始化其他 Mod。
配置、命令与日志¶
IConfig 按 Category/Key 获取 IProperty。属性类型为 String、Boolean、
Integer、Float 或 Keyboard Key,支持设置当前值、默认值、注释和 Category
注释。它没有 UTF-16 专用属性接口;需要编码转换时使用 BML.h 中的显式
转换函数。
ICommand 提供命令名、别名、说明、作弊标记、执行函数和 Tab 补全,并附带
Integer、Float、Boolean 的基础解析函数。ILogger 提供三个日志级别。
IBML::RegisterCommand 接收裸 ICommand *,Loader 从不删除它。注册成功时没有
任何返回信息,只在失败时写日志;失败的情况包括命令为空指针、命令名或别名非法、
命令名已被注册。
IBML 没有注销函数,注销命令要用 BML.h 里的 BML_UnregisterCommand:
void MyMod::OnUnload() {
if (BML_UnregisterCommand("mycmd") == BML_OK)
delete m_Command; // 到这一步删除才是安全的
}
只有注册该命令的那个 DLL 才能注销它。Loader 记下是哪个模块调用了
RegisterCommand,据此判断:注销别人的命令返回 BML_ERROR_ACCESS_DENIED,名字
不存在返回 BML_ERROR_NOT_FOUND。名字的匹配方式与控制台一致,因此用别名也能指到
同一个命令。该函数应在游戏线程调用。
不调用它,注册过的命令就会一直留在命令表里直到进程结束。特别注意:命令还在注册
状态时不要在 OnUnload 里删除 ICommand,卸载 Mod 本身不会移除它注册的命令,
删除后命令表里会留下悬空指针,控制台仍会尝试执行它。
ParseFloat 的默认取值范围是整个有限 float 范围。早期版本的默认下界是
FLT_MIN,即最小正规格化数,因此负数输入会被静默截断到约 1.17e-38。需要更
窄的范围时显式传入上下界。
Loader 能力¶
旧式 C++ 接口冻结之后新增的能力,都以带版本的 interface struct 发布,通过
BML_GetInterface 按 id 与主版本号取用。每个都有自己在 include/BML 下的头文件,
每个头文件里还声明了一层 inline C++ 命名空间,把取用与参数检查折进去:
BML::Runtime:运行状态、时钟和分数。BML::Scene:对象信息、实体变换和按名查找。BML::Gameplay:关卡、能量、目录、检查点和重置点。BML::UI:消息板、Mod/地图菜单和 HUD。BML::Speedrun:共享 Speedrun 计时器。
版本规则写在 Interface.h 里:结构体只能在末尾追加成员并提升次版本号,而
BML_IFACE_HAS 用来询问正在运行的 Loader 有没有某个比 Mod 编译时的头文件更晚
加入的成员。
原生 BML::Gameplay 的集合读取函数会在调用方持有的 std::vector 中返回完整
快照。目录应在初始化时读取,检查点和重置点应在关卡变化时刷新;这些调用会
传输完整集合,不适合逐帧轮询。
这些 inline C++ 操作都返回 BML 状态码,并标记为 [[nodiscard]]。调用方应处理返回
状态;只有明确忽略尽力清理的结果时才使用显式 (void) 转换。
跨 Mod 通信¶
一个 Mod 发布给别的 Mod 的接口不是 interface struct:只有 Loader 才回答
BML_GetInterface。这种场景优先使用 IMC:
.imc文件只描述接口;字段编号是稳定的线格式标识,不是数组下标。bml_target_imc_api在构建时生成 C++ 绑定并加入目标。- RPC 支持同步调用、Future、取消、超时和完成回调。
- Topic 支持有界订阅队列、退订和丢弃计数。
- 热路径使用已生成的类型与缓存的 ID,不在每次调用时解析文本描述。
- 使用方在运行期就能发现某条路由在不在,因此提供方与使用方可以各自独立发布。
返回 BML 状态码的 C++ IMC 操作同样标记为 [[nodiscard]]。
DataShare 适合共享少量命名字节数据,调用方必须遵守引用计数和借用指针
有效期。接口需要按自己的节奏演进,或需要 RPC/Topic 语义时,使用 IMC。
三种 UI 接口¶
Bui直接绘制 Ballance 风格的 ImGui 控件,适合原生覆盖层界面。BGui创建和操作由 Virtools 2D Entity/Behavior 组成的游戏内 UI。BML::UI不绘制控件,而是控制 Loader 已有的消息、菜单和 HUD,且每个调用 都必须在游戏线程上发出。
三者解决的问题不同,不应互相替代或混用命名。
在 OnProcess 中绘制 ImGui¶
ImGui 帧由 Loader 掌管。它在 Mod 回调之前开帧,并在 OnProcess 返回后立即
结束该帧:
- Loader 在每帧的 Mod 回调之前调用
ImGui::NewFrame。 - 所有 Mod 的
OnProcess在这一帧内部运行。 - Loader 调用
ImGui::Render,该帧结束。 OnRender运行。- Loader 提交已记录的绘制数据。
因此 ImGui 和 Bui 调用必须写在 OnProcess 里。同样的调用放在 OnRender
中时帧已经结束,既不会绘制出任何内容,还可能触发 ImGui 断言。BML::UI 的
消息、菜单和 HUD 调用不受影响,因为它们修改的是 Loader 状态,而不是记录绘制
命令。
C API 的所有权¶
BML.h 和 DataShare.h 可由 C ABI 调用。凡是 BML 返回的新分配字符串、宽
字符串、字符串数组、宽字符串数组或二进制缓冲区,都应使用对应的
BML_Free* 函数释放,
不要跨 DLL 直接调用 CRT free。BML_DataShare_Get 返回借用指针;同一键
再次 Set/Remove 或实例销毁后立即失效,需要稳定副本时使用
BML_DataShare_CopyEx。
Loader 与 Mod 所在目录¶
Mod 关于文件系统的两个问题都由 BML.h 回答:
// Loader 自己的目录。借用指针,进程期间有效,不要释放。
const char *loaderDir = BML_GetLoaderPathUtf8(BML_DIR_LOADER);
// 自己的安装目录。新分配的字符串,用完要释放。
char *modRoot = BML_GetModRootUtf8(nullptr);
// ... 使用 modRoot ...
BML_FreeString(modRoot);
BML_GetLoaderPathW 和 BML_GetLoaderPathUtf8 接收一个 BML_LoaderDirectory:
BML_DIR_WORKING、BML_DIR_TEMP、BML_DIR_GAME、BML_DIR_LOADER 或
BML_DIR_CONFIG。返回的指针归 Loader 所有,不要释放。不要与
BML_GetDirectoryA/W/Utf8 混淆:后者只是取出所给路径字符串的目录部分。
BML_GetModRootW 和 BML_GetModRootUtf8 返回某个 Mod 的安装目录。查询自己时传
nullptr,它按调用方 DLL 解析,不需要 Mod 已注册,因此在构造函数中即可使用。传入
Mod id 则查询该 Mod:原生 Mod 返回其 DLL 所在目录,脚本 Mod 返回其脚本根目录。两者
都会分配内存,结果需用 BML_FreeWString 或 BML_FreeString 释放。
Loader 尚未初始化完成或目录无法解析时两者都返回空指针。BML_GetModRoot 还会获取
Loader 的 Mod 注册表锁,因此应在 Mod 内调用,不要放在 DllMain 中。