From 25dc011df2e30934cd3a8f32cd37346de7181531 Mon Sep 17 00:00:00 2001
From: Eric Myllyoja <ericmyllyoja@gmail.com>
Date: Tue, 29 Mar 2022 21:56:04 -0400
Subject: [PATCH] Many bug fixes; combo meter now works; main menu graphics
 split up

---
 .../introMod/_append/data/introText.txt       |   1 -
 source/funkin/Conductor.hx                    |  34 +++-
 source/funkin/InitState.hx                    |   1 +
 source/funkin/LatencyState.hx                 |   2 +-
 source/funkin/LoadingState.hx                 |   8 +-
 source/funkin/MainMenuState.hx                | 105 ++++------
 source/funkin/MusicBeatState.hx               |  38 +++-
 source/funkin/TitleState.hx                   |  33 +++-
 source/funkin/charting/ChartingState.hx       |  12 +-
 source/funkin/modding/IScriptedClass.hx       |   2 +-
 source/funkin/modding/events/ScriptEvent.hx   |  23 ++-
 .../modding/events/ScriptEventDispatcher.hx   |   2 +-
 source/funkin/modding/module/Module.hx        |   2 +-
 source/funkin/play/Countdown.hx               |  25 +--
 source/funkin/play/PicoFight.hx               |  16 +-
 source/funkin/play/PlayState.hx               | 185 +++++++++---------
 source/funkin/play/character/CharacterData.hx |  50 ++---
 source/funkin/play/stage/Bopper.hx            |   2 +-
 source/funkin/play/stage/Stage.hx             |   3 +-
 source/funkin/ui/AtlasMenuList.hx             |  19 +-
 source/funkin/ui/AtlasText.hx                 |  19 +-
 source/funkin/ui/ControlsMenu.hx              |   6 +-
 source/funkin/ui/OptionsState.hx              |   2 +-
 source/funkin/ui/PreferencesMenu.hx           |   2 +-
 source/funkin/ui/TextMenuList.hx              |   4 +-
 source/funkin/util/SortUtil.hx                |   2 +
 26 files changed, 345 insertions(+), 253 deletions(-)
 delete mode 100644 example_mods/introMod/_append/data/introText.txt

diff --git a/example_mods/introMod/_append/data/introText.txt b/example_mods/introMod/_append/data/introText.txt
deleted file mode 100644
index 45e0c08ab..000000000
--- a/example_mods/introMod/_append/data/introText.txt
+++ /dev/null
@@ -1 +0,0 @@
-swagshit--moneymoney
\ No newline at end of file
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 765686740..331dd46f7 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -15,9 +15,31 @@ typedef BPMChangeEvent =
 
 class Conductor
 {
+	/**
+	 * Beats per minute of the song.
+	 */
 	public static var bpm:Float = 100;
-	public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
-	public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
+
+	/**
+	 * Duration of a beat in millisecond.
+	 */
+	public static var crochet(get, null):Float;
+
+	static function get_crochet():Float
+	{
+		return ((60 / bpm) * 1000);
+	}
+
+	/**
+	 * Duration of a step in milliseconds.
+	 */
+	public static var stepCrochet(get, null):Float;
+
+	static function get_stepCrochet():Float
+	{
+		return crochet / 4;
+	}
+
 	public static var songPosition:Float;
 	public static var lastSongPos:Float;
 	public static var offset:Float = 0;
@@ -52,12 +74,4 @@ class Conductor
 		}
 		trace("new BPM map BUDDY " + bpmChangeMap);
 	}
-
-	public static function changeBPM(newBpm:Float)
-	{
-		bpm = newBpm;
-
-		crochet = ((60 / bpm) * 1000);
-		stepCrochet = crochet / 4;
-	}
 }
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 58aae986c..f7ec0ff4b 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -124,6 +124,7 @@ class InitState extends FlxTransitionableState
 
 		StageDataParser.loadStageCache();
 		CharacterDataParser.loadCharacterCache();
+		ModuleHandler.buildModuleCallbacks();
 		ModuleHandler.loadModuleCache();
 
 		#if song
diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx
index 4e0ca07af..0239bea53 100644
--- a/source/funkin/LatencyState.hx
+++ b/source/funkin/LatencyState.hx
@@ -31,7 +31,7 @@ class LatencyState extends FlxState
 		strumLine = new FlxSprite(FlxG.width / 2, 100).makeGraphic(FlxG.width, 5);
 		add(strumLine);
 
-		Conductor.changeBPM(120);
+		Conductor.bpm = 120;
 
 		super.create();
 	}
diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx
index b163b49d0..bbe20ff2d 100644
--- a/source/funkin/LoadingState.hx
+++ b/source/funkin/LoadingState.hx
@@ -117,9 +117,11 @@ class LoadingState extends MusicBeatState
 		}
 	}
 
-	override function beatHit()
+	override function beatHit():Bool
 	{
-		super.beatHit();
+		// super.beatHit() returns false if a module cancelled the event.
+		if (!super.beatHit())
+			return false;
 
 		// logo.animation.play('bump');
 		danceLeft = !danceLeft;
@@ -128,6 +130,8 @@ class LoadingState extends MusicBeatState
 				gfDance.animation.play('danceRight');
 			else
 				gfDance.animation.play('danceLeft'); */
+
+		return true;
 	}
 
 	var targetShit:Float = 0;
diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index bc20714e1..39eeb87e7 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -20,6 +20,7 @@ import funkin.NGio;
 import funkin.shaderslmfao.ScreenWipeShader;
 import funkin.ui.AtlasMenuList;
 import funkin.ui.MenuList;
+import funkin.ui.MenuList.MenuItem;
 import funkin.ui.OptionsState;
 import funkin.ui.PreferencesMenu;
 import funkin.ui.Prompt;
@@ -40,7 +41,7 @@ import funkin.ui.NgPrompt;
 
 class MainMenuState extends MusicBeatState
 {
-	var menuItems:MainMenuList;
+	var menuItems:MenuTypedList<AtlasMenuItem>;
 
 	var magenta:FlxSprite;
 	var camFollow:FlxObject;
@@ -88,7 +89,7 @@ class MainMenuState extends MusicBeatState
 			add(magenta);
 		// magenta.scrollFactor.set();
 
-		menuItems = new MainMenuList();
+		menuItems = new MenuTypedList<AtlasMenuItem>();
 		add(menuItems);
 		menuItems.onChange.add(onMenuItemChange);
 		menuItems.onAcceptPress.add(function(_)
@@ -104,31 +105,37 @@ class MainMenuState extends MusicBeatState
 		});
 
 		menuItems.enabled = false; // disable for intro
-		menuItems.createItem('story mode', function() startExitState(new StoryMenuState()));
-		menuItems.createItem('freeplay', function()
+
+		createMenuItem('storymode', 'mainmenu/storymode', function()
+		{
+			startExitState(new StoryMenuState());
+		});
+
+		createMenuItem('freeplay', 'mainmenu/freeplay', function()
 		{
 			persistentDraw = true;
 			persistentUpdate = false;
 			openSubState(new FreeplayState());
 		});
-		// addMenuItem('options', function () startExitState(new OptionMenu()));
 		#if CAN_OPEN_LINKS
 		var hasPopupBlocker = #if web true #else false #end;
 
 		if (VideoState.seenVideo)
-			menuItems.createItem('kickstarter', selectDonate, hasPopupBlocker);
+		{
+			createMenuItem('kickstarter', 'mainmenu/kickstarter', selectDonate, hasPopupBlocker);
+		}
 		else
-			menuItems.createItem('donate', selectDonate, hasPopupBlocker);
+		{
+			createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker);
+		}
 		#end
-		menuItems.createItem('options', function() startExitState(new OptionsState()));
-		// #if newgrounds
-		// 	if (NGio.isLoggedIn)
-		// 		menuItems.createItem("logout", selectLogout);
-		// 	else
-		// 		menuItems.createItem("login", selectLogin);
-		// #end
 
-		// center vertically
+		createMenuItem('options', 'mainmenu/options', function()
+		{
+			startExitState(new OptionsState());
+		});
+
+		// Reset position of menu items.
 		var spacing = 160;
 		var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
 		for (i in 0...menuItems.length)
@@ -146,19 +153,26 @@ class MainMenuState extends MusicBeatState
 
 		// This has to come AFTER!
 		this.leftWatermarkText.text = Constants.VERSION;
-		this.rightWatermarkText.text = "blablabla test";
-
-		// var versionStr = 'v${Application.current.meta.get('version')}';
-		// versionStr += ' (secret week 8 build do not leak)';
-		//
-		// var versionShit:FlxText = new FlxText(5, FlxG.height - 18, 0, versionStr, 12);
-		// versionShit.scrollFactor.set();
-		// versionShit.setFormat("VCR OSD Mono", 16, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
-		// add(versionShit);
+		// this.rightWatermarkText.text = "blablabla test";
 
 		// NG.core.calls.event.logEvent('swag').send();
 	}
 
+	function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void
+	{
+		var item = new AtlasMenuItem(name, Paths.getSparrowAtlas(atlas), callback);
+		item.fireInstantly = fireInstantly;
+		item.ID = menuItems.length;
+
+		item.scrollFactor.set();
+
+		// Set the offset of the item so the sprite is centered on the origin.
+		item.centered = true;
+		item.changeAnim('idle');
+
+		menuItems.addItem(name, item);
+	}
+
 	override function closeSubState()
 	{
 		magenta.visible = false;
@@ -308,46 +322,3 @@ class MainMenuState extends MusicBeatState
 		}
 	}
 }
-
-private class MainMenuList extends MenuTypedList<MainMenuItem>
-{
-	public var atlas:FlxAtlasFrames;
-
-	public function new()
-	{
-		atlas = Paths.getSparrowAtlas('main_menu');
-		super(Vertical);
-	}
-
-	public function createItem(x = 0.0, y = 0.0, name:String, callback, fireInstantly = false)
-	{
-		var item = new MainMenuItem(x, y, name, atlas, callback);
-		item.fireInstantly = fireInstantly;
-		item.ID = length;
-
-		return addItem(name, item);
-	}
-
-	override function destroy()
-	{
-		super.destroy();
-		atlas = null;
-	}
-}
-
-private class MainMenuItem extends AtlasMenuItem
-{
-	public function new(x = 0.0, y = 0.0, name, atlas, callback)
-	{
-		super(x, y, name, atlas, callback);
-		scrollFactor.set();
-	}
-
-	override function changeAnim(anim:String)
-	{
-		super.changeAnim(anim);
-		// position by center
-		centerOrigin();
-		offset.copyFrom(origin);
-	}
-}
diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx
index 9ee8d0e36..a11d468ca 100644
--- a/source/funkin/MusicBeatState.hx
+++ b/source/funkin/MusicBeatState.hx
@@ -1,5 +1,7 @@
 package funkin;
 
+import flixel.util.FlxSort;
+import funkin.util.SortUtil;
 import funkin.play.stage.StageData.StageDataParser;
 import funkin.play.character.CharacterData.CharacterDataParser;
 import flixel.FlxState;
@@ -135,16 +137,46 @@ class MusicBeatState extends FlxUIState
 		curStep = lastChange.stepTime + Math.floor((Conductor.songPosition - lastChange.songTime) / Conductor.stepCrochet);
 	}
 
-	public function stepHit():Void
+	public function stepHit():Bool
 	{
+		var event = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep);
+
+		dispatchEvent(event);
+
+		if (event.eventCanceled)
+		{
+			return false;
+		}
+
 		if (curStep % 4 == 0)
 			beatHit();
+
+		return true;
 	}
 
-	public function beatHit():Void
+	public function beatHit():Bool
 	{
+		var event = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep);
+
+		dispatchEvent(event);
+
+		if (event.eventCanceled)
+		{
+			return false;
+		}
+
 		lastBeatHitTime = Conductor.songPosition;
-		// do literally nothing dumbass
+
+		return true;
+	}
+
+	/**
+	 * Refreshes the state, by redoing the render order of all sprites.
+	 * It does this based on the `zIndex` of each prop.
+	 */
+	public function refresh()
+	{
+		sort(SortUtil.byZIndex, FlxSort.ASCENDING);
 	}
 
 	override function switchTo(nextState:FlxState):Bool
diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx
index 76749b47b..df91a5841 100644
--- a/source/funkin/TitleState.hx
+++ b/source/funkin/TitleState.hx
@@ -1,5 +1,6 @@
 package funkin;
 
+import funkin.ui.AtlasText.BoldText;
 import funkin.audiovis.SpectogramSprite;
 import flixel.FlxObject;
 import flixel.FlxSprite;
@@ -160,7 +161,7 @@ class TitleState extends MusicBeatState
 			FlxG.sound.music.fadeIn(4, 0, 0.7);
 		}
 
-		Conductor.changeBPM(102);
+		Conductor.bpm = 102;
 		persistentUpdate = true;
 
 		var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
@@ -218,6 +219,7 @@ class TitleState extends MusicBeatState
 
 		blackScreen = bg.clone();
 		credGroup.add(blackScreen);
+		credGroup.add(textGroup);
 
 		// var atlasBullShit:FlxSprite = new FlxSprite();
 		// atlasBullShit.frames = CoolUtil.fromAnimate(Paths.image('money'), Paths.file('images/money.json'));
@@ -495,39 +497,48 @@ class TitleState extends MusicBeatState
 		var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music);
 		add(spec);
 
-		Conductor.changeBPM(190);
+		Conductor.bpm = 190;
 		FlxG.camera.flash(FlxColor.WHITE, 1);
 		FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
 	}
 
 	function createCoolText(textArray:Array<String>)
 	{
+		if (credGroup == null || textGroup == null)
+			return;
+
 		for (i in 0...textArray.length)
 		{
-			var money:Alphabet = new Alphabet(0, 0, textArray[i], true, false);
+			var money:BoldText = new BoldText(0, 0, textArray[i]);
 			money.screenCenter(X);
 			money.y += (i * 60) + 200;
-			credGroup.add(money);
+			// credGroup.add(money);
 			textGroup.add(money);
 		}
 	}
 
 	function addMoreText(text:String)
 	{
+		if (credGroup == null || textGroup == null)
+			return;
+
 		lime.ui.Haptic.vibrate(100, 100);
 
-		var coolText:Alphabet = new Alphabet(0, 0, text, true, false);
+		var coolText:BoldText = new BoldText(0, 0, text);
 		coolText.screenCenter(X);
 		coolText.y += (textGroup.length * 60) + 200;
-		credGroup.add(coolText);
+		// credGroup.add(coolText);
 		textGroup.add(coolText);
 	}
 
 	function deleteCoolText()
 	{
+		if (credGroup == null || textGroup == null)
+			return;
+
 		while (textGroup.members.length > 0)
 		{
-			credGroup.remove(textGroup.members[0], true);
+			// credGroup.remove(textGroup.members[0], true);
 			textGroup.remove(textGroup.members[0], true);
 		}
 	}
@@ -535,9 +546,11 @@ class TitleState extends MusicBeatState
 	var isRainbow:Bool = false;
 	var skippedIntro:Bool = false;
 
-	override function beatHit()
+	override function beatHit():Bool
 	{
-		super.beatHit();
+		// super.beatHit() returns false if a module cancelled the event.
+		if (!super.beatHit())
+			return false;
 
 		if (!skippedIntro)
 		{
@@ -597,6 +610,8 @@ class TitleState extends MusicBeatState
 			else
 				gfDance.animation.play('danceLeft');
 		}
+
+		return true;
 	}
 
 	function skipIntro():Void
diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx
index 7c5e8bd27..507d8dec6 100644
--- a/source/funkin/charting/ChartingState.hx
+++ b/source/funkin/charting/ChartingState.hx
@@ -145,7 +145,7 @@ class ChartingState extends MusicBeatState
 		updateGrid();
 
 		loadSong(_song.song);
-		Conductor.changeBPM(_song.bpm);
+		Conductor.bpm = _song.bpm;
 		Conductor.mapBPMChanges(_song);
 
 		bpmTxt = new FlxText(1000, 50, 0, "", 16);
@@ -546,7 +546,7 @@ class ChartingState extends MusicBeatState
 			{
 				tempBpm = nums.value;
 				Conductor.mapBPMChanges(_song);
-				Conductor.changeBPM(nums.value);
+				Conductor.bpm = nums.value;
 			}
 			else if (wname == 'note_susLength')
 			{
@@ -976,9 +976,9 @@ class ChartingState extends MusicBeatState
 		_song.bpm = tempBpm;
 
 		/* if (FlxG.keys.justPressed.UP)
-				Conductor.changeBPM(Conductor.bpm + 1);
+				Conductor.bpm += 1;
 			if (FlxG.keys.justPressed.DOWN)
-				Conductor.changeBPM(Conductor.bpm - 1); */
+				Conductor.bpm -= 1; */
 
 		var shiftThing:Int = 1;
 		if (FlxG.keys.pressed.SHIFT)
@@ -1214,7 +1214,7 @@ class ChartingState extends MusicBeatState
 
 		if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0)
 		{
-			Conductor.changeBPM(SongLoad.getSong()[curSection].bpm);
+			Conductor.bpm = SongLoad.getSong()[curSection].bpm;
 			FlxG.log.add('CHANGED BPM!');
 		}
 		else
@@ -1224,7 +1224,7 @@ class ChartingState extends MusicBeatState
 			for (i in 0...curSection)
 				if (SongLoad.getSong()[i].changeBPM)
 					daBPM = SongLoad.getSong()[i].bpm;
-			Conductor.changeBPM(daBPM);
+			Conductor.bpm = daBPM;
 		}
 
 		/* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index 70ab28983..4261032a2 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -54,7 +54,7 @@ interface INoteScriptedClass extends IScriptedClass
  */
 interface IPlayStateScriptedClass extends IScriptedClass
 {
-	public function onPause(event:ScriptEvent):Void;
+	public function onPause(event:PauseScriptEvent):Void;
 	public function onResume(event:ScriptEvent):Void;
 
 	public function onSongLoaded(eent:SongLoadScriptEvent):Void;
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index 1c55616fc..ef8fc3a06 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -220,7 +220,7 @@ class ScriptEvent
 	 */
 	/**
 	 * If true, the behavior associated with this event can be prevented.
-	 * For example, cancelling COUNTDOWN_BEGIN should prevent the countdown from starting,
+	 * For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
 	 * until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
 	 */
 	public var cancelable(default, null):Bool;
@@ -250,7 +250,7 @@ class ScriptEvent
 
 	/**
 	 * Call this function on a cancelable event to cancel the associated behavior.
-	 * For example, cancelling COUNTDOWN_BEGIN will prevent the countdown from starting.
+	 * For example, cancelling COUNTDOWN_START will prevent the countdown from starting.
 	 */
 	public function cancelEvent():Void
 	{
@@ -400,7 +400,7 @@ class SongTimeScriptEvent extends ScriptEvent
 
 	public function new(type:ScriptEventType, beat:Int, step:Int):Void
 	{
-		super(type, false);
+		super(type, true);
 		this.beat = beat;
 		this.step = step;
 	}
@@ -535,3 +535,20 @@ class SubStateScriptEvent extends ScriptEvent
 		return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')';
 	}
 }
+
+/**
+ * An event which is called when the player attempts to pause the game.
+ */
+class PauseScriptEvent extends ScriptEvent
+{
+	/**
+	 * Whether to use the Gitaroo Man pause.
+	 */
+	public var gitaroo(default, default):Bool;
+
+	public function new(gitaroo:Bool):Void
+	{
+		super(ScriptEvent.PAUSE, true);
+		this.gitaroo = gitaroo;
+	}
+}
diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx
index 45edf5214..bfb62b9d1 100644
--- a/source/funkin/modding/events/ScriptEventDispatcher.hx
+++ b/source/funkin/modding/events/ScriptEventDispatcher.hx
@@ -68,7 +68,7 @@ class ScriptEventDispatcher
 					t.onGameOver(event);
 					return;
 				case ScriptEvent.PAUSE:
-					t.onPause(event);
+					t.onPause(cast event);
 					return;
 				case ScriptEvent.RESUME:
 					t.onResume(event);
diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx
index 207fc2a4a..74f04c3bd 100644
--- a/source/funkin/modding/module/Module.hx
+++ b/source/funkin/modding/module/Module.hx
@@ -79,7 +79,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
 
 	public function onUpdate(event:UpdateScriptEvent) {}
 
-	public function onPause(event:ScriptEvent) {}
+	public function onPause(event:PauseScriptEvent) {}
 
 	public function onResume(event:ScriptEvent) {}
 
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index ec6fd9dce..235db587e 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -28,23 +28,24 @@ class Countdown
 	 * Performs the countdown.
 	 * Pauses the song, plays the countdown graphics/sound, and then starts the song.
 	 * This will automatically stop and restart the countdown if it is already running.
+	 * @returns `false` if the countdown was cancelled by a script.
 	 */
-	public static function performCountdown(isPixelStyle:Bool):Void
+	public static function performCountdown(isPixelStyle:Bool):Bool
 	{
+		countdownStep = BEFORE;
+		var cancelled:Bool = propagateCountdownEvent(countdownStep);
+		if (cancelled)
+			return false;
+
 		// Stop any existing countdown.
 		stopCountdown();
 
 		PlayState.isInCountdown = true;
 		Conductor.songPosition = Conductor.crochet * -5;
-		countdownStep = BEFORE;
 		// Handle onBeatHit events manually
 		@:privateAccess
 		PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0));
 
-		var cancelled:Bool = propagateCountdownEvent(countdownStep);
-		if (cancelled)
-			return;
-
 		// The timer function gets called based on the beat of the song.
 		countdownTimer = new FlxTimer();
 
@@ -72,7 +73,9 @@ class Countdown
 			{
 				stopCountdown();
 			}
-		}, 6); // Before, 3, 2, 1, GO!, After
+		}, 5); // Before, 3, 2, 1, GO!, After
+
+		return true;
 	}
 
 	/**
@@ -94,11 +97,9 @@ class Countdown
 				return true;
 		}
 
-		// Stage
-		ScriptEventDispatcher.callEvent(PlayState.instance.currentStage, event);
-
-		// Modules
-		ModuleHandler.callEvent(event);
+		// Modules, stages, characters.
+		@:privateAccess
+		PlayState.instance.dispatchEvent(event);
 
 		return event.eventCanceled;
 	}
diff --git a/source/funkin/play/PicoFight.hx b/source/funkin/play/PicoFight.hx
index 9a4dded9a..016fada3f 100644
--- a/source/funkin/play/PicoFight.hx
+++ b/source/funkin/play/PicoFight.hx
@@ -37,7 +37,7 @@ class PicoFight extends MusicBeatState
 		FlxG.sound.playMusic(Paths.inst("blazin"));
 
 		SongLoad.loadFromJson('blazin', "blazin");
-		Conductor.changeBPM(SongLoad.songData.bpm);
+		Conductor.bpm = SongLoad.songData.bpm;
 
 		for (dumbassSection in SongLoad.songData.noteMap['hard'])
 		{
@@ -184,13 +184,17 @@ class PicoFight extends MusicBeatState
 		super.update(elapsed);
 	}
 
-	override function stepHit()
+	override function stepHit():Bool
 	{
-		super.stepHit();
+		return super.stepHit();
 	}
 
-	override function beatHit()
+	override function beatHit():Bool
 	{
+		// super.beatHit() returns false if a module cancelled the event.
+		if (!super.beatHit())
+			return false;
+
 		funnyWave.thickness = 10;
 		funnyWave.waveAmplitude = 300;
 		funnyWave.realtimeVisLenght = 0.1;
@@ -198,7 +202,7 @@ class PicoFight extends MusicBeatState
 		picoHealth += 1;
 
 		makeNotes();
-		// trace(picoHealth);
-		super.beatHit();
+
+		return true;
 	}
 }
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 2faa894b2..09cdc0c06 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -140,8 +140,9 @@ class PlayState extends MusicBeatState implements IHook
 	 * An empty FlxObject contained in the scene.
 	 * The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
 	 * 
-	 * NOTE: This must be an FlxObject, not an FlxPoint, because it needs to be added to the scene.
-	 * Once it's added to the scene, the camera can be configured to follow it.
+	 * This is an FlxSprite for two reasons:
+	 * 1. It needs to be an object in the scene for the camera to be configured to follow it.
+	 * 2. It needs to be an FlxSprite to allow a graphic (optionally, for debug purposes) to be drawn on it.
 	 */
 	public var cameraFollowPoint:FlxSprite = new FlxSprite(0, 0);
 
@@ -286,6 +287,7 @@ class PlayState extends MusicBeatState implements IHook
 		// Displays the camera follow point as a sprite for debug purposes.
 		// TODO: Put this on a toggle?
 		cameraFollowPoint.makeGraphic(8, 8, 0xFF00FF00);
+		cameraFollowPoint.visible = false;
 		cameraFollowPoint.zIndex = 1000000;
 
 		// Reduce physics accuracy (who cares!!!) to improve animation quality.
@@ -313,7 +315,7 @@ class PlayState extends MusicBeatState implements IHook
 			currentSong = SongLoad.loadFromJson('tutorial');
 
 		Conductor.mapBPMChanges(currentSong);
-		Conductor.changeBPM(currentSong.bpm);
+		Conductor.bpm = currentSong.bpm;
 
 		switch (currentSong.song.toLowerCase())
 		{
@@ -555,7 +557,6 @@ class PlayState extends MusicBeatState implements IHook
 		if (dad != null)
 		{
 			dad.characterType = CharacterType.DAD;
-			cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
 		}
 
 		switch (currentSong.player2)
@@ -578,24 +579,6 @@ class PlayState extends MusicBeatState implements IHook
 			boyfriend.characterType = CharacterType.BF;
 		}
 
-		// REPOSITIONING PER STAGE
-		switch (currentStageId)
-		{
-			case "tank":
-				girlfriend.y += 10;
-				girlfriend.x -= 30;
-				boyfriend.x += 40;
-				boyfriend.y += 0;
-				dad.y += 60;
-				dad.x -= 80;
-
-				if (gfVersion != 'pico-speaker')
-				{
-					girlfriend.x -= 170;
-					girlfriend.y -= 75;
-				}
-		}
-
 		if (currentStage != null)
 		{
 			// We're using Eric's stage handler.
@@ -604,15 +587,12 @@ class PlayState extends MusicBeatState implements IHook
 			currentStage.addCharacter(boyfriend, BF);
 			currentStage.addCharacter(dad, DAD);
 
+			// Camera starts at dad.
+			cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
+
 			// Redo z-indexes.
 			currentStage.refresh();
 		}
-		else
-		{
-			add(girlfriend);
-			add(dad);
-			add(boyfriend);
-		}
 	}
 
 	/**
@@ -802,7 +782,7 @@ class PlayState extends MusicBeatState implements IHook
 	{
 		// FlxG.log.add(ChartParser.parse());
 
-		Conductor.changeBPM(currentSong.bpm);
+		Conductor.bpm = currentSong.bpm;
 
 		currentSong.song = currentSong.song;
 
@@ -1024,28 +1004,35 @@ class PlayState extends MusicBeatState implements IHook
 
 		if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
 		{
-			persistentUpdate = false;
-			persistentDraw = true;
+			var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
 
-			// There is a 1/1000 change to use a special pause menu.
-			// This prevents the player from resuming, but that's the point.
-			// It's a reference to Gitaroo Man, which doesn't let you pause the game.
-			if (FlxG.random.bool(1 / 1000))
-			{
-				FlxG.switchState(new GitarooPause());
-			}
-			else
-			{
-				var boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
-				var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y);
-				openSubState(pauseSubState);
-				pauseSubState.camera = camHUD;
-				boyfriendPos.put();
-			}
+			dispatchEvent(event);
 
-			#if discord_rpc
-			DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
-			#end
+			if (!event.eventCanceled)
+			{
+				persistentUpdate = false;
+				persistentDraw = true;
+
+				// There is a 1/1000 change to use a special pause menu.
+				// This prevents the player from resuming, but that's the point.
+				// It's a reference to Gitaroo Man, which doesn't let you pause the game.
+				if (event.gitaroo)
+				{
+					FlxG.switchState(new GitarooPause());
+				}
+				else
+				{
+					var boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
+					var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y);
+					openSubState(pauseSubState);
+					pauseSubState.camera = camHUD;
+					boyfriendPos.put();
+				}
+
+				#if discord_rpc
+				DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+				#end
+			}
 		}
 
 		if (FlxG.keys.justPressed.SEVEN)
@@ -1076,13 +1063,6 @@ class PlayState extends MusicBeatState implements IHook
 			changeSection(-1);
 		#end
 
-		if (generatedMusic && SongLoad.getSong()[Std.int(curStep / 16)] != null)
-		{
-			cameraRightSide = SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection;
-
-			controlCamera();
-		}
-
 		if (camZooming)
 		{
 			FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
@@ -1091,6 +1071,7 @@ class PlayState extends MusicBeatState implements IHook
 
 		FlxG.watch.addQuick("beatShit", curBeat);
 		FlxG.watch.addQuick("stepShit", curStep);
+		FlxG.watch.addQuick("songPos", Conductor.songPosition);
 
 		if (currentSong.song == 'Fresh')
 		{
@@ -1722,10 +1703,14 @@ class PlayState extends MusicBeatState implements IHook
 		}
 	}
 
-	override function stepHit()
+	override function stepHit():Bool
 	{
-		super.stepHit();
-		if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20
+		// super.stepHit() returns false if a module cancelled the event.
+		if (!super.stepHit())
+			return false;
+
+		if (!isInCutscene
+			&& Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20
 			|| (currentSong.needsVoices && Math.abs(vocals.time - (Conductor.songPosition - Conductor.offset)) > 20))
 		{
 			resyncVocals();
@@ -1734,23 +1719,32 @@ class PlayState extends MusicBeatState implements IHook
 		iconP1.onStepHit(curStep);
 		iconP2.onStepHit(curStep);
 
-		dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep));
+		return true;
 	}
 
-	override function beatHit()
+	override function beatHit():Bool
 	{
-		super.beatHit();
+		// super.beatHit() returns false if a module cancelled the event.
+		if (!super.beatHit())
+			return false;
 
 		if (generatedMusic)
 		{
 			activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
 		}
 
+		// Moving this code into the `beatHit` function allows for scripts and modules to control the camera better.
+		if (generatedMusic && SongLoad.getSong()[Std.int(curStep / 16)] != null)
+		{
+			cameraRightSide = SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection;
+			controlCamera();
+		}
+
 		if (SongLoad.getSong()[Math.floor(curStep / 16)] != null)
 		{
 			if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM)
 			{
-				Conductor.changeBPM(SongLoad.getSong()[Math.floor(curStep / 16)].bpm);
+				Conductor.bpm = SongLoad.getSong()[Math.floor(curStep / 16)].bpm;
 				FlxG.log.add('CHANGED BPM!');
 			}
 		}
@@ -1772,11 +1766,34 @@ class PlayState extends MusicBeatState implements IHook
 			}
 		}
 
+		// That combo counter that got spoiled that one time.
+		// Comes with NEAT visual and audio effects.
+
+		var shouldShowComboText:Bool = (curBeat % 8 == 7) // End of measure. TODO: Is this always the correct time?
+			&& (SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection) // Current section is BF's.
+			&& (combo > 5) // Don't want to show on small combos.
+			&& ((SongLoad.getSong().length < Std.int(curStep / 16)) // Show at the end of the song.
+				|| (!SongLoad.getSong()[Std.int(curStep / 16) + 1].mustHitSection) // Or when the next section is Dad's.
+			);
+
+		if (shouldShowComboText)
+		{
+			var animShit:ComboCounter = new ComboCounter(-100, 300, combo);
+			animShit.scrollFactor.set(0.6, 0.6);
+			add(animShit);
+
+			var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
+
+			new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr)
+			{
+				animShit.forceFinish();
+			});
+		}
+
 		// Make the characters dance on the beat
 		danceOnBeat();
 
-		// Call any relevant event handlers.
-		dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep));
+		return true;
 	}
 
 	/**
@@ -1876,13 +1893,6 @@ class PlayState extends MusicBeatState implements IHook
 			Countdown.pauseCountdown();
 		}
 
-		var event:ScriptEvent = new ScriptEvent(ScriptEvent.PAUSE, true);
-
-		dispatchEvent(event);
-
-		if (event.eventCanceled)
-			return;
-
 		super.openSubState(subState);
 	}
 
@@ -1894,7 +1904,14 @@ class PlayState extends MusicBeatState implements IHook
 	{
 		if (isGamePaused)
 		{
-			if (FlxG.sound.music != null && !startingSong)
+			var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
+
+			dispatchEvent(event);
+
+			if (event.eventCanceled)
+				return;
+
+			if (FlxG.sound.music != null && !startingSong && !isInCutscene)
 				resyncVocals();
 
 			// Resume the countdown.
@@ -1909,13 +1926,6 @@ class PlayState extends MusicBeatState implements IHook
 			#end
 		}
 
-		var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
-
-		dispatchEvent(event);
-
-		if (event.eventCanceled)
-			return;
-
 		super.closeSubState();
 	}
 
@@ -1925,13 +1935,15 @@ class PlayState extends MusicBeatState implements IHook
 	 */
 	function startCountdown():Void
 	{
+		var result = Countdown.performCountdown(currentStageId.startsWith('school'));
+		if (!result)
+			return;
+
 		isInCutscene = false;
 		camHUD.visible = true;
 		talking = false;
 
 		buildStrumlines();
-
-		Countdown.performCountdown(currentStageId.startsWith('school'));
 	}
 
 	override function dispatchEvent(event:ScriptEvent):Void
@@ -2001,15 +2013,6 @@ class PlayState extends MusicBeatState implements IHook
 		instance = null;
 	}
 
-	/**
-	 * Refreshes the state, by redoing the render order of all elements.
-	 * It does this based on the `zIndex` of each element.
-	 */
-	public function refresh()
-	{
-		sort(SortUtil.byZIndex, FlxSort.ASCENDING);
-	}
-
 	/**
 	 * This function is called whenever Flixel switches switching to a new FlxState.
 	 * @return Whether to actually switch to the new state.
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index f4ce9823e..d38f6cc72 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -104,41 +104,47 @@ class CharacterDataParser
 		}
 
 		var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
-		trace('  Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
-		for (charCls in scriptedCharClassNames3)
+		if (scriptedCharClassNames3.length > 0)
 		{
-			var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
-			if (character == null)
+			trace('  Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
+			for (charCls in scriptedCharClassNames3)
 			{
-				trace('    Failed to instantiate scripted character: ${charCls}');
-				continue;
+				var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
+				if (character == null)
+				{
+					trace('    Failed to instantiate scripted character: ${charCls}');
+					continue;
+				}
+				characterScriptedClass.set(character.characterId, charCls);
 			}
-			characterScriptedClass.set(character.characterId, charCls);
 		}
 
 		// NOTE: Only instantiate the ones not populated above.
 		// ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes.
 		var scriptedCharClassNames:Array<String> = ScriptedBaseCharacter.listScriptClasses();
-		scriptedCharClassNames.filter(function(charCls:String):Bool
+		scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool
 		{
-			return !scriptedCharClassNames1.contains(charCls)
-				&& !scriptedCharClassNames2.contains(charCls)
-				&& !scriptedCharClassNames3.contains(charCls);
+			return !(scriptedCharClassNames1.contains(charCls)
+				|| scriptedCharClassNames2.contains(charCls)
+				|| scriptedCharClassNames3.contains(charCls));
 		});
 
-		trace('  Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
-		for (charCls in scriptedCharClassNames)
+		if (scriptedCharClassNames.length > 0)
 		{
-			var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
-			if (character == null)
+			trace('  Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
+			for (charCls in scriptedCharClassNames)
 			{
-				trace('    Failed to instantiate scripted character: ${charCls}');
-				continue;
-			}
-			else
-			{
-				trace('    Successfully instantiated scripted character: ${charCls}');
-				characterScriptedClass.set(character.characterId, charCls);
+				var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
+				if (character == null)
+				{
+					trace('    Failed to instantiate scripted character: ${charCls}');
+					continue;
+				}
+				else
+				{
+					trace('    Successfully instantiated scripted character: ${charCls}');
+					characterScriptedClass.set(character.characterId, charCls);
+				}
 			}
 		}
 
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 9462c3ada..0b026ed22 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -236,7 +236,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
 
 	public function onUpdate(event:UpdateScriptEvent) {}
 
-	public function onPause(event:ScriptEvent) {}
+	public function onPause(event:PauseScriptEvent) {}
 
 	public function onResume(event:ScriptEvent) {}
 
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index 3cafd79a2..2b8c46acb 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -247,6 +247,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
 		// TODO: Make this a toggle? It's useful to turn on from time to time.
 		var debugIcon:FlxSprite = new FlxSprite(0, 0);
 		debugIcon.makeGraphic(8, 8, 0xffff00ff);
+		debugIcon.visible = false;
 		debugIcon.zIndex = 1000000;
 		#end
 
@@ -480,7 +481,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte
 
 	public function onScriptEvent(event:ScriptEvent) {}
 
-	public function onPause(event:ScriptEvent) {}
+	public function onPause(event:PauseScriptEvent) {}
 
 	public function onResume(event:ScriptEvent) {}
 
diff --git a/source/funkin/ui/AtlasMenuList.hx b/source/funkin/ui/AtlasMenuList.hx
index 7a217a44c..922ef9369 100644
--- a/source/funkin/ui/AtlasMenuList.hx
+++ b/source/funkin/ui/AtlasMenuList.hx
@@ -5,6 +5,9 @@ import flixel.graphics.frames.FlxAtlasFrames;
 
 typedef AtlasAsset = flixel.util.typeLimit.OneOfTwo<String, FlxAtlasFrames>;
 
+/**
+ * A menulist whose items share a single texture atlas.
+ */
 class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
 {
 	public var atlas:FlxAtlasFrames;
@@ -33,11 +36,16 @@ class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
 	}
 }
 
+/**
+ * A menu list item which uses single texture atlas.
+ */
 class AtlasMenuItem extends MenuItem
 {
 	var atlas:FlxAtlasFrames;
 
-	public function new(x = 0.0, y = 0.0, name:String, atlas:FlxAtlasFrames, callback)
+	public var centered:Bool = false;
+
+	public function new(x = 0.0, y = 0.0, name:String, atlas, callback)
 	{
 		this.atlas = atlas;
 		super(x, y, name, callback);
@@ -52,10 +60,17 @@ class AtlasMenuItem extends MenuItem
 		super.setData(name, callback);
 	}
 
-	function changeAnim(animName:String)
+	public function changeAnim(animName:String)
 	{
 		animation.play(animName);
 		updateHitbox();
+
+		if (centered)
+		{
+			// position by center
+			centerOrigin();
+			offset.copyFrom(origin);
+		}
 	}
 
 	override function idle()
diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx
index 8eb0d041d..3006f3994 100644
--- a/source/funkin/ui/AtlasText.hx
+++ b/source/funkin/ui/AtlasText.hx
@@ -10,7 +10,7 @@ abstract BoldText(AtlasText) from AtlasText to AtlasText
 {
 	inline public function new(x = 0.0, y = 0.0, text:String)
 	{
-		this = new AtlasText(x, y, text, Bold);
+		this = new AtlasText(x, y, text, AtlasFont.BOLD);
 	}
 }
 
@@ -42,7 +42,7 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
 	inline function get_maxHeight()
 		return font.maxHeight;
 
-	public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = Default)
+	public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = AtlasFont.DEFAULT)
 	{
 		if (!fonts.exists(fontName))
 			fonts[fontName] = new AtlasFontData(fontName);
@@ -247,7 +247,14 @@ private class AtlasFontData
 
 	public function new(name:AtlasFont)
 	{
-		atlas = Paths.getSparrowAtlas("fonts/" + name.getName().toLowerCase());
+		var fontName:String = name;
+		atlas = Paths.getSparrowAtlas('fonts/${fontName.toLowerCase()}');
+		if (atlas == null)
+		{
+			FlxG.log.warn('Could not find font atlas for font "${fontName}".');
+			return;
+		}
+
 		atlas.parent.destroyOnNoUse = false;
 		atlas.parent.persist = true;
 
@@ -277,8 +284,8 @@ enum Case
 	Lower;
 }
 
-enum AtlasFont
+enum abstract AtlasFont(String) from String to String
 {
-	Default;
-	Bold;
+	var DEFAULT = "default";
+	var BOLD = "bold";
 }
diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx
index fb4144b5b..62445be96 100644
--- a/source/funkin/ui/ControlsMenu.hx
+++ b/source/funkin/ui/ControlsMenu.hx
@@ -66,11 +66,11 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
 
 			var item;
 
-			item = deviceList.createItem("Keyboard", Bold, selectDevice.bind(Keys));
+			item = deviceList.createItem("Keyboard", AtlasFont.BOLD, selectDevice.bind(Keys));
 			item.x = FlxG.width / 2 - item.width - 30;
 			item.y = (devicesBg.height - item.height) / 2;
 
-			item = deviceList.createItem("Gamepad", Bold, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id)));
+			item = deviceList.createItem("Gamepad", AtlasFont.BOLD, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id)));
 			item.x = FlxG.width / 2 + 30;
 			item.y = (devicesBg.height - item.height) / 2;
 		}
@@ -317,7 +317,7 @@ class InputItem extends TextMenuItem
 		this.index = index;
 		this.input = getInput();
 
-		super(x, y, getLabel(input), Default, callback);
+		super(x, y, getLabel(input), DEFAULT, callback);
 	}
 
 	public function updateDevice(device:Device)
diff --git a/source/funkin/ui/OptionsState.hx b/source/funkin/ui/OptionsState.hx
index fc5c928c2..32b511d3e 100644
--- a/source/funkin/ui/OptionsState.hx
+++ b/source/funkin/ui/OptionsState.hx
@@ -194,7 +194,7 @@ class OptionsMenu extends Page
 
 	function createItem(name:String, callback:Void->Void, fireInstantly = false)
 	{
-		var item = items.createItem(0, 100 + items.length * 100, name, Bold, callback);
+		var item = items.createItem(0, 100 + items.length * 100, name, BOLD, callback);
 		item.fireInstantly = fireInstantly;
 		item.screenCenter(X);
 		return item;
diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx
index d6faf7b27..a5aa4e368 100644
--- a/source/funkin/ui/PreferencesMenu.hx
+++ b/source/funkin/ui/PreferencesMenu.hx
@@ -84,7 +84,7 @@ class PreferencesMenu extends Page
 
 	private function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void
 	{
-		items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.Bold, function()
+		items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function()
 		{
 			preferenceCheck(prefString, prefValue);
 
diff --git a/source/funkin/ui/TextMenuList.hx b/source/funkin/ui/TextMenuList.hx
index ae3ed7bff..fe1a13a7c 100644
--- a/source/funkin/ui/TextMenuList.hx
+++ b/source/funkin/ui/TextMenuList.hx
@@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
 		super(navControls, wrapMode);
 	}
 
-	public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback, fireInstantly = false)
+	public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false)
 	{
 		var item = new TextMenuItem(x, y, name, font, callback);
 		item.fireInstantly = fireInstantly;
@@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
 
 class TextMenuItem extends TextTypedMenuItem<AtlasText>
 {
-	public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback)
+	public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback)
 	{
 		super(x, y, new AtlasText(0, 0, name, font), name, callback);
 		setEmptyBackground();
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index b0bd798dc..6e92d6b68 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -13,6 +13,8 @@ class SortUtil
 	 */
 	public static inline function byZIndex(Order:Int, Obj1:FlxBasic, Obj2:FlxBasic):Int
 	{
+		if (Obj1 == null || Obj2 == null)
+			return 0;
 		return FlxSort.byValues(Order, Obj1.zIndex, Obj2.zIndex);
 	}