接入与生命周期
从 gradle 依赖到第一个能跑的回调:怎么拿到 API、怎么让 Control Flex 发现你的插件、六个回调各在什么时机调用、装进去的 JSON 落在哪一层;最后是多加载器工程布局与发布前的检查表。
本节: 概览 · 接入与生命周期 · 接口速查 · 官方实现 · 谁在用这套 API
接入 control-flex-api
- 独立仓库 ControlFlexMC/control-flex-api,经 JitPack 发布,当前 0.9.2 —— 与 0.9.0 是同一份源码,0.9.2 只是为 JitPack 重新打的 tag。
- 编译产物是 Java 8 字节码(class 52),JVM 8+ 都能加载,桥接模组不必跟主模组用同一套工具链。
- 只做
compileOnly:运行时由 Control Flex 本体提供,不要把 API 打进你的 jar。 - 能力范围:读手柄、查动作状态、推模组状态、声明交互式叠层、接管视角、安装 compat / guide 资源。
// build.gradle
repositories { maven { url 'https://jitpack.io' } }
dependencies {
compileOnly 'com.github.ControlFlexMC:control-flex-api:0.9.2'
compileOnly 'org.jetbrains:annotations:24.0.1'
}
# mods.toml (Forge / NeoForge)
[[dependencies.your_mod_id]]
modId = "controlflex"
mandatory = true
versionRange = "[0.8.8,)"
ordering = "AFTER"
side = "CLIENT"
# fabric.mod.json (Fabric)
{ "depends": { "controlflex": ">=0.8.8" } }
第一行永远是可用性检查 —— Control Flex 可能没装,SDL3 也可能初始化失败。所有 provider 拿到的都可能是 null。
if (!ControlFlexApi.isAvailable()) {
return; // Control Flex missing or not initialized: skip every controller path
}
插件骨架:IControlFlexPlugin
| 回调 | 什么时候调用 / 干什么 |
|---|---|
getModId() |
插件标识,用于日志与去重。 |
onInstallCompatConfigs() |
在 ready 之前调用,用 installer.install() 把 compat JSON 装到 compat/mods/。 |
onInstallGuideAssets() |
同上,装到 guides/mods/。 |
onControlFlexReady() |
唯一的初始化回调,此时所有 provider 已就绪。 |
onControllerConnectionChanged() |
手柄插拔时触发。 |
cameraLookConsumer() |
返回 ICameraLookConsumer 可接管每帧视角增量(0.8.8+,见相机接管);返回 null 表示不接管。 |
requireApiVersion() |
默认方法,检查最小 API 版本,不满足返回 false。 |
package com.example.mymod;
import com.ifels.controlflex.api.*;
public class MyPlugin implements IControlFlexPlugin {
@Override
public String getModId() { return "my_bridge_mod"; }
@Override
public void onInstallCompatConfigs(ICompatAssetInstaller installer) {
installer.install("/assets/my_bridge_mod/compat/targetmod.json", "targetmod.json");
}
@Override
public void onControlFlexReady() {
if (!requireApiVersion("0.8.8")) return;
if (!ControlFlexApi.isAvailable()) return;
ControlFlexApi.getPlayerStateRegistry().setState("mymod:initialized", true);
}
}
注册靠 SPI 服务文件,Control Flex 会自动发现:
src/main/resources/META-INF/services/com.ifels.controlflex.api.IControlFlexPlugin
com.example.mymod.MyPlugin
发现与回调的顺序(决定了你在哪个回调里能干什么):
Control Flex startup
└── initializeClient()
├── providers injected → isAvailable() becomes true
└── built-in configs extracted to default/
└── onKeyMappingsReady()
└── discover SPI plugins
├── new CompatAssetInstaller(plugin.class, compatDir)
├── plugin.onInstallCompatConfigs(installer)
├── new CompatAssetInstaller(plugin.class, guideDir)
├── plugin.onInstallGuideAssets(installer)
└── plugin.onControlFlexReady()
runtime
├── plugin.onControllerConnectionChanged(connected)
└── plugin.cameraLookConsumer().consumeLook(yaw, pitch) // per frame, if non-null
安装的 JSON 落在 compat/mods/:优先级在 default/ 之上、user/ 之下 —— 所以玩家仍然可以用 user 层覆盖你装的默认值,不会被桥接锁死。同名文件按内容整体覆盖,不会做字段级合并。
多加载器工程布局
官方五个桥接都用同一套三模块布局:逻辑只写一遍放 common/,加载器差异(mixin 配置、mod 元数据、事件总线)各自留在 forge/ 与 fabric/。这样加一条 MC 版本线时只需要动元数据,不用把逻辑抄第二遍。
cfx-compat-mymod/
├── common/
│ ├── build.gradle # compileOnly control-flex-api + the target mod
│ └── src/main/
│ ├── java/com/ifels/cfx/mymod/
│ │ ├── CfxMyModPlugin.java # implements IControlFlexPlugin
│ │ └── MyModBridge.java # the only place that imports the target mod
│ └── resources/
│ ├── META-INF/services/com.ifels.controlflex.api.IControlFlexPlugin
│ └── assets/cfx_compat_mymod/compat/mymod.json
├── forge/ # mods.toml, mixin config, Forge events
└── fabric/ # fabric.mod.json, mixin config, Fabric events
两个容易踩的点:一是 SPI 服务文件必须放在 common 的 resources/META-INF/services,只放加载器模块里 Fabric 会读不到;二是目标模组的依赖要么全用 compileOnly,要么把「目标缺失就静默退出」写成显式分支 —— 否则你的桥接会连带把整个游戏拖进崩溃。
调试与发布
- 日志前缀
[ModCompat]:配置加载、覆盖、跳过原因都在这里;桥接自己的日志另开一个 logger(LogManager.getLogger("my-mod-id"))。 - 用
requireApiVersion("0.8.8")守住最低 API 版本,并在mods.toml/fabric.mod.json里把 Control Flex 声明为mandatory依赖。 - API 只
compileOnly;打包成独立 JAR,不要 shade 进去,否则会跟本体重复。 - 目标模组尽量也
compileOnly(CurseForge Maven / Modrinth Maven),并在mandatory之外留一条「目标模组缺失就静默退出」的路径 —— 载具桥接就是这么做成「可有可无」的。 - 多加载器项目用
common/+forge/+fabric/三模块:逻辑放 common,mixin 配置与 mod 元数据各写各的。 - 发布时写清:解决什么、测试过哪些模组版本与加载器、有没有可选的 JSON 默认值、与哪些模组互斥。
常见错误
| 现象 | 原因 / 处置 |
|---|---|
| 插件回调一个都没触发 | SPI 文件路径或类名写错;或 getPluginId() 返回了空串被去重丢掉。用 [ModCompat] 日志确认发现阶段。 |
onControlFlexReady 里拿到 null |
Control Flex 的 provider 是分阶段注册的,只有 ready 之后才全量就绪;更早的回调里先只做资源安装。 |
| 装了 JSON 但键位没出现 | 文件必须落在 compat/mods/ 且文件名与 mod_id 一致;mod_versions[].version 不匹配当前模组版本也会被跳过。 |
| Fabric 上 mixin 不生效 | mixin 配置要分别登记在 xxx.mixins.json 并写进 fabric.mod.json 的 mixins 字段;NeoForge 1.20.5+ 之后走 META-INF/neoforge.mods.toml。 |
| 和别的 pad 模组打架 | 同一个目标模组只允许一个手柄后端。Epic Fight 桥接与 Controlify 互斥,发布说明里必须写清。 |