# Friday Night Funkin' – Codename Engine Friday Night Funkin': Codename Engine is a cross-platform, modding-focused game engine built on HaxeFlixel, serving as the official successor to Yoshi Engine. It is designed to make creating and distributing FNF mods as powerful as source-code modification through a comprehensive softcoding system — everything from stages and characters to game states can be scripted without recompiling. The engine compiles to Windows x64, macOS x64, and Linux x64 and targets both desktop releases and experimental builds distributed via GitHub Actions. At its core, Codename Engine provides a full HScript (hscript-improved) scripting runtime, an XML-driven asset system for characters and stages, a flexible mod-folder architecture (compatible as addons on top of the core mod), advanced in-game editors (chart editor, character editor), a Discord RPC integration, video cutscene support via hxvlc, modcharting via FunkinModchart, and an auto-updater. The scripting runtime exposes Flixel, OpenFL, and all engine classes directly to scripts, giving modders near-parity with Haxe source code. --- ## Script.create — Load a Script from an Asset Path The primary entry point for instantiating any script. Automatically detects the language from the file extension (`.hx`, `.hxs`, `.hscript`, `.hsc`, `.pack`). Returns a `DummyScript` (no-op) when the asset does not exist so call-sites never need null checks. ```haxe // In Haxe source — loading a song script var script = Script.create(Paths.script("data/songs/bopeebo/scripts/myScript")); // Script auto-detects .hx extension; path resolves to assets/data/songs/bopeebo/scripts/myScript.hx script.load(); // From inside an already-running HScript — importing another script into the same ScriptPack var imported = importScript("data/shared/helper"); // throws if path not found imported.call("init", []); ``` --- ## Script.fromString — Create a Script from a String Creates and immediately executes a script from raw source code rather than a file path. Useful for dynamic code generation or `.pack` file format processing. ```haxe var code = " function onPlayerHit(event) { trace('hit! direction=' + event.direction); } "; var script = Script.fromString(code, "dynamicScript.hx"); script.load(); // script is now live and will respond to onPlayerHit calls ``` --- ## Script.call / ScriptPack.call — Invoke a Script Function Calls a named function defined inside a script (or all active scripts in a `ScriptPack`). Returns the result of the function, or `null` for void/missing functions. ```haxe // Single script var result = script.call("myFunction", [42, "hello"]); // ScriptPack (PlayState uses this internally) // calls onPlayerHit on every active script in order scripts.call("onPlayerHit", [event]); // Accessing a variable from a script var hp = script.get("customHealth"); // Dynamic script.set("customHealth", 1.5); ``` --- ## ScriptPack.event — Fire a Cancellable Event Across All Scripts Dispatches a `CancellableEvent` subclass through every active script in the pack. Scripts can cancel propagation (stop subsequent scripts) by calling `event.cancel()`. Used for every gameplay hook in `PlayState`. ```haxe // Firing NoteHitEvent across all PlayState scripts var event = new NoteHitEvent(); event.note = theNote; event.direction = 3; // Right event.score = 350; event.healthGain = 0.023; scripts.event("onPlayerHit", event); if (!event.cancelled) { // apply score / health as normal health += event.healthGain; } ``` --- ## CancellableEvent.preventDefault / cancel — Stop Default Behaviour Base class for all engine events passed to scripts. Calling `cancel()` (or `preventDefault()`) prevents the default engine action. An optional `bool` argument controls whether later scripts in the pack still receive the event. ```haxe // In a song HScript — prevent the miss sound and keep combo function onPlayerMiss(event:NoteMissEvent) { if (event.note.noteType == "Silent Miss") { event.preventMissSound(); // specific helper event.preventResetCombo(); // specific helper event.cancel(); // also marks cancelled, stops engine default } } // In a PlayState script — override countdown sprite function onCountdown(event:CountdownEvent) { if (event.swagCounter == 3) { event.spritePath = "ui/custom_ready"; // replace "ready" graphic event.scale = 0.8; // don't cancel — let engine create sprite with new path } } ``` --- ## PlayState Script Hooks — Full Lifecycle Callbacks Every function listed here can be defined in a song script (`songs//scripts/`) or the global chart script (`data/charts/global-script.hx`). The engine calls them at the corresponding moment during gameplay. ```haxe // songs/bopeebo/scripts/bopeebo.hx function create() { // called when PlayState finishes building the stage boyfriend.x += 50; camGameZoom = 0.85; } function postCreate() { // called after the very last setup step trace("Song ready: " + PlayState.SONG.meta.name); } function update(elapsed:Float) { // called every frame if (Conductor.curBeat % 8 == 0 && Conductor.curBeat != lastBeat) { lastBeat = Conductor.curBeat; FlxTween.tween(stage.getSprite("bg"), {alpha: 0.5}, 0.1, {type: FlxTweenType.PINGPONG}); } } function onPlayerHit(event:NoteHitEvent) { if (event.rating == "sick") { event.animSuffix = "-alt"; // play alt sing anim on sicks event.showSplash = true; } } function onPlayerMiss(event:NoteMissEvent) { event.healthGain = -0.1; // custom health drain } function onCameraMove(event:CamMoveEvent) { event.position.x += 30; // offset camera right permanently } function beatHit(curBeat:Int) { if (curBeat % 4 == 0) dad.playAnim("taunt", true); } function stepHit(curStep:Int) {} function onSongEnd() { trace("Song finished!"); } function onGameOver(event:GameOverEvent) { event.character = "bf-dead"; // force specific death character } ``` --- ## GlobalScript — Background Script Active Across All States A single script (or set of scripts, one per loaded mod) that persists across every state switch. Place it at `data/global/global.hx`. Receives Flixel global signals and Conductor beat/step hits. ```haxe // data/global/global.hx — runs the entire session var fpsDisplay:FlxText; function preStateCreate(state) { trace("Switching to: " + Type.getClassName(Type.getClass(state))); } function postStateSwitch() { // re-attach overlay after state switched fpsDisplay = new FlxText(10, 10, 0, "", 14); fpsDisplay.scrollFactor.set(0, 0); FlxG.state.add(fpsDisplay); } function postUpdate(elapsed) { if (fpsDisplay != null) fpsDisplay.text = "FPS: " + Math.round(1 / elapsed); } function beatHit(curBeat) { trace("beat " + curBeat); } ``` --- ## Flags — Engine Configuration via flags.ini `Flags` is a statically built class that reads `data/config/flags.ini` (INI format) at runtime. All public static fields are configurable. This lets mods change defaults without source changes. ```ini # data/config/flags.ini — lives inside your mod folder [Common] NAME = My Cool Mod DESCRIPTION = A Friday Night Funkin mod AUTHOR = YourName [Discord] CLIENT_ID = 1234567890123456789 LOGO_KEY = my-mod-icon LOGO_TEXT = My Cool Mod [Flags] DEFAULT_SCROLL_SPEED = 2.7 DEFAULT_DIFFICULTY = hard DISABLE_EDITORS = false DEFAULT_GAMEOVER_CHARACTER = bf-dead DEFAULT_CAM_ZOOM = 1.0 DEFAULT_PAUSE_ITEMS = Resume, Restart Song, Exit to menu ``` ```haxe // Reading flags at runtime from an HScript trace(Flags.MOD_NAME); // "My Cool Mod" trace(Flags.DEFAULT_SCROLL_SPEED); // 2.7 trace(Flags.VERSION); // "1.0.1" trace(Flags.COMMIT_NUMBER); // e.g. 2675 trace(Flags.SOUND_EXT); // "ogg" (desktop) / "mp3" (web) ``` --- ## Paths — Asset Resolution Utility Static helper class for resolving all asset paths. All methods return the correct library-scoped path string that OpenFL/Lime can load. On case-insensitive Linux systems, Paths automatically corrects casing. ```haxe // Images var img = Paths.image("characters/bf"); // assets/images/characters/bf.png var atlas = Paths.image("characters/bf", null, true); // checks for atlas/spritemap automatically // Audio var inst = Paths.inst("bopeebo"); // assets/songs/bopeebo/song/Inst.ogg var voices = Paths.voices("bopeebo", "hard"); // Voices-hard.ogg or Voices.ogg fallback var sfx = Paths.sound("missnote1"); // assets/sounds/missnote1.ogg var music = Paths.music("freakyMenu"); // assets/music/freakyMenu.ogg // Data files var chart = Paths.chart("bopeebo", "hard"); // assets/songs/bopeebo/charts/hard.json var xml = Paths.xml("characters/bf"); // assets/data/characters/bf.xml var json = Paths.json("stages/stage"); // assets/data/stages/stage.json // Scripts var path = Paths.script("data/songs/bopeebo/scripts/bopeebo"); // resolves .hx/.hxs/.hsc automatically // Videos var video = Paths.video("cutscene_week7"); // assets/videos/cutscene_week7.mp4 ``` --- ## Character XML — Softcoded Character Definition Characters are fully defined in XML files at `data/characters/.xml`. Spritesheets live in `images/characters/`, health icons in `images/icons/`. No Haxe source changes are needed. ```xml ``` --- ## Stage XML — Softcoded Stage Definition Stages are entirely XML-driven. Sprites are layered by declaration order. Character anchor elements (``, ``, ``) can be repositioned and reordered anywhere in the layer stack. ```xml ``` --- ## Song File Structure — Meta, Chart, Audio, Scripts Every song lives in `songs//`. Difficulties are auto-detected by filename. Audio variants per difficulty are supported via the `-` suffix convention. ``` songs/ bopeebo/ charts/ easy.json ← difficulty chart (named by difficulty only, not song name) normal.json hard.json erect.json ← custom difficulty, auto-detected scripts/ bopeebo.hx ← song-specific gameplay script intro.hx ← additional script, loaded alongside song/ Inst.ogg ← default instrumental Inst-erect.ogg ← difficulty-specific instrumental Voices.ogg ← default vocals Voices-erect.ogg ← difficulty-specific vocals meta.json ← song metadata (title, BPM, freeplay info, etc.) cutscene.hx ← intro cutscene script dialogue.xml ← dialogue shown before/after song events.json ← global event track ``` ```json // songs/bopeebo/meta.json { "name": "Bopeebo", "bpm": 100, "difficulties": ["easy", "normal", "hard"], "icon": "dad", "color": "#9271FD", "coopAllowed": false, "opponentModeAllowed": false, "preview": { "start": 2000, "end": 17000 } } ``` --- ## Cutscene Script — Intro and Outro Cutscenes Intro cutscenes are at `data/cutscenes/.hx`, outros at `data/cutscenes/-end.hx`. Calling `close()` from within them either starts the song (intro) or ends it (outro). ```haxe // data/cutscenes/tutorial.hx — example video + dialogue cutscene function create() { // Play a video; close() starts the song after it ends startVideo(Paths.video("tutorial-intro"), function() { close(); }); } // Alternatively, start a dialogue cutscene: // function create() { // startDialogue(Paths.xml("dialogue/tutorial"), function() { // close(); // }); // } ``` --- ## ModState / ModSubState — Fully Scripted Game States A `ModState` or `ModSubState` is an entire HaxeFlixel state/substate implemented purely in HScript. The script is placed at `data/states/.hx` and switched to with `FlxG.switchState(new ModState("myState"))`. ```haxe // Switching to a scripted state from HScript FlxG.switchState(new ModState("myMenu", {option: "value"})); // Pushing a scripted substate FlxG.state.openSubState(new ModSubState("myPanel")); ``` ```haxe // data/states/myMenu.hx — the full state is written in HScript var title:FlxText; var options:Array = ["Start", "Options", "Credits", "Exit"]; var selected:Int = 0; function create() { title = new FlxText(0, 80, FlxG.width, "MY MOD", 48); title.alignment = CENTER; add(title); for (i in 0...options.length) { var t = new FlxText(0, 200 + i * 50, FlxG.width, options[i], 28); t.alignment = CENTER; add(t); } } function update(elapsed:Float) { if (FlxG.keys.justPressed.DOWN) selected = (selected + 1) % options.length; if (FlxG.keys.justPressed.UP) selected = (selected - 1 + options.length) % options.length; if (FlxG.keys.justPressed.ENTER) { switch (options[selected]) { case "Start": FlxG.switchState(new PlayState()); case "Exit": FlxG.switchState(new TitleState()); } } } ``` --- ## FunkinShader / CustomShader — GLSL Shaders in Scripts `FunkinShader` wraps OpenFL shaders to be fully accessible and tweakable from HScript. `CustomShader` loads `.frag` / `.vert` files from the `shaders/` asset folder. ```haxe // data/states/InGame.hx or any HScript — apply a custom chromatic aberration shader import funkin.backend.shaders.CustomShader; var chromatic:CustomShader; function create() { // shaders/chromatic.frag must exist in the mod's shaders folder chromatic = new CustomShader("chromatic"); chromatic.setFloat("offset", 0.005); // Apply to the game camera FlxG.camera.setFilters([new openfl.filters.ShaderFilter(chromatic)]); } function update(elapsed:Float) { // Animate the shader uniform over time chromatic.setFloat("offset", 0.003 + Math.sin(Conductor.songPosition * 0.001) * 0.004); } ``` --- ## Conductor — Song Timing & Beat Signals `Conductor` is the global time-keeping singleton. It fires `FlxTypedSignal` events at every step, beat, and measure. Scripts can listen via hooks or poll the values directly. ```haxe // Reading timing values from any HScript trace(Conductor.songPosition); // current time in milliseconds trace(Conductor.curBeat); // integer current beat trace(Conductor.curBeatFloat); // fractional beat (for smooth interpolation) trace(Conductor.curStep); // current step (beats × stepsPerBeat) trace(Conductor.bpm); // current BPM // Subscribing in Haxe source (e.g. custom HaxeFlixel state) Conductor.onBeatHit.add(function(beat:Int) { trace("Beat " + beat + " fired"); }); Conductor.onBPMChange.add(function(oldBPM:Float, newBPM:Float) { trace('BPM changed from $oldBPM to $newBPM'); }); // Utility: get snapped beat offset for a zoom interval var snapped = Conductor.getBeats(BEAT, 4, 0); // snaps to every 4 beats ``` --- ## FunkinSprite — Extended Sprite With Beat & XML Animations `FunkinSprite` extends `FlxAnimate` (animate-atlas capable) and implements `IBeatReceiver`. It supports XML-driven animations, beat-synced idle loops (`animType="beat"`), offset-compatible transforms, and an `extra` metadata map. ```haxe // Creating and configuring a FunkinSprite from HScript var s = new FunkinSprite(); s.frames = Paths.getFrames(Paths.image("characters/bf")); // atlas frames // Load animations from the character XML helper XMLUtil.loadAnimationsFromXML(s, Paths.xml("characters/bf")); // auto-applies offsets // Or manually add an animation s.addAnim("idle", "BF idle dance", 24, false, []); s.addAnim("singLEFT", "BF NOTE LEFT0", 24, false, []); // Beat-synced playback — plays "idle" every 2 beats automatically s.spriteAnimType = BEAT; s.beatInterval = 2; // Extra metadata storage s.extra.set("hitCount", 0); trace(s.extra.get("hitCount")); // 0 // Tween a shader uniform on this sprite var sh = new CustomShader("blur"); s.shader = sh; FlxTween.tween(sh, {"blurAmount.value[0]": 5.0}, 0.5, {ease: FlxEase.sineOut}); ``` --- ## ScriptPack — Managing Multiple Scripts Together `ScriptPack` groups scripts and broadcasts calls/events to all of them. `PlayState.scripts` is always a `ScriptPack`. Mods can add runtime scripts or import helpers. ```haxe // In a PlayState HScript — import a shared utility script var util = importScript("data/shared/scoreUtil"); // throws if missing // Accessing a public variable from any script in the pack // (public vars are shared across all scripts in the same ScriptPack) // In scriptA.hx: // public var sharedScore:Int = 0; // In scriptB.hx: // trace(sharedScore); // reads scriptA's public variable // Building a ScriptPack manually in Haxe source var pack = new ScriptPack("myFeature"); pack.setParent(PlayState.instance); // "boyfriend", "dad", etc. become direct identifiers pack.add(Script.create(Paths.script("data/feature/effect1"))); pack.add(Script.create(Paths.script("data/feature/effect2"))); pack.load(); // Calling across the pack pack.call("initialize", []); pack.event("onPlayerHit", event); // Hot-reload all scripts in a pack (works during dev with SHIFT + dev reload key) pack.reload(); ``` --- ## Summary Codename Engine's primary use cases are building full Friday Night Funkin' mods — from simple song packs with custom characters and stages to standalone sub-engines with completely custom menus, UI, and gameplay rules — all without touching Haxe source. The scripting system (`HScript`, `ScriptPack`, `GlobalScript`, `ModState`) makes every part of the game hookable: developers drop `.hx` files into standardised folders and use the same class APIs available in compiled code. The XML-driven pipeline (characters, stages, weeks, dialogue, note skins) handles the data layer, while `Flags`/`flags.ini` gives per-mod compile-time-style tuning of engine defaults at runtime. For integration, the recommended pattern is to start from the `assets/` folder (which is itself a functional mod), study the built-in characters and stages as templates, use the in-game editors for chart and character data, and write gameplay scripts in the song's `scripts/` subfolder. Cross-cutting features (HUD overlays, Discord presence, input remapping, global sound effects) belong in `data/global/global.hx` via `GlobalScript`. More complex scenarios — custom freeplay screens, title cards, options menus — use `ModState`/`ModSubState` with full HScript, enabling mods that look and feel completely distinct from the base engine while still benefiting from all its optimisation, platform support, and tooling.