CFXControl Flex Wiki
English中文

Compat JSON

Compat JSON is the shareable unit behind the whole system: the in-game UI writes it, the community repo distributes it, bridge mods install it. This page covers where files go, how the fields work, how to contribute a config, and how to run your own source.

On this page: Files and loading rules · Built-in configs · Community repo · Field reference · Contributing · Your own source · Troubleshooting

1. Where files go, and when they load

2. What ships in the JAR

Only three configs are bundled, extracted to config/controlflex/compat/default/ on every launch:

Mod What it provides
Just Enough Items (JEI) Recipe / uses keys placed on the screen layer, plus a batch of internal keys added to ignoreKeys
Epic Fight Battle-mode keys and screen rules, pointing at the official bridge cfx-compat-epicfight
Nightfall Key and screen adaptation for the mod

exempt_mods.json in the same directory is not a compat config — it declares library mods (Fabric API, Kotlin language support and friends) so they never clutter the adaptation lists. Everything else is covered by auto-discovery or the community repo.

3. The community repo

The official source is ControlFlexMC/cfx-compat-configs: a purely static directory tree, split by MC version and loader, one JSON per mod, plus a manifest.json recording each path and 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/

The client requests exactly two kinds of URL: {baseUrl}/{mcVersion}/{loader}/manifest.json and the content files referenced by path in that manifest. Only the current loader's directory is read; a missing version / loader is a 404 — normal, not a failure, and it does not affect other sources.

In game: Settings → Mod Adaptation → “More” (top right) → Sync latest configs. The mod's detail page then shows a “N community adaptation configs found” banner → pick a variant → read-only preview → Apply. Apply backs up the old user file, overwrites atomically, records it in .applied.json, and hot-reloads that mod — no restart.

Same mod_id with different content = parallel variants for the player to choose from; the same sha256 in several sources = mirrors, retried in source-list order. Content is cached by sha256 at compat/repo/content/<mod_id>/<sha256>.json; those files are immutable.

How the official source pins versions

What the repo covers today

Directories are split by MC version / loader, and 12 mods have configs so far. The same config is usually copied into every loader directory it applies to, so coverage differs between version lines:

MC version Loader Mods covered
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 and 1.19.2 Forge only) · mts
1.16.5 · 1.12.2 Forge mts

This table follows the repo; the directory tree is authoritative. The create / emi configs also require their bridge mod through dependencies — the keys come from the bridge.

4. Field reference

Top-level fields

Field Type Required Description
mod_id string Yes The mod ID this config belongs to (e.g. jei, epicfight). Empty, or the mod not installed → the whole file is skipped; it is part of the activation gate itself.
dependencies object[] No Extra gate: { "mod_id": "...", "mod_name": "...", "home_pages": [...] }. Only mod_id is read at runtime; all of them must be loaded for the config to activate.
contributors string[] No Author names, shown on the community-config preview page.
config_version string No Version of this config itself — display only, no runtime logic.
mod_versions array Yes Version-specific rule sets, see below. The legacy name versions is no longer accepted.
mod_name · home_page string No Pure metadata for the manifest generator and the preview UI; ignored at runtime.

How dependencies is actually used

It is not "install dependencies for you" but an activation gate: the config only applies when every mod_id in the array is loaded. Real configs use it in two situations — the config depends on keys a bridge mod provides, or the config belongs to a standalone compat 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": {} } } ]
}

Only mod_id is read (case-insensitive); mod_name / home_pages are display-only on the community preview page. An array of plain strings, or a non-array value, is ignored wholesale. A config whose target is the mod itself does not need it.

mod_versions and version matching

The array is evaluated in order and the first match wins; if nothing matches the installed mod version, the entire config has no effect (a warning is logged).

Form Example Meaning
Wildcard "" (or "(,)") Matches every version
Exact "1.21.1" Exactly equal
Interval "[1.0,2.0)" 1.0 ≤ v < 2.0 (brackets inclusive, parens exclusive)
Unbounded "[21.1,)" / "(,1.20]" v ≥ 21.1 / v ≤ 1.20

The three sections and dispatch channels

All three sections are objects mapping a bare key name to per-key settings. A bare key name is KeyMapping.getName() without the modid: prefix; the action ID is still {modid}:{keyName}. A key listed in several sections belongs to all of them (union); a key declared in none defaults to in-game only.

"inGameKeys": {
  "channels": ["keyMapping"],
  "key.epicfight.dodge": {},
  "key.epicfight.attack": { "channels": ["keyMapping", "eventBus"],
                            "extraBehaviors": ["suppressLayerSwitch"] }
}
Channel Meaning
keyMapping Writes KeyMapping.setPressed() / isDown state — the same path vanilla uses.
eventBus Broadcasts a Forge InputEvent.Key (merged per tick).
glfwPoll Mirrors the hold into synthetic InputConstants.isKeyDown() state, for mods that poll GLFW themselves.
screenInput While a screen is open, feeds screen.keyPressed/keyReleased (or a synthetic click for mouse-bound keys).
virtualKbm Injected through the virtual keyboard / mouse as real hardware input. Exclusive: replaces every other channel and is not user-overridable.

Defaults when a section declares no channels:

Section Default channels
inGameKeyskeyMapping, eventBus
screenKeyskeyMapping, screenInput
overlayKeyskeyMapping, eventBus

extraBehaviors

Per-key behavior flags; there is one value today: suppressLayerSwitch — while that action's binding is held, layer switching is suppressed, and the binding is forced to HOLD on save. Use it for modded modifier-style keys (Iron's Spells' key.irons_spellbooks.spell_bar_modifier, for example). An explicit [] means explicitly off.

phasePersistentKeys

Declares that a key's binding should follow it across a phase switch:

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

ignoreKeys

Hides actions from the binding UI (debug keys, cheat keys, duplicates): "ignoreKeys": ["key.epicfight.config"]. The modid: prefix is added automatically when matching action IDs.

itemSuppressKeys

Suppresses actions while the main-hand item matches. Typical case: the mod takes over input for specific items (in battle mode, attack / interact are handled by the mod's combo and weapon-skill systems), so the generic actions are suppressed to stop them fighting each other.

"itemSuppressKeys": [
  { "item": "efn:yamato_dmc*", "playerState": ["epicfight:battle_mode"], "suppressActions": ["use"] },
  { "tag": "minecraft:swords", "playerState": ["epicfight:battle_mode"], "suppressActions": ["attack", "use"] }
]
Field Description
itemItem ID (modid:item_id); * wildcard supported. Mutually exclusive with tag.
tagItem tag. Mutually exclusive with item.
playerStateOptional modId:state list (OR); empty = suppress whenever the item matches. Unknown states warn and count as no-match.
suppressActionsAction short names (attack, use) or full action IDs.

Evaluated once per tick against the main-hand item; actions matched by phasePersistentKeys are exempt.

tips

Hint text shown on binding rows:

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

Language codes have no whitelist: the runtime uses the player's language and falls back to en_us; with no en_us nothing is shown. Always include en_us.

Stick and cursor behavior per screen / overlay

Keys are fully qualified class names: screen.getClass().getName() for screens; the opener class name for overlays, with "*" as the mod-level fallback.

"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 Meaning
DEFAULTEngine default: incremental virtual mouse on the left stick, nothing on the right stick in a GUI
VIRTUAL_MOUSEThe stick drives the incremental virtual cursor
RADIAL_PATHPolar cursor from the screen center (wheel-style UIs)
DISABLEDThe axis is ignored; stick-click bindings still work
Parameter Default Description
threshold0.5Stick deadzone
cursorRadius100Distance from the screen center (logical pixels × GUI scale)
cursorSpeed1.0Cursor speed (clamped 0.1–3.0)
showCursortrueWhether to draw the cursor (false still tracks position, just does not draw it)
keepLastPositiontrueStay in place when the stick is released
angleBias1.0Angle sensitivity

Entries also accept cursorStyle (system / bedrock / hand) and disabled: true (opt out entirely, no fallback). Resolution: user layer before built-ins, a specific overlay entry before the same mod's "*", and a hit that lacks the requested side (left / right) falls through to the next config.

Migrating from the legacy format (≤ 0.8.5)

Since 0.8.7 old files are migrated automatically on startup (backed up, then rewritten). If you maintain configs by hand, or have to read an old one, use this table:

Legacy Now
versionsmod_versions
guiKeys: ["k"]screenKeys: { "k": {} }
required_mod: "x"dependencies: [{ "mod_id": "x" }]
the loader fieldRemoved; the source directory (forge / fabric / neoforge) separates loaders
GLFW_COMPAT flagchannels: ["glfwPoll"]
PHASE_PERSISTENT flagphasePersistentKeys + declared once in the game section and once in the destination section
legacy stick arrays (screens + mode)screen object keyed by class name + behavior; RADIAL_CURSOR → RADIAL_PATH, returnToCenter: false → keepLastPosition: true
—overlayKeys · per-overlay overlay config · phase persistence with an overlay destination

Self-check before you ship a config

  1. The three sections must be objects; the array form is dropped wholesale, with a single warning.
  2. channels inside a section is a reserved key (section defaults) — never use it as a key name.
  3. Per-key channels: [] and extraBehaviors: [] are meaningful explicit empties, not the same as omitting the field.
  4. mod_versions is first-match-wins: specific intervals first, a "" fallback last; no matching entry means the whole config is inert.
  5. A malformed version discards the whole entry, not just the field.
  6. Identity comes from the JSON's mod_id, but user-layer files must be named <mod_id>.json.
  7. phasePersistentKeys only works when the key is declared in both sections; the pairing alone is ignored.
  8. UTF-8, no trailing commas; add channels overrides only when the default behavior misbehaves.

Minimal template

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

The above is the quick-reference version. The complete field set, the real behavior of each channel, and the migration table from the ≤ 0.8.5 format live in the authoritative reference: English / Chinese.

5. Contributing a config

  1. Fork the repo and switch to the matching branch. The config format changes between ControlFlex versions and sources are consumed per branch; if your fork only has the default branch, pull the version branch down first.
  2. Create <mc-version>/<loader>/mods/<mod_id>.json. Use the bare key names from the mod's Controls category (no modid: prefix); the [ModCompat] log lines help you find them.
  3. Regenerate the manifest: python3 generate_manifest.py. Editing content without recomputing sha256 is the number one cause of "content download failed" on the player side.
  4. Verify in game. Add your fork as a source, or drop the file into compat/user/ and restart. Check the [ModCompat] log to confirm it loaded.
  5. Open a PR stating the mod version tested, the loader(s), and what the config fixes.

If a config only matters on one loader, keep it in that loader's directory; if it behaves the same everywhere, copy the file into the other directories. Files are capped at 2 MB (real configs are a few KB).

6. Adding another config source

A "source" is just a static HTTP directory tree: {baseUrl}/{mcVersion}/{loader}/manifest.json plus content files. No server-side logic, so any static host will do.

Two routes

manifest.json key fields

{
  "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"]
    }
  ]
}
Field Description
schemaVersionNumber; greater than 1 is rejected (the client shows an upgrade hint); missing or 0 is tolerated.
items[].mod_idMust match ^[a-z0-9_-]+$ and be unique within the manifest; a duplicate fails the entire manifest parse.
items[].pathRelative to the manifest's directory; absolute paths, backslashes, empty segments and .. are rejected. The convention is mods/<file>.json.
items[].sha256SHA-256 of the content file, 64 lowercase hex characters. Verified after download; a mismatch tries the next mirror. It is the sole identity of a variant.
home_pages · description · dependenciesDisplay-only fields (dependencies is for display; the client never installs dependencies).

The official repo's generate_manifest.py can be reused as is: it scans <mc-version>/<loader>/mods/*.json, extracts mod_id / mod_name / home_page, computes the sha256 and writes the manifest back idempotently.

Hosting options and hard limits

Host baseUrl example Caching
GitHub raw https://raw.githubusercontent.com/<user>/<repo>/refs/heads/<branch> About 5 minutes; the official built-in sources use this form
jsDelivr https://cdn.jsdelivr.net/gh/<user>/<repo>@<branch> Branch refs cache for roughly 12 hours; prefer a tag
Self-hosted / object storage https://files.example.com/cfx-source/ Up to you
Item Limit
baseUrlMust be http:// or https:// with a host name
Per file≤ 2 MB (manifests and content files alike)
Timeouts5 s connect / 30 s read
Redirects301/302/303/307/308 followed, max 5 hops; https → http downgrades rejected
EncodingUTF-8 JSON

Adding the source in game

  1. Go to Settings → Mod Adaptation → “More” (top right) → Edit source list.
  2. Paste the baseUrl into the input at the bottom → Add → Done; closing the dialog with a changed list triggers a background sync.
  3. Open “Sync latest configs” and confirm your source synced (per-source progress and logs); the mod cards then show a community-config badge.

Source-list behavior: built-ins can be disabled and reordered but not deleted; your own sources can be added, removed and reordered freely; “Restore default sources” only resets the two built-ins. Each source caches under compat/repo/manifest/<source-id>/, where the id is derived from the URL (lowercase, with -1 / -2 on conflicts).

Local testing

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

http:// sources are allowed (only https → http redirect downgrades are rejected), so local testing needs no certificate. Edit files, then hit “Sync latest configs” once.

7. Troubleshooting

Symptom Check
Source shows “Never synced” / sync fails Open {baseUrl}/{mcVersion}/{loader}/manifest.json in a browser; directory names must exactly match the game version name and loader name (1.20.1, not 1.20).
Manifest parse error Duplicate mod_id, path with an absolute path / backslash / .., sha256 that is not 64 lowercase hex characters, schemaVersion > 1, or a JSON syntax error.
Manifest synced but content download fails The content bytes do not match the manifest sha256 (edited the JSON but forgot to regenerate the manifest is the usual cause), the file exceeds 2 MB, or every mirror failed in that round.
Applied but nothing changed The config's own gates failed: the mod_id mod is missing, dependencies are not all loaded, no mod_versions entry matches (first match wins — mind interval syntax and order), or a key name is wrong. Search the [ModCompat] log.
Players never see an update jsDelivr caches @branch for about 12 hours; switch to the raw URL, cut a tag, or wait for the cache to expire.
Garbled non-ASCII text The file must be UTF-8 (BOM-free preferred).