CFXControl Flex Wiki
English中文

兼容配置(compat JSON)

compat JSON 是整套适配体系的可分享载体:游戏内界面写出来的是它,社区配置库分发的是它,桥接模组安装的也是它。这一页从「文件放哪」讲到「字段怎么写」,再到「怎么贡献一份、怎么自建一个配置源」。

本页内容: 文件与生效规则 · 内置配置 · 社区配置库 · 字段说明 · 贡献配置 · 自建配置源 · 排障

一、文件放在哪,什么情况下才会生效

二、内置了什么

JAR 内只带三份配置,每次启动释放到 config/controlflex/compat/default/:

模组 内置了什么
Just Enough Items (JEI) 配方 / 用途键位落在界面层,一批不需要暴露的内部按键加入 ignoreKeys
Epic Fight 战斗模式相关的键位与界面规则,并指向官方桥接模组 cfx-compat-epicfight
Nightfall 该模组的按键与界面适配

同目录下的 exempt_mods.json 不是兼容配置,它声明「库类模组」名单,让 Fabric API、Kotlin 语言支持这类库不进入适配列表。其余模组靠自动发现或社区配置库补充。

三、社区配置库

官方源是 ControlFlexMC/cfx-compat-configs:一棵纯静态目录树,按 MC 版本与加载器分目录,每个模组一份 JSON,外加一份 manifest.json 记录路径与 sha256。

<repo-root>/                   # this directory's URL is the baseUrl players add
├── 1.20.1/                   # MC version dir (must match the game version name exactly)
│   ├── forge/
│   │   ├── manifest.json
│   │   └── mods/
│   │       ├── jei.json
│   │       └── epicfight.json
│   └── fabric/
│       ├── manifest.json
│       └── mods/
│           └── jei.json
└── 1.21.1/
    └── neoforge/

客户端只会请求两种 URL:{baseUrl}/{mcVersion}/{loader}/manifest.json 与 manifest 里 path 指向的内容文件。只读当前加载器目录,某个版本 / 加载器没做就是 404 —— 这是正常情况,不算失败,也不影响其他源。

游戏内路径:设置 → 模组适配 → 右上「更多」→ 同步最新配置。模组详情页会出现「找到 N 个社区适配配置」横幅 → 选变体 → 只读预览 → 应用。应用会先备份旧的 user 文件、原子覆盖、写入 .applied.json,再热重载该模组,无需重启。

同一个 mod_id 但内容不同 = 并行变体,由玩家挑选;同一个 sha256 出现在多个源 = 互为镜像,按源列表顺序回退重试。内容按 sha256 缓存到 compat/repo/content/<mod_id>/<sha256>.json,文件不可变。

官方源是怎么钉版本的

仓库里现在覆盖了什么

按「MC 版本 / 加载器」分目录,目前已收录 12 个模组的配置。同一份配置通常会复制到适用的每个加载器目录,所以不同版本线的覆盖并不一致:

MC 版本 加载器 已收录模组
1.20.1 Forge create · dragonminez · emi · epicfight · exposure · irons_spellbooks · jei · minemenu · mts · nightfall · superior_tweaks · tacz
1.20.1 Fabric create · emi · exposure · jei
1.21.1 NeoForge create · dragonminez · emi · epicfight · exposure · irons_spellbooks · jei · minemenu · mts
1.21.1 Fabric emi · exposure · jei · minemenu
1.21.11 · 26.1.2 · 26.2 NeoForge / Fabric jei · minemenu
1.18.2 · 1.19.2 Fabric / Forge emi(仅 Fabric 与 1.19.2 Forge)· mts
1.16.5 · 1.12.2 Forge mts

这张表会随仓库变,以目录树为准;create / emi 的配置还通过 dependencies 要求对应桥接模组已安装 —— 它们的键位是桥接提供的。

四、字段说明

顶层字段

字段 类型 必填 说明
mod_id string 是 配置所属的模组 ID(如 jei、epicfight)。为空、或该模组未安装 → 整份跳过,它本身就是激活门槛的一部分。
dependencies object[] 否 附加门槛:{ "mod_id": "...", "mod_name": "...", "home_pages": [...] },运行时只读 mod_id,全部已加载才激活。
contributors string[] 否 作者名,显示在社区配置预览页。
config_version string 否 这份配置自身的版本号,只做展示,不参与逻辑。
mod_versions array 是 按模组版本分的规则集合,见下。旧名 versions 已不再接受。
mod_name · home_page string 否 纯元数据,给 manifest 生成器和预览界面用,运行时忽略。

dependencies 的实际用法

它不是「自动装依赖」,而是一道激活闸门:数组里每个 mod_id 都已加载,这份配置才生效。真实配置里主要用在两种场景 —— 配置依赖某个桥接模组提供的键位,或者这份配置本身就属于一个独立兼容 mod。

{
  "mod_id": "emi",
  "dependencies": [
    { "mod_id": "cfx_compat_emi", "mod_name": "Control Flex x EMI",
      "home_pages": ["https://www.curseforge.com/minecraft/mc-mods/cfx-x-emi"] }
  ],
  "mod_versions": [ { "version": "", "screenKeys": { "key.emi.view_recipes": {} } } ]
}

只读 mod_id(大小写不敏感),mod_name / home_pages 只在社区配置预览页展示。写成字符串数组或非数组会被整段忽略。目标模组就是自己的配置不需要它。

mod_versions 与版本匹配

数组按顺序求值,首个匹配生效;如果没有任何一条匹配当前模组版本,整份配置不生效(日志会给警告)。

写法 示例 含义
通配 ""(或 "(,)") 匹配所有版本
精确 "1.21.1" 完全相等
区间 "[1.0,2.0)" 1.0 ≤ v < 2.0(方括号包含,圆括号不包含)
单边 "[21.1,)" / "(,1.20]" v ≥ 21.1 / v ≤ 1.20

三个按键分区与派发通道

三个分区都是「裸键名 → 每键设置」的对象。裸键名 = KeyMapping.getName() 去掉 modid: 前缀;内部动作 ID 仍是 {modid}:{keyName}。一个键出现在多个分区就是多分区的成员(并集);一个都没写的键默认只在游戏内生效。

"inGameKeys": {
  "channels": ["keyMapping"],
  "key.epicfight.dodge": {},
  "key.epicfight.attack": { "channels": ["keyMapping", "eventBus"],
                            "extraBehaviors": ["suppressLayerSwitch"] }
}
通道 含义
keyMapping 写 KeyMapping.setPressed() / isDown 状态(原版同款路径)。
eventBus 广播一个 Forge InputEvent.Key(每 tick 合并)。
glfwPoll 把按住状态镜像进合成的 InputConstants.isKeyDown(),伺候那些自己轮询 GLFW 的模组。
screenInput 界面打开时喂 screen.keyPressed/keyReleased(鼠标键则合成一次点击)。
virtualKbm 走虚拟键鼠,当作真实硬件输入注入。独占:替换其他所有通道,玩家不可覆盖。

分区没写 channels 时的默认值:

分区 默认通道
inGameKeyskeyMapping, eventBus
screenKeyskeyMapping, screenInput
overlayKeyskeyMapping, eventBus

extraBehaviors

每键行为开关,目前只有一个取值:suppressLayerSwitch —— 该动作的绑定被按住期间,压制层级切换动作;保存时这个绑定会被强制成 HOLD 模式。用于模组自带的「修饰键式」按键(例如 Iron's Spells 的 key.irons_spellbooks.spell_bar_modifier)。显式写 [] 表示明确关闭。

phasePersistentKeys

声明某个键的绑定要跨相位切换保持一致:

"inGameKeys":  { "key.irons_spellbooks.spell_wheel": {} },
"overlayKeys": { "key.irons_spellbooks.spell_wheel": {} },
"phasePersistentKeys": { "key.irons_spellbooks.spell_wheel": ["inGame", "overlay"] }

ignoreKeys

把动作从绑定界面里藏掉(调试键、作弊键、重复键):"ignoreKeys": ["key.epicfight.config"]。匹配动作 ID 时会自动补 modid: 前缀。

itemSuppressKeys

主手物品匹配时压制指定动作。典型场景:模组对特定物品接管了输入(战斗模式下普攻 / 交互由模组的连招与武器技能系统处理),把通用动作压掉避免打架。

"itemSuppressKeys": [
  { "item": "efn:yamato_dmc*", "playerState": ["epicfight:battle_mode"], "suppressActions": ["use"] },
  { "tag": "minecraft:swords", "playerState": ["epicfight:battle_mode"], "suppressActions": ["attack", "use"] }
]
字段 说明
item物品 ID(modid:item_id),支持 * 通配。与 tag 互斥。
tag物品标签。与 item 互斥。
playerState可选的 modId:state 列表(OR 关系),空 = 只要物品匹配就压制。未知状态会警告并按不匹配处理。
suppressActions动作短名(attack、use)或完整动作 ID。

每 tick 对主手物品求值一次;被 phasePersistentKeys 命中的动作豁免。

tips

绑定行上的提示文案:

"tips": [
  { "key": "key.epicfight.weapon_innate_skill",
    "text": { "en_us": "Bind to the same button as Attack; use HOLD mode.",
              "zh_cn": "请绑定为与「攻击」相同的按键,并使用 HOLD 模式。" } }
]

语言码没有白名单,运行时按玩家语言取,回落 en_us;连 en_us 都没有就不显示。至少写 en_us。

界面与叠层的摇杆、光标

键是全限定类名:界面用 screen.getClass().getName();叠层用「打开者」类名,"*" 表示该模组的兜底。

"screen": {
  "com.dragonminez.client.gui.UtilityMenuScreen": {
    "leftStick":  { "behavior": "VIRTUAL_MOUSE" },
    "rightStick": { "behavior": "RADIAL_PATH", "threshold": 0.5, "cursorRadius": 90 }
  }
},
"overlay": {
  "io.redspace.ironsspellbooks.gui.overlays.SpellWheelOverlay": {
    "rightStick": { "behavior": "RADIAL_PATH", "threshold": 0.3, "cursorRadius": 40, "showCursor": false }
  }
}
behavior 含义
DEFAULT引擎默认:左摇杆增量虚拟鼠标,右摇杆在界面里不做事
VIRTUAL_MOUSE摇杆驱动增量虚拟光标
RADIAL_PATH以屏幕中心为原点的极坐标光标(轮盘类界面)
DISABLED忽略该轴;摇杆按下的绑定仍然有效
参数 默认 说明
threshold0.5摇杆死区
cursorRadius100离屏幕中心的距离(逻辑像素 × GUI 缩放)
cursorSpeed1.0光标速度(夹在 0.1–3.0)
showCursortrue是否画出光标(false 仍跟踪位置,只是不画)
keepLastPositiontrue松开摇杆后停在原地
angleBias1.0角度灵敏度

条目还支持 cursorStyle(system / bedrock / hand)与 disabled: true(完全退出,不再回退)。解析顺序:user 层优先于内置,具体叠层条目优先于同模组的 "*",命中但缺所需那一侧(左 / 右)就继续往下一个配置找。

从旧格式迁移(≤ 0.8.5)

0.8.7 起启动时会自动迁移旧文件(先备份再改字段)。如果你在手工维护配置,或者要读一份老配置,对照这张表:

旧格式 现在
versionsmod_versions
guiKeys: ["k"]screenKeys: { "k": {} }
required_mod: "x"dependencies: [{ "mod_id": "x" }]
loader 字段删除,改由源目录(forge / fabric / neoforge)区分
GLFW_COMPAT 标记channels: ["glfwPoll"]
PHASE_PERSISTENT 标记phasePersistentKeys + 在游戏内 / 目标分区各声明一次
旧摇杆数组(screens + mode)screen 对象(按类名)+ behavior;RADIAL_CURSOR → RADIAL_PATH、returnToCenter: false → keepLastPosition: true
——overlayKeys · 按叠层的 overlay 配置 · overlay 目标端的相位保持

写完先自查这几条

  1. 三个分区必须是对象,写成数组会被整段丢弃(只留一条警告)。
  2. 分区里的 channels 是保留键(分区默认值),不要拿它当键名。
  3. 每键的 channels: [] 与 extraBehaviors: [] 是有意义的显式空,不等于省略。
  4. mod_versions 首个匹配生效:具体区间放前面,"" 兜底放最后;没有一条匹配 = 整份配置不生效。
  5. 写坏的 version 会丢掉整条 entry,不只是这个字段。
  6. 身份来自 JSON 里的 mod_id,但 user 层文件名必须叫 <mod_id>.json。
  7. phasePersistentKeys 必须两边都声明才生效;只写配对会被忽略。
  8. UTF-8、无 trailing comma;只有默认行为不对时才去加 channels 覆盖。

最小模板

{
  "mod_id": "examplemod",
  "contributors": ["yourname"],
  "config_version": "1.0.0",
  "mod_versions": [
    {
      "version": "",
      "inGameKeys": {},
      "screenKeys": {},
      "overlayKeys": {},
      "ignoreKeys": [],
      "tips": [],
      "screen": {},
      "overlay": {}
    }
  ]
}

上文是速查版;字段全集、每个通道的真实行为、以及从 ≤ 0.8.5 旧格式迁移的对照表,以官方字段参考为准:英文 / 中文。

五、如何贡献一份配置

  1. Fork 仓库,并切到匹配分支。配置格式随 ControlFlex 版本变化,源是按分支消费的;只 fork 到默认分支时要先把对应版本分支拉下来。
  2. 新建 <mc-version>/<loader>/mods/<mod_id>.json。键名用模组 Controls 分类里的裸键名(不带 modid: 前缀),找不到就开日志里的 [ModCompat] 行。
  3. 重新生成 manifest:python3 generate_manifest.py。改了内容不重算 sha256,是玩家侧「内容下载失败」的头号原因。
  4. 游戏内验证。把你的 fork 当源加进源列表,或直接把文件丢进 compat/user/ 重启。加载是否成功看 [ModCompat] 日志。
  5. 提 PR,写清测试过的模组版本、加载器,以及这份配置修好了什么。

只影响某个加载器就只放那个目录;两边行为一致就复制一份进另一个目录。文件大小上限 2 MB(实际配置通常只有几 KB)。

六、如何添加其他配置源

一个「源」就是一棵静态 HTTP 目录树:{baseUrl}/{mcVersion}/{loader}/manifest.json 加内容文件。没有服务端逻辑,任何静态托管都能当源。

两条路线

manifest.json 关键字段

{
  "schemaVersion": 1,
  "name": "My Pack Configs",
  "items": [
    {
      "mod_id": "jei",
      "mod_name": "Just Enough Items (JEI)",
      "path": "mods/jei.json",
      "sha256": "a4dcfadbca3d0a8b19be2ec5d191fc12801d199583393ef0fec13007e06b927f",
      "home_pages": ["https://www.curseforge.com/minecraft/mc-mods/jei"]
    }
  ]
}
字段 说明
schemaVersion数字;大于 1 会被拒绝(客户端提示升级),缺失或 0 可接受。
items[].mod_id需匹配 ^[a-z0-9_-]+$,且在 manifest 内唯一;重复会让整份 manifest 解析失败。
items[].path相对 manifest 所在目录;禁止绝对路径、反斜杠、空段与 ..。惯例是 mods/<file>.json。
items[].sha256内容文件的 SHA-256,64 位小写十六进制。下载后校验,不匹配就换下一个镜像。它是变体的唯一身份。
home_pages · description · dependencies纯展示字段(dependencies 只用来显示,客户端不会自动装依赖)。

官方仓库的 generate_manifest.py 可以照搬到自己的源:扫描 <mc-version>/<loader>/mods/*.json,抽出 mod_id / mod_name / home_page,算 sha256,幂等写回 manifest。

托管选择与硬性限制

托管 baseUrl 示例 缓存
GitHub raw https://raw.githubusercontent.com/<user>/<repo>/refs/heads/<branch> 约 5 分钟;官方内置源用的就是这种
jsDelivr https://cdn.jsdelivr.net/gh/<user>/<repo>@<branch> 分支引用约 12 小时,建议打 tag
自建 / 对象存储 https://files.example.com/cfx-source/ 自己定
项目 限制
baseUrl必须是带主机名的 http:// 或 https://
单文件大小≤ 2 MB(manifest 与内容文件都算)
超时连接 5 秒 / 读取 30 秒
重定向跟随 301/302/303/307/308,最多 5 跳;拒绝 https → http 降级
编码UTF-8 JSON

在游戏里添加源

  1. 进 设置 → 模组适配 → 右上「更多」→ 编辑源列表。
  2. 把 baseUrl 粘到下方输入框 → 添加 → 完成;列表有变化时关闭对话框会触发一次后台同步。
  3. 打开「同步最新配置」,确认你这个源同步成功(有分源进度与日志);之后模组卡片会出现社区配置角标。

源列表行为:内置源可以禁用 / 调序但不能删除;你自己的源可增删调序;「恢复默认源」只重置那两个内置源。每个源缓存在 compat/repo/manifest/<source-id>/,id 由 URL 派生(小写,冲突时加 -1 / -2)。

本地测试

cd <your-source-root>
python3 -m http.server 8000
# then add http://127.0.0.1:8000 in "Edit source list"

允许 http:// 源(只拒绝 https → http 的重定向降级),本地测试不需要证书。改完文件点一次「同步最新配置」即可。

七、排障

现象 检查
源显示「从未同步」/ 同步失败 浏览器打开 {baseUrl}/{mcVersion}/{loader}/manifest.json 能不能看到;目录名必须与游戏版本名、加载器名完全一致(1.20.1 不是 1.20)。
manifest 解析报错 重复的 mod_id、path 带绝对路径 / 反斜杠 / ..、sha256 不是 64 位小写十六进制、schemaVersion > 1、JSON 语法错误。
manifest 同步了,内容下载失败 内容字节与 manifest 里的 sha256 对不上(改了 JSON 忘了重新生成 manifest 是最常见原因)、文件超过 2 MB、或本轮所有镜像都失败。
应用了但没变化 配置自己的门槛没过:mod_id 模组没装、dependencies 没全加载、mod_versions 没有一条匹配当前版本(首个匹配生效,注意区间写法与顺序)、键名写错。搜 [ModCompat] 日志。
玩家看不到更新 jsDelivr 的 @branch 缓存约 12 小时;换 raw 地址、打 tag,或者等缓存过期。
中文乱码 文件必须是 UTF-8(建议无 BOM)。