What is Polymod?
Polymod is an atomic modding framework for Haxe games. It provides a complete system for:
Loading mods from filesystem or ZIP archives
Replacing game assets transparently
Merging data files intelligently
Sandboxing scripts securely
Managing mod dependencies and versioning
Polymod on GitHub Polymod is open source and maintained by Lars Doucet
How FNF Uses Polymod
Friday Night Funkin’ integrates Polymod through the PolymodHandler class:
PolymodHandler.hx (Initialization)
public static function loadModsById ( ids : Array < String >) : Void
{
var loadedModList : Array < ModMetadata > = polymod. Polymod . init ({
// Root directory for all mods
modRoot : MOD_FOLDER ,
// The directories for one or more mods to load
dirs : ids ,
// Framework being used to load assets
framework : OPENFL ,
// The current version of our API
apiVersionRule : API_VERSION_RULE ,
// Call this function any time an error occurs
errorCallback : PolymodErrorHandler . onPolymodError ,
// Custom filesystem for ZIP support
customFilesystem : modFileSystem ,
// Framework-specific parameters
frameworkParams : buildFrameworkParams (),
// List of filenames to ignore in mods
ignoredFiles : buildIgnoreList (),
// Parsing rules for various data formats
parseRules : buildParseRules (),
// Parse hxc files and register scripted classes
useScriptedClasses : true ,
loadScriptsAsync : #if html5 true #else false #end ,
});
}
Mod Folder Structure
Polymod scans the mods directory for valid mods:
mods/
├── mod1/
│ ├── _polymod_meta.json # Required metadata
│ ├── _polymod_icon.png # Optional icon
│ ├── images/ # Asset replacements
│ ├── data/ # Data files
│ └── _append/ # Files to merge
└── mod2/
└── ...
Mod Root Location
The mod folder location varies by build configuration:
PolymodHandler.hx (Mod Folder)
static final MOD_FOLDER : String =
#if (REDIRECT_ASSETS_FOLDER && macos)
'../../../../../../../example_mods'
#elseif REDIRECT_ASSETS_FOLDER
'../../../../example_mods'
#else
'mods'
#end ;
Release Builds
Development
Mods are in the mods/ folder next to the executable.
With REDIRECT_ASSETS_FOLDER, mods load from example_mods/ in the source tree.
Asset Replacement
How It Works
When the game requests an asset, Polymod intercepts the request:
Check if any loaded mod has a replacement for that path
If yes, return the mod’s version
If no, return the base game’s version
Example
Game requests asset
var logo = Assets . getBitmapData ( 'images/newgrounds_logo.png' );
Polymod checks mods
Looks for images/newgrounds_logo.png in loaded mods, in order: mods/testing123/images/newgrounds_logo.png ← Found!
Returns mod asset
Returns the custom logo from testing123 instead of the base game’s.
Asset Types
Polymod supports all OpenFL asset types:
IMAGE - PNG, JPG, GIF
AUDIO_MUSIC - OGG, MP3 (long tracks)
AUDIO_SOUND - OGG, MP3 (short effects)
TEXT - TXT, JSON, XML, CSV
BINARY - Any other file type
#if FEATURE_DEBUG_FUNCTIONS
var fileList : Array < String > = Polymod . listModFiles ( PolymodAssetType . IMAGE );
trace ( 'Installed mods have replaced ${ fileList . length } images.' );
for ( item in fileList )
{
trace ( ' * $ item ' );
}
#end
Asset Merging
Merge vs Replace
Instead of replacing entire files, you can merge your changes:
Normal asset path: mymod/
└── data/
└── introText.txt
Result: Completely replaces the base game’s introText.txt Using _append folder: mymod/
└── _append/
└── data/
└── introText.txt
Result: Appends your lines to the base game’s introText.txt
Parse Rules
Polymod needs to know how to parse files for merging:
PolymodHandler.hx (Parse Rules)
static function buildParseRules () : polymod.format. ParseRules
{
var output : polymod.format. ParseRules = polymod.format. ParseRules . getDefault ();
// Ensure TXT files have merge support (line-by-line)
output . addType ( 'txt' , TextFileFormat . LINES );
// Ensure script files have merge support (plaintext)
output . addType ( 'hscript' , TextFileFormat . PLAINTEXT );
output . addType ( 'hxs' , TextFileFormat . PLAINTEXT );
output . addType ( 'hxc' , TextFileFormat . PLAINTEXT );
output . addType ( 'hx' , TextFileFormat . PLAINTEXT );
return output ;
}
LINES Text files treated as arrays of lines. Append mode adds lines to the end.
PLAINTEXT Raw text concatenation. Useful for scripts.
JSON Deep object merging. Properties from mods override base game properties.
CSV Row-based appending. New rows added to the end.
XML Node-based merging. Matching nodes get merged or appended.
Merge Example: Intro Text
The introMod example demonstrates text merging:
Base Game
Mod File
Result
data/introText.txt: shoutouts to tom fulp--lmao
Ludum dare--extraordinaire
cyberzone--coming soon
love to thriftman--swag
introMod/_append/data/introText.txt: awesomes tream--really awesome
Merged in-game: shoutouts to tom fulp--lmao
Ludum dare--extraordinaire
cyberzone--coming soon
love to thriftman--swag
awesomes tream--really awesome ← Added by mod
Version Management
API Version
Mods declare compatibility via api_version:
{
"api_version" : "0.8.0" ,
"mod_version" : "1.0.0"
}
The game checks this against its version rule:
/**
* The API version for the current version of the game.
*/
public static var API_VERSION ( get , never ) : String ;
static function get_API_VERSION () : String
{
return Constants . VERSION ;
}
/**
* The Semantic Versioning rule.
* Indicates which mods are compatible with this version of the game.
*/
public static final API_VERSION_RULE : String = ">=0.8.0 <0.9.0" ;
Version Rule Syntax
Polymod uses semantic versioning rules:
>=0.8.0 - At least version 0.8.0
<0.9.0 - Less than version 0.9.0
>=0.8.0 <0.9.0 - Between 0.8.0 and 0.9.0
1.2.3 - Exactly version 1.2.3
Mods with incompatible api_version will not load and will show an error.
Script Integration
Scripted Classes
Polymod can load HScript classes and register them:
useScriptedClasses : true ,
loadScriptsAsync : #if html5 true #else false #end ,
This allows .hxc files to define classes that extend base classes.
Import Management
Polymod handles imports automatically:
PolymodHandler.hx (Default Imports)
static final DEFAULT_IMPORTS : Array < Class < Dynamic >> = [
funkin. Assets ,
funkin. Paths ,
funkin. Preferences ,
funkin.util. Constants ,
flixel. FlxG
];
for ( cls in DEFAULT_IMPORTS )
{
Polymod . addDefaultImport ( cls );
}
Import Aliases
Some classes are aliased for compatibility or security:
// Backward compatibility for moved classes
Polymod . addImportAlias ( 'funkin.modding.base.ScriptedFunkinSprite' ,
funkin.graphics. ScriptedFunkinSprite );
// Security sandboxing
Polymod . addImportAlias ( 'funkin.util.FileUtil' ,
funkin.util. FileUtilSandboxed );
Polymod . addImportAlias ( 'Reflect' ,
funkin.util. ReflectUtil );
Blacklisting
Dangerous classes are blacklisted:
// System access
Polymod . blacklistImport ( 'Sys' );
Polymod . blacklistImport ( 'cpp.Lib' );
Polymod . blacklistImport ( 'lime.system.System' );
// Script parsing (could bypass sandbox)
Polymod . blacklistImport ( 'hscript.*' );
Polymod . blacklistImport ( 'polymod.*' );
// Score manipulation
Polymod . blacklistImport ( 'funkin.api.newgrounds.*' );
Scripts attempting to use blacklisted classes will fail with an error.
File System Support
ZIP File System
Polymod can load mods from ZIP archives:
PolymodHandler.hx (ZIP Support)
static var modFileSystem : Null < ZipFileSystem > = null ;
static function buildFileSystem () : polymod.fs. ZipFileSystem
{
polymod. Polymod . onError = PolymodErrorHandler . onPolymodError ;
return new ZipFileSystem ({
modRoot : MOD_FOLDER ,
autoScan : true
});
}
This allows distributing mods as:
Folders - Extracted mod folders
ZIP files - Single .zip files in the mods folder
Auto Scanning
With autoScan: true, Polymod automatically detects:
New mods added to the folder
ZIP files alongside folder mods
Changes to mod metadata
Framework Parameters
FNF configures OpenFL-specific parameters:
PolymodHandler.hx (Framework Params)
static inline function buildFrameworkParams () : polymod. Polymod . FrameworkParams
{
return {
assetLibraryPaths : [
'default' => 'preload' ,
'shared' => 'shared' ,
'songs' => 'songs' ,
'videos' => 'videos' ,
'tutorial' => 'tutorial' ,
'week1' => 'week1' ,
'week2' => 'week2' ,
'week3' => 'week3' ,
'week4' => 'week4' ,
'week5' => 'week5' ,
'week6' => 'week6' ,
'week7' => 'week7' ,
'weekend1' => 'weekend1' ,
'sserafim' => 'sserafim'
],
coreAssetRedirect : CORE_FOLDER ,
}
}
This maps OpenFL asset libraries to filesystem paths.
Ignored Files
Certain files are ignored when loading mods:
PolymodHandler.hx (Ignore List)
static function buildIgnoreList () : Array < String >
{
var result = Polymod . getDefaultIgnoreList ();
result . push ( '.vscode' );
result . push ( '.idea' );
result . push ( '.git' );
result . push ( '.gitignore' );
result . push ( '.gitattributes' );
result . push ( 'README.md' );
return result ;
}
Default ignored files include:
_polymod_meta.json (metadata, not an asset)
_polymod_icon.png (icon, not an asset)
_polymod_pack.txt (pack definition)
.DS_Store (macOS metadata)
Hot Reloading
During development, reload mods without restarting:
PolymodHandler.hx (Force Reload)
/**
* Clear and reload from disk all data assets.
* Useful for "hot reloading" for fast iteration!
*/
public static function forceReloadAssets () : Void
{
// Forcibly clear scripts so that scripts can be edited
ModuleHandler . clearModuleCache ();
Polymod . clearScripts ();
// Forcibly reload Polymod so it finds any new files
funkin.modding. PolymodHandler . loadAllMods ();
// Reload all registries
SongEventRegistry . loadEventCache ();
SongRegistry . instance . loadEntries ();
LevelRegistry . instance . loadEntries ();
NoteStyleRegistry . instance . loadEntries ();
// ... etc
}
Hot reloading is a development feature . It requires debug builds and may cause instability.
Mod Management
Scanning Mods
public static function getAllMods () : Array < ModMetadata >
{
if ( modFileSystem == null ) modFileSystem = buildFileSystem ();
var modMetadata : Array < ModMetadata > = Polymod . scan ({
modRoot : MOD_FOLDER ,
apiVersionRule : API_VERSION_RULE ,
fileSystem : modFileSystem ,
errorCallback : PolymodErrorHandler . onPolymodError
});
return modMetadata ;
}
Loading Specific Mods
All Mods
Enabled Mods
Specific IDs
No Mods
PolymodHandler . loadAllMods ();
Loads every detected mod. PolymodHandler . loadEnabledMods ();
Loads only mods enabled in save data. PolymodHandler . loadModsById ([ 'mod1' , 'mod2' ]);
Loads specific mods by ID. PolymodHandler . loadNoMods ();
Initializes Polymod but loads no mods.
Load Order
Mods are loaded in the order specified in the dirs array:
loadModsById ([ 'baseMod' , 'skinMod' , 'tweakMod' ]);
baseMod loads first
skinMod loads second, can override baseMod
tweakMod loads last, can override both previous mods
Later mods take priority. If multiple mods replace the same asset, the last one wins.
Error Handling
Polymod reports errors via callback:
errorCallback : PolymodErrorHandler . onPolymodError ,
Common errors:
Missing metadata - No _polymod_meta.json
Invalid version - api_version incompatible
Parse error - Malformed JSON/XML/script
Missing dependency - Required mod not loaded
Advanced Techniques
Custom Merge Logic
You can specify merge behavior per file:
output . addFile ( "data/specialFile.txt" , TextFileFormat . PLAINTEXT );
Conditional Asset Loading
Load different assets based on conditions:
public function onCreate ( event : ScriptEvent )
{
var difficulty = PlayState . instance . currentDifficulty ;
if ( difficulty == 'hard' )
{
// Load harder version of assets
var sprite = Assets . getBitmapData ( 'images/hard/enemy.png' );
}
}
Multi-Mod Compatibility
Design mods to work together:
// Check if another mod is loaded
if ( PolymodHandler . loadedModIds . contains ( 'othermod' ))
{
// Adjust behavior for compatibility
}
Dynamic Asset Replacement
Modify assets at runtime:
public function onSongStart ( event : ScriptEvent )
{
// Change character sprite based on song
if ( event . id == 'tutorial' )
{
boyfriend . loadGraphic ( Paths . image ( 'characters/bf_tutorial' ));
}
}
Polymod caches asset lookups. First access is slower, subsequent accesses are fast.
HScript files are compiled once on load. Avoid reloading unless necessary.
Text merging is fast. JSON/XML merging can be slower for large files.
ZIP files have slight overhead. Folders are faster for development.
Debugging Tips
Enable Debug Logging
Compile with FEATURE_DEBUG_FUNCTIONS to see detailed mod info:
#if FEATURE_DEBUG_FUNCTIONS
var fileList : Array < String > = Polymod . listModFiles ( PolymodAssetType . IMAGE );
trace ( 'Installed mods have replaced ${ fileList . length } images.' );
#end
Check Loaded Mods
trace ( 'Loaded mods: ' + PolymodHandler . loadedModIds );
Verify Asset Sources
Check which mod provided an asset:
var source = Polymod . getAssetSource ( 'images/test.png' );
trace ( 'Asset from: ' + source ); // "basegame" or "modname"
Next Steps
Scripting Guide Master HScript for advanced mod functionality
Creating Mods Build your own mods from scratch
Modding Overview Return to the modding overview