CFXControl Flex Wiki
English中文

接入与生命周期

从 gradle 依赖到第一个能跑的回调:怎么拿到 API、怎么让 Control Flex 发现你的插件、六个回调各在什么时机调用、装进去的 JSON 落在哪一层;最后是多加载器工程布局与发布前的检查表。

本节: 概览 · 接入与生命周期 · 接口速查 · 官方实现 · 谁在用这套 API

接入 control-flex-api

// 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,要么把「目标缺失就静默退出」写成显式分支 —— 否则你的桥接会连带把整个游戏拖进崩溃。

调试与发布

常见错误

现象 原因 / 处置
插件回调一个都没触发 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 互斥,发布说明里必须写清。

继续读