Mod folder structure
A mod is a folder inside mods/. Nothing is registered or compiled — the engine
discovers folders at startup and resolves assets through them.
mods/
├── modsList.txt is in the game root, not here
└── MyMod/
├── pack.json # metadata (optional but recommended)
├── data/ # charts, song JSON, per-song scripts, scripted states
│ ├── states/ # <StateName>.hx overlay or ModState replacement
│ │ └── FreeplayState.hx
│ └── mySong/
│ ├── mySong.json
│ ├── mySong-hard.json
│ ├── events.json
│ └── mySong.lua
├── songs/ # audio
│ └── mySong/
│ ├── Inst.ogg
│ └── Voices.ogg
├── weeks/ # week definitions
│ └── myWeek.json
├── characters/ # character JSON
│ └── my-char.json
├── stages/ # stage JSON + stage scripts
│ ├── myStage.json
│ └── myStage.lua
├── images/ # PNG + XML/TXT/JSON atlas data
├── sounds/ # SFX
├── music/ # music tracks (menu overrides included)
├── videos/ # cutscenes
├── fonts/ # TTF/OTF
├── shaders/ # .frag / .vert for runtime shaders
├── scripts/ # global scripts (always loaded in gameplay)
├── custom_events/ # <eventName>.lua / .py / .hx / .hscript
├── custom_notetypes/ # <noteType>.lua / .py / .hx / .hscript
└── achievements/ # custom achievements
Those folder names are exactly the list in Mods.ignoreModFolders — they are content
folders, so the mods manager never treats them as nested mods.
pack.json
Read by Mods.getPack(folder), parsed with the tolerant tjson:
{
"name": "My Mod",
"description": "What it does.",
"restart": false,
"color": [255, 128, 0],
"runsGlobally": false,
"scriptSandbox": true,
"scriptSandboxAllowlist": []
}
| Field | Effect |
|---|---|
name, description | shown in the mods menu |
color | accent colour in the list |
restart | ask for a restart when toggled |
runsGlobally | important — see below |
scriptSandbox | defaults to true; setting false gives this mod's scripts full engine privileges and prints a warning |
scriptSandboxAllowlist | exact script-security rule IDs this trusted mod is allowed to use |
Keep the sandbox enabled unless the mod is trusted and cannot use a narrower allowlist. See Script security sandbox for the denylist, runtime guards and log format.
Global mods
A mod with "runsGlobally": true is collected by Mods.pushGlobalMods() at startup and
its assets and scripts/ apply even when another mod is the active one. Use it for
things like a global HUD replacement or a universal script; use a normal mod for a song
pack.
Enabling mods
modsList.txt in the game root, one entry per line:
bf-clicker|1
MyMod|0
1 = enabled, 0 = disabled. Mods.parseList() reads it and
Mods.updateModList() keeps it in sync with the folders actually present. The in-game
Mods menu (states.ModsMenuState) writes the same file, and ordering in the list is the
priority order.
Resolution order
For any asset key, Paths.modFolders(key) checks:
mods/<currentMod>/<key>— the active mod,mods/<globalMod>/<key>for each global mod, in list order,mods/<key>— loose files directly inmods/,
and only then falls back to the engine's own assets/ libraries. So a mod overrides a
base-game asset simply by using the same path:
mods/MyMod/images/characters/BOYFRIEND.png # replaces the default BF atlas
mods/MyMod/music/freakyMenu.ogg # replaces the menu theme
Mods.mergeAllTextsNamed(path, defaultDirectory, allowDuplicates) merges list files
(like credits or achievement lists) across every enabled mod instead of letting one win,
and Mods.directoriesWithFile(path, fileToFind) finds every mod providing a given file.
Scripts the engine loads automatically
| Path | When |
|---|---|
scripts/*.lua, *.py, *.hx, *.hscript | every song (global scripts) |
data/states/<StateName>.hx | overlay on that state/substate, or a ModState replacement — see Scripted states |
data/<song>/*.lua / .py / .hx / .hscript | that song |
stages/<stage>.lua / .py / .hx / .hscript | when that stage loads |
characters/<name>.lua / .py / .hx / .hscript | when that character is added |
custom_notetypes/<type>.lua / .py / .hx / .hscript | when a note of that type exists in the chart |
custom_events/<event>.lua / .py / .hx / .hscript | when that event exists in the chart |
For HScript, <name>.hx and <name>.hscript are the same logical script: in the
folder-scanned scopes the .hx spelling wins inside a folder, and the first folder in
priority order wins across folders.
Global scripts are deduplicated by file name across folders, and the first match in
priority order wins. Global mods are checked before the active mod, so a global mod
shadows an active-mod script of the same name — the opposite of asset lookup, where
mods/<currentMod>/ is searched first.
Distributing
Zip the mod folder itself (so the zip contains MyMod/, not its contents), and tell users
to extract into mods/. example_mods/modTemplate.zip is a ready-made skeleton.
Songs added to assets/ require recompiling the game. Put custom songs — chart and
audio and week JSON — inside a mod folder, or they will not be found.