diff --git a/Project.xml b/Project.xml
index 8dbb43618..a83db1677 100644
--- a/Project.xml
+++ b/Project.xml
@@ -20,6 +20,7 @@
 	<!--Mobile-specific-->
 	<window if="mobile" orientation="landscape" fullscreen="true" width="0" height="0" resizable="false" />
 	<!-- _____________________________ Path Settings ____________________________ -->
+
 	<set name="BUILD_DIR" value="export/debug" if="debug" />
 	<set name="BUILD_DIR" value="export/release" unless="debug" />
 	<set name="BUILD_DIR" value="export/32bit" if="32bit" />
@@ -96,8 +97,9 @@
 	<haxelib name="lime" /> <!-- Game engine backend -->
 	<haxelib name="openfl" /> <!-- Game engine backend -->
 	<haxelib name="flixel" /> <!-- Game engine -->
+
 	<haxedev set="webgl" />
-	<!--In case you want to use the addons package-->
+
 	<haxelib name="flixel-addons" /> <!-- Additional utilities for Flixel -->
 	<haxelib name="hscript" /> <!-- Scripting -->
 	<haxelib name="flixel-ui" /> <!-- UI framework (deprecate this? -->
@@ -150,8 +152,11 @@
 	<haxeflag name="--macro" value="include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])" />
 	<!-- Necessary to provide stack traces for HScript. -->
 	<haxedef name="hscriptPos" />
+	<haxedef name="safeMode"/>
 	<haxedef name="HXCPP_CHECK_POINTER" />
 	<haxedef name="HXCPP_STACK_LINE" />
+	<haxedef name="HXCPP_STACK_TRACE" />
+	<haxedef name="openfl-enable-handle-error" />
 	<!-- This macro allows addition of new functionality to existing Flixel. -->
 	<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
 	<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
@@ -159,7 +164,6 @@
 	<icon path="art/icon32.png" size="32" />
 	<icon path="art/icon64.png" size="64" />
 	<icon path="art/iconOG.png" />
-	<!-- <haxedef name="SKIP_TO_PLAYSTATE" if="debug" /> -->
 	<haxedef name="CAN_OPEN_LINKS" unless="switch" />
 	<haxedef name="CAN_CHEAT" if="switch debug" />
 	<haxedef name="haxeui_no_mouse_reset" />
@@ -172,7 +176,6 @@
 		<!-- Difficulty, only used for week or song, defaults to 1 -->
 		<!-- <haxedef name="dif" value="2" if="debug"/> -->
 	</section>
-	<!-- <haxedef name="CLEAR_INPUT_SAVE"/> -->
 	<section if="newgrounds">
 		<!-- Enables Ng.core.verbose -->
 		<!-- <haxedef name="NG_VERBOSE" /> -->
@@ -182,18 +185,21 @@
 		<!-- <haxedef name="NG_FORCE_EXPIRED_SESSION" if="debug" /> -->
 	</section>
 
+	<!-- Uncomment this to wipe your input settings. -->
+	<!-- <haxedef name="CLEAR_INPUT_SAVE"/> -->
+
 	<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5">
 		<!--
 			Use the parent assets folder rather than the exported one
 			No more will we accidentally undo our changes!
-			TODO: Add a thing to disable this on builds meant for itch.io.
 		-->
 		<haxedef name="REDIRECT_ASSETS_FOLDER" />
 	</section>
 
-	<!-- <prebuild haxe="trace('prebuilding');"/> -->
-	<!-- <postbuild haxe="art/Postbuild.hx"/> -->
-	<!-- <config:ios allow-provisioning-updates="true" team-id="" /> -->
+	<!-- Run a script before and after building. -->
+	<postbuild haxe="source/Prebuild.hx"/> -->
+	<postbuild haxe="source/Postbuild.hx"/> -->
+
 	<!-- Options for Polymod -->
 	<section if="polymod">
 		<!-- Turns on additional debug logging. -->
@@ -213,12 +219,4 @@
 		<!-- Determines the file in the mod folder used for the icon. -->
 		<haxedef name="POLYMOD_MOD_ICON_FILE" value="_polymod_icon.png" />
 	</section>
-	<section if="TOOLS">
-		<!-- Compiles tool for old song conversion shit -->
-		<!-- Assumes you use it on windows/desktop!!!! -->
-		<postbuild command="haxe -main art/SongConverter.hx --cs export/songShit" />
-		<assets path="export/songShit/bin/SongConverter.exe" rename="SongConverter.exe" />
-		<!-- <postbuild command='ren export/songShit/bin export/songShit/tools '/> -->
-		<!-- <postbuild command='move export/songShit/tools export/release/windows/bin'/> -->
-	</section>
 </project>
diff --git a/hmm.json b/hmm.json
index 150a4f242..a3226281b 100644
--- a/hmm.json
+++ b/hmm.json
@@ -47,7 +47,7 @@
       "name": "haxeui-core",
       "type": "git",
       "dir": null,
-      "ref": "3590c94858fc6dbcf9b4d522cd644ad571269677",
+      "ref": "f5daafe93bdfa957538f199294a54e0476c805b7",
       "url": "https://github.com/haxeui/haxeui-core/"
     },
     {
@@ -128,7 +128,7 @@
       "name": "openfl",
       "type": "git",
       "dir": null,
-      "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08",
+      "ref": "1591a6c5f1f72e65d711f7e17e8055df41424d94",
       "url": "https://github.com/EliteMasterEric/openfl"
     },
     {
diff --git a/source/Main.hx b/source/Main.hx
index 1d7b73bb8..72209cd30 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -2,12 +2,13 @@ package;
 
 import flixel.FlxGame;
 import flixel.FlxState;
+import funkin.util.logging.CrashHandler;
 import funkin.MemoryCounter;
 import haxe.ui.Toolkit;
-import openfl.Lib;
 import openfl.display.FPS;
 import openfl.display.Sprite;
 import openfl.events.Event;
+import openfl.Lib;
 import openfl.media.Video;
 import openfl.net.NetStream;
 
@@ -77,10 +78,18 @@ class Main extends Sprite
      * -Eric
      */
 
+    CrashHandler.initialize();
+
+    CrashHandler.queryStatus();
+
     initHaxeUI();
 
     addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));
 
+    #if hxcpp_debug_server
+    trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
+    #end
+
     #if debug
     fpsCounter = new FPS(10, 3, 0xFFFFFF);
     addChild(fpsCounter);
diff --git a/source/Postbuild.hx b/source/Postbuild.hx
new file mode 100644
index 000000000..d48b670a4
--- /dev/null
+++ b/source/Postbuild.hx
@@ -0,0 +1,11 @@
+package source; // Yeah, I know...
+
+class Postbuild
+{
+  static function main()
+  {
+    trace('Postbuild');
+
+    // TODO: Maybe put a 'Build took X seconds' message here?
+  }
+}
diff --git a/source/Prebuild.hx b/source/Prebuild.hx
new file mode 100644
index 000000000..63782fc56
--- /dev/null
+++ b/source/Prebuild.hx
@@ -0,0 +1,9 @@
+package source; // Yeah, I know...
+
+class Prebuild
+{
+  static function main()
+  {
+    trace('Prebuild');
+  }
+}
diff --git a/source/funkin/Alphabet.hx b/source/funkin/Alphabet.hx
index 670496727..45e9a2aee 100644
--- a/source/funkin/Alphabet.hx
+++ b/source/funkin/Alphabet.hx
@@ -38,7 +38,7 @@ class Alphabet extends FlxSpriteGroup
 
   var isBold:Bool = false;
 
-  public function new(x:Float = 0.0, y:Float = 0.0, text:String = "", ?bold:Bool = false, typed:Bool = false)
+  public function new(x:Float = 0.0, y:Float = 0.0, text:String = "", bold:Bool = false, typed:Bool = false)
   {
     super(x, y);
 
diff --git a/source/funkin/CoolUtil.hx b/source/funkin/CoolUtil.hx
index 93fa937da..d07bb4e22 100644
--- a/source/funkin/CoolUtil.hx
+++ b/source/funkin/CoolUtil.hx
@@ -12,7 +12,6 @@ import flixel.tweens.FlxEase;
 import flixel.tweens.FlxTween;
 import funkin.play.PlayState;
 import funkin.shaderslmfao.ScreenWipeShader;
-import haxe.Json;
 import haxe.format.JsonParser;
 import lime.math.Rectangle;
 import lime.utils.Assets;
@@ -120,31 +119,6 @@ class CoolUtil
     FlxG.camera.setFilters([new ShaderFilter(screenWipeShit)]);
   }
 
-  /**
-   * Just saves the json with some default values hehe
-   * @param json
-   * @return String
-   */
-  public static inline function jsonStringify(data:Dynamic):String
-  {
-    return Json.stringify(data, null, "\t");
-  }
-
-  /**
-   * Hashlink json encoding fix for some wacky bullshit
-   * https://github.com/HaxeFoundation/haxe/issues/6930#issuecomment-384570392
-   */
-  public static function coolJSON(fileData:String)
-  {
-    var cont = fileData;
-    function is(n:Int, what:Int)
-      return cont.charCodeAt(n) == what;
-    return JsonParser.parse(cont.substr(if (is(0, 65279)) /// looks like a HL target, skipping only first character here:
-      1 else if (is(0, 239) && is(1, 187) && is(2, 191)) /// it seems to be Neko or PHP, start from position 3:
-      3 else /// all other targets, that prepare the UTF string correctly
-      0));
-  }
-
   /*
    * frame dependant lerp kinda lol
    */
diff --git a/source/funkin/Discord.hx b/source/funkin/Discord.hx
index 4fb6e9dcf..d2cf12535 100644
--- a/source/funkin/Discord.hx
+++ b/source/funkin/Discord.hx
@@ -64,7 +64,7 @@ class DiscordClient
     trace("Discord Client initialized");
   }
 
-  public static function changePresence(details:String, state:Null<String>, ?smallImageKey:String, ?hasStartTimestamp:Bool, ?endTimestamp:Float)
+  public static function changePresence(details:String, ?state:String, ?smallImageKey:String, ?hasStartTimestamp:Bool, ?endTimestamp:Float)
   {
     var startTimestamp:Float = if (hasStartTimestamp) Date.now().getTime() else 0;
 
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index dd87e7d36..c31e8c77b 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -464,7 +464,7 @@ class FreeplayState extends MusicBeatSubState
     });
   }
 
-  public function generateSongList(?filterStuff:SongFilter, ?force:Bool = false)
+  public function generateSongList(?filterStuff:SongFilter, force:Bool = false)
   {
     curSelected = 0;
 
@@ -1045,7 +1045,7 @@ class FreeplaySongData
   public var songCharacter:String = "";
   public var isFav:Bool = false;
 
-  public function new(song:String, levelId:String, songCharacter:String, ?isFav:Bool = false)
+  public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false)
   {
     this.songName = song;
     this.levelId = levelId;
diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx
index 9133a8fab..791a4bb9a 100644
--- a/source/funkin/PauseSubState.hx
+++ b/source/funkin/PauseSubState.hx
@@ -41,7 +41,7 @@ class PauseSubState extends MusicBeatSubState
 
   var isChartingMode:Bool;
 
-  public function new(?isChartingMode:Bool = false)
+  public function new(isChartingMode:Bool = false)
   {
     super();
 
diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
index 74b348142..ae7a5708c 100644
--- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
+++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
@@ -82,7 +82,7 @@ class FlxAtlasSprite extends FlxAnimate
    * @param restart Whether to restart the animation if it is already playing.
    * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
    */
-  public function playAnimation(id:String, ?restart:Bool = false, ?ignoreOther:Bool = false):Void
+  public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false):Void
   {
     // Skip if not allowed to play animations.
     if ((!canPlayOtherAnims && !ignoreOther)) return;
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index b53937361..15ed0421e 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -255,7 +255,7 @@ class GameOverSubState extends MusicBeatSubState
    * Starts the death music at the appropriate volume.
    * @param startingVolume
    */
-  function startDeathMusic(?startingVolume:Float = 1, ?force:Bool = false):Void
+  function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
   {
     var musicPath = Paths.music('gameOver' + musicSuffix);
     if (isEnding)
diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx
index 4f4b3f8f7..3523ec994 100644
--- a/source/funkin/play/character/AnimateAtlasCharacter.hx
+++ b/source/funkin/play/character/AnimateAtlasCharacter.hx
@@ -81,7 +81,7 @@ class AnimateAtlasCharacter extends BaseCharacter
     super.onCreate(event);
   }
 
-  public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reverse:Bool = false):Void
+  public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
   {
     if ((!canPlayOtherAnims && !ignoreOther)) return;
 
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 72f968538..c7b58c393 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -570,7 +570,7 @@ class BaseCharacter extends Bopper
    * @param miss If true, play the miss animation instead of the sing animation.
    * @param suffix A suffix to append to the animation name, like `alt`.
    */
-  public function playSingAnimation(dir:NoteDirection, ?miss:Bool = false, ?suffix:String = ''):Void
+  public function playSingAnimation(dir:NoteDirection, miss:Bool = false, ?suffix:String = ''):Void
   {
     var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}';
 
@@ -578,7 +578,7 @@ class BaseCharacter extends Bopper
     playAnimation(anim, true);
   }
 
-  public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reversed:Bool = false):Void
+  public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void
   {
     FlxG.watch.addQuick('playAnim(${characterName})', name);
     // trace('playAnim(${characterName}): ${name}');
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 710eb884b..f1b316b7f 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -190,7 +190,7 @@ class CharacterDataParser
    * @param charId The character ID to fetch.
    * @return The character instance, or null if the character was not found.
    */
-  public static function fetchCharacter(charId:String, ?debug:Bool = false):Null<BaseCharacter>
+  public static function fetchCharacter(charId:String, debug:Bool = false):Null<BaseCharacter>
   {
     if (charId == null || charId == '' || !characterCache.exists(charId))
     {
diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx
index 34d89362f..968f613ff 100644
--- a/source/funkin/play/character/MultiSparrowCharacter.hx
+++ b/source/funkin/play/character/MultiSparrowCharacter.hx
@@ -181,7 +181,7 @@ class MultiSparrowCharacter extends BaseCharacter
     trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
   }
 
-  public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reverse:Bool = false):Void
+  public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
   {
     // Make sure we ignore other animations if we're currently playing a forced one,
     // unless we're forcing a new animation.
diff --git a/source/funkin/play/cutscene/dialogue/ConversationData.hx b/source/funkin/play/cutscene/dialogue/ConversationData.hx
index d2e3b74cf..749f1b7a1 100644
--- a/source/funkin/play/cutscene/dialogue/ConversationData.hx
+++ b/source/funkin/play/cutscene/dialogue/ConversationData.hx
@@ -208,7 +208,7 @@ class OutroData
   public var type:OutroType;
   public var data:Dynamic;
 
-  public function new(typeStr:Null<String>, data:Dynamic)
+  public function new(?typeStr:String, data:Dynamic)
   {
     this.type = typeStr ?? OutroType.NONE;
     this.data = data;
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
index 52564010a..bfc0e9233 100644
--- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
@@ -172,7 +172,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
 
   /**
    * Set the sprite scale to the appropriate value.
-   * @param scale 
+   * @param scale
    */
   public function setScale(scale:Null<Float>):Void
   {
@@ -218,7 +218,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
    * @param name The name of the current animation.
    * @param frameNumber The number of the current frame.
    * @param frameIndex The index of the current frame.
-   * 
+   *
    * For example, if an animation was defined as having the indexes [3, 0, 1, 2],
    * then the first callback would have frameNumber = 0 and frameIndex = 3.
    */
@@ -253,7 +253,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
    * @param restart Whether to restart the animation if it is already playing.
    * @param reversed If true, play the animation backwards, from the last frame to the first.
    */
-  public function playAnimation(name:String, restart:Bool = false, ?reversed:Bool = false):Void
+  public function playAnimation(name:String, restart:Bool = false, reversed:Bool = false):Void
   {
     var correctName:String = correctAnimationName(name);
     if (correctName == null) return;
@@ -266,7 +266,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
   /**
    * Ensure that a given animation exists before playing it.
    * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
-   * @param name 
+   * @param name
    */
   function correctAnimationName(name:String):String
   {
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
index 537a27129..801a01dd7 100644
--- a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
+++ b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
@@ -93,7 +93,7 @@ class DialogueBoxTextData
   public var shadowColor:Null<String>;
   public var shadowWidth:Null<Int>;
 
-  public function new(offsets:Null<Array<Float>>, width:Null<Int>, size:Null<Int>, color:String, shadowColor:Null<String>, shadowWidth:Null<Int>)
+  public function new(offsets:Null<Array<Float>>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null<Int>)
   {
     this.offsets = offsets ?? [0, 0];
     this.width = width ?? 300;
diff --git a/source/funkin/play/cutscene/dialogue/SpeakerData.hx b/source/funkin/play/cutscene/dialogue/SpeakerData.hx
index a0f9a3300..88883ead8 100644
--- a/source/funkin/play/cutscene/dialogue/SpeakerData.hx
+++ b/source/funkin/play/cutscene/dialogue/SpeakerData.hx
@@ -19,8 +19,8 @@ class SpeakerData
   public var scale:Float;
   public var animations:Array<AnimationData>;
 
-  public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, ?flipX:Bool = false,
-      ?isPixel:Bool = false, ?scale:Float = 1.0)
+  public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, flipX:Bool = false,
+      isPixel:Bool = false, ?scale:Float = 1.0)
   {
     this.version = version;
     this.name = name;
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index b343bee86..8847636bd 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -271,7 +271,7 @@ class Strumline extends FlxSpriteGroup
    * @param strumTime
    * @return Float
    */
-  static function calculateNoteYPos(strumTime:Float, ?vwoosh:Bool = true):Float
+  static function calculateNoteYPos(strumTime:Float, vwoosh:Bool = true):Float
   {
     // Make the note move faster visually as it moves offscreen.
     var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx
index fd45342a0..97871b657 100644
--- a/source/funkin/play/notes/notestyle/NoteStyle.hx
+++ b/source/funkin/play/notes/notestyle/NoteStyle.hx
@@ -113,7 +113,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
     return noteFrames;
   }
 
-  function getNoteAssetPath(?raw:Bool = false):String
+  function getNoteAssetPath(raw:Bool = false):String
   {
     if (raw)
     {
@@ -161,7 +161,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
     return (result == null) ? fallback.fetchNoteAnimationData(dir) : result;
   }
 
-  public function getHoldNoteAssetPath(?raw:Bool = false):String
+  public function getHoldNoteAssetPath(raw:Bool = false):String
   {
     if (raw)
     {
@@ -209,7 +209,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
     target.antialiasing = !_data.assets.noteStrumline.isPixel;
   }
 
-  function getStrumlineAssetPath(?raw:Bool = false):String
+  function getStrumlineAssetPath(raw:Bool = false):String
   {
     if (raw)
     {
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index ec89d8706..63610950f 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -72,7 +72,7 @@ class Song implements IPlayStateScriptedClass
 
   @:allow(funkin.play.song.Song)
   public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
-      ?validScore:Bool = false):Song
+      validScore:Bool = false):Song
   {
     var result:Song = new Song(songId, true);
 
@@ -150,7 +150,7 @@ class Song implements IPlayStateScriptedClass
   /**
    * Parse and cache the chart for all difficulties of this song.
    */
-  public function cacheCharts(?force:Bool = false):Void
+  public function cacheCharts(force:Bool = false):Void
   {
     if (force)
     {
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index 938ee0708..bf574c399 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -920,7 +920,7 @@ typedef RawSongTimeChange =
  */
 abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
 {
-  public function new(timeStamp:Float, beatTime:Null<Float>, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
+  public function new(timeStamp:Float, ?beatTime:Float, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
   {
     this =
       {
diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx
index 750d5f54b..a7cbd1b6c 100644
--- a/source/funkin/play/song/SongDataUtils.hx
+++ b/source/funkin/play/song/SongDataUtils.hx
@@ -123,7 +123,7 @@ class SongDataUtils
   /**
    * Sort an array of notes by strum time.
    */
-  public static function sortNotes(notes:Array<SongNoteData>, ?desc:Bool = false):Array<SongNoteData>
+  public static function sortNotes(notes:Array<SongNoteData>, desc:Bool = false):Array<SongNoteData>
   {
     // TODO: Modifies the array in place. Is this okay?
     notes.sort(function(a:SongNoteData, b:SongNoteData):Int {
@@ -135,7 +135,7 @@ class SongDataUtils
   /**
    * Sort an array of events by strum time.
    */
-  public static function sortEvents(events:Array<SongEventData>, ?desc:Bool = false):Array<SongEventData>
+  public static function sortEvents(events:Array<SongEventData>, desc:Bool = false):Array<SongEventData>
   {
     // TODO: Modifies the array in place. Is this okay?
     events.sort(function(a:SongEventData, b:SongEventData):Int {
diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx
index d91dda1d9..16ea88664 100644
--- a/source/funkin/play/song/SongValidator.hx
+++ b/source/funkin/play/song/SongValidator.hx
@@ -63,9 +63,15 @@ class SongValidator
     }
 
     input.timeChanges = validateTimeChanges(input.timeChanges, songId);
+    if (input.timeChanges == null)
+    {
+      trace('[SONGDATA] Song ${songId} is missing a timeChanges field. ');
+      return null;
+    }
+
     input.playData = validatePlayData(input.playData, songId);
 
-    input.variation = '';
+    if (input.variation == null) input.variation = '';
 
     return input;
   }
@@ -79,6 +85,12 @@ class SongValidator
    */
   public static function validatePlayData(input:SongPlayData, songId:String = 'unknown'):SongPlayData
   {
+    if (input == null)
+    {
+      trace('[SONGDATA] Could not parse metadata.playData for song ${songId}');
+      return null;
+    }
+
     return input;
   }
 
@@ -91,6 +103,12 @@ class SongValidator
    */
   public static function validateTimeChange(input:SongTimeChange, songId:String = 'unknown'):SongTimeChange
   {
+    if (input == null)
+    {
+      trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}');
+      return null;
+    }
+
     return input;
   }
 
@@ -101,8 +119,8 @@ class SongValidator
   {
     if (input == null)
     {
-      trace('[SONGDATA] Song ${songId} is missing a timeChanges field. ');
-      return [];
+      trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}');
+      return null;
     }
 
     input = input.map((timeChange) -> validateTimeChange(timeChange, songId));
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index a144026f5..187b5ec32 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -268,7 +268,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
    * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
    * @param reversed If true, play the animation backwards, from the last frame to the first.
    */
-  public function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reversed:Bool = false):Void
+  public function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void
   {
     if (!canPlayOtherAnims && !ignoreOther) return;
 
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index f4f380a0b..1ac9b0b67 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -450,7 +450,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
    * @param pop If true, the character will be removed from the stage as well.
    * @return The Boyfriend character.
    */
-  public function getBoyfriend(?pop:Bool = false):BaseCharacter
+  public function getBoyfriend(pop:Bool = false):BaseCharacter
   {
     if (pop)
     {
@@ -473,7 +473,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
    * @param pop If true, the character will be removed from the stage as well.
    * @return The player/Boyfriend character.
    */
-  public function getPlayer(?pop:Bool = false):BaseCharacter
+  public function getPlayer(pop:Bool = false):BaseCharacter
   {
     return getBoyfriend(pop);
   }
@@ -483,7 +483,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
    * @param pop If true, the character will be removed from the stage as well.
    * @return The Girlfriend character.
    */
-  public function getGirlfriend(?pop:Bool = false):BaseCharacter
+  public function getGirlfriend(pop:Bool = false):BaseCharacter
   {
     if (pop)
     {
@@ -506,7 +506,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
    * @param pop If true, the character will be removed from the stage as well.
    * @return The Dad character.
    */
-  public function getDad(?pop:Bool = false):BaseCharacter
+  public function getDad(pop:Bool = false):BaseCharacter
   {
     if (pop)
     {
@@ -529,7 +529,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
    * @param pop If true, the character will be removed from the stage as well.
    * @return The opponent character.
    */
-  public function getOpponent(?pop:Bool = false):BaseCharacter
+  public function getOpponent(pop:Bool = false):BaseCharacter
   {
     return getDad(pop);
   }
diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx
index 867c6e1a5..c14e05aaf 100644
--- a/source/funkin/play/stage/StageData.hx
+++ b/source/funkin/play/stage/StageData.hx
@@ -503,7 +503,7 @@ typedef StageDataCharacter =
    * Again, just like CSS.
    * @default 0
    */
-  zIndex:Null<Int>,
+  ?zIndex:Int,
 
   /**
    * The position to render the character at.
diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx
index bf4d710dd..fd179c481 100644
--- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx
@@ -225,7 +225,7 @@ class AddEventsCommand implements ChartEditorCommand
   var events:Array<SongEventData>;
   var appendToSelection:Bool;
 
-  public function new(events:Array<SongEventData>, ?appendToSelection:Bool = false)
+  public function new(events:Array<SongEventData>, appendToSelection:Bool = false)
   {
     this.events = events;
     this.appendToSelection = appendToSelection;
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index df5a25b62..63dc8bd92 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -258,7 +258,7 @@ class ChartEditorDialogHandler
    * @return The dialog that was opened.
    */
   @:haxe.warning("-WVarInit")
-  public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
+  public static function openUploadInstDialog(state:ChartEditorState, closable:Bool = true):Dialog
   {
     var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);
 
@@ -578,7 +578,7 @@ class ChartEditorDialogHandler
    * @param closable Whether the dialog can be closed by the user.
    * @return The dialog that was opened.
    */
-  public static function openUploadVocalsDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
+  public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
   {
     var charIdsForVocals:Array<String> = [];
 
@@ -692,7 +692,7 @@ class ChartEditorDialogHandler
    * @return The dialog that was opened.
    */
   @:haxe.warning('-WVarInit')
-  public static function openChartDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
+  public static function openChartDialog(state:ChartEditorState, closable:Bool = true):Dialog
   {
     var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable);
 
@@ -765,6 +765,19 @@ class ChartEditorDialogHandler
       var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import');
       songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import');
 
+      if (songMetadataVariation == null)
+      {
+        // Tell the user the load was not successful.
+        NotificationManager.instance.addNotification(
+          {
+            title: 'Failure',
+            body: 'Could not load metadata file (${path.file}.${path.ext})',
+            type: NotificationType.Error,
+            expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+          });
+        return;
+      }
+
       songMetadata.set(variation, songMetadataVariation);
 
       // Tell the user the load was successful.
@@ -879,7 +892,7 @@ class ChartEditorDialogHandler
    * @param closable
    * @return Dialog
    */
-  public static function openImportChartDialog(state:ChartEditorState, format:String, ?closable:Bool = true):Dialog
+  public static function openImportChartDialog(state:ChartEditorState, format:String, closable:Bool = true):Dialog
   {
     var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable);
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
index 2cd9ab2fe..2524f014c 100644
--- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
@@ -26,12 +26,12 @@ class ChartEditorEventSprite extends FlxSprite
    * The note data that this sprite represents.
    * You can set this to null to kill the sprite and flag it for recycling.
    */
-  public var eventData(default, set):SongEventData;
+  public var eventData(default, set):Null<SongEventData> = null;
 
   /**
    * The image used for all song events. Cached for performance.
    */
-  static var eventSpriteBasic:BitmapData;
+  static var eventSpriteBasic:Null<BitmapData> = null;
 
   public function new(parent:ChartEditorState)
   {
@@ -49,7 +49,7 @@ class ChartEditorEventSprite extends FlxSprite
    * Build a set of animations to allow displaying different types of chart events.
    * @param force `true` to force rebuilding the frames.
    */
-  static function buildFrames(?force:Bool = false):FlxFramesCollection
+  static function buildFrames(force:Bool = false):FlxFramesCollection
   {
     static var eventFrames:FlxFramesCollection = null;
 
@@ -112,7 +112,7 @@ class ChartEditorEventSprite extends FlxSprite
     this.updateHitbox();
   }
 
-  function set_eventData(value:SongEventData):SongEventData
+  function set_eventData(value:Null<SongEventData>):Null<SongEventData>
   {
     this.eventData = value;
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
index 14ffa3a76..0adbf1a20 100644
--- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
@@ -27,7 +27,7 @@ class ChartEditorNoteSprite extends FlxSprite
    * The note data that this sprite represents.
    * You can set this to null to kill the sprite and flag it for recycling.
    */
-  public var noteData(default, set):SongNoteData;
+  public var noteData(default, set):Null<SongNoteData>;
 
   /**
    * The name of the note style currently in use.
@@ -70,7 +70,7 @@ class ChartEditorNoteSprite extends FlxSprite
     this.animation.addByPrefix('tapRightPixel', 'pixel7');
   }
 
-  static var noteFrameCollection:FlxFramesCollection = null;
+  static var noteFrameCollection:Null<FlxFramesCollection> = null;
 
   /**
    * We load all the note frames once, then reuse them.
@@ -108,7 +108,7 @@ class ChartEditorNoteSprite extends FlxSprite
     }
   }
 
-  function set_noteData(value:SongNoteData):SongNoteData
+  function set_noteData(value:Null<SongNoteData>):Null<SongNoteData>
   {
     this.noteData = value;
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index aa6e70714..83c052050 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,6 @@
 package funkin.ui.debug.charting;
 
+import flixel.system.FlxAssets.FlxSoundAsset;
 import flixel.math.FlxMath;
 import haxe.ui.components.TextField;
 import haxe.ui.components.DropDown;
@@ -85,6 +86,7 @@ using Lambda;
  */
 // Give other classes access to private instance fields
 
+@:nullSafety
 @:allow(funkin.ui.debug.charting.ChartEditorCommand)
 @:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
 @:allow(funkin.ui.debug.charting.ChartEditorThemeHandler)
@@ -238,7 +240,7 @@ class ChartEditorState extends HaxeUIState
    * 40 means the playhead is 1 grid length below the base position.
    * -40 means the playhead is 1 grid length above the base position.
    */
-  var playheadPositionInPixels(default, set):Float;
+  var playheadPositionInPixels(default, set):Float = 0.0;
 
   function set_playheadPositionInPixels(value:Float):Float
   {
@@ -258,28 +260,40 @@ class ChartEditorState extends HaxeUIState
    * playheadPosition, converted to steps.
    * NOT dependant on BPM, because the size of a grid square does not change with BPM.
    */
-  var playheadPositionInSteps(get, null):Float;
+  var playheadPositionInSteps(get, set):Float;
 
   function get_playheadPositionInSteps():Float
   {
     return playheadPositionInPixels / GRID_SIZE;
   }
 
+  function set_playheadPositionInSteps(value:Float):Float
+  {
+    playheadPositionInPixels = value * GRID_SIZE;
+    return value;
+  }
+
   /**
    * playheadPosition, converted to milliseconds.
    * DEPENDANT on BPM, because the duration of a grid square changes with BPM.
    */
-  var playheadPositionInMs(get, null):Float;
+  var playheadPositionInMs(get, set):Float;
 
   function get_playheadPositionInMs():Float
   {
     return Conductor.getStepTimeInMs(playheadPositionInSteps);
   }
 
+  function set_playheadPositionInMs(value:Float):Float
+  {
+    playheadPositionInSteps = Conductor.getTimeInSteps(value);
+    return value;
+  }
+
   /**
    * songLength, in milliseconds.
    */
-  @:isVar var songLengthInMs(get, set):Float;
+  @:isVar var songLengthInMs(get, set):Float = 0;
 
   function get_songLengthInMs():Float
   {
@@ -337,7 +351,7 @@ class ChartEditorState extends HaxeUIState
    * Dictates the appearance of many UI elements.
    * Currently hardcoded to just Light and Dark.
    */
-  var currentTheme(default, set):ChartEditorTheme = null;
+  var currentTheme(default, set):ChartEditorTheme = ChartEditorTheme.Light;
 
   function set_currentTheme(value:ChartEditorTheme):ChartEditorTheme
   {
@@ -350,9 +364,10 @@ class ChartEditorState extends HaxeUIState
 
   /**
    * Whether a skip button has been pressed on the playbar, and which one.
+   * `null` if no button has been pressed.
    * This will be used to update the scrollPosition (in the same function that handles the scroll wheel), then cleared.
    */
-  var playbarButtonPressed:String = null;
+  var playbarButtonPressed:Null<String> = null;
 
   /**
    * Whether the head of the playbar is currently being dragged with the mouse by the user.
@@ -392,13 +407,15 @@ class ChartEditorState extends HaxeUIState
 
   /**
    * The character sprite in the Player Preview window.
+   * `null` until accessed.
    */
-  var currentPlayerCharacterPlayer:CharacterPlayer = null;
+  var currentPlayerCharacterPlayer:Null<CharacterPlayer> = null;
 
   /**
    * The character sprite in the Opponent Preview window.
+   * `null` until accessed.
    */
-  var currentOpponentCharacterPlayer:CharacterPlayer = null;
+  var currentOpponentCharacterPlayer:Null<CharacterPlayer> = null;
 
   /**
    * The currently selected live input style.
@@ -431,7 +448,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * Whether hitsounds are enabled for at least one character.
    */
-  var hitsoundsEnabled(get, null):Bool;
+  var hitsoundsEnabled(get, never):Bool;
 
   function get_hitsoundsEnabled():Bool
   {
@@ -452,14 +469,14 @@ class ChartEditorState extends HaxeUIState
    * Whether the user's mouse cursor is hovering over a SOLID component of the HaxeUI.
    * If so, ignore mouse events underneath.
    */
-  var isCursorOverHaxeUI(get, null):Bool;
+  var isCursorOverHaxeUI(get, never):Bool;
 
   function get_isCursorOverHaxeUI():Bool
   {
     return Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
   }
 
-  var isCursorOverHaxeUIButton(get, null):Bool;
+  var isCursorOverHaxeUIButton(get, never):Bool;
 
   function get_isCursorOverHaxeUIButton():Bool
   {
@@ -575,10 +592,13 @@ class ChartEditorState extends HaxeUIState
     }
     else
     {
-      // Stop the auto-save timer.
-      autoSaveTimer.cancel();
-      autoSaveTimer.destroy();
-      autoSaveTimer = null;
+      if (autoSaveTimer != null)
+      {
+        // Stop the auto-save timer.
+        autoSaveTimer.cancel();
+        autoSaveTimer.destroy();
+        autoSaveTimer = null;
+      }
     }
 
     return saveDataDirty = value;
@@ -587,7 +607,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * A timer used to auto-save the chart after a period of inactivity.
    */
-  var autoSaveTimer:FlxTimer;
+  var autoSaveTimer:Null<FlxTimer> = null;
 
   /**
    * Whether the difficulty tree view in the toolbox has been modified and needs to be updated.
@@ -672,9 +692,10 @@ class ChartEditorState extends HaxeUIState
 
   /**
    * The position where the user clicked to start a selection.
+   * `null` if the user isn't currently selecting anything.
    * The selection box extends from this point to the current mouse position.
    */
-  var selectionBoxStartPos:FlxPoint = null;
+  var selectionBoxStartPos:Null<FlxPoint> = null;
 
   /**
    * Whether the user's last mouse click was on the playhead scroll area.
@@ -685,13 +706,14 @@ class ChartEditorState extends HaxeUIState
    * Where the user's last mouse click was on the note preview scroll area.
    * `null` if the user isn't clicking on the note preview.
    */
-  var notePreviewScrollAreaStartPos:FlxPoint = null;
+  var notePreviewScrollAreaStartPos:Null<FlxPoint> = null;
 
   /**
    * The SongNoteData which is currently being placed.
+   * `null` if the user isn't currently placing a note.
    * As the user drags, we will update this note's sustain length.
    */
-  var currentPlaceNoteData:SongNoteData = null;
+  var currentPlaceNoteData:Null<SongNoteData> = null;
 
   /**
    * The Dialog components representing the currently available tool windows.
@@ -706,18 +728,21 @@ class ChartEditorState extends HaxeUIState
 
   /**
    * The audio track for the instrumental.
+   * `null` until an instrumental track is loaded.
    */
-  var audioInstTrack:FlxSound;
+  var audioInstTrack:Null<FlxSound> = null;
 
   /**
    * The raw byte data for the instrumental audio track.
+   * `null` until an instrumental track is loaded.
    */
-  var audioInstTrackData:Bytes = null;
+  var audioInstTrackData:Null<Bytes> = null;
 
   /**
    * The audio track for the vocals.
+   * `null` until vocal track(s) are loaded.
    */
-  var audioVocalTrackGroup:VoicesGroup;
+  var audioVocalTrackGroup:Null<VoicesGroup> = null;
 
   /**
    * A map of the audio tracks for each character's vocals.
@@ -738,12 +763,12 @@ class ChartEditorState extends HaxeUIState
    * - Keys are the variation IDs. At least one (`default`) must exist.
    * - Values are the relevant metadata, ready to be serialized to JSON.
    */
-  var songMetadata:Map<String, SongMetadata>;
+  var songMetadata:Map<String, SongMetadata> = [];
 
   /**
    * Retrieves the list of variations for the current song.
    */
-  var availableVariations(get, null):Array<String>;
+  var availableVariations(get, never):Array<String>;
 
   function get_availableVariations():Array<String>
   {
@@ -756,21 +781,28 @@ class ChartEditorState extends HaxeUIState
    * Retrieves the list of difficulties for the current variation of the current song.
    * ONLY CONTAINS DIFFICULTIES FOR THE CURRENT VARIATION so if on the default variation, erect/nightmare won't be included.
    */
-  var availableDifficulties(get, null):Array<String>;
+  var availableDifficulties(get, never):Array<String>;
 
   function get_availableDifficulties():Array<String>
   {
-    return songMetadata.get(selectedVariation).playData.difficulties;
+    var m:Null<SongMetadata> = songMetadata.get(selectedVariation);
+    return m?.playData?.difficulties ?? [];
   }
 
   /**
    * Retrieves the list of difficulties for ALL variations of the current song.
    */
-  var allDifficulties(get, null):Array<String>;
+  var allDifficulties(get, never):Array<String>;
 
   function get_allDifficulties():Array<String>
   {
-    var result:Array<Array<String>> = [for (x in availableVariations) songMetadata.get(x).playData.difficulties];
+    var result:Array<Array<String>> = [
+      for (x in availableVariations)
+      {
+        var m:Null<SongMetadata> = songMetadata.get(x);
+        m?.playData?.difficulties ?? [];
+      }
+    ];
     return result.flatten();
   }
 
@@ -779,7 +811,7 @@ class ChartEditorState extends HaxeUIState
    * - Keys are the variation IDs. At least one (`default`) must exist.
    * - Values are the relevant chart data, ready to be serialized to JSON.
    */
-  var songChartData:Map<String, SongChartData>;
+  var songChartData:Map<String, SongChartData> = [];
 
   /**
    * Convenience property to get the chart data for the current variation.
@@ -788,7 +820,7 @@ class ChartEditorState extends HaxeUIState
 
   function get_currentSongMetadata():SongMetadata
   {
-    var result:SongMetadata = songMetadata.get(selectedVariation);
+    var result:Null<SongMetadata> = songMetadata.get(selectedVariation);
     if (result == null)
     {
       result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation);
@@ -810,7 +842,7 @@ class ChartEditorState extends HaxeUIState
 
   function get_currentSongChartData():SongChartData
   {
-    var result:SongChartData = songChartData.get(selectedVariation);
+    var result:Null<SongChartData> = songChartData.get(selectedVariation);
     if (result == null)
     {
       result = new SongChartData(1.0, [], []);
@@ -944,7 +976,7 @@ class ChartEditorState extends HaxeUIState
     return currentSongMetadata.songName = value;
   }
 
-  var currentSongId(get, null):String;
+  var currentSongId(get, never):String;
 
   function get_currentSongId():String
   {
@@ -968,7 +1000,7 @@ class ChartEditorState extends HaxeUIState
     return currentSongMetadata.artist = value;
   }
 
-  var currentSongPlayableCharacters(get, null):Array<String>;
+  var currentSongPlayableCharacters(get, never):Array<String>;
 
   function get_currentSongPlayableCharacters():Array<String>
   {
@@ -1025,7 +1057,7 @@ class ChartEditorState extends HaxeUIState
    * SIGNALS
    */
   // ==============================
-  // public var onDifficultyChange(default, null):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>();
+  // public var onDifficultyChange(default, never):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>();
   /**
    * RENDER OBJECTS
    */
@@ -1034,7 +1066,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * The IMAGE used for the grid. Updated by ChartEditorThemeHandler.
    */
-  var gridBitmap:BitmapData;
+  var gridBitmap:Null<BitmapData> = null;
 
   /**
    * The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler.
@@ -1042,100 +1074,114 @@ class ChartEditorState extends HaxeUIState
    * 1. A sprite is given this bitmap and placed over selected notes.
    * 2. The image is split and used for a 9-slice sprite for the selection box.
    */
-  var selectionSquareBitmap:BitmapData = null;
+  var selectionSquareBitmap:Null<BitmapData> = null;
 
   /**
    * The IMAGE used for the note preview bitmap. Updated by ChartEditorThemeHandler.
    * The image is split and used for a 9-slice sprite for the box over the note preview.
    */
-  var notePreviewViewportBitmap:BitmapData = null;
+  var notePreviewViewportBitmap:Null<BitmapData> = null;
 
   /**
    * The tiled sprite used to display the grid.
    * The height is the length of the song, and scrolling is done by simply the sprite.
    */
-  var gridTiledSprite:FlxSprite;
+  var gridTiledSprite:Null<FlxSprite> = null;
 
   /**
    * The playhead representing the current position in the song.
    * Can move around on the grid independently of the view.
    */
-  var gridPlayhead:FlxSpriteGroup;
+  var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup();
 
-  var gridPlayheadScrollArea:FlxSprite;
+  var gridPlayheadScrollArea:Null<FlxSprite> = null;
 
   /**
    * A sprite used to indicate the note that will be placed on click.
    */
-  var gridGhostNote:ChartEditorNoteSprite;
+  var gridGhostNote:Null<ChartEditorNoteSprite> = null;
 
   /**
    * A sprite used to indicate the event that will be placed on click.
    */
-  var gridGhostEvent:ChartEditorEventSprite;
+  var gridGhostEvent:Null<ChartEditorEventSprite> = null;
 
   /**
    * The waveform which (optionally) displays over the grid, underneath the notes and playhead.
    */
-  var gridSpectrogram:PolygonSpectogram;
+  var gridSpectrogram:Null<PolygonSpectogram> = null;
 
   /**
    * The sprite used to display the note preview area.
    * We move this up and down to scroll the preview.
    */
-  var notePreview:ChartEditorNotePreview;
+  var notePreview:Null<ChartEditorNotePreview> = null;
 
   /**
    * The rectangular sprite used for representing the current viewport on the note preview.
    * We move this up and down and resize it to represent the visible area.
    */
-  var notePreviewViewport:FlxSliceSprite;
+  var notePreviewViewport:Null<FlxSliceSprite> = null;
 
   /**
    * The rectangular sprite used for rendering the selection box.
    * Uses a 9-slice to stretch the selection box to the correct size without warping.
    */
-  var selectionBoxSprite:FlxSliceSprite;
+  var selectionBoxSprite:Null<FlxSliceSprite> = null;
 
   /**
    * The opponent's health icon.
    */
-  var healthIconDad:HealthIcon;
+  var healthIconDad:Null<HealthIcon> = null;
 
   /**
    * The player's health icon.
    */
-  var healthIconBF:HealthIcon;
+  var healthIconBF:Null<HealthIcon> = null;
 
   /**
    * The purple background sprite.
    */
-  var menuBG:FlxSprite;
+  var menuBG:Null<FlxSprite> = null;
+
+  /**
+   * The layout containing the playbar head slider.
+   */
+  var playbarHeadLayout:Null<Component> = null;
+
+  /**
+   * The playbar head slider.
+   */
+  var playbarHead:Null<Slider> = null;
+
+  /**
+   * The current process that is lerping the scroll position.
+   * Used to cancel the previous lerp if the user scrolls again.
+   */
+  var currentScrollEase:Null<VarTween>;
 
   /**
    * The sprite group containing the note graphics.
    * Only displays a subset of the data from `currentSongChartNoteData`,
    * and kills notes that are off-screen to be recycled later.
    */
-  var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite>;
+  var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite> = new FlxTypedSpriteGroup<ChartEditorNoteSprite>();
 
   /**
    * The sprite group containing the hold note graphics.
    * Only displays a subset of the data from `currentSongChartNoteData`,
    * and kills notes that are off-screen to be recycled later.
    */
-  var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>;
+  var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite> = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>();
 
   /**
    * The sprite group containing the song events.
    * Only displays a subset of the data from `currentSongChartEventData`,
    * and kills events that are off-screen to be recycled later.
    */
-  var renderedEvents:FlxTypedSpriteGroup<ChartEditorEventSprite>;
+  var renderedEvents:FlxTypedSpriteGroup<ChartEditorEventSprite> = new FlxTypedSpriteGroup<ChartEditorEventSprite>();
 
-  var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite>;
-
-  var playbarHead:Slider;
+  var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>();
 
   public function new()
   {
@@ -1159,7 +1205,7 @@ class ChartEditorState extends HaxeUIState
 
     buildBackground();
 
-    currentTheme = ChartEditorTheme.Light;
+    ChartEditorThemeHandler.updateTheme(this);
 
     buildGrid();
     // buildSpectrogram(audioInstTrack);
@@ -1213,6 +1259,8 @@ class ChartEditorState extends HaxeUIState
    */
   function buildGrid():Void
   {
+    if (gridBitmap == null) throw 'ERROR: Tried to build grid, but gridBitmap is null! Check ChartEditorThemeHandler.updateTheme().';
+
     gridTiledSprite = new FlxTiledSprite(gridBitmap, gridBitmap.width, 1000, false, true);
     gridTiledSprite.x = FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE; // Center the grid.
     gridTiledSprite.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; // Push down to account for the menu bar.
@@ -1241,7 +1289,6 @@ class ChartEditorState extends HaxeUIState
     gridPlayheadScrollArea.zIndex = 25;
 
     // The playhead that show the current position in the song.
-    gridPlayhead = new FlxSpriteGroup();
     add(gridPlayhead);
     gridPlayhead.zIndex = 30;
 
@@ -1279,6 +1326,8 @@ class ChartEditorState extends HaxeUIState
 
   function buildSelectionBox():Void
   {
+    if (selectionBoxSprite == null) throw 'ERROR: Tried to build selection box, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().';
+
     selectionBoxSprite.scrollFactor.set(0, 0);
     add(selectionBoxSprite);
     selectionBoxSprite.zIndex = 30;
@@ -1288,6 +1337,9 @@ class ChartEditorState extends HaxeUIState
 
   function setSelectionBoxBounds(bounds:FlxRect = null):Void
   {
+    if (selectionBoxSprite == null)
+      throw 'ERROR: Tried to set selection box bounds, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().';
+
     if (bounds == null)
     {
       selectionBoxSprite.visible = false;
@@ -1312,6 +1364,8 @@ class ChartEditorState extends HaxeUIState
     notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
     add(notePreview);
 
+    if (notePreviewViewport == null) throw 'ERROR: Tried to build note preview, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().';
+
     notePreviewViewport.scrollFactor.set(0, 0);
     add(notePreviewViewport);
     notePreviewViewport.zIndex = 30;
@@ -1323,6 +1377,9 @@ class ChartEditorState extends HaxeUIState
   {
     var bounds:FlxRect = new FlxRect();
 
+    // Return 0, 0, 0, 0 if the note preview doesn't exist for some reason.
+    if (notePreview == null) return bounds;
+
     // Horizontal position and width are constant.
     bounds.x = notePreview.x;
     bounds.width = notePreview.width;
@@ -1356,6 +1413,9 @@ class ChartEditorState extends HaxeUIState
 
   function setNotePreviewViewportBounds(bounds:FlxRect = null):Void
   {
+    if (notePreviewViewport == null)
+      throw 'ERROR: Tried to set note preview viewport bounds, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().';
+
     if (bounds == null)
     {
       notePreviewViewport.visible = false;
@@ -1387,32 +1447,31 @@ class ChartEditorState extends HaxeUIState
    */
   function buildNoteGroup():Void
   {
-    renderedHoldNotes = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>();
+    if (gridTiledSprite == null) throw 'ERROR: Tried to build note groups, but gridTiledSprite is null! Check ChartEditorState.buildGrid().';
+
     renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     add(renderedHoldNotes);
     renderedHoldNotes.zIndex = 24;
 
-    renderedNotes = new FlxTypedSpriteGroup<ChartEditorNoteSprite>();
     renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     add(renderedNotes);
     renderedNotes.zIndex = 25;
 
-    renderedEvents = new FlxTypedSpriteGroup<ChartEditorEventSprite>();
     renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     add(renderedEvents);
     renderedNotes.zIndex = 25;
 
-    renderedSelectionSquares = new FlxTypedSpriteGroup<FlxSprite>();
     renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     add(renderedSelectionSquares);
     renderedNotes.zIndex = 26;
   }
 
-  var playbarHeadLayout:Component;
-
   function buildAdditionalUI():Void
   {
     playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT);
+
+    if (playbarHeadLayout == null) throw 'ERROR: Failed to construct playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".';
+
     playbarHeadLayout.zIndex = 110;
 
     playbarHeadLayout.width = FlxG.width - 8;
@@ -1421,6 +1480,7 @@ class ChartEditorState extends HaxeUIState
     playbarHeadLayout.y = FlxG.height - 48 - 8;
 
     playbarHead = playbarHeadLayout.findComponent('playbarHead', Slider);
+    if (playbarHead == null) throw 'ERROR: Failed to fetch playbarHead from playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".';
     playbarHead.allowFocus = false;
     playbarHead.width = FlxG.width;
     playbarHead.height = 10;
@@ -1445,7 +1505,7 @@ class ChartEditorState extends HaxeUIState
       playbarHeadDragging = false;
 
       // Set the song position to where the playhead was moved to.
-      scrollPositionInPixels = songLengthInPixels * (playbarHead.value / 100);
+      scrollPositionInPixels = songLengthInPixels * (playbarHead?.value ?? 0 / 100);
       // Update the conductor and audio tracks to match.
       moveSongToScrollPosition();
 
@@ -1584,31 +1644,40 @@ class ChartEditorState extends HaxeUIState
     addUIChangeListener('menubarItemOpponentHitsounds', event -> hitsoundsEnabledOpponent = event.value);
     setUICheckboxSelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
 
-    var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label);
-    addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
-      var volume:Float = event.value / 100.0;
-      if (audioInstTrack != null) audioInstTrack.volume = volume;
-      instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
-    });
+    var instVolumeLabel:Null<Label> = findComponent('menubarLabelVolumeInstrumental', Label);
+    if (instVolumeLabel != null)
+    {
+      addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
+        var volume:Float = event?.value ?? 0 / 100.0;
+        if (audioInstTrack != null) audioInstTrack.volume = volume;
+        instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
+      });
+    }
 
-    var vocalsVolumeLabel:Label = findComponent('menubarLabelVolumeVocals', Label);
-    addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
-      var volume:Float = event.value / 100.0;
-      if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
-      vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
-    });
+    var vocalsVolumeLabel:Null<Label> = findComponent('menubarLabelVolumeVocals', Label);
+    if (vocalsVolumeLabel != null)
+    {
+      addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
+        var volume:Float = event?.value ?? 0 / 100.0;
+        if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
+        vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
+      });
+    }
 
-    var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label);
-    addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) {
-      var pitch:Float = event.value * 2.0 / 100.0;
-      pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
-      #if FLX_PITCH
-      if (audioInstTrack != null) audioInstTrack.pitch = pitch;
-      if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
-      #end
-      var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
-      playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x';
-    });
+    var playbackSpeedLabel:Null<Label> = findComponent('menubarLabelPlaybackSpeed', Label);
+    if (playbackSpeedLabel != null)
+    {
+      addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) {
+        var pitch:Float = event.value * 2.0 / 100.0;
+        pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
+        #if FLX_PITCH
+        if (audioInstTrack != null) audioInstTrack.pitch = pitch;
+        if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
+        #end
+        var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
+        playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x';
+      });
+    }
 
     addUIChangeListener('menubarItemToggleToolboxTools',
       event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT, event.value));
@@ -1725,12 +1794,14 @@ class ChartEditorState extends HaxeUIState
     #end
 
     // Right align the BF health icon.
-
-    // Base X position to the right of the grid.
-    var baseHealthIconXPos:Float = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15;
-    // Will be 0 when not bopping. When bopping, will increase to push the icon left.
-    var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
-    healthIconBF.x = baseHealthIconXPos - healthIconOffset;
+    if (healthIconBF != null)
+    {
+      // Base X position to the right of the grid.
+      var baseHealthIconXPos:Float = gridTiledSprite?.x ?? 0.0 + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15;
+      // Will be 0 when not bopping. When bopping, will increase to push the icon left.
+      var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
+      healthIconBF.x = baseHealthIconXPos - healthIconOffset;
+    }
   }
 
   /**
@@ -1759,8 +1830,8 @@ class ChartEditorState extends HaxeUIState
 
     if (audioInstTrack != null && audioInstTrack.playing)
     {
-      healthIconDad.onStepHit(Conductor.currentStep);
-      healthIconBF.onStepHit(Conductor.currentStep);
+      if (healthIconDad != null) healthIconDad.onStepHit(Conductor.currentStep);
+      if (healthIconBF != null) healthIconBF.onStepHit(Conductor.currentStep);
     }
 
     // Updating these every step keeps it more accurate.
@@ -1976,6 +2047,8 @@ class ChartEditorState extends HaxeUIState
 
     if (shouldHandleCursor)
     {
+      if (gridTiledSprite == null) throw "ERROR: Tried to handle cursor, but gridTiledSprite is null! Check ChartEditorState.buildGrid()";
+
       var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite);
 
       // Cursor position relative to the grid.
@@ -1989,11 +2062,11 @@ class ChartEditorState extends HaxeUIState
 
       if (FlxG.mouse.justPressed)
       {
-        if (FlxG.mouse.overlaps(gridPlayheadScrollArea))
+        if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea))
         {
           gridPlayheadScrollAreaPressed = true;
         }
-        else if (FlxG.mouse.overlaps(notePreview))
+        else if (notePreview != null && FlxG.mouse.overlaps(notePreview))
         {
           // Clicked note preview
           notePreviewScrollAreaStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
@@ -2019,7 +2092,7 @@ class ChartEditorState extends HaxeUIState
       {
         Cursor.cursorMode = Pointer;
       }
-      else if (FlxG.mouse.overlaps(gridPlayheadScrollArea))
+      else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea))
       {
         Cursor.cursorMode = Pointer;
       }
@@ -2180,10 +2253,10 @@ class ChartEditorState extends HaxeUIState
               trace('Scroll up: ' + diff);
               moveSongToScrollPosition();
             }
-            else if (FlxG.mouse.screenY > playbarHeadLayout.y)
+            else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0))
             {
               // Scroll down.
-              var diff:Float = FlxG.mouse.screenY - playbarHeadLayout.y;
+              var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0);
               scrollPositionInPixels += diff * 0.5; // Too fast!
               trace('Scroll down: ' + diff);
               moveSongToScrollPosition();
@@ -2209,11 +2282,11 @@ class ChartEditorState extends HaxeUIState
             // We clicked on the grid without moving the mouse.
 
             // Find the first note that is at the cursor position.
-            var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
+            var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
               // If note.alive is false, the note is dead and awaiting recycling.
               return note.alive && FlxG.mouse.overlaps(note);
             });
-            var highlightedEvent:ChartEditorEventSprite = null;
+            var highlightedEvent:Null<ChartEditorEventSprite> = null;
             if (highlightedNote == null)
             {
               highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool {
@@ -2223,7 +2296,7 @@ class ChartEditorState extends HaxeUIState
 
             if (FlxG.keys.pressed.CONTROL)
             {
-              if (highlightedNote != null)
+              if (highlightedNote != null && highlightedNote.noteData != null)
               {
                 // TODO: Handle the case of clicking on a sustain piece.
                 // Control click to select/deselect an individual note.
@@ -2236,7 +2309,7 @@ class ChartEditorState extends HaxeUIState
                   performCommand(new SelectItemsCommand([highlightedNote.noteData], []));
                 }
               }
-              else if (highlightedEvent != null)
+              else if (highlightedEvent != null && highlightedEvent.eventData != null)
               {
                 // Control click to select/deselect an individual note.
                 if (isEventSelected(highlightedEvent.eventData))
@@ -2255,12 +2328,12 @@ class ChartEditorState extends HaxeUIState
             }
             else
             {
-              if (highlightedNote != null)
+              if (highlightedNote != null && highlightedNote.noteData != null)
               {
                 // Click a note to select it.
                 performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection));
               }
-              else if (highlightedEvent != null)
+              else if (highlightedEvent != null && highlightedEvent.eventData != null)
               {
                 // Click an event to select it.
                 performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection));
@@ -2288,7 +2361,8 @@ class ChartEditorState extends HaxeUIState
       else if (notePreviewScrollAreaStartPos != null)
       {
         trace('Updating current song time while clicking and holding...');
-        var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, notePreview.y, notePreview.y + notePreview.height, 0, songLengthInPixels);
+        var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, (notePreview?.y ?? 0.0),
+          (notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0), 0, songLengthInPixels);
 
         scrollPositionInPixels = clickedPosInPixels;
         moveSongToScrollPosition();
@@ -2329,11 +2403,11 @@ class ChartEditorState extends HaxeUIState
             // We clicked on the grid without moving the mouse.
 
             // Find the first note that is at the cursor position.
-            var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
+            var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
               // If note.alive is false, the note is dead and awaiting recycling.
               return note.alive && FlxG.mouse.overlaps(note);
             });
-            var highlightedEvent:ChartEditorEventSprite = null;
+            var highlightedEvent:Null<ChartEditorEventSprite> = null;
             if (highlightedNote == null)
             {
               highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool {
@@ -2345,7 +2419,7 @@ class ChartEditorState extends HaxeUIState
             if (FlxG.keys.pressed.CONTROL)
             {
               // Control click to select/deselect an individual note.
-              if (highlightedNote != null)
+              if (highlightedNote != null && highlightedNote.noteData != null)
               {
                 if (isNoteSelected(highlightedNote.noteData))
                 {
@@ -2356,7 +2430,7 @@ class ChartEditorState extends HaxeUIState
                   performCommand(new SelectItemsCommand([highlightedNote.noteData], []));
                 }
               }
-              else if (highlightedEvent != null)
+              else if (highlightedEvent != null && highlightedEvent.eventData != null)
               {
                 if (isEventSelected(highlightedEvent.eventData))
                 {
@@ -2374,12 +2448,12 @@ class ChartEditorState extends HaxeUIState
             }
             else
             {
-              if (highlightedNote != null)
+              if (highlightedNote != null && highlightedNote.noteData != null)
               {
                 // Click a note to select it.
                 performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection));
               }
-              else if (highlightedEvent != null)
+              else if (highlightedEvent != null && highlightedEvent.eventData != null)
               {
                 // Click an event to select it.
                 performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection));
@@ -2421,11 +2495,11 @@ class ChartEditorState extends HaxeUIState
           // We right clicked on the grid.
 
           // Find the first note that is at the cursor position.
-          var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
+          var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
             // If note.alive is false, the note is dead and awaiting recycling.
             return note.alive && FlxG.mouse.overlaps(note);
           });
-          var highlightedEvent:ChartEditorEventSprite = null;
+          var highlightedEvent:Null<ChartEditorEventSprite> = null;
           if (highlightedNote == null)
           {
             highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool {
@@ -2434,13 +2508,13 @@ class ChartEditorState extends HaxeUIState
             });
           }
 
-          if (highlightedNote != null)
+          if (highlightedNote != null && highlightedNote.noteData != null)
           {
             // TODO: Handle the case of clicking on a sustain piece.
             // Remove the note.
             performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
           }
-          else if (highlightedEvent != null)
+          else if (highlightedEvent != null && highlightedEvent.eventData != null)
           {
             // Remove the event.
             performCommand(new RemoveEventsCommand([highlightedEvent.eventData]));
@@ -2460,30 +2534,41 @@ class ChartEditorState extends HaxeUIState
 
           if (cursorColumn == eventColumn)
           {
-            gridGhostEvent.visible = true;
-            gridGhostNote.visible = false;
+            if (gridGhostNote != null) gridGhostNote.visible = false;
 
-            if (selectedEventKind != gridGhostEvent.eventData.event)
+            if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()";
+
+            var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, selectedEventKind, null);
+
+            if (selectedEventKind != eventData.event)
             {
-              gridGhostEvent.eventData.event = selectedEventKind;
+              eventData.event = selectedEventKind;
             }
+            eventData.time = cursorMs;
 
-            gridGhostEvent.eventData.time = cursorMs;
+            gridGhostEvent.visible = true;
+            gridGhostEvent.eventData = eventData;
             gridGhostEvent.updateEventPosition(renderedEvents);
           }
           else
           {
-            gridGhostEvent.visible = false;
-            gridGhostNote.visible = true;
+            if (gridGhostEvent != null) gridGhostEvent.visible = false;
 
-            if (cursorColumn != gridGhostNote.noteData.data || selectedNoteKind != gridGhostNote.noteData.kind)
+            if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
+
+            var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0,
+              selectedNoteKind);
+
+            if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind)
             {
-              gridGhostNote.noteData.kind = selectedNoteKind;
-              gridGhostNote.noteData.data = cursorColumn;
+              noteData.kind = selectedNoteKind;
+              noteData.data = cursorColumn;
               gridGhostNote.playNoteAnimation();
             }
+            noteData.time = cursorMs;
 
-            gridGhostNote.noteData.time = cursorMs;
+            gridGhostNote.visible = true;
+            gridGhostNote.noteData = noteData;
             gridGhostNote.updateNotePosition(renderedNotes);
           }
 
@@ -2494,16 +2579,16 @@ class ChartEditorState extends HaxeUIState
         }
         else
         {
-          gridGhostNote.visible = false;
-          gridGhostEvent.visible = false;
+          if (gridGhostNote != null) gridGhostNote.visible = false;
+          if (gridGhostEvent != null) gridGhostEvent.visible = false;
           Cursor.cursorMode = Default;
         }
       }
     }
     else
     {
-      gridGhostNote.visible = false;
-      gridGhostEvent.visible = false;
+      if (gridGhostNote != null) gridGhostNote.visible = false;
+      if (gridGhostEvent != null) gridGhostEvent.visible = false;
     }
 
     if (isCursorOverHaxeUIButton && Cursor.cursorMode == Default)
@@ -2533,7 +2618,7 @@ class ChartEditorState extends HaxeUIState
       var displayedNoteData:Array<SongNoteData> = [];
       for (noteSprite in renderedNotes.members)
       {
-        if (noteSprite == null || !noteSprite.exists || !noteSprite.visible) continue;
+        if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue;
 
         if (!noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels))
         {
@@ -2590,7 +2675,7 @@ class ChartEditorState extends HaxeUIState
       var displayedEventData:Array<SongEventData> = [];
       for (eventSprite in renderedEvents.members)
       {
-        if (eventSprite == null || !eventSprite.exists || !eventSprite.visible) continue;
+        if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue;
 
         if (!eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
         {
@@ -2618,7 +2703,7 @@ class ChartEditorState extends HaxeUIState
       for (noteData in currentSongChartNoteData)
       {
         // Remember if we are already displaying this note.
-        if (displayedNoteData.indexOf(noteData) != -1)
+        if (noteData == null || displayedNoteData.indexOf(noteData) != -1)
         {
           continue;
         }
@@ -2633,14 +2718,14 @@ class ChartEditorState extends HaxeUIState
         trace('Creating new Note... (${renderedNotes.members.length})');
         noteSprite.parentState = this;
 
-        // The note sprite handles animation playback and positioning.
-        noteSprite.noteData = noteData;
-
         // Setting note data resets position relative to the grid so we fix that.
         noteSprite.updateNotePosition(renderedNotes);
 
+        // The note sprite handles animation playback and positioning.
+        noteSprite.noteData = noteData;
+
         // Add hold notes that are now visible (and not already displayed).
-        if (noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteData) == -1)
+        if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1)
         {
           var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this));
           trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
@@ -2688,7 +2773,7 @@ class ChartEditorState extends HaxeUIState
       for (noteData in currentSongChartNoteData)
       {
         // Is the note a hold note?
-        if (noteData.length <= 0) continue;
+        if (noteData == null || noteData.length <= 0) continue;
 
         // Is the hold note rendered already?
         if (displayedHoldNoteData.indexOf(noteData) != -1) continue;
@@ -2769,6 +2854,9 @@ class ChartEditorState extends HaxeUIState
 
   function buildSelectionSquare():FlxSprite
   {
+    if (selectionSquareBitmap == null)
+      throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()";
+
     return new FlxSprite().loadGraphic(selectionSquareBitmap);
   }
 
@@ -2777,6 +2865,9 @@ class ChartEditorState extends HaxeUIState
    */
   function handlePlaybar():Void
   {
+    if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!";
+    if (playbarHead == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!";
+
     // Make sure the playbar is never nudged out of the correct spot.
     playbarHeadLayout.x = 4;
     playbarHeadLayout.y = FlxG.height - 48 - 8;
@@ -3038,9 +3129,8 @@ class ChartEditorState extends HaxeUIState
 
       // Manage the Select Difficulty tree view.
       var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
-      if (difficultyToolbox == null) return;
 
-      var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree');
+      var treeView:Null<TreeView> = difficultyToolbox.findComponent('difficultyToolboxTree');
       if (treeView == null) return;
 
       // Clear the tree view so we can rebuild it.
@@ -3050,12 +3140,10 @@ class ChartEditorState extends HaxeUIState
       var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'});
       treeSong.expanded = true;
 
-      var variations = Reflect.copy(availableVariations);
-      variations.sort(SortUtil.alphabetically);
-
-      for (curVariation in variations)
+      for (curVariation in availableVariations)
       {
-        var variationMetadata:SongMetadata = songMetadata.get(curVariation);
+        var variationMetadata:Null<SongMetadata> = songMetadata.get(curVariation);
+        if (variationMetadata == null) continue;
 
         var treeVariation:TreeViewNode = treeSong.addNode(
           {
@@ -3093,8 +3181,8 @@ class ChartEditorState extends HaxeUIState
       if (treeView == null) return;
     }
 
-    trace(treeView);
-    treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+    var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
+    if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
   }
 
   function handlePlayerPreviewToolbox():Void
@@ -3103,7 +3191,7 @@ class ChartEditorState extends HaxeUIState
     var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
     if (charPreviewToolbox == null) return;
 
-    var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer');
+    var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer');
     if (charPlayer == null) return;
 
     currentPlayerCharacterPlayer = charPlayer;
@@ -3114,7 +3202,7 @@ class ChartEditorState extends HaxeUIState
 
       if (currentSongCharacterPlayer != charPlayer.charId)
       {
-        healthIconBF.characterId = currentSongCharacterPlayer;
+        if (healthIconBF != null) healthIconBF.characterId = currentSongCharacterPlayer;
 
         charPlayer.loadCharacter(currentSongCharacterPlayer);
         charPlayer.characterType = CharacterType.BF;
@@ -3138,7 +3226,7 @@ class ChartEditorState extends HaxeUIState
     var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
     if (charPreviewToolbox == null) return;
 
-    var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer');
+    var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer');
     if (charPlayer == null) return;
 
     currentOpponentCharacterPlayer = charPlayer;
@@ -3149,7 +3237,7 @@ class ChartEditorState extends HaxeUIState
 
       if (currentSongCharacterOpponent != charPlayer.charId)
       {
-        healthIconDad.characterId = currentSongCharacterOpponent;
+        if (healthIconDad != null) healthIconDad.characterId = currentSongCharacterOpponent;
 
         charPlayer.loadCharacter(currentSongCharacterOpponent);
         charPlayer.characterType = CharacterType.DAD;
@@ -3203,7 +3291,7 @@ class ChartEditorState extends HaxeUIState
     }
   }
 
-  function getCurrentTreeDifficultyNode(?treeView:TreeView = null):TreeViewNode
+  function getCurrentTreeDifficultyNode(?treeView:TreeView = null):Null<TreeViewNode>
   {
     if (treeView == null)
     {
@@ -3235,7 +3323,8 @@ class ChartEditorState extends HaxeUIState
     {
       trace('No target node!');
       // Reset the user's selection.
-      treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+      var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
+      if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
       return;
     }
 
@@ -3258,7 +3347,8 @@ class ChartEditorState extends HaxeUIState
       default:
         // Reset the user's selection.
         trace('Selected wrong node type, resetting selection.');
-        treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+        var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
+        if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
         refreshSongMetadataToolbox();
     }
   }
@@ -3270,31 +3360,31 @@ class ChartEditorState extends HaxeUIState
   {
     var toolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
 
-    var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
-    inputSongName.value = currentSongMetadata.songName;
+    var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField);
+    if (inputSongName != null) inputSongName.value = currentSongMetadata.songName;
 
-    var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
-    inputSongArtist.value = currentSongMetadata.artist;
+    var inputSongArtist:Null<TextField> = toolbox.findComponent('inputSongArtist', TextField);
+    if (inputSongArtist != null) inputSongArtist.value = currentSongMetadata.artist;
 
-    var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
-    inputStage.value = currentSongMetadata.playData.stage;
+    var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
+    if (inputStage != null) inputStage.value = currentSongMetadata.playData.stage;
 
-    var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
-    inputNoteSkin.value = currentSongMetadata.playData.noteSkin;
+    var inputNoteSkin:Null<DropDown> = toolbox.findComponent('inputNoteSkin', DropDown);
+    if (inputNoteSkin != null) inputNoteSkin.value = currentSongMetadata.playData.noteSkin;
 
-    var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
-    inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
+    var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper);
+    if (inputBPM != null) inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
 
-    var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label);
-    labelScrollSpeed.text = 'Scroll Speed: ${currentSongChartScrollSpeed}x';
+    var labelScrollSpeed:Null<Label> = toolbox.findComponent('labelScrollSpeed', Label);
+    if (labelScrollSpeed != null) labelScrollSpeed.text = 'Scroll Speed: ${currentSongChartScrollSpeed}x';
 
-    var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
-    inputScrollSpeed.value = currentSongChartScrollSpeed;
+    var inputScrollSpeed:Null<Slider> = toolbox.findComponent('inputScrollSpeed', Slider);
+    if (inputScrollSpeed != null) inputScrollSpeed.value = currentSongChartScrollSpeed;
 
-    var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame);
-    frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}';
-    var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame);
-    frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}';
+    var frameVariation:Null<Frame> = toolbox.findComponent('frameVariation', Frame);
+    if (frameVariation != null) frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}';
+    var frameDifficulty:Null<Frame> = toolbox.findComponent('frameDifficulty', Frame);
+    if (frameDifficulty != null) frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}';
   }
 
   function addDifficulty(variation:String):Void {}
@@ -3322,7 +3412,7 @@ class ChartEditorState extends HaxeUIState
    */
   function handleNotePreview():Void
   {
-    if (notePreviewDirty)
+    if (notePreviewDirty && notePreview != null)
     {
       notePreviewDirty = false;
 
@@ -3349,7 +3439,7 @@ class ChartEditorState extends HaxeUIState
       commandHistoryDirty = false;
 
       // Update the Undo and Redo buttons.
-      var undoButton:MenuItem = findComponent('menubarItemUndo', MenuItem);
+      var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem);
 
       if (undoButton != null)
       {
@@ -3371,7 +3461,7 @@ class ChartEditorState extends HaxeUIState
         trace('undoButton is null');
       }
 
-      var redoButton:MenuItem = findComponent('menubarItemRedo', MenuItem);
+      var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem);
 
       if (redoButton != null)
       {
@@ -3499,8 +3589,11 @@ class ChartEditorState extends HaxeUIState
 
   function startAudioPlayback():Void
   {
-    if (audioInstTrack != null) audioInstTrack.play(false, audioInstTrack.time);
-    if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time);
+    if (audioInstTrack != null)
+    {
+      audioInstTrack.play(false, audioInstTrack.time);
+      if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time);
+    }
 
     setComponentText('playbarPlay', '||');
   }
@@ -3595,17 +3688,17 @@ class ChartEditorState extends HaxeUIState
     // Move the grid sprite to the correct position.
     if (isViewDownscroll)
     {
-      gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
+      if (gridTiledSprite != null) gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
     }
     else
     {
-      gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
+      if (gridTiledSprite != null) gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
     }
     // Move the rendered notes to the correct position.
-    renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
-    renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
-    renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
-    renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
+    renderedNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
+    renderedHoldNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
+    renderedEvents.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
+    renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
 
     // Offset the selection box start position, if we are dragging.
     if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
@@ -3618,7 +3711,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * Transitions to the Play State to test the song
    */
-  public function testSongInPlayState(?minimal:Bool = false):Void
+  public function testSongInPlayState(minimal:Bool = false):Void
   {
     var startTimestamp:Float = 0;
     if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
@@ -3665,8 +3758,8 @@ class ChartEditorState extends HaxeUIState
       });
 
     // Override music.
-    FlxG.sound.music = audioInstTrack;
-    targetState.vocals = audioVocalTrackGroup;
+    if (audioInstTrack != null) FlxG.sound.music = audioInstTrack;
+    if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup;
 
     openSubState(targetState);
   }
@@ -3697,7 +3790,7 @@ class ChartEditorState extends HaxeUIState
   {
     #if sys
     // Validate file extension.
-    if (!SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+    if (path.ext != null && !SUPPORTED_MUSIC_FORMATS.contains(path.ext))
     {
       return false;
     }
@@ -3759,17 +3852,20 @@ class ChartEditorState extends HaxeUIState
 
   public function postLoadInstrumental():Void
   {
-    // Prevent the time from skipping back to 0 when the song ends.
-    audioInstTrack.onComplete = function() {
-      if (audioInstTrack != null) audioInstTrack.pause();
-      if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
-    };
+    if (audioInstTrack != null)
+    {
+      // Prevent the time from skipping back to 0 when the song ends.
+      audioInstTrack.onComplete = function() {
+        if (audioInstTrack != null) audioInstTrack.pause();
+        if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
+      };
 
-    songLengthInMs = audioInstTrack.length;
+      songLengthInMs = audioInstTrack.length;
 
-    gridTiledSprite.height = songLengthInPixels;
+      if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels;
 
-    buildSpectrogram(audioInstTrack);
+      buildSpectrogram(audioInstTrack);
+    }
 
     scrollPositionInPixels = 0;
     playheadPositionInPixels = 0;
@@ -3798,7 +3894,7 @@ class ChartEditorState extends HaxeUIState
    */
   public function clearVocals():Void
   {
-    audioVocalTrackGroup.clear();
+    if (audioVocalTrackGroup != null) audioVocalTrackGroup.clear();
   }
 
   /**
@@ -3816,13 +3912,13 @@ class ChartEditorState extends HaxeUIState
       switch (charType)
       {
         case CharacterType.BF:
-          audioVocalTrackGroup.addPlayerVoice(vocalTrack);
+          if (audioVocalTrackGroup != null) audioVocalTrackGroup.addPlayerVoice(vocalTrack);
           audioVocalTrackData.set(currentSongCharacterPlayer, Assets.getBytes(path));
         case CharacterType.DAD:
-          audioVocalTrackGroup.addOpponentVoice(vocalTrack);
+          if (audioVocalTrackGroup != null) audioVocalTrackGroup.addOpponentVoice(vocalTrack);
           audioVocalTrackData.set(currentSongCharacterOpponent, Assets.getBytes(path));
         default:
-          audioVocalTrackGroup.add(vocalTrack);
+          if (audioVocalTrackGroup != null) audioVocalTrackGroup.add(vocalTrack);
           audioVocalTrackData.set('default', Assets.getBytes(path));
       }
 
@@ -3834,12 +3930,12 @@ class ChartEditorState extends HaxeUIState
   /**
    * Loads a vocal track from audio byte data.
    */
-  public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool
+  public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = ''):Bool
   {
     var openflSound:openfl.media.Sound = new openfl.media.Sound();
     openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
     var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
-    audioVocalTrackGroup.add(vocalTrack);
+    if (audioVocalTrackGroup != null) audioVocalTrackGroup.add(vocalTrack);
     audioVocalTrackData.set(charKey, bytes);
     return true;
   }
@@ -3849,12 +3945,9 @@ class ChartEditorState extends HaxeUIState
    */
   public function loadSongAsTemplate(songId:String):Void
   {
-    var song:Song = SongDataParser.fetchSong(songId);
+    var song:Null<Song> = SongDataParser.fetchSong(songId);
 
-    if (song == null)
-    {
-      return;
-    }
+    if (song == null) return;
 
     // Load the song metadata.
     var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata();
@@ -3863,8 +3956,13 @@ class ChartEditorState extends HaxeUIState
 
     for (metadata in rawSongMetadata)
     {
+      if (metadata == null) continue;
       var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
-      songMetadata.set(variation, Reflect.copy(metadata));
+
+      // Clone to prevent modifying the original.
+      var metadataClone = Reflect.copy(metadata);
+      if (metadataClone != null) songMetadata.set(variation, metadataClone);
+
       songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
     }
 
@@ -3888,7 +3986,8 @@ class ChartEditorState extends HaxeUIState
 
     loadInstrumentalFromAsset(Paths.inst(songId));
 
-    var voiceList:Array<String> = song.getDifficulty(selectedDifficulty).buildVoiceList(currentSongCharacterPlayer);
+    var diff:Null<SongDifficulty> = song.getDifficulty(selectedDifficulty);
+    var voiceList:Array<String> = diff != null ? diff.buildVoiceList(currentSongCharacterPlayer) : [];
     if (voiceList.length == 2)
     {
       loadVocalsFromAsset(voiceList[0], BF);
@@ -3959,8 +4058,6 @@ class ChartEditorState extends HaxeUIState
     noteDisplayDirty = true;
   }
 
-  var currentScrollEase:VarTween;
-
   function easeSongToScrollPosition(targetScrollPosition:Float):Void
   {
     if (currentScrollEase != null) cancelScrollEase(currentScrollEase);
@@ -3998,7 +4095,7 @@ class ChartEditorState extends HaxeUIState
    * @param command The command to perform.
    * @param purgeRedoStack If true, the redo stack will be cleared.
    */
-  function performCommand(command:ChartEditorCommand, ?purgeRedoStack:Bool = true):Void
+  function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void
   {
     command.execute(this);
     undoHistory.push(command);
@@ -4022,13 +4119,12 @@ class ChartEditorState extends HaxeUIState
    */
   function undoLastCommand():Void
   {
-    if (undoHistory.length == 0)
+    var command:Null<ChartEditorCommand> = undoHistory.pop();
+    if (command == null)
     {
       trace('No actions to undo.');
       return;
     }
-
-    var command:ChartEditorCommand = undoHistory.pop();
     undoCommand(command);
   }
 
@@ -4037,13 +4133,12 @@ class ChartEditorState extends HaxeUIState
    */
   function redoLastCommand():Void
   {
-    if (redoHistory.length == 0)
+    var command:Null<ChartEditorCommand> = redoHistory.pop();
+    if (command == null)
     {
       trace('No actions to redo.');
       return;
     }
-
-    var command:ChartEditorCommand = redoHistory.pop();
     performCommand(command, false);
   }
 
@@ -4058,19 +4153,19 @@ class ChartEditorState extends HaxeUIState
     });
   }
 
-  function playMetronomeTick(?high:Bool = false):Void
+  function playMetronomeTick(high:Bool = false):Void
   {
     playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}'));
   }
 
-  function isNoteSelected(note:SongNoteData):Bool
+  function isNoteSelected(note:Null<SongNoteData>):Bool
   {
-    return currentNoteSelection.indexOf(note) != -1;
+    return note != null && currentNoteSelection.indexOf(note) != -1;
   }
 
-  function isEventSelected(event:SongEventData):Bool
+  function isEventSelected(event:Null<SongEventData>):Bool
   {
-    return currentEventSelection.indexOf(event) != -1;
+    return event != null && currentEventSelection.indexOf(event) != -1;
   }
 
   /**
@@ -4079,8 +4174,16 @@ class ChartEditorState extends HaxeUIState
    */
   function playSound(path:String):Void
   {
-    var snd:FlxSound = FlxG.sound.list.recycle(FlxSound);
-    snd.loadEmbedded(FlxG.sound.cache(path));
+    var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
+
+    var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
+    if (asset == null)
+    {
+      trace('WARN: Failed to play sound $path, asset not found.');
+      return;
+    }
+
+    snd.loadEmbedded(asset);
     snd.autoDestroy = true;
     FlxG.sound.list.add(snd);
     snd.play();
@@ -4108,7 +4211,7 @@ class ChartEditorState extends HaxeUIState
    * @param force Whether to force the export without prompting the user for a file location.
    * @param tmp If true, save to the temporary directory instead of the local `backup` directory.
    */
-  public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void
+  public function exportAllSongData(force:Bool = false, tmp:Bool = false):Void
   {
     var zipEntries:Array<haxe.zip.Entry> = [];
 
@@ -4122,24 +4225,27 @@ class ChartEditorState extends HaxeUIState
 
       if (variationId == '')
       {
-        var variationMetadata:SongMetadata = songMetadata.get(variation);
-        zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata)));
-        var variationChart:SongChartData = songChartData.get(variation);
-        zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart)));
+        var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
+        if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata)));
+        var variationChart:Null<SongChartData> = songChartData.get(variation);
+        if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart)));
       }
       else
       {
-        var variationMetadata:SongMetadata = songMetadata.get(variation);
-        zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata)));
-        var variationChart:SongChartData = songChartData.get(variation);
-        zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart)));
+        var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
+        if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json',
+          SerializerUtil.toJSON(variationMetadata)));
+        var variationChart:Null<SongChartData> = songChartData.get(variation);
+        if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart)));
       }
     }
 
-    zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData));
+    if (audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData));
     for (charId in audioVocalTrackData.keys())
     {
-      zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', audioVocalTrackData.get(charId)));
+      var entryData = audioVocalTrackData.get(charId);
+      if (entryData == null) continue;
+      zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData));
     }
 
     trace('Exporting ${zipEntries.length} files to ZIP...');
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index a6e230f6e..152615568 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -168,9 +168,9 @@ class ChartEditorToolboxHandler
       case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
         toolbox = buildToolboxCharactersLayout(state);
       case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
-        toolbox = null; // buildToolboxPlayerPreviewLayout(state);
+        toolbox = buildToolboxPlayerPreviewLayout(state);
       case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
-        toolbox = null; // buildToolboxOpponentPreviewLayout(state);
+        toolbox = buildToolboxOpponentPreviewLayout(state);
       default:
         // This happens if you try to load an unknown layout.
         trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
@@ -200,6 +200,8 @@ class ChartEditorToolboxHandler
     // Initialize the toolbox without showing it.
     if (toolbox == null) toolbox = initToolbox(state, id);
 
+    if (toolbox == null) throw 'ChartEditorToolboxHandler.getToolbox() - Could not retrieve or build toolbox: $id';
+
     return toolbox;
   }
 
diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
index 0e6981535..c638e8a72 100644
--- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx
+++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
@@ -32,7 +32,7 @@ class CharacterPlayer extends Box
 {
   var character:BaseCharacter;
 
-  public function new(?defaultToBf:Bool = true)
+  public function new(defaultToBf:Bool = true)
   {
     super();
     _overrideSkipTransformChildren = false;
diff --git a/source/funkin/ui/stageBuildShit/StageEditorCommand.hx b/source/funkin/ui/stageBuildShit/StageEditorCommand.hx
index 3248d16d8..d61281e07 100644
--- a/source/funkin/ui/stageBuildShit/StageEditorCommand.hx
+++ b/source/funkin/ui/stageBuildShit/StageEditorCommand.hx
@@ -21,7 +21,7 @@ class MovePropCommand implements StageEditorCommand
   var yDiff:Float;
   var realMove:Bool; // if needs a move!
 
-  public function new(xDiff:Float = 0, yDiff:Float = 0, ?realMove:Bool = true)
+  public function new(xDiff:Float = 0, yDiff:Float = 0, realMove:Bool = true)
   {
     this.xDiff = xDiff;
     this.yDiff = yDiff;
diff --git a/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx b/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx
index dff0cca6b..a6aa6fa68 100644
--- a/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx
+++ b/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx
@@ -3,17 +3,18 @@ package funkin.ui.stageBuildShit;
 import flixel.FlxSprite;
 import flixel.input.mouse.FlxMouseEvent;
 import flixel.math.FlxPoint;
-import funkin.play.PlayState;
 import funkin.play.character.BaseCharacter;
+import funkin.play.PlayState;
 import funkin.play.stage.StageData;
 import funkin.play.stage.StageProp;
 import funkin.shaderslmfao.StrokeShader;
 import funkin.ui.haxeui.HaxeUISubState;
 import funkin.ui.stageBuildShit.StageEditorCommand;
-import haxe.ui.RuntimeComponentBuilder;
+import funkin.util.SerializerUtil;
 import haxe.ui.containers.ListView;
 import haxe.ui.core.Component;
 import haxe.ui.events.UIEvent;
+import haxe.ui.RuntimeComponentBuilder;
 import openfl.events.Event;
 import openfl.events.IOErrorEvent;
 import openfl.net.FileReference;
@@ -376,6 +377,6 @@ class StageOffsetSubState extends HaxeUISubState
     stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
     stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
 
-    return CoolUtil.jsonStringify(stageLol);
+    return SerializerUtil.toJSON(stageLol);
   }
 }
diff --git a/source/funkin/util/ClipboardUtil.hx b/source/funkin/util/ClipboardUtil.hx
index 292cb111f..2796bcad6 100644
--- a/source/funkin/util/ClipboardUtil.hx
+++ b/source/funkin/util/ClipboardUtil.hx
@@ -14,7 +14,7 @@ class ClipboardUtil
    * @param	once If true, the callback will only execute once and then be deleted.
    * @param priority Set the priority at which the callback will be executed. Higher values execute first.
    */
-  public static function addListener(callback:Void->Void, ?once:Bool = false, ?priority:Int = 0):Void
+  public static function addListener(callback:Void->Void, once:Bool = false, ?priority:Int = 0):Void
   {
     lime.system.Clipboard.onUpdate.add(callback, once, priority);
   }
diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx
index 3494e620b..21c2920d9 100644
--- a/source/funkin/util/FileUtil.hx
+++ b/source/funkin/util/FileUtil.hx
@@ -209,7 +209,7 @@ class FileUtil
    * @return Whether the file dialog was opened successfully.
    */
   public static function saveMultipleFiles(resources:Array<Entry>, ?onSaveAll:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
-      ?force:Bool = false):Bool
+      force:Bool = false):Bool
   {
     #if desktop
     // Prompt the user for a directory, then write all of the files to there.
@@ -257,7 +257,7 @@ class FileUtil
    * Takes an array of file entries and prompts the user to save them as a ZIP file.
    */
   public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
-      ?force:Bool = false):Bool
+      force:Bool = false):Bool
   {
     // Create a ZIP file.
     var zipBytes:Bytes = createZIPFromEntries(resources);
@@ -278,7 +278,7 @@ class FileUtil
    * Use `saveFilesAsZIP` instead.
    * @param force Whether to force overwrite an existing file.
    */
-  public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, ?force:Bool = false):Bool
+  public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, force:Bool = false):Bool
   {
     #if desktop
     // Create a ZIP file.
diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx
index 0075e83c0..26563efce 100644
--- a/source/funkin/util/SerializerUtil.hx
+++ b/source/funkin/util/SerializerUtil.hx
@@ -21,7 +21,7 @@ class SerializerUtil
   /**
    * Convert a Haxe object to a JSON string.
    */
-  public static function toJSON(input:Dynamic, ?pretty:Bool = true):String
+  public static function toJSON(input:Dynamic, pretty:Bool = true):String
   {
     return Json.stringify(input, replacer, pretty ? INDENT_CHAR : null);
   }
diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx
new file mode 100644
index 000000000..a295f9bf1
--- /dev/null
+++ b/source/funkin/util/logging/CrashHandler.hx
@@ -0,0 +1,143 @@
+package funkin.util.logging;
+
+import openfl.Lib;
+import openfl.events.UncaughtErrorEvent;
+
+/**
+ * A custom crash handler that writes to a log file and displays a message box.
+ */
+@:nullSafety
+class CrashHandler
+{
+  static final LOG_FOLDER = 'logs';
+
+  /**
+   * Initializes
+   */
+  public static function initialize():Void
+  {
+    trace('[LOG] Enabling standard uncaught error handler...');
+    Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);
+
+    #if cpp
+    trace('[LOG] Enabling C++ critical error handler...');
+    untyped __global__.__hxcpp_set_critical_error_handler(onCriticalError);
+    #end
+  }
+
+  /**
+   * Called when an uncaught error occurs.
+   * This handles most thrown errors, and is sufficient to handle everything alone on HTML5.
+   * @param error Information on the error that was thrown.
+   */
+  static function onUncaughtError(error:UncaughtErrorEvent):Void
+  {
+    try
+    {
+      #if sys
+      logError(error);
+      #end
+
+      displayError(error);
+    }
+    catch (e:Dynamic)
+    {
+      trace('Error while handling crash: ' + e);
+    }
+  }
+
+  static function onCriticalError(message:String):Void
+  {
+    try
+    {
+      #if sys
+      logErrorMessage(message, true);
+      #end
+
+      displayErrorMessage(message);
+    }
+    catch (e:Dynamic)
+    {
+      trace('Error while handling crash: $e');
+
+      trace('Message: $message');
+    }
+  }
+
+  static function displayError(error:UncaughtErrorEvent):Void
+  {
+    displayErrorMessage(generateErrorMessage(error));
+  }
+
+  static function displayErrorMessage(message:String):Void
+  {
+    lime.app.Application.current.window.alert(message, "Fatal Uncaught Exception");
+  }
+
+  #if sys
+  static function logError(error:UncaughtErrorEvent):Void
+  {
+    logErrorMessage(generateErrorMessage(error));
+  }
+
+  static function logErrorMessage(message:String, critical:Bool = false):Void
+  {
+    FileUtil.createDirIfNotExists(LOG_FOLDER);
+
+    sys.io.File.saveContent('$LOG_FOLDER/crash${critical ? '-critical' : ''}-${DateUtil.generateTimestamp()}.log', message);
+  }
+  #end
+
+  static function generateErrorMessage(error:UncaughtErrorEvent):String
+  {
+    var errorMessage:String = "";
+    var callStack:Array<haxe.CallStack.StackItem> = haxe.CallStack.exceptionStack(true);
+
+    errorMessage += '${error.error}\n';
+
+    for (stackItem in callStack)
+    {
+      switch (stackItem)
+      {
+        case FilePos(innerStackItem, file, line, column):
+          errorMessage += '  in ${file}#${line}';
+          if (column != null) errorMessage += ':${column}';
+        case CFunction:
+          errorMessage += '[Function] ';
+        case Module(m):
+          errorMessage += '[Module(${m})] ';
+        case Method(classname, method):
+          errorMessage += '[Function(${classname}.${method})] ';
+        case LocalFunction(v):
+          errorMessage += '[LocalFunction(${v})] ';
+      }
+      errorMessage += '\n';
+    }
+
+    return errorMessage;
+  }
+
+  public static function queryStatus():Void
+  {
+    @:privateAccess
+    var currentStatus = Lib.current.stage.__uncaughtErrorEvents.__enabled;
+    trace('ERROR HANDLER STATUS: ' + currentStatus);
+
+    #if openfl_enable_handle_error
+    trace('Define: openfl_enable_handle_error is enabled');
+    #else
+    trace('Define: openfl_enable_handle_error is disabled');
+    #end
+
+    #if openfl_disable_handle_error
+    trace('Define: openfl_disable_handle_error is enabled');
+    #else
+    trace('Define: openfl_disable_handle_error is disabled');
+    #end
+  }
+
+  public static function induceBasicCrash():Void
+  {
+    throw "This is an example of an uncaught exception.";
+  }
+}
diff --git a/source/funkin/util/macro/ClassMacro.hx b/source/funkin/util/macro/ClassMacro.hx
index 43b437dda..59d96e2a8 100644
--- a/source/funkin/util/macro/ClassMacro.hx
+++ b/source/funkin/util/macro/ClassMacro.hx
@@ -22,7 +22,7 @@ class ClassMacro
    * @param includeSubPackages Whether to include classes located in sub-packages of the target package.
    * @return A list of classes matching the specified criteria.
    */
-  public static macro function listClassesInPackage(targetPackage:String, ?includeSubPackages:Bool = true):ExprOf<Iterable<Class<Dynamic>>>
+  public static macro function listClassesInPackage(targetPackage:String, includeSubPackages:Bool = true):ExprOf<Iterable<Class<Dynamic>>>
   {
     if (!onGenerateCallbackRegistered)
     {