diff --git a/assets b/assets
index 1f00d2413..75ac8ec25 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 1f00d24134231433180affecc67a617d54169ffa
+Subproject commit 75ac8ec2564c9a56e8282b0853091ecd8b4f2dfd
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 399f52498..0ad3c19b8 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -50,11 +50,13 @@ class InitState extends FlxState
    */
   public override function create():Void
   {
+    // Setup a bunch of important Flixel stuff.
     setupShit();
 
-    // loadSaveData(); // Moved to Main.hx
     // Load player options from save data.
+    // Flixel has already loaded the save data, so we can just use it.
     Preferences.init();
+
     // Load controls from save data.
     PlayerSettings.init();
 
@@ -198,8 +200,13 @@ class InitState extends FlxState
     //
     // FLIXEL PLUGINS
     //
+    // Plugins provide a useful interface for globally active Flixel objects,
+    // that receive update events regardless of the current state.
+    // TODO: Move Module behavior to a Flixel plugin.
     funkin.util.plugins.EvacuateDebugPlugin.initialize();
     funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
+    funkin.util.plugins.ScreenshotPlugin.initialize();
+    funkin.util.plugins.VolumePlugin.initialize();
     funkin.util.plugins.WatchPlugin.initialize();
 
     //
@@ -302,15 +309,11 @@ class InitState extends FlxState
       return;
     }
 
-    // Load and cache the song's charts.
-    // TODO: Do this in the loading state.
-    songData.cacheCharts(true);
-
-    LoadingState.loadAndSwitchState(() -> new funkin.play.PlayState(
+    LoadingState.loadPlayState(
       {
         targetSong: songData,
         targetDifficulty: difficultyId,
-      }));
+      });
   }
 
   /**
@@ -336,11 +339,11 @@ class InitState extends FlxState
 
     var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
 
-    LoadingState.loadAndSwitchState(() -> new funkin.play.PlayState(
+    LoadingState.loadPlayState(
       {
         targetSong: targetSong,
         targetDifficulty: difficultyId,
-      }));
+      });
   }
 
   function defineSong():String
diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx
index 6b0911ede..039a4c285 100644
--- a/source/funkin/Preferences.hx
+++ b/source/funkin/Preferences.hx
@@ -20,7 +20,10 @@ class Preferences
 
   static function set_naughtyness(value:Bool):Bool
   {
-    return Save.get().options.naughtyness = value;
+    var save = Save.get();
+    save.options.naughtyness = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -36,7 +39,10 @@ class Preferences
 
   static function set_downscroll(value:Bool):Bool
   {
-    return Save.get().options.downscroll = value;
+    var save = Save.get();
+    save.options.downscroll = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -52,7 +58,10 @@ class Preferences
 
   static function set_flashingLights(value:Bool):Bool
   {
-    return Save.get().options.flashingLights = value;
+    var save = Save.get();
+    save.options.flashingLights = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -68,7 +77,10 @@ class Preferences
 
   static function set_zoomCamera(value:Bool):Bool
   {
-    return Save.get().options.zoomCamera = value;
+    var save = Save.get();
+    save.options.zoomCamera = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -89,7 +101,10 @@ class Preferences
       toggleDebugDisplay(value);
     }
 
-    return Save.get().options.debugDisplay = value;
+    var save = Save.get();
+    save.options.debugDisplay = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -107,7 +122,10 @@ class Preferences
   {
     if (value != Save.get().options.autoPause) FlxG.autoPause = value;
 
-    return Save.get().options.autoPause = value;
+    var save = Save.get();
+    save.options.autoPause = value;
+    save.flush();
+    return value;
   }
 
   public static function init():Void
diff --git a/source/funkin/Preloader.hx b/source/funkin/Preloader.hx
index 24015be05..2a73d8199 100644
--- a/source/funkin/Preloader.hx
+++ b/source/funkin/Preloader.hx
@@ -8,6 +8,9 @@ import flash.display.Sprite;
 import flixel.system.FlxBasePreloader;
 import openfl.display.Sprite;
 import funkin.util.CLIUtil;
+import openfl.text.TextField;
+import openfl.text.TextFormat;
+import flixel.system.FlxAssets;
 
 @:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {}
 
@@ -21,12 +24,26 @@ class Preloader extends FlxBasePreloader
   }
 
   var logo:Sprite;
+  var _text:TextField;
 
   override function create():Void
   {
     this._width = Lib.current.stage.stageWidth;
     this._height = Lib.current.stage.stageHeight;
 
+    _text = new TextField();
+    _text.width = 500;
+    _text.text = "Loading FNF";
+    _text.defaultTextFormat = new TextFormat(FlxAssets.FONT_DEFAULT, 16, 0xFFFFFFFF);
+    _text.embedFonts = true;
+    _text.selectable = false;
+    _text.multiline = false;
+    _text.wordWrap = false;
+    _text.autoSize = LEFT;
+    _text.x = 2;
+    _text.y = 2;
+    addChild(_text);
+
     var ratio:Float = this._width / 2560; // This allows us to scale assets depending on the size of the screen.
 
     logo = new Sprite();
@@ -34,27 +51,14 @@ class Preloader extends FlxBasePreloader
     logo.scaleX = logo.scaleY = ratio;
     logo.x = ((this._width) / 2) - ((logo.width) / 2);
     logo.y = (this._height / 2) - ((logo.height) / 2);
-    addChild(logo); // Adds the graphic to the NMEPreloader's buffer.
+    // addChild(logo); // Adds the graphic to the NMEPreloader's buffer.
 
     super.create();
   }
 
   override function update(Percent:Float):Void
   {
-    if (Percent < 69)
-    {
-      logo.scaleX += Percent / 1920;
-      logo.scaleY += Percent / 1920;
-      logo.x -= Percent * 0.6;
-      logo.y -= Percent / 2;
-    }
-    else
-    {
-      logo.scaleX = this._width / 1280;
-      logo.scaleY = this._width / 1280;
-      logo.x = ((this._width) / 2) - ((logo.width) / 2);
-      logo.y = (this._height / 2) - ((logo.height) / 2);
-    }
+    _text.text = "FNF: " + Math.round(Percent * 100) + "%";
 
     super.update(Percent);
   }
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 73ecbce14..bba5f899f 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -898,20 +898,26 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
   /**
    * The kind of the note.
    * This can allow the note to include information used for custom behavior.
-   * Defaults to blank or `Constants.DEFAULT_DIFFICULTY`.
+   * Defaults to `null` for no kind.
    */
   @:alias("k")
-  @:default("normal")
   @:optional
-  public var kind(get, default):String = '';
+  @:isVar
+  public var kind(get, set):Null<String> = null;
 
-  function get_kind():String
+  function get_kind():Null<String>
   {
-    if (this.kind == null || this.kind == '') return 'normal';
+    if (this.kind == null || this.kind == '') return null;
 
     return this.kind;
   }
 
+  function set_kind(value:Null<String>):Null<String>
+  {
+    if (value == '') value = null;
+    return this.kind = value;
+  }
+
   public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
   {
     this.time = time;
@@ -1061,13 +1067,13 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
     if (this == null) return other == null;
     if (other == null) return false;
 
-    if (this.kind == '')
+    if (this.kind == null || this.kind == '')
     {
-      if (other.kind != '' && other.kind != 'normal') return false;
+      if (other.kind != '' && this.kind != null) return false;
     }
     else
     {
-      if (other.kind == '' || other.kind != this.kind) return false;
+      if (other.kind == '' || this.kind == null) return false;
     }
 
     return this.time == other.time && this.data == other.data && this.length == other.length;
@@ -1082,11 +1088,11 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
 
     if (this.kind == '')
     {
-      if (other.kind != '' && other.kind != 'normal') return true;
+      if (other.kind != '') return true;
     }
     else
     {
-      if (other.kind == '' || other.kind != this.kind) return true;
+      if (other.kind == '') return true;
     }
 
     return this.time != other.time || this.data != other.data || this.length != other.length;
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 01ea2da32..7f3b01eb4 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -210,14 +210,13 @@ class SongDataUtils
    */
   public static function writeItemsToClipboard(data:SongClipboardItems):Void
   {
-    var writer = new json2object.JsonWriter<SongClipboardItems>();
+    var ignoreNullOptionals = true;
+    var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals);
     var dataString:String = writer.write(data, '  ');
 
     ClipboardUtil.setClipboard(dataString);
 
     trace('Wrote ' + data.notes.length + ' notes and ' + data.events.length + ' events to clipboard.');
-
-    trace(dataString);
   }
 
   /**
diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx
index 201c222a3..c4760cf5f 100644
--- a/source/funkin/input/Controls.hx
+++ b/source/funkin/input/Controls.hx
@@ -63,12 +63,10 @@ class Controls extends FlxActionSet
   var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU);
   var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART);
   var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE);
+  var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
   var _volume_up = new FlxActionDigital(Action.VOLUME_UP);
   var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN);
   var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE);
-  #if CAN_CHEAT
-  var _cheat = new FlxActionDigital(Action.CHEAT);
-  #end
 
   var byName:Map<String, FlxActionDigital> = new Map<String, FlxActionDigital>();
 
@@ -235,6 +233,11 @@ class Controls extends FlxActionSet
   inline function get_DEBUG_STAGE()
     return _debug_stage.check();
 
+  public var SCREENSHOT(get, never):Bool;
+
+  inline function get_SCREENSHOT()
+    return _screenshot.check();
+
   public var VOLUME_UP(get, never):Bool;
 
   inline function get_VOLUME_UP()
@@ -255,13 +258,6 @@ class Controls extends FlxActionSet
   inline function get_RESET()
     return _reset.check();
 
-  #if CAN_CHEAT
-  public var CHEAT(get, never):Bool;
-
-  inline function get_CHEAT()
-    return _cheat.check();
-  #end
-
   public function new(name, scheme:KeyboardScheme = null)
   {
     super(name);
@@ -295,13 +291,14 @@ class Controls extends FlxActionSet
     add(_pause);
     add(_cutscene_advance);
     add(_cutscene_skip);
+    add(_debug_menu);
+    add(_debug_chart);
+    add(_debug_stage);
+    add(_screenshot);
     add(_volume_up);
     add(_volume_down);
     add(_volume_mute);
     add(_reset);
-    #if CAN_CHEAT
-    add(_cheat);
-    #end
 
     for (action in digitalActions)
       byName[action.name] = action;
@@ -391,12 +388,10 @@ class Controls extends FlxActionSet
       case DEBUG_MENU: _debug_menu;
       case DEBUG_CHART: _debug_chart;
       case DEBUG_STAGE: _debug_stage;
+      case SCREENSHOT: _screenshot;
       case VOLUME_UP: _volume_up;
       case VOLUME_DOWN: _volume_down;
       case VOLUME_MUTE: _volume_mute;
-      #if CAN_CHEAT
-      case CHEAT: _cheat;
-      #end
     }
   }
 
@@ -464,6 +459,8 @@ class Controls extends FlxActionSet
         func(_debug_chart, JUST_PRESSED);
       case DEBUG_STAGE:
         func(_debug_stage, JUST_PRESSED);
+      case SCREENSHOT:
+        func(_screenshot, JUST_PRESSED);
       case VOLUME_UP:
         func(_volume_up, JUST_PRESSED);
       case VOLUME_DOWN:
@@ -472,10 +469,6 @@ class Controls extends FlxActionSet
         func(_volume_mute, JUST_PRESSED);
       case RESET:
         func(_reset, JUST_PRESSED);
-      #if CAN_CHEAT
-      case CHEAT:
-        func(_cheat, JUST_PRESSED);
-      #end
     }
   }
 
@@ -666,6 +659,8 @@ class Controls extends FlxActionSet
     bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
     bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
     bindKeys(Control.DEBUG_STAGE, getDefaultKeybinds(scheme, Control.DEBUG_STAGE));
+    bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET));
+    bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT));
     bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
     bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
     bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
@@ -693,6 +688,7 @@ class Controls extends FlxActionSet
           case Control.DEBUG_MENU: return [GRAVEACCENT];
           case Control.DEBUG_CHART: return [];
           case Control.DEBUG_STAGE: return [];
+          case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen
           case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
           case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
           case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
@@ -716,6 +712,7 @@ class Controls extends FlxActionSet
           case Control.DEBUG_MENU: return [GRAVEACCENT];
           case Control.DEBUG_CHART: return [];
           case Control.DEBUG_STAGE: return [];
+          case Control.SCREENSHOT: return [PRINTSCREEN];
           case Control.VOLUME_UP: return [PLUS];
           case Control.VOLUME_DOWN: return [MINUS];
           case Control.VOLUME_MUTE: return [ZERO];
@@ -739,6 +736,7 @@ class Controls extends FlxActionSet
           case Control.DEBUG_MENU: return [GRAVEACCENT];
           case Control.DEBUG_CHART: return [];
           case Control.DEBUG_STAGE: return [];
+          case Control.SCREENSHOT: return [PRINTSCREEN];
           case Control.VOLUME_UP: return [NUMPADPLUS];
           case Control.VOLUME_DOWN: return [NUMPADMINUS];
           case Control.VOLUME_MUTE: return [NUMPADZERO];
@@ -845,6 +843,7 @@ class Controls extends FlxActionSet
       Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT),
       Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT),
       Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE),
+      // Control.SCREENSHOT => [],
       // Control.VOLUME_UP => [RIGHT_SHOULDER],
       // Control.VOLUME_DOWN => [LEFT_SHOULDER],
       // Control.VOLUME_MUTE => [RIGHT_TRIGGER],
@@ -852,8 +851,7 @@ class Controls extends FlxActionSet
       Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP),
       // Control.DEBUG_MENU
       // Control.DEBUG_CHART
-      Control.RESET => getDefaultGamepadBinds(Control.RESET),
-      #if CAN_CHEAT, Control.CHEAT => getDefaultGamepadBinds(Control.CHEAT) #end
+      Control.RESET => getDefaultGamepadBinds(Control.RESET)
     ]);
   }
 
@@ -870,6 +868,7 @@ class Controls extends FlxActionSet
       case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
       case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
       case Control.PAUSE: return [START];
+      case Control.SCREENSHOT: return [];
       case Control.VOLUME_UP: return [];
       case Control.VOLUME_DOWN: return [];
       case Control.VOLUME_MUTE: return [];
@@ -878,7 +877,6 @@ class Controls extends FlxActionSet
       case Control.DEBUG_MENU: return [];
       case Control.DEBUG_CHART: return [];
       case Control.RESET: return [RIGHT_SHOULDER];
-      #if CAN_CHEAT, Control.CHEAT: return [X]; #end
       default:
         // Fallthrough.
     }
@@ -1236,6 +1234,8 @@ enum Control
   // CUTSCENE
   CUTSCENE_ADVANCE;
   CUTSCENE_SKIP;
+  // SCREENSHOT
+  SCREENSHOT;
   // VOLUME
   VOLUME_UP;
   VOLUME_DOWN;
@@ -1244,9 +1244,6 @@ enum Control
   DEBUG_MENU;
   DEBUG_CHART;
   DEBUG_STAGE;
-  #if CAN_CHEAT
-  CHEAT;
-  #end
 }
 
 enum
@@ -1289,13 +1286,12 @@ abstract Action(String) to String from String
   var VOLUME_UP = "volume_up";
   var VOLUME_DOWN = "volume_down";
   var VOLUME_MUTE = "volume_mute";
+  // SCREENSHOT
+  var SCREENSHOT = "screenshot";
   // DEBUG
   var DEBUG_MENU = "debug_menu";
   var DEBUG_CHART = "debug_chart";
   var DEBUG_STAGE = "debug_stage";
-  #if CAN_CHEAT
-  var CHEAT = "cheat";
-  #end
 }
 
 enum Device
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index be4fab254..1dbba5b54 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -123,6 +123,11 @@ typedef PlayStateParams =
    * and must be loaded externally.
    */
   ?overrideMusic:Bool,
+  /**
+   * The initial camera follow point.
+   * Used to persist the position of the `cameraFollowPosition` between levels.
+   */
+  ?cameraFollowPoint:FlxPoint,
 }
 
 /**
@@ -216,7 +221,7 @@ class PlayState extends MusicBeatSubState
    * The camera follow point from the last stage.
    * Used to persist the position of the `cameraFollowPosition` between levels.
    */
-  public var previousCameraFollowPoint:FlxSprite = null;
+  public var previousCameraFollowPoint:FlxPoint = null;
 
   /**
    * The current camera zoom level.
@@ -354,6 +359,11 @@ class PlayState extends MusicBeatSubState
    */
   var startingSong:Bool = false;
 
+  /**
+   * False if `FlxG.sound.music`
+   */
+  var musicPausedBySubState:Bool = false;
+
   /**
    * False until `create()` has completed.
    */
@@ -539,6 +549,7 @@ class PlayState extends MusicBeatSubState
     isMinimalMode = params.minimalMode ?? false;
     startTimestamp = params.startTimestamp ?? 0.0;
     overrideMusic = params.overrideMusic ?? false;
+    previousCameraFollowPoint = params.cameraFollowPoint;
 
     // Don't do anything else here! Wait until create() when we attach to the camera.
   }
@@ -697,7 +708,7 @@ class PlayState extends MusicBeatSubState
   function assertChartExists():Bool
   {
     // Returns null if the song failed to load or doesn't have the selected difficulty.
-    if (currentSong == null || currentChart == null)
+    if (currentSong == null || currentChart == null || currentChart.notes == null)
     {
       // We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic.
       criticalFailure = true;
@@ -716,6 +727,10 @@ class PlayState extends MusicBeatSubState
       {
         message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
       }
+      else if (currentChart.notes == null)
+      {
+        message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
+      }
 
       // Display a popup. This blocks the application until the user clicks OK.
       lime.app.Application.current.window.alert(message, 'Error loading PlayState');
@@ -1042,6 +1057,7 @@ class PlayState extends MusicBeatSubState
       // Pause the music.
       if (FlxG.sound.music != null)
       {
+        musicPausedBySubState = FlxG.sound.music.playing;
         FlxG.sound.music.pause();
         if (vocals != null) vocals.pause();
       }
@@ -1049,7 +1065,6 @@ class PlayState extends MusicBeatSubState
       // Pause the countdown.
       Countdown.pauseCountdown();
     }
-    else {}
 
     super.openSubState(subState);
   }
@@ -1069,7 +1084,10 @@ class PlayState extends MusicBeatSubState
       if (event.eventCanceled) return;
 
       // Resume
-      FlxG.sound.music.play(FlxG.sound.music.time);
+      if (musicPausedBySubState)
+      {
+        FlxG.sound.music.play(FlxG.sound.music.time);
+      }
 
       if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();
 
@@ -2613,38 +2631,25 @@ class PlayState extends MusicBeatSubState
           FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
             // no camFollow so it centers on horror tree
             var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
-            // Load and cache the song's charts.
-            // TODO: Do this in the loading state.
-            targetSong.cacheCharts(true);
-
-            LoadingState.loadAndSwitchState(() -> {
-              var nextPlayState:PlayState = new PlayState(
-                {
-                  targetSong: targetSong,
-                  targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-                  targetVariation: currentVariation,
-                });
-              nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
-              return nextPlayState;
-            });
+            LoadingState.loadPlayState(
+              {
+                targetSong: targetSong,
+                targetDifficulty: PlayStatePlaylist.campaignDifficulty,
+                targetVariation: currentVariation,
+                cameraFollowPoint: cameraFollowPoint.getPosition(),
+              });
           });
         }
         else
         {
           var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
-          // Load and cache the song's charts.
-          // TODO: Do this in the loading state.
-          targetSong.cacheCharts(true);
-          LoadingState.loadAndSwitchState(() -> {
-            var nextPlayState:PlayState = new PlayState(
-              {
-                targetSong: targetSong,
-                targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-                targetVariation: currentVariation,
-              });
-            nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
-            return nextPlayState;
-          });
+          LoadingState.loadPlayState(
+            {
+              targetSong: targetSong,
+              targetDifficulty: PlayStatePlaylist.campaignDifficulty,
+              targetVariation: currentVariation,
+              cameraFollowPoint: cameraFollowPoint.getPosition(),
+            });
         }
       }
     }
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index ce06950f2..b1468bd29 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -115,6 +115,9 @@ abstract Save(RawSaveData)
       };
   }
 
+  /**
+   * NOTE: Modifications will not be saved without calling `Save.flush()`!
+   */
   public var options(get, never):SaveDataOptions;
 
   function get_options():SaveDataOptions
@@ -122,6 +125,9 @@ abstract Save(RawSaveData)
     return this.options;
   }
 
+  /**
+   * NOTE: Modifications will not be saved without calling `Save.flush()`!
+   */
   public var modOptions(get, never):Map<String, Dynamic>;
 
   function get_modOptions():Map<String, Dynamic>
diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx
index 3d91e3a9a..d54fd5b8f 100644
--- a/source/funkin/ui/MusicBeatState.hx
+++ b/source/funkin/ui/MusicBeatState.hx
@@ -59,19 +59,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
     Conductor.stepHit.remove(this.stepHit);
   }
 
-  function handleControls():Void
-  {
-    var isHaxeUIFocused:Bool = haxe.ui.focus.FocusManager.instance?.focus != null;
-
-    if (!isHaxeUIFocused)
-    {
-      // Rebindable volume keys.
-      if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
-      else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
-      else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
-    }
-  }
-
   function handleFunctionControls():Void
   {
     // Emergency exit button.
@@ -85,8 +72,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
   {
     super.update(elapsed);
 
-    handleControls();
-
     dispatchEvent(new UpdateScriptEvent(elapsed));
   }
 
diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx
index 4671a9063..91822c557 100644
--- a/source/funkin/ui/MusicBeatSubState.hx
+++ b/source/funkin/ui/MusicBeatSubState.hx
@@ -54,11 +54,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
   {
     super.update(elapsed);
 
-    // Rebindable volume keys.
-    if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
-    else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
-    else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
-
     // Emergency exit button.
     if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
 
diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx
index 718130356..8caf105d3 100644
--- a/source/funkin/ui/debug/DebugMenuSubState.hx
+++ b/source/funkin/ui/debug/DebugMenuSubState.hx
@@ -9,6 +9,7 @@ import funkin.ui.debug.charting.ChartEditorState;
 import funkin.ui.MusicBeatSubState;
 import funkin.util.logging.CrashHandler;
 import flixel.addons.transition.FlxTransitionableState;
+import funkin.util.FileUtil;
 
 class DebugMenuSubState extends MusicBeatSubState
 {
@@ -121,16 +122,7 @@ class DebugMenuSubState extends MusicBeatSubState
   #if sys
   function openLogFolder()
   {
-    #if windows
-    Sys.command('explorer', [CrashHandler.LOG_FOLDER]);
-    #elseif mac
-    // mac could be fuckie with where the log folder is relative to the game file...
-    // if this comment is still here... it means it has NOT been verified on mac yet!
-    Sys.command('open', [CrashHandler.LOG_FOLDER]);
-    #end
-
-    // TODO: implement linux
-    // some shit with xdg-open :thinking: emoji...
+    FileUtil.openFolder(CrashHandler.LOG_FOLDER);
   }
   #end
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 6c64f952b..48a6e70c9 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -162,8 +162,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   public static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview');
   public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
   public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets');
-  public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
-  public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
+  public static final CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/note-data');
+  public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/event-data');
   public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay');
   public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
 
@@ -538,9 +538,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   // Tools Status
 
   /**
-   * The note kind to use for notes being placed in the chart. Defaults to `''`.
+   * The note kind to use for notes being placed in the chart. Defaults to `null`.
    */
-  var noteKindToPlace:String = '';
+  var noteKindToPlace:Null<String> = null;
 
   /**
    * The event type to use for events being placed in the chart. Defaults to `''`.
@@ -2458,11 +2458,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   override public function draw():Void
   {
-    if (selectionBoxStartPos != null)
-    {
-      trace('selectionBoxSprite: ${selectionBoxSprite.visible} ${selectionBoxSprite.exists} ${this.members.contains(selectionBoxSprite)}');
-    }
-
     super.draw();
   }
 
@@ -2968,7 +2963,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
     menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
     menubarItemToggleToolboxOffsets.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT, event.value);
-    menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
+    menubarItemToggleToolboxNoteData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT, event.value);
     menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
     menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value);
     menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
@@ -5286,6 +5281,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   {
     FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
 
+    FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
+    FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
+
     FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
     FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels);
 
diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
index d6c5beeac..88f73cfed 100644
--- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
@@ -59,6 +59,17 @@ class SelectItemsCommand implements ChartEditorCommand
       state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
     }
 
+    // If we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
+    if (this.events.length == 0 && this.notes.length >= 1)
+    {
+      var noteSelected = this.notes[0];
+
+      state.noteKindToPlace = noteSelected.kind;
+
+      // This code is here to parse note data that's not built as a struct for some reason.
+      state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT);
+    }
+
     state.noteDisplayDirty = true;
     state.notePreviewDirty = true;
   }
diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
index 35a00e562..5cc89e137 100644
--- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
@@ -56,6 +56,16 @@ class SetItemSelectionCommand implements ChartEditorCommand
       state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
     }
 
+    // IF we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
+    if (this.events.length == 0 && this.notes.length >= 1)
+    {
+      var noteSelected = this.notes[0];
+
+      state.noteKindToPlace = noteSelected.kind;
+
+      state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT);
+    }
+
     state.noteDisplayDirty = true;
   }
 
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
index 9e22ba833..f32cc2bfb 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
@@ -38,6 +38,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
 import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
 import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox;
 import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
+import funkin.ui.debug.charting.toolboxes.ChartEditorNoteDataToolbox;
 import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
 import haxe.ui.containers.Frame;
 import haxe.ui.containers.Grid;
@@ -79,17 +80,16 @@ class ChartEditorToolboxHandler
 
       switch (id)
       {
-        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
-          onShowToolboxNoteData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:
+          cast(toolbox, ChartEditorBaseToolbox).refresh();
         case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
-          // TODO: Fix this.
+          // TODO: Make these better.
           cast(toolbox, ChartEditorBaseToolbox).refresh();
         case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
           onShowToolboxPlaytestProperties(state, toolbox);
         case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
           cast(toolbox, ChartEditorBaseToolbox).refresh();
         case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
-          // TODO: Fix this.
           cast(toolbox, ChartEditorBaseToolbox).refresh();
         case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:
           cast(toolbox, ChartEditorBaseToolbox).refresh();
@@ -124,10 +124,6 @@ class ChartEditorToolboxHandler
 
       switch (id)
       {
-        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
-          onHideToolboxNoteData(state, toolbox);
-        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
-          onHideToolboxEventData(state, toolbox);
         case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
           onHideToolboxPlaytestProperties(state, toolbox);
         case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
@@ -196,7 +192,7 @@ class ChartEditorToolboxHandler
     var toolbox:Null<CollapsibleDialog> = null;
     switch (id)
     {
-      case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
+      case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:
         toolbox = buildToolboxNoteDataLayout(state);
       case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
         toolbox = buildToolboxEventDataLayout(state);
@@ -262,58 +258,13 @@ class ChartEditorToolboxHandler
 
   static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
   {
-    var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
+    var toolbox:ChartEditorBaseToolbox = ChartEditorNoteDataToolbox.build(state);
 
     if (toolbox == null) return null;
 
-    // Starting position.
-    toolbox.x = 75;
-    toolbox.y = 100;
-
-    toolbox.onDialogClosed = function(event:DialogEvent) {
-      state.menubarItemToggleToolboxNotes.selected = false;
-    }
-
-    var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
-    if (toolboxNotesNoteKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesNoteKind component.';
-    var toolboxNotesCustomKindLabel:Null<Label> = toolbox.findComponent('toolboxNotesCustomKindLabel', Label);
-    if (toolboxNotesCustomKindLabel == null)
-      throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKindLabel component.';
-    var toolboxNotesCustomKind:Null<TextField> = toolbox.findComponent('toolboxNotesCustomKind', TextField);
-    if (toolboxNotesCustomKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKind component.';
-
-    toolboxNotesNoteKind.onChange = function(event:UIEvent) {
-      var isCustom:Bool = (event.data.id == '~CUSTOM~');
-
-      if (isCustom)
-      {
-        toolboxNotesCustomKindLabel.hidden = false;
-        toolboxNotesCustomKind.hidden = false;
-
-        state.noteKindToPlace = toolboxNotesCustomKind.text;
-      }
-      else
-      {
-        toolboxNotesCustomKindLabel.hidden = true;
-        toolboxNotesCustomKind.hidden = true;
-
-        state.noteKindToPlace = event.data.id;
-      }
-    }
-
-    toolboxNotesCustomKind.onChange = function(event:UIEvent) {
-      state.noteKindToPlace = toolboxNotesCustomKind.text;
-    }
-
     return toolbox;
   }
 
-  static function onShowToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
-
-  static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
-
-  static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
-
   static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 
   static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
new file mode 100644
index 000000000..d4fc69fc1
--- /dev/null
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -0,0 +1,115 @@
+package funkin.ui.debug.charting.toolboxes;
+
+import haxe.ui.components.DropDown;
+import haxe.ui.components.TextField;
+import haxe.ui.events.UIEvent;
+import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+
+/**
+ * The toolbox which allows modifying information like Note Kind.
+ */
+@:access(funkin.ui.debug.charting.ChartEditorState)
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
+class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
+{
+  var toolboxNotesNoteKind:DropDown;
+  var toolboxNotesCustomKind:TextField;
+
+  var _initializing:Bool = true;
+
+  public function new(chartEditorState2:ChartEditorState)
+  {
+    super(chartEditorState2);
+
+    initialize();
+
+    this.onDialogClosed = onClose;
+
+    this._initializing = false;
+  }
+
+  function onClose(event:UIEvent)
+  {
+    chartEditorState.menubarItemToggleToolboxNoteData.selected = false;
+  }
+
+  function initialize():Void
+  {
+    toolboxNotesNoteKind.onChange = function(event:UIEvent) {
+      var noteKind:Null<String> = event?.data?.id ?? null;
+      if (noteKind == '') noteKind = null;
+
+      trace('ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Note kind changed: $noteKind');
+
+      // Edit the note data to place.
+      if (noteKind == '~CUSTOM~')
+      {
+        showCustom();
+        toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+      }
+      else
+      {
+        hideCustom();
+        chartEditorState.noteKindToPlace = noteKind;
+        toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+      }
+
+      if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
+      {
+        // Edit the note data of any selected notes.
+        for (note in chartEditorState.currentNoteSelection)
+        {
+          note.kind = chartEditorState.noteKindToPlace;
+        }
+        chartEditorState.saveDataDirty = true;
+        chartEditorState.noteDisplayDirty = true;
+        chartEditorState.notePreviewDirty = true;
+      }
+    };
+    var startingValueNoteKind = ChartEditorDropdowns.populateDropdownWithNoteKinds(toolboxNotesNoteKind, '');
+    toolboxNotesNoteKind.value = startingValueNoteKind;
+
+    toolboxNotesCustomKind.onChange = function(event:UIEvent) {
+      var customKind:Null<String> = event?.target?.text;
+      chartEditorState.noteKindToPlace = customKind;
+
+      if (chartEditorState.currentEventSelection.length > 0)
+      {
+        // Edit the note data of any selected notes.
+        for (note in chartEditorState.currentNoteSelection)
+        {
+          note.kind = chartEditorState.noteKindToPlace;
+        }
+        chartEditorState.saveDataDirty = true;
+        chartEditorState.noteDisplayDirty = true;
+        chartEditorState.notePreviewDirty = true;
+      }
+    };
+    toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+  }
+
+  public override function refresh():Void
+  {
+    super.refresh();
+
+    toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace);
+    toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+  }
+
+  function showCustom():Void
+  {
+    toolboxNotesCustomKindLabel.hidden = false;
+    toolboxNotesCustomKind.hidden = false;
+  }
+
+  function hideCustom():Void
+  {
+    toolboxNotesCustomKindLabel.hidden = true;
+    toolboxNotesCustomKind.hidden = true;
+  }
+
+  public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
+  {
+    return new ChartEditorNoteDataToolbox(chartEditorState);
+  }
+}
diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index 14c07440b..b26082f98 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -108,6 +108,55 @@ class ChartEditorDropdowns
     return returnValue;
   }
 
+  static final NOTE_KINDS:Map<String, String> = [
+    // Base
+    "" => "Default",
+    "~CUSTOM~" => "Custom",
+    // Weeks 1-7
+    "mom" => "Mom Sings (Week 5)",
+    "ugh" => "Ugh (Week 7)",
+    "hehPrettyGood" => "Heh, Pretty Good (Week 7)",
+    // Weekend 1
+    "weekend-1-lightcan" => "Light Can (2hot)",
+    "weekend-1-kickcan" => "Kick Can (2hot)",
+    "weekend-1-kneecan" => "Knee Can (2hot)",
+    "weekend-1-cockgun" => "Cock Gun (2hot)",
+    "weekend-1-firegun" => "Fire Gun (2hot)",
+    "weekend-1-punchlow" => "Punch Low (Blazin)",
+    "weekend-1-punchhigh" => "Punch High (Blazin)",
+    "weekend-1-punchlowblocked" => "Punch Low Blocked (Blazin)",
+    "weekend-1-punchhighblocked" => "Punch High Blocked (Blazin)",
+    "weekend-1-dodgelow" => "Dodge Low (Blazin)",
+    "weekend-1-blockhigh" => "Block High (Blazin)",
+    "weekend-1-fakeout" => "Fakeout (Blazin)",
+  ];
+
+  public static function populateDropdownWithNoteKinds(dropDown:DropDown, startingKindId:String):DropDownEntry
+  {
+    dropDown.dataSource.clear();
+
+    var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM');
+
+    for (noteKindId in NOTE_KINDS.keys())
+    {
+      var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default';
+
+      var value:DropDownEntry = {id: noteKindId, text: noteKind};
+      if (startingKindId == noteKindId) returnValue = value;
+
+      dropDown.dataSource.add(value);
+    }
+
+    return returnValue;
+  }
+
+  public static function lookupNoteKind(noteKindId:Null<String>):DropDownEntry
+  {
+    if (noteKindId == null) return lookupNoteKind('');
+    if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'};
+    return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'};
+  }
+
   /**
    * Populate a dropdown with a list of song variations.
    */
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index e7c615313..39cab8759 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1135,18 +1135,14 @@ class FreeplayState extends MusicBeatSubState
     FlxG.sound.play(Paths.sound('confirmMenu'));
     dj.confirm();
 
-    // Load and cache the song's charts.
-    // TODO: Do this in the loading state.
-    targetSong.cacheCharts(true);
-
     new FlxTimer().start(1, function(tmr:FlxTimer) {
       Paths.setCurrentLevel(cap.songData.levelId);
-      LoadingState.loadAndSwitchState(() -> new PlayState(
+      LoadingState.loadPlayState(
         {
           targetSong: targetSong,
           targetDifficulty: targetDifficulty,
           targetVariation: targetVariation,
-        }), true);
+        }, true);
     });
   }
 
diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx
index ea6940c4a..c93ad41a6 100644
--- a/source/funkin/ui/story/Level.hx
+++ b/source/funkin/ui/story/Level.hx
@@ -187,6 +187,10 @@ class Level implements IRegistryEntry<LevelData>
 
     if (_data.props.length == 0) return props;
 
+    var hiddenProps:Array<LevelProp> = props.splice(_data.props.length - 1, props.length - 1);
+    for (hiddenProp in hiddenProps)
+      hiddenProp.visible = false;
+
     for (propIndex in 0..._data.props.length)
     {
       var propData = _data.props[propIndex];
@@ -198,6 +202,7 @@ class Level implements IRegistryEntry<LevelData>
       {
         existingProp.propData = propData;
         existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
+        existingProp.visible = true;
       }
       else
       {
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 1905a7c57..bd7a05f91 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -554,22 +554,15 @@ class StoryMenuState extends MusicBeatState
     PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
     PlayStatePlaylist.campaignDifficulty = currentDifficultyId;
 
-    if (targetSong != null)
-    {
-      // Load and cache the song's charts.
-      // TODO: Do this in the loading state.
-      targetSong.cacheCharts(true);
-    }
-
     new FlxTimer().start(1, function(tmr:FlxTimer) {
       FlxTransitionableState.skipNextTransIn = false;
       FlxTransitionableState.skipNextTransOut = false;
 
-      LoadingState.loadAndSwitchState(() -> new PlayState(
+      LoadingState.loadPlayState(
         {
           targetSong: targetSong,
           targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-        }), true);
+        }, true);
     });
   }
 
@@ -597,7 +590,9 @@ class StoryMenuState extends MusicBeatState
         {
           // Both the previous and current level were simple backgrounds.
           // Fade between colors directly, rather than fading one background out and another in.
-          FlxTween.color(levelBackground, 0.4, previousColor, currentColor);
+          // cancels potential tween in progress, and tweens from there
+          FlxTween.cancelTweensOf(levelBackground);
+          FlxTween.color(levelBackground, 0.9, levelBackground.color, currentColor, {ease: FlxEase.quartOut});
         }
         else
         {
@@ -637,10 +632,10 @@ class StoryMenuState extends MusicBeatState
 
   function updateProps():Void
   {
-    for (prop in currentLevel.buildProps(levelProps.members))
+    for (ind => prop in currentLevel.buildProps(levelProps.members))
     {
       prop.zIndex = 1000;
-      levelProps.add(prop);
+      if (levelProps.members[ind] != prop) levelProps.replace(levelProps.members[ind], prop) ?? levelProps.add(prop);
     }
 
     refresh();
diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index 63dcb8f68..86f443d1d 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -9,7 +9,6 @@ import funkin.graphics.shaders.ScreenWipeShader;
 import funkin.play.PlayState;
 import funkin.play.PlayStatePlaylist;
 import funkin.play.song.Song.SongDifficulty;
-import funkin.ui.mainmenu.MainMenuState;
 import funkin.ui.MusicBeatState;
 import haxe.io.Path;
 import funkin.graphics.FunkinSprite;
@@ -27,17 +26,19 @@ class LoadingState extends MusicBeatState
   inline static var MIN_TIME = 1.0;
 
   var target:NextState;
-  var stopMusic = false;
+  var playParams:Null<PlayStateParams>;
+  var stopMusic:Bool = false;
   var callbacks:MultiCallback;
-  var danceLeft = false;
+  var danceLeft:Bool = false;
 
   var loadBar:FlxSprite;
   var funkay:FlxSprite;
 
-  function new(target:NextState, stopMusic:Bool)
+  function new(target:NextState, stopMusic:Bool, playParams:Null<PlayStateParams> = null)
   {
     super();
     this.target = target;
+    this.playParams = playParams;
     this.stopMusic = stopMusic;
   }
 
@@ -62,10 +63,18 @@ class LoadingState extends MusicBeatState
       callbacks = new MultiCallback(onLoad);
       var introComplete = callbacks.add('introComplete');
 
-      if (Std.isOfType(target, PlayState))
+      if (playParams != null)
       {
-        var targetPlayState:PlayState = cast target;
-        var targetChart:SongDifficulty = targetPlayState.currentChart;
+        // Load and cache the song's charts.
+        if (playParams.targetSong != null)
+        {
+          playParams.targetSong.cacheCharts(true);
+        }
+
+        // Preload the song for the play state.
+        var difficulty:String = playParams.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY;
+        var variation:String = playParams.targetVariation ?? Constants.DEFAULT_VARIATION;
+        var targetChart:SongDifficulty = playParams.targetSong?.getDifficulty(difficulty, variation);
         var instPath:String = Paths.inst(targetChart.song.id);
         var voicesPaths:Array<String> = targetChart.buildVoiceList();
 
@@ -172,25 +181,36 @@ class LoadingState extends MusicBeatState
     return Paths.inst(PlayState.instance.currentSong.id);
   }
 
-  inline static public function loadAndSwitchState(nextState:NextState, shouldStopMusic = false):Void
-  {
-    FlxG.switchState(getNextState(nextState, shouldStopMusic));
-  }
-
-  static function getNextState(nextState:NextState, shouldStopMusic = false):NextState
+  /**
+   * Starts the transition to a new `PlayState` to start a new song.
+   * First switches to the `LoadingState` if assets need to be loaded.
+   * @param params The parameters for the next `PlayState`.
+   * @param shouldStopMusic Whether to stop the current music while loading.
+   */
+  public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false):Void
   {
     Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
+    var playStateCtor:NextState = () -> new PlayState(params);
 
     #if NO_PRELOAD_ALL
-    // var loaded = isSoundLoaded(getSongPath())
-    //  && (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath()))
-    //  && isLibraryLoaded('shared');
-    //
-    if (true) return () -> new LoadingState(nextState, shouldStopMusic);
-    #end
-    if (shouldStopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
+    // Switch to loading state while we load assets (default on HTML5 target).
+    var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params);
+    FlxG.switchState(loadStateCtor);
+    #else
+    // All assets preloaded, switch directly to play state (defualt on other targets).
+    if (shouldStopMusic && FlxG.sound.music != null)
+    {
+      FlxG.sound.music.stop();
+    }
 
-    return nextState;
+    // Load and cache the song's charts.
+    if (params?.targetSong != null)
+    {
+      params.targetSong.cacheCharts(true);
+    }
+
+    FlxG.switchState(playStateCtor);
+    #end
   }
 
   #if NO_PRELOAD_ALL
diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx
index 612737680..7a7b1422c 100644
--- a/source/funkin/util/FileUtil.hx
+++ b/source/funkin/util/FileUtil.hx
@@ -20,6 +20,7 @@ class FileUtil
 {
   public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc");
   public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
+  public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");
 
   public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
     {
@@ -31,6 +32,11 @@ class FileUtil
       extension: 'zip',
       label: 'ZIP Archive',
     };
+  public static final FILE_EXTENSION_INFO_PNG:FileDialogExtensionInfo =
+    {
+      extension: 'png',
+      label: 'PNG Image',
+    };
 
   /**
    * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
@@ -639,6 +645,23 @@ class FileUtil
     };
   }
 
+  public static function openFolder(pathFolder:String)
+  {
+    #if windows
+    Sys.command('explorer', [pathFolder]);
+    #elseif mac
+    // mac could be fuckie with where the log folder is relative to the game file...
+    // if this comment is still here... it means it has NOT been verified on mac yet!
+    //
+    // FileUtil.hx note: this was originally used to open the logs specifically!
+    // thats why the above comment is there!
+    Sys.command('open', [pathFolder]);
+    #end
+
+    // TODO: implement linux
+    // some shit with xdg-open :thinking: emoji...
+  }
+
   static function convertTypeFilter(typeFilter:Array<FileFilter>):String
   {
     var filter:String = null;
diff --git a/source/funkin/util/MathUtil.hx b/source/funkin/util/MathUtil.hx
index 3cb6621a7..5fed1d3e1 100644
--- a/source/funkin/util/MathUtil.hx
+++ b/source/funkin/util/MathUtil.hx
@@ -5,6 +5,12 @@ package funkin.util;
  */
 class MathUtil
 {
+  /**
+   * Euler's constant and the base of the natural logarithm.
+   * Math.E is not a constant in Haxe, so we'll just define it ourselves.
+   */
+  public static final E:Float = 2.71828182845904523536;
+
   /**
    * Perform linear interpolation between the base and the target, based on the current framerate.
    */
@@ -24,8 +30,44 @@ class MathUtil
    * @param value The value to get the logarithm of.
    * @return `log_base(value)`
    */
-  public static function logBase(base:Float, value:Float):Float
+  public static function logBase(base:Float, value:Float)
   {
     return Math.log(value) / Math.log(base);
   }
+
+  /**
+   * @returns `2^x`
+   */
+  public static function exp2(x:Float)
+  {
+    return Math.pow(2, x);
+  }
+
+  /**
+   * Linearly interpolate between two values.
+   * @param base The starting value, when `progress <= 0`.
+   * @param target The ending value, when `progress >= 1`.
+   * @param progress Value used to interpolate between `base` and `target`.
+   */
+  public static function lerp(base:Float, target:Float, progress:Float)
+  {
+    return base + progress * (target - base);
+  }
+
+  /**
+   * Perform a framerate-independent linear interpolation between the base value and the target.
+   * @param current The current value.
+   * @param target The target value.
+   * @param elapsed The time elapsed since the last frame.
+   * @param duration The total duration of the interpolation. Nominal duration until remaining distance is less than `precision`.
+   * @param precision The target precision of the interpolation. Defaults to 1% of distance remaining.
+   * @see https://twitter.com/FreyaHolmer/status/1757918211679650262
+   */
+  public static function smoothLerp(current:Float, target:Float, elapsed:Float, duration:Float, precision:Float = 1 / 100):Float
+  {
+    // var halfLife:Float = -duration / logBase(2, precision);
+    // lerp(current, target, 1 - exp2(-elapsed / halfLife));
+
+    return lerp(current, target, 1 - Math.pow(precision, elapsed / duration));
+  }
 }
diff --git a/source/funkin/util/plugins/ScreenshotPlugin.hx b/source/funkin/util/plugins/ScreenshotPlugin.hx
new file mode 100644
index 000000000..16d0c7244
--- /dev/null
+++ b/source/funkin/util/plugins/ScreenshotPlugin.hx
@@ -0,0 +1,320 @@
+package funkin.util.plugins;
+
+import flixel.FlxBasic;
+import flixel.FlxCamera;
+import flixel.FlxG;
+import flixel.FlxState;
+import flixel.graphics.FlxGraphic;
+import flixel.input.keyboard.FlxKey;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxColor;
+import flixel.util.FlxSignal;
+import flixel.util.FlxTimer;
+import funkin.graphics.FunkinSprite;
+import funkin.input.Cursor;
+import openfl.display.Bitmap;
+import openfl.display.Sprite;
+import openfl.display.BitmapData;
+import openfl.display.PNGEncoderOptions;
+import openfl.geom.Matrix;
+import openfl.geom.Rectangle;
+import openfl.utils.ByteArray;
+import openfl.events.MouseEvent;
+
+typedef ScreenshotPluginParams =
+{
+  hotkeys:Array<FlxKey>,
+  ?region:Rectangle,
+  shouldHideMouse:Bool,
+  flashColor:Null<FlxColor>,
+  fancyPreview:Bool,
+};
+
+/**
+ * What if `flixel.addons.plugin.screengrab.FlxScreenGrab` but it's better?
+ * TODO: Contribute this upstream.
+ */
+class ScreenshotPlugin extends FlxBasic
+{
+  public static final SCREENSHOT_FOLDER = 'screenshots';
+
+  var _hotkeys:Array<FlxKey>;
+
+  var _region:Null<Rectangle>;
+
+  var _shouldHideMouse:Bool;
+
+  var _flashColor:Null<FlxColor>;
+
+  var _fancyPreview:Bool;
+
+  /**
+   * A signal fired before the screenshot is taken.
+   */
+  public var onPreScreenshot(default, null):FlxTypedSignal<Void->Void>;
+
+  /**
+   * A signal fired after the screenshot is taken.
+   * @param bitmap The bitmap that was captured.
+   */
+  public var onPostScreenshot(default, null):FlxTypedSignal<Bitmap->Void>;
+
+  public function new(params:ScreenshotPluginParams)
+  {
+    super();
+
+    _hotkeys = params.hotkeys;
+    _region = params.region ?? null;
+    _shouldHideMouse = params.shouldHideMouse;
+    _flashColor = params.flashColor;
+    _fancyPreview = params.fancyPreview;
+
+    onPreScreenshot = new FlxTypedSignal<Void->Void>();
+    onPostScreenshot = new FlxTypedSignal<Bitmap->Void>();
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (hasPressedScreenshot())
+    {
+      capture();
+    }
+  }
+
+  /**
+   * Initialize the screenshot plugin.
+   */
+  public static function initialize():Void
+  {
+    FlxG.plugins.addPlugin(new ScreenshotPlugin(
+      {
+        flashColor: Preferences.flashingLights ? FlxColor.WHITE : null, // Was originally a black flash.
+
+        // TODO: Add a way to configure screenshots from the options menu.
+        hotkeys: [FlxKey.F3],
+        shouldHideMouse: false,
+        fancyPreview: true,
+      }));
+  }
+
+  public function hasPressedScreenshot():Bool
+  {
+    return PlayerSettings.player1.controls.SCREENSHOT;
+  }
+
+  public function updatePreferences():Void
+  {
+    _flashColor = Preferences.flashingLights ? FlxColor.WHITE : null;
+  }
+
+  /**
+   * Defines the region of the screen that should be captured.
+   * You don't need to call this method if you want to capture the entire screen, that's the default behavior.
+   */
+  public function defineCaptureRegion(x:Int, y:Int, width:Int, height:Int):Void
+  {
+    _region = new Rectangle(x, y, width, height);
+  }
+
+  /**
+   * Capture the game screen as a bitmap.
+   */
+  public function capture():Void
+  {
+    onPreScreenshot.dispatch();
+
+    var captureRegion = _region != null ? _region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight);
+
+    var wasMouseHidden = false;
+    if (_shouldHideMouse && FlxG.mouse.visible)
+    {
+      wasMouseHidden = true;
+      Cursor.hide();
+    }
+
+    // The actual work.
+    // var bitmap = new Bitmap(new BitmapData(Math.floor(captureRegion.width), Math.floor(captureRegion.height), true, 0x00000000)); // Create a transparent empty bitmap.
+    // var drawMatrix = new Matrix(1, 0, 0, 1, -captureRegion.x, -captureRegion.y); // Modifying this will scale or skew the bitmap.
+    // bitmap.bitmapData.draw(FlxG.stage, drawMatrix);
+    var bitmap = new Bitmap(BitmapData.fromImage(FlxG.stage.window.readPixels()));
+
+    if (wasMouseHidden)
+    {
+      Cursor.show();
+    }
+
+    // Save the bitmap to a file.
+    saveScreenshot(bitmap);
+
+    // Show some feedback.
+    showCaptureFeedback();
+    if (_fancyPreview)
+    {
+      showFancyPreview(bitmap);
+    }
+
+    onPostScreenshot.dispatch(bitmap);
+  }
+
+  final CAMERA_FLASH_DURATION = 0.25;
+
+  /**
+   * Visual (and audio?) feedback when a screenshot is taken.
+   */
+  function showCaptureFeedback():Void
+  {
+    var flashBitmap = new Bitmap(new BitmapData(Std.int(FlxG.stage.width), Std.int(FlxG.stage.height), false, 0xFFFFFFFF));
+    var flashSpr = new Sprite();
+    flashSpr.addChild(flashBitmap);
+    FlxG.stage.addChild(flashSpr);
+    FlxTween.tween(flashSpr, {alpha: 0}, 0.15, {ease: FlxEase.quadOut, onComplete: _ -> FlxG.stage.removeChild(flashSpr)});
+  }
+
+  static final PREVIEW_INITIAL_DELAY = 0.25; // How long before the preview starts fading in.
+  static final PREVIEW_FADE_IN_DURATION = 0.3; // How long the preview takes to fade in.
+  static final PREVIEW_FADE_OUT_DELAY = 1.25; // How long the preview stays on screen.
+  static final PREVIEW_FADE_OUT_DURATION = 0.3; // How long the preview takes to fade out.
+
+  function showFancyPreview(bitmap:Bitmap):Void
+  {
+    // ermmm stealing this??
+    var wasMouseHidden = false;
+    if (!FlxG.mouse.visible)
+    {
+      wasMouseHidden = true;
+      Cursor.show();
+    }
+
+    // so that it doesnt change the alpha when tweening in/out
+    var changingAlpha:Bool = false;
+
+    // fuck it, cursed locally scoped functions, purely because im lazy
+    // (and so we can check changingAlpha, which is locally scoped.... because I'm lazy...)
+    var onHover = function(e:MouseEvent) {
+      if (!changingAlpha) e.target.alpha = 0.6;
+    };
+
+    var onHoverOut = function(e:MouseEvent) {
+      if (!changingAlpha) e.target.alpha = 1;
+    }
+
+    var scale:Float = 0.25;
+    var w:Int = Std.int(bitmap.bitmapData.width * scale);
+    var h:Int = Std.int(bitmap.bitmapData.height * scale);
+
+    var preview:BitmapData = new BitmapData(w, h, true);
+    var matrix:openfl.geom.Matrix = new openfl.geom.Matrix();
+    matrix.scale(scale, scale);
+    preview.draw(bitmap.bitmapData, matrix);
+
+    // used for movement + button stuff
+    var previewSprite = new Sprite();
+
+    previewSprite.buttonMode = true;
+    previewSprite.addEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder);
+    previewSprite.addEventListener(MouseEvent.MOUSE_OVER, onHover);
+    previewSprite.addEventListener(MouseEvent.MOUSE_OUT, onHoverOut);
+
+    FlxG.stage.addChild(previewSprite);
+
+    previewSprite.alpha = 0.0;
+    previewSprite.y -= 10;
+
+    var previewBitmap = new Bitmap(preview);
+    previewSprite.addChild(previewBitmap);
+
+    // Wait to fade in.
+    new FlxTimer().start(PREVIEW_INITIAL_DELAY, function(_) {
+      // Fade in.
+      changingAlpha = true;
+      FlxTween.tween(previewSprite, {alpha: 1.0, y: 0}, PREVIEW_FADE_IN_DURATION,
+        {
+          ease: FlxEase.quartOut,
+          onComplete: function(_) {
+            changingAlpha = false;
+            // Wait to fade out.
+            new FlxTimer().start(PREVIEW_FADE_OUT_DELAY, function(_) {
+              changingAlpha = true;
+              // Fade out.
+              FlxTween.tween(previewSprite, {alpha: 0.0, y: 10}, PREVIEW_FADE_OUT_DURATION,
+                {
+                  ease: FlxEase.quartInOut,
+                  onComplete: function(_) {
+                    if (wasMouseHidden)
+                    {
+                      Cursor.hide();
+                    }
+
+                    previewSprite.removeEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder);
+                    previewSprite.removeEventListener(MouseEvent.MOUSE_OVER, onHover);
+                    previewSprite.removeEventListener(MouseEvent.MOUSE_OUT, onHoverOut);
+
+                    FlxG.stage.removeChild(previewSprite);
+                  }
+                });
+            });
+          }
+        });
+    });
+  }
+
+  function openScreenshotsFolder(e:MouseEvent):Void
+  {
+    FileUtil.openFolder(SCREENSHOT_FOLDER);
+  }
+
+  static function getCurrentState():FlxState
+  {
+    var state = FlxG.state;
+    while (state.subState != null)
+    {
+      state = state.subState;
+    }
+    return state;
+  }
+
+  static function getScreenshotPath():String
+  {
+    return '$SCREENSHOT_FOLDER/screenshot-${DateUtil.generateTimestamp()}.png';
+  }
+
+  static function makeScreenshotPath():Void
+  {
+    FileUtil.createDirIfNotExists(SCREENSHOT_FOLDER);
+  }
+
+  /**
+   * Convert a Bitmap to a PNG ByteArray to save to a file.
+   */
+  static function encodePNG(bitmap:Bitmap):ByteArray
+  {
+    return bitmap.bitmapData.encode(bitmap.bitmapData.rect, new PNGEncoderOptions());
+  }
+
+  /**
+   * Save the generated bitmap to a file.
+   * @param bitmap The bitmap to save.
+   */
+  static function saveScreenshot(bitmap:Bitmap)
+  {
+    makeScreenshotPath();
+    var targetPath:String = getScreenshotPath();
+
+    var pngData = encodePNG(bitmap);
+
+    if (pngData == null)
+    {
+      trace('[WARN] Failed to encode PNG data.');
+      return;
+    }
+    else
+    {
+      trace('Saving screenshot to: ' + targetPath);
+      // TODO: Make this work on browser.
+      FileUtil.writeBytesToPath(targetPath, pngData);
+    }
+  }
+}
diff --git a/source/funkin/util/plugins/VolumePlugin.hx b/source/funkin/util/plugins/VolumePlugin.hx
new file mode 100644
index 000000000..5dbe60abf
--- /dev/null
+++ b/source/funkin/util/plugins/VolumePlugin.hx
@@ -0,0 +1,34 @@
+package funkin.util.plugins;
+
+import flixel.FlxBasic;
+
+/**
+ * Handles volume control in a way that is compatible with alternate control schemes.
+ */
+class VolumePlugin extends FlxBasic
+{
+  public function new()
+  {
+    super();
+  }
+
+  public static function initialize()
+  {
+    FlxG.plugins.addPlugin(new VolumePlugin());
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    var isHaxeUIFocused:Bool = haxe.ui.focus.FocusManager.instance?.focus != null;
+
+    if (!isHaxeUIFocused)
+    {
+      // Rebindable volume keys.
+      if (PlayerSettings.player1.controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
+      else if (PlayerSettings.player1.controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
+      else if (PlayerSettings.player1.controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
+    }
+  }
+}