diff --git a/Project.xml b/Project.xml
index 74da3b749..f5d506688 100644
--- a/Project.xml
+++ b/Project.xml
@@ -111,7 +111,7 @@
 	<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
 	<haxelib name="thx.semver" /> <!-- Version string handling -->
 
-	<haxelib name="hmm" if="debug" /> <!-- Read library version data at compile time -->
+	<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
 	<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
 
 	<!--Disable the Flixel core focus lost screen-->
@@ -131,8 +131,8 @@
 	<haxedef name="message.reporting" value="pretty" />
 
 	<!-- _________________________________ Custom _______________________________ -->
-	<!-- Disable trace() calls in release builds to bump up performance. -->
-	<haxeflag name="--no-traces" unless="debug" />
+	<!-- Disable trace() calls in release builds to bump up performance.
+		<haxeflag name="- -no-traces" unless="debug" />-->
 	<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
 	<haxeflag name="-dce no" />
 	<!-- Ensure all Funkin' classes are available at runtime. -->
diff --git a/assets b/assets
index 7e31e86db..f1e42601b 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 7e31e86dbeec3df5076895dedc62a45cc14d66e1
+Subproject commit f1e42601b6ea2026c6e2f4627c5738bfb8b7b524
diff --git a/hmm.json b/hmm.json
index 8d05a7a2e..67ba4d18d 100644
--- a/hmm.json
+++ b/hmm.json
@@ -107,7 +107,7 @@
       "name": "lime",
       "type": "git",
       "dir": null,
-      "ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
+      "ref": "fff39ba6fc64969cd51987ef7491d9345043dc5d",
       "url": "https://github.com/FunkinCrew/lime"
     },
     {
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 708881429..07ea38741 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -93,7 +93,7 @@ class SongMetadata implements ICloneable<SongMetadata>
     result.version = this.version;
     result.timeFormat = this.timeFormat;
     result.divisions = this.divisions;
-    result.offsets = this.offsets.clone();
+    result.offsets = this.offsets != null ? this.offsets.clone() : new SongOffsets(); // if no song offsets found (aka null), so just create new ones
     result.timeChanges = this.timeChanges.deepClone();
     result.looped = this.looped;
     result.playData = this.playData.clone();
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 309676884..275106f3a 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -273,7 +273,7 @@ class SongDataUtils
   }
 
   /**
-   * Filter a list of notes to only include notes whose data is within the given range.
+   * Filter a list of notes to only include notes whose data is within the given range, inclusive.
    */
   public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
   {
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 995797dd1..9f98d3b04 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2270,8 +2270,10 @@ class PlayState extends MusicBeatSubState
     vocals.playerVolume = 1;
 
     // Calculate the input latency (do this as late as possible).
-    var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - input.timestamp) / 1000.0 / 1000.0;
-    trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
+    // trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
+    var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
+    var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
+    // trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
 
     // Get the offset and compensate for input latency.
     // Round inward (trim remainder) for consistency.
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 9e5de6143..cde068f42 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -176,6 +176,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
         difficulty.generatedBy = metadata.generatedBy;
         difficulty.offsets = metadata.offsets;
 
+        difficulty.difficultyRating = metadata.playData.ratings.get(diffId) ?? 0;
+        difficulty.album = metadata.playData.album;
+
         difficulty.stage = metadata.playData.stage;
         difficulty.noteStyle = metadata.playData.noteStyle;
 
@@ -405,6 +408,9 @@ class SongDifficulty
 
   public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
 
+  public var difficultyRating:Int = 0;
+  public var album:Null<String> = null;
+
   public function new(song:Song, diffId:String, variation:String)
   {
     this.song = song;
diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx
index fea09de54..186d87c2a 100644
--- a/source/funkin/ui/AtlasText.hx
+++ b/source/funkin/ui/AtlasText.hx
@@ -274,4 +274,5 @@ enum abstract AtlasFont(String) from String to String
 {
   var DEFAULT = "default";
   var BOLD = "bold";
+  var FREEPLAY_CLEAR = "freeplay-clear";
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 33bba450f..05ac983ca 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -63,6 +63,7 @@ import funkin.ui.debug.charting.commands.AddNotesCommand;
 import funkin.ui.debug.charting.commands.ChartEditorCommand;
 import funkin.ui.debug.charting.commands.ChartEditorCommand;
 import funkin.ui.debug.charting.commands.CutItemsCommand;
+import funkin.ui.debug.charting.commands.CopyItemsCommand;
 import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
 import funkin.ui.debug.charting.commands.DeselectItemsCommand;
 import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
@@ -106,6 +107,7 @@ import haxe.ui.components.Slider;
 import haxe.ui.components.TextField;
 import haxe.ui.containers.dialogs.CollapsibleDialog;
 import haxe.ui.containers.Frame;
+import haxe.ui.containers.Box;
 import haxe.ui.containers.menus.Menu;
 import haxe.ui.containers.menus.MenuBar;
 import haxe.ui.containers.menus.MenuItem;
@@ -193,10 +195,40 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   public static final PLAYBAR_HEIGHT:Int = 48;
 
+  /**
+   * The height of the note selection buttons above the grid.
+   */
+  public static final NOTE_SELECT_BUTTON_HEIGHT:Int = 24;
+
   /**
    * The amount of padding between the menu bar and the chart grid when fully scrolled up.
    */
-  public static final GRID_TOP_PAD:Int = 8;
+  public static final GRID_TOP_PAD:Int = NOTE_SELECT_BUTTON_HEIGHT + 12;
+
+  /**
+   * The initial vertical position of the chart grid.
+   */
+  public static final GRID_INITIAL_Y_POS:Int = MENU_BAR_HEIGHT + GRID_TOP_PAD;
+
+  /**
+   * The X position of the note preview area.
+   */
+  public static final NOTE_PREVIEW_X_POS:Int = 350;
+
+  /**
+   * The Y position of the note preview area.
+   */
+  public static final NOTE_PREVIEW_Y_POS:Int = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 4;
+
+  /**
+   * The X position of the note grid.
+   */
+  public static var GRID_X_POS(get, never):Float;
+
+  static function get_GRID_X_POS():Float
+  {
+    return FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE;
+  }
 
   // Colors
   // Background color tint.
@@ -341,21 +373,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     {
       if (isViewDownscroll)
       {
-        gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
+        gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
         measureTicks.y = gridTiledSprite.y;
       }
       else
       {
-        gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
+        gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
         measureTicks.y = gridTiledSprite.y;
 
         if (audioVisGroup != null && audioVisGroup.playerVis != null)
         {
-          audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, MENU_BAR_HEIGHT);
+          audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS);
         }
         if (audioVisGroup != null && audioVisGroup.opponentVis != null)
         {
-          audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, MENU_BAR_HEIGHT);
+          audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS);
         }
       }
     }
@@ -427,7 +459,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     this.playheadPositionInPixels = value;
 
     // Move the playhead sprite to the correct position.
-    gridPlayhead.y = this.playheadPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
+    gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS;
 
     return this.playheadPositionInPixels;
   }
@@ -1684,6 +1716,24 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var playbarEnd:Button;
 
+  /**
+   * The button above the grid that selects all notes on the opponent's side.
+   * Constructed manually and added to the layout so we can control its position.
+   */
+  var buttonSelectOpponent:Button;
+
+  /**
+   * The button above the grid that selects all notes on the player's side.
+   * Constructed manually and added to the layout so we can control its position.
+   */
+  var buttonSelectPlayer:Button;
+
+  /**
+   * The button above the grid that selects all song events.
+   * Constructed manually and added to the layout so we can control its position.
+   */
+  var buttonSelectEvent:Button;
+
   /**
    * RENDER OBJECTS
    */
@@ -2119,8 +2169,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     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.
+    gridTiledSprite.x = GRID_X_POS; // Center the grid.
+    gridTiledSprite.y = GRID_INITIAL_Y_POS; // Push down to account for the menu bar.
     add(gridTiledSprite);
     gridTiledSprite.zIndex = 10;
 
@@ -2151,9 +2201,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     add(gridPlayhead);
     gridPlayhead.zIndex = 30;
 
-    var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH);
-    var playheadBaseYPos:Float = MENU_BAR_HEIGHT + GRID_TOP_PAD;
-    gridPlayhead.setPosition(gridTiledSprite.x, playheadBaseYPos);
+    var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
+    var playheadBaseYPos:Float = GRID_INITIAL_Y_POS;
+    gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos);
     var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
     playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH;
     playheadSprite.y = 0;
@@ -2195,10 +2245,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
   function buildNotePreview():Void
   {
-    var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - PLAYBAR_HEIGHT - GRID_TOP_PAD - GRID_TOP_PAD;
-    notePreview = new ChartEditorNotePreview(height);
-    notePreview.x = 320;
-    notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
+    var playbarHeightWithPad = PLAYBAR_HEIGHT + 10;
+    var notePreviewHeight:Int = FlxG.height - NOTE_PREVIEW_Y_POS - playbarHeightWithPad;
+    notePreview = new ChartEditorNotePreview(notePreviewHeight);
+    notePreview.x = NOTE_PREVIEW_X_POS;
+    notePreview.y = NOTE_PREVIEW_Y_POS;
     add(notePreview);
 
     if (notePreviewViewport == null) throw 'ERROR: Tried to build note preview, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().';
@@ -2384,6 +2435,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     add(playbarHeadLayout);
 
+    // Little text that shows up when you copy something.
     txtCopyNotif = new FlxText(0, 0, 0, '', 24);
     txtCopyNotif.setBorderStyle(OUTLINE, 0xFF074809, 1);
     txtCopyNotif.color = 0xFF52FF77;
@@ -2408,6 +2460,77 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         this.openCharacterDropdown(CharacterType.BF, true);
       }
     });
+
+    buttonSelectOpponent = new Button();
+    buttonSelectOpponent.allowFocus = false;
+    buttonSelectOpponent.text = "Opponent"; // Default text.
+    buttonSelectOpponent.x = GRID_X_POS;
+    buttonSelectOpponent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 8;
+    buttonSelectOpponent.width = GRID_SIZE * 4;
+    buttonSelectOpponent.height = NOTE_SELECT_BUTTON_HEIGHT;
+    buttonSelectOpponent.tooltip = "Click to set selection to all notes on this side.\nShift-click to add all notes on this side to selection.";
+    buttonSelectOpponent.zIndex = 110;
+    add(buttonSelectOpponent);
+
+    buttonSelectOpponent.onClick = (_) -> {
+      var notesToSelect:Array<SongNoteData> = currentSongChartNoteData;
+      notesToSelect = SongDataUtils.getNotesInDataRange(notesToSelect, STRUMLINE_SIZE, STRUMLINE_SIZE * 2 - 1);
+      if (FlxG.keys.pressed.SHIFT)
+      {
+        performCommand(new SelectItemsCommand(notesToSelect, []));
+      }
+      else
+      {
+        performCommand(new SetItemSelectionCommand(notesToSelect, []));
+      }
+    }
+
+    buttonSelectPlayer = new Button();
+    buttonSelectPlayer.allowFocus = false;
+    buttonSelectPlayer.text = "Player"; // Default text.
+    buttonSelectPlayer.x = buttonSelectOpponent.x + buttonSelectOpponent.width;
+    buttonSelectPlayer.y = buttonSelectOpponent.y;
+    buttonSelectPlayer.width = GRID_SIZE * 4;
+    buttonSelectPlayer.height = NOTE_SELECT_BUTTON_HEIGHT;
+    buttonSelectPlayer.tooltip = "Click to set selection to all notes on this side.\nShift-click to add all notes on this side to selection.";
+    buttonSelectPlayer.zIndex = 110;
+    add(buttonSelectPlayer);
+
+    buttonSelectPlayer.onClick = (_) -> {
+      var notesToSelect:Array<SongNoteData> = currentSongChartNoteData;
+      notesToSelect = SongDataUtils.getNotesInDataRange(notesToSelect, 0, STRUMLINE_SIZE - 1);
+      if (FlxG.keys.pressed.SHIFT)
+      {
+        performCommand(new SelectItemsCommand(notesToSelect, []));
+      }
+      else
+      {
+        performCommand(new SetItemSelectionCommand(notesToSelect, []));
+      }
+    }
+
+    buttonSelectEvent = new Button();
+    buttonSelectEvent.allowFocus = false;
+    buttonSelectEvent.icon = Paths.image('ui/chart-editor/events/Default');
+    buttonSelectEvent.iconPosition = "top";
+    buttonSelectEvent.x = buttonSelectPlayer.x + buttonSelectPlayer.width;
+    buttonSelectEvent.y = buttonSelectPlayer.y;
+    buttonSelectEvent.width = GRID_SIZE;
+    buttonSelectEvent.height = NOTE_SELECT_BUTTON_HEIGHT;
+    buttonSelectEvent.tooltip = "Click to set selection to all events.\nShift-click to add all events to selection.";
+    buttonSelectEvent.zIndex = 110;
+    add(buttonSelectEvent);
+
+    buttonSelectEvent.onClick = (_) -> {
+      if (FlxG.keys.pressed.SHIFT)
+      {
+        performCommand(new SelectItemsCommand([], currentSongChartEventData));
+      }
+      else
+      {
+        performCommand(new SetItemSelectionCommand([], currentSongChartEventData));
+      }
+    }
   }
 
   /**
@@ -2534,11 +2657,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection));
 
-    menubarItemSelectAll.onClick = _ -> performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection));
+    menubarItemSelectAllNotes.onClick = _ -> performCommand(new SelectAllItemsCommand(true, false));
 
-    menubarItemSelectInverse.onClick = _ -> performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection));
+    menubarItemSelectAllEvents.onClick = _ -> performCommand(new SelectAllItemsCommand(false, true));
 
-    menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+    menubarItemSelectInverse.onClick = _ -> performCommand(new InvertSelectedItemsCommand());
+
+    menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand());
 
     menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false);
     menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true);
@@ -3100,7 +3225,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         // Resolve an issue where dragging an event too far would cause it to be hidden.
         var isSelectedAndDragged = currentEventSelection.fastContains(eventSprite.eventData) && (dragTargetCurrentStep != 0);
 
-        if ((eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)
+        if ((eventSprite.isEventVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT)
           && currentSongChartEventData.fastContains(eventSprite.eventData))
           || isSelectedAndDragged)
         {
@@ -3662,7 +3787,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       {
         // Clicked on the playhead scroll area.
         // Move the playhead to the cursor position.
-        this.playheadPositionInPixels = FlxG.mouse.screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD;
+        this.playheadPositionInPixels = FlxG.mouse.screenY - (GRID_INITIAL_Y_POS);
         moveSongToScrollPosition();
 
         // Cursor should be a grabby hand.
@@ -3755,7 +3880,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 else
                 {
                   // Set the selection.
-                  performCommand(new SetItemSelectionCommand(notesToSelect, eventsToSelect, currentNoteSelection, currentEventSelection));
+                  performCommand(new SetItemSelectionCommand(notesToSelect, eventsToSelect));
                 }
               }
               else
@@ -3768,7 +3893,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                   var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0);
                   if (shouldDeselect)
                   {
-                    performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+                    performCommand(new DeselectAllItemsCommand());
                   }
                 }
               }
@@ -3891,17 +4016,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
               if (highlightedNote != null && highlightedNote.noteData != null)
               {
                 // Click a note to select it.
-                performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection));
+                performCommand(new SetItemSelectionCommand([highlightedNote.noteData], []));
               }
               else if (highlightedEvent != null && highlightedEvent.eventData != null)
               {
                 // Click an event to select it.
-                performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection));
+                performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData]));
               }
               else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
               {
                 // Click a hold note to select it.
-                performCommand(new SetItemSelectionCommand([highlightedHoldNote.noteData], [], currentNoteSelection, currentEventSelection));
+                performCommand(new SetItemSelectionCommand([highlightedHoldNote.noteData], []));
               }
               else
               {
@@ -3909,7 +4034,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0);
                 if (shouldDeselect)
                 {
-                  performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+                  performCommand(new DeselectAllItemsCommand());
                 }
               }
             }
@@ -3924,7 +4049,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
               var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0);
               if (shouldDeselect)
               {
-                performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+                performCommand(new DeselectAllItemsCommand());
               }
             }
           }
@@ -4193,7 +4318,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 else
                 {
                   // If you click an unselected note, and aren't holding Control, deselect everything else.
-                  performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection));
+                  performCommand(new SetItemSelectionCommand([highlightedNote.noteData], []));
                 }
               }
               else if (highlightedEvent != null && highlightedEvent.eventData != null)
@@ -4206,7 +4331,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 else
                 {
                   // If you click an unselected event, and aren't holding Control, deselect everything else.
-                  performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection));
+                  performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData]));
                 }
               }
               else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
@@ -4559,7 +4684,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
       if (currentSongMetadata.playData.characters.player != charPlayer.charId)
       {
-        if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
+        if (healthIconBF != null)
+        {
+          healthIconBF.characterId = currentSongMetadata.playData.characters.player;
+        }
 
         charPlayer.loadCharacter(currentSongMetadata.playData.characters.player);
         charPlayer.characterType = CharacterType.BF;
@@ -4595,7 +4723,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
       if (currentSongMetadata.playData.characters.opponent != charPlayer.charId)
       {
-        if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
+        if (healthIconDad != null)
+        {
+          healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
+        }
 
         charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent);
         charPlayer.characterType = CharacterType.DAD;
@@ -4613,6 +4744,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     }
   }
 
+  function handleSelectionButtons():Void
+  {
+    // Make sure buttons are never nudged out of the correct spot.
+    // TODO: Why do these even move in the first place? The camera never moves, LOL.
+    buttonSelectOpponent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2;
+    buttonSelectPlayer.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2;
+    buttonSelectEvent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2;
+  }
+
   /**
    * Handles display elements for the playbar at the bottom.
    */
@@ -4721,11 +4861,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         healthIconBF.size *= 0.5; // Make the icon smaller in Chart Editor.
         healthIconBF.flipX = !healthIconBF.flipX; // BF faces the other way.
       }
+      if (buttonSelectPlayer != null)
+      {
+        buttonSelectPlayer.text = charDataBF?.name ?? 'Player';
+      }
       if (healthIconDad != null)
       {
         healthIconDad.configure(charDataDad?.healthIcon);
         healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor.
       }
+      if (buttonSelectOpponent != null)
+      {
+        buttonSelectOpponent.text = charDataDad?.name ?? 'Opponent';
+      }
       healthIconsDirty = false;
     }
 
@@ -4733,15 +4881,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (healthIconBF != null)
     {
       // Base X position to the right of the grid.
-      healthIconBF.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 45 - (healthIconBF.width / 2));
-      healthIconBF.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconBF.height / 2));
+      var xOffset = 45 - (healthIconBF.width / 2);
+      healthIconBF.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS + gridTiledSprite.width + xOffset);
+      var yOffset = 30 - (healthIconBF.height / 2);
+      healthIconBF.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset;
     }
 
     // Visibly center the Dad health icon.
     if (healthIconDad != null)
     {
-      healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 75 - (healthIconDad.width / 2));
-      healthIconDad.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconDad.height / 2));
+      var xOffset = 45 + (healthIconDad.width / 2);
+      healthIconDad.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS - xOffset);
+      var yOffset = 30 - (healthIconDad.height / 2);
+      healthIconDad.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset;
     }
   }
 
@@ -4823,54 +4975,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     // CTRL + C = Copy
     if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.C)
     {
-      if (currentNoteSelection.length > 0)
-      {
-        txtCopyNotif.visible = true;
-        txtCopyNotif.text = "Copied " + currentNoteSelection.length + " notes to clipboard";
-        txtCopyNotif.x = FlxG.mouse.x - (txtCopyNotif.width / 2);
-        txtCopyNotif.y = FlxG.mouse.y - 16;
-        FlxTween.tween(txtCopyNotif, {y: txtCopyNotif.y - 32}, 0.5,
-          {
-            type: FlxTween.ONESHOT,
-            ease: FlxEase.quadOut,
-            onComplete: function(_) {
-              txtCopyNotif.visible = false;
-            }
-          });
-
-        for (note in renderedNotes.members)
-        {
-          if (isNoteSelected(note.noteData))
-          {
-            FlxTween.globalManager.cancelTweensOf(note);
-            FlxTween.globalManager.cancelTweensOf(note.scale);
-            note.playNoteAnimation();
-            var prevX:Float = note.scale.x;
-            var prevY:Float = note.scale.y;
-
-            note.scale.x *= 1.2;
-            note.scale.y *= 1.2;
-
-            note.angle = FlxG.random.bool() ? -10 : 10;
-            FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
-
-            FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7,
-              {
-                ease: FlxEase.elasticOut,
-                onComplete: function(_) {
-                  note.playNoteAnimation();
-                }
-              });
-          }
-        }
-      }
-
-      // We don't need a command for this since we can't undo it.
-      SongDataUtils.writeItemsToClipboard(
-        {
-          notes: SongDataUtils.buildNoteClipboard(currentNoteSelection),
-          events: SongDataUtils.buildEventClipboard(currentEventSelection),
-        });
+      performCommand(new CopyItemsCommand(currentNoteSelection, currentEventSelection));
     }
 
     // CTRL + X = Cut
@@ -4931,25 +5036,50 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       performCommand(new FlipNotesCommand(currentNoteSelection));
     }
 
-    // CTRL + A = Select All
+    // CTRL + A = Select All Notes
     if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.A)
     {
       // Select all items.
-      performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection));
+      if (FlxG.keys.pressed.ALT)
+      {
+        if (FlxG.keys.pressed.SHIFT)
+        {
+          // CTRL + ALT + SHIFT + A = Append All Events to Selection
+          performCommand(new SelectItemsCommand([], currentSongChartEventData));
+        }
+        else
+        {
+          // CTRL + ALT + A = Set Selection to All Events
+          performCommand(new SelectAllItemsCommand(false, true));
+        }
+      }
+      else
+      {
+        if (FlxG.keys.pressed.SHIFT)
+        {
+          // CTRL + SHIFT + A = Append All Notes to Selection
+          performCommand(new SelectItemsCommand(currentSongChartNoteData, []));
+        }
+        else
+        {
+          // CTRL + A = Set Selection to All Notes
+          performCommand(new SelectAllItemsCommand(true, false));
+        }
+      }
     }
 
     // CTRL + I = Select Inverse
     if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.I)
     {
       // Select unselected items and deselect selected items.
-      performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection));
+      performCommand(new InvertSelectedItemsCommand());
     }
 
     // CTRL + D = Select None
     if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.D)
     {
       // Deselect all items.
-      performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+      performCommand(new DeselectAllItemsCommand());
     }
   }
 
@@ -5113,13 +5243,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    * Perform (or redo) a command, then add it to the undo stack.
    *
    * @param command The command to perform.
-   * @param purgeRedoStack If true, the redo stack will be cleared.
+   * @param purgeRedoStack If `true`, the redo stack will be cleared after performing the command.
    */
   function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void
   {
     command.execute(this);
-    undoHistory.push(command);
-    commandHistoryDirty = true;
+    if (command.shouldAddToHistory(this))
+    {
+      undoHistory.push(command);
+      commandHistoryDirty = true;
+    }
     if (purgeRedoStack) redoHistory = [];
   }
 
@@ -5130,6 +5263,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   function undoCommand(command:ChartEditorCommand):Void
   {
     command.undo(this);
+    // Note, if we are undoing a command, it should already be in the history,
+    // therefore we don't need to check `shouldAddToHistory(state)`
     redoHistory.push(command);
     commandHistoryDirty = true;
   }
@@ -5467,6 +5602,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (displayAutosavePopup)
     {
       displayAutosavePopup = false;
+      #if sys
       Toolkit.callLater(() -> {
         var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
         this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
@@ -5476,6 +5612,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
           }
         ]);
       });
+      #else
+      // TODO: No auto-save on HTML5?
+      #end
     }
 
     moveSongToScrollPosition();
@@ -5778,6 +5917,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     @:privateAccess
     ChartEditorNoteSprite.noteFrameCollection = null;
+
+    // Stop the music.
+    welcomeMusic.destroy();
+    audioInstTrack.destroy();
+    audioVocalTrackGroup.destroy();
   }
 
   function applyCanQuickSave():Void
diff --git a/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx b/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx
index 9bf8ec3db..a878ee687 100644
--- a/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx
@@ -59,6 +59,12 @@ class AddEventsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (events.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = events.length;
diff --git a/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx b/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx
index ce4e73ea2..ea984c82d 100644
--- a/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx
@@ -59,6 +59,12 @@ class AddNotesCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0);
+  }
+
   public function toString():String
   {
     if (notes.length == 1)
diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx
index ea821afa9..bd832fab3 100644
--- a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx
@@ -64,6 +64,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
     Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (targetBPM != previousBPM);
+  }
+
   public function toString():String
   {
     return 'Change Starting BPM to ${targetBPM}';
diff --git a/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx
index cfa169908..1fa86ad94 100644
--- a/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx
@@ -6,6 +6,8 @@ package funkin.ui.debug.charting.commands;
  *
  * To make a functionality compatible with the undo/redo history, create a new class
  * that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
+ *
+ * NOTE: Make the constructor very simple, as it may be called without executing by the command palette.
  */
 interface ChartEditorCommand
 {
@@ -22,6 +24,15 @@ interface ChartEditorCommand
    */
   public function undo(state:ChartEditorState):Void;
 
+  /**
+   * Return whether or not this command should be appended to the in the undo/redo history.
+   * Generally this should be true, it should only be false if the command is minor and non-destructive,
+   * like copying to the clipboard.
+   *
+   * Called after `execute()` is performed.
+   */
+  public function shouldAddToHistory(state:ChartEditorState):Bool;
+
   /**
    * Get a short description of the action (for the UI).
    * For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
diff --git a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
new file mode 100644
index 000000000..4361f867f
--- /dev/null
+++ b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
@@ -0,0 +1,144 @@
+package funkin.ui.debug.charting.commands;
+
+import funkin.data.song.SongData.SongNoteData;
+import funkin.data.song.SongData.SongEventData;
+import funkin.data.song.SongDataUtils;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+
+/**
+ * Command that copies a given set of notes and song events to the clipboard,
+ * without deleting them from the chart editor.
+ */
+@:nullSafety
+@:access(funkin.ui.debug.charting.ChartEditorState)
+class CopyItemsCommand implements ChartEditorCommand
+{
+  var notes:Array<SongNoteData>;
+  var events:Array<SongEventData>;
+
+  public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
+  {
+    this.notes = notes;
+    this.events = events;
+  }
+
+  public function execute(state:ChartEditorState):Void
+  {
+    // Calculate a single time offset for all the notes and events.
+    var timeOffset:Null<Int> = state.currentNoteSelection.length > 0 ? Std.int(state.currentNoteSelection[0].time) : null;
+    if (state.currentEventSelection.length > 0)
+    {
+      if (timeOffset == null || state.currentEventSelection[0].time < timeOffset)
+      {
+        timeOffset = Std.int(state.currentEventSelection[0].time);
+      }
+    }
+
+    SongDataUtils.writeItemsToClipboard(
+      {
+        notes: SongDataUtils.buildNoteClipboard(state.currentNoteSelection, timeOffset),
+        events: SongDataUtils.buildEventClipboard(state.currentEventSelection, timeOffset),
+      });
+
+    performVisuals(state);
+  }
+
+  function performVisuals(state:ChartEditorState):Void
+  {
+    if (state.currentNoteSelection.length > 0)
+    {
+      // Display the "Copied Notes" text.
+      if (state.txtCopyNotif != null)
+      {
+        state.txtCopyNotif.visible = true;
+        state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard";
+        state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
+        state.txtCopyNotif.y = FlxG.mouse.y - 16;
+        FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
+          {
+            type: FlxTween.ONESHOT,
+            ease: FlxEase.quadOut,
+            onComplete: function(_) {
+              state.txtCopyNotif.visible = false;
+            }
+          });
+      }
+
+      // Wiggle the notes.
+      for (note in state.renderedNotes.members)
+      {
+        if (state.isNoteSelected(note.noteData))
+        {
+          FlxTween.globalManager.cancelTweensOf(note);
+          FlxTween.globalManager.cancelTweensOf(note.scale);
+          note.playNoteAnimation();
+          var prevX:Float = note.scale.x;
+          var prevY:Float = note.scale.y;
+
+          note.scale.x *= 1.2;
+          note.scale.y *= 1.2;
+
+          note.angle = FlxG.random.bool() ? -10 : 10;
+          FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
+
+          FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7,
+            {
+              ease: FlxEase.elasticOut,
+              onComplete: function(_) {
+                note.playNoteAnimation();
+              }
+            });
+        }
+      }
+
+      // Wiggle the events.
+      for (event in state.renderedEvents.members)
+      {
+        if (state.isEventSelected(event.eventData))
+        {
+          FlxTween.globalManager.cancelTweensOf(event);
+          FlxTween.globalManager.cancelTweensOf(event.scale);
+          event.playAnimation();
+          var prevX:Float = event.scale.x;
+          var prevY:Float = event.scale.y;
+
+          event.scale.x *= 1.2;
+          event.scale.y *= 1.2;
+
+          event.angle = FlxG.random.bool() ? -10 : 10;
+          FlxTween.tween(event, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
+
+          FlxTween.tween(event.scale, {"y": prevX, "x": prevY}, 0.7,
+            {
+              ease: FlxEase.elasticOut,
+              onComplete: function(_) {
+                event.playAnimation();
+              }
+            });
+        }
+      }
+    }
+  }
+
+  public function undo(state:ChartEditorState):Void
+  {
+    // This command is not undoable. Do nothing.
+  }
+
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is not undoable. Don't add it to the history.
+    return false;
+  }
+
+  public function toString():String
+  {
+    var len:Int = notes.length + events.length;
+
+    if (notes.length == 0) return 'Copy $len Events to Clipboard';
+    else if (events.length == 0) return 'Copy $len Notes to Clipboard';
+    else
+      return 'Copy $len Items to Clipboard';
+  }
+}
diff --git a/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx
index d0301b1ec..6cf674f80 100644
--- a/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx
@@ -56,6 +56,12 @@ class CutItemsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Always add it to the history.
+    return (notes.length > 0 || events.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = notes.length + events.length;
diff --git a/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx b/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx
index cbde0ab3d..5bfef76cc 100644
--- a/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx
@@ -10,17 +10,16 @@ import funkin.data.song.SongData.SongEventData;
 @:access(funkin.ui.debug.charting.ChartEditorState)
 class DeselectAllItemsCommand implements ChartEditorCommand
 {
-  var previousNoteSelection:Array<SongNoteData>;
-  var previousEventSelection:Array<SongEventData>;
+  var previousNoteSelection:Array<SongNoteData> = [];
+  var previousEventSelection:Array<SongEventData> = [];
 
-  public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
-  {
-    this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
-    this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
-  }
+  public function new() {}
 
   public function execute(state:ChartEditorState):Void
   {
+    this.previousNoteSelection = state.currentNoteSelection;
+    this.previousEventSelection = state.currentEventSelection;
+
     state.currentNoteSelection = [];
     state.currentEventSelection = [];
 
@@ -35,6 +34,12 @@ class DeselectAllItemsCommand implements ChartEditorCommand
     state.noteDisplayDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
+  }
+
   public function toString():String
   {
     return 'Deselect All Items';
diff --git a/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx
index d679b5363..6a115a26a 100644
--- a/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx
@@ -45,16 +45,27 @@ class DeselectItemsCommand implements ChartEditorCommand
     state.notePreviewDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0 || events.length > 0);
+  }
+
   public function toString():String
   {
-    var noteCount = notes.length + events.length;
+    var isPlural = (notes.length + events.length) > 1;
+    var notesOnly = (notes.length > 0 && events.length == 0);
+    var eventsOnly = (notes.length == 0 && events.length > 0);
 
-    if (noteCount == 1)
+    if (notesOnly)
     {
-      var dir:String = notes[0].getDirectionName();
-      return 'Deselect $dir Items';
+      return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
+    }
+    else if (eventsOnly)
+    {
+      return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}';
     }
 
-    return 'Deselect ${noteCount} Items';
+    return 'Deselect ${notes.length + events.length} Items';
   }
 }
diff --git a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx
index 62ffe63b9..3ef9f22d1 100644
--- a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx
@@ -54,6 +54,12 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (oldLength != newLength);
+  }
+
   public function toString():String
   {
     if (oldLength == 0)
diff --git a/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx b/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx
index da8ec7fbc..f54ffed15 100644
--- a/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx
@@ -51,6 +51,12 @@ class FlipNotesCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = notes.length;
diff --git a/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx b/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx
index 6e37bcc03..d9a28f463 100644
--- a/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx
@@ -12,19 +12,19 @@ import funkin.data.song.SongDataUtils;
 @:access(funkin.ui.debug.charting.ChartEditorState)
 class InvertSelectedItemsCommand implements ChartEditorCommand
 {
-  var previousNoteSelection:Array<SongNoteData>;
-  var previousEventSelection:Array<SongEventData>;
+  var previousNoteSelection:Array<SongNoteData> = [];
+  var previousEventSelection:Array<SongEventData> = [];
 
-  public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
-  {
-    this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
-    this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
-  }
+  public function new() {}
 
   public function execute(state:ChartEditorState):Void
   {
+    this.previousNoteSelection = state.currentNoteSelection;
+    this.previousEventSelection = state.currentEventSelection;
+
     state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
     state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
+
     state.noteDisplayDirty = true;
   }
 
@@ -36,6 +36,12 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
     state.noteDisplayDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
+  }
+
   public function toString():String
   {
     return 'Invert Selected Items';
diff --git a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx
index 8331ed397..ed50ad33e 100644
--- a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx
@@ -65,6 +65,12 @@ class MoveEventsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (events.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = events.length;
diff --git a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx
index 9fac8a0c4..f44cb973a 100644
--- a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx
@@ -88,6 +88,12 @@ class MoveItemsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0 || events.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = notes.length + events.length;
diff --git a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx
index 0308d8fc8..51aeb5bbc 100644
--- a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx
@@ -67,6 +67,12 @@ class MoveNotesCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = notes.length;
diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx
index 7e40bc49b..257db94b4 100644
--- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx
@@ -71,6 +71,12 @@ class PasteItemsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (addedNotes.length > 0 || addedEvents.length > 0);
+  }
+
   public function toString():String
   {
     var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
diff --git a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx
index 7e620c210..b4d913607 100644
--- a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx
@@ -48,6 +48,12 @@ class RemoveEventsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (events.length > 0);
+  }
+
   public function toString():String
   {
     if (events.length == 1 && events[0] != null)
diff --git a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx
index 77184209e..69317aff4 100644
--- a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx
@@ -62,6 +62,12 @@ class RemoveItemsCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0 || events.length > 0);
+  }
+
   public function toString():String
   {
     return 'Remove ${notes.length + events.length} Items';
diff --git a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx
index e189be83e..4811f831d 100644
--- a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx
@@ -50,6 +50,12 @@ class RemoveNotesCommand implements ChartEditorCommand
     state.sortChartData();
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0);
+  }
+
   public function toString():String
   {
     if (notes.length == 1 && notes[0] != null)
diff --git a/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx
index e1a4dceaa..f550e044b 100644
--- a/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx
@@ -10,19 +10,25 @@ import funkin.data.song.SongData.SongEventData;
 @:access(funkin.ui.debug.charting.ChartEditorState)
 class SelectAllItemsCommand implements ChartEditorCommand
 {
-  var previousNoteSelection:Array<SongNoteData>;
-  var previousEventSelection:Array<SongEventData>;
+  var shouldSelectNotes:Bool;
+  var shouldSelectEvents:Bool;
 
-  public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
+  var previousNoteSelection:Array<SongNoteData> = [];
+  var previousEventSelection:Array<SongEventData> = [];
+
+  public function new(shouldSelectNotes:Bool, shouldSelectEvents:Bool)
   {
-    this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
-    this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
+    this.shouldSelectNotes = shouldSelectNotes;
+    this.shouldSelectEvents = shouldSelectEvents;
   }
 
   public function execute(state:ChartEditorState):Void
   {
-    state.currentNoteSelection = state.currentSongChartNoteData;
-    state.currentEventSelection = state.currentSongChartEventData;
+    this.previousNoteSelection = state.currentNoteSelection;
+    this.previousEventSelection = state.currentEventSelection;
+
+    state.currentNoteSelection = shouldSelectNotes ? state.currentSongChartNoteData : [];
+    state.currentEventSelection = shouldSelectEvents ? state.currentSongChartEventData : [];
 
     state.noteDisplayDirty = true;
   }
@@ -35,8 +41,29 @@ class SelectAllItemsCommand implements ChartEditorCommand
     state.noteDisplayDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (state.currentNoteSelection.length > 0 || state.currentEventSelection.length > 0);
+  }
+
   public function toString():String
   {
-    return 'Select All Items';
+    if (shouldSelectNotes && !shouldSelectEvents)
+    {
+      return 'Select All Notes';
+    }
+    else if (shouldSelectEvents && !shouldSelectNotes)
+    {
+      return 'Select All Events';
+    }
+    else if (shouldSelectNotes && shouldSelectEvents)
+    {
+      return 'Select All Notes and Events';
+    }
+    else
+    {
+      return 'Select Nothing (Huh?)';
+    }
   }
 }
diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
index 49b2ba585..d6c5beeac 100644
--- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
@@ -15,10 +15,10 @@ class SelectItemsCommand implements ChartEditorCommand
   var notes:Array<SongNoteData>;
   var events:Array<SongEventData>;
 
-  public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
+  public function new(?notes:Array<SongNoteData>, ?events:Array<SongEventData>)
   {
-    this.notes = notes;
-    this.events = events;
+    this.notes = notes ?? [];
+    this.events = events ?? [];
   }
 
   public function execute(state:ChartEditorState):Void
@@ -72,6 +72,12 @@ class SelectItemsCommand implements ChartEditorCommand
     state.notePreviewDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // This command is undoable. Add to the history if we actually performed an action.
+    return (notes.length > 0 || events.length > 0);
+  }
+
   public function toString():String
   {
     var len:Int = notes.length + events.length;
diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
index 4725fd275..35a00e562 100644
--- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
@@ -13,20 +13,20 @@ class SetItemSelectionCommand implements ChartEditorCommand
 {
   var notes:Array<SongNoteData>;
   var events:Array<SongEventData>;
-  var previousNoteSelection:Array<SongNoteData>;
-  var previousEventSelection:Array<SongEventData>;
+  var previousNoteSelection:Array<SongNoteData> = [];
+  var previousEventSelection:Array<SongEventData> = [];
 
-  public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
-      previousEventSelection:Array<SongEventData>)
+  public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
   {
     this.notes = notes;
     this.events = events;
-    this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
-    this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
   }
 
   public function execute(state:ChartEditorState):Void
   {
+    this.previousNoteSelection = state.currentNoteSelection;
+    this.previousEventSelection = state.currentEventSelection;
+
     state.currentNoteSelection = notes;
     state.currentEventSelection = events;
 
@@ -67,8 +67,14 @@ class SetItemSelectionCommand implements ChartEditorCommand
     state.noteDisplayDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // Add to the history if we actually performed an action.
+    return (state.currentNoteSelection != previousNoteSelection && state.currentEventSelection != previousEventSelection);
+  }
+
   public function toString():String
   {
-    return 'Select ${notes.length} Items';
+    return 'Select ${notes.length + events.length} Items';
   }
 }
diff --git a/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx b/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx
index 75e7e5afe..30c2edb61 100644
--- a/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx
@@ -38,6 +38,12 @@ class SwitchDifficultyCommand implements ChartEditorCommand
     state.notePreviewDirty = true;
   }
 
+  public function shouldAddToHistory(state:ChartEditorState):Bool
+  {
+    // Add to the history if we actually performed an action.
+    return (prevVariation != newVariation || prevDifficulty != newDifficulty);
+  }
+
   public function toString():String
   {
     return 'Switch Difficulty';
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
index 79bcd59af..36c6b1d2f 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
@@ -119,8 +119,10 @@ class ChartEditorEventSprite extends FlxSprite
     return DEFAULT_EVENT;
   }
 
-  public function playAnimation(name:String):Void
+  public function playAnimation(?name:String):Void
   {
+    if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
+
     var correctedName = correctAnimationName(name);
     this.animation.play(correctedName);
     refresh();
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx
index 598cbb544..8d9ec6743 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx
@@ -70,9 +70,9 @@ class ChartEditorNotePreview extends FlxSprite
    * @param event The data for the event.
    * @param songLengthInMs The total length of the song in milliseconds.
    */
-  public function addEvent(event:SongEventData, songLengthInMs:Int):Void
+  public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void
   {
-    drawNote(-1, false, Std.int(event.time), songLengthInMs);
+    drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection);
   }
 
   /**
@@ -114,6 +114,19 @@ class ChartEditorNotePreview extends FlxSprite
     }
   }
 
+  /**
+   * Add an array of selected events to the preview.
+   * @param events The data for the events.
+   * @param songLengthInMs The total length of the song in milliseconds.
+   */
+  public function addSelectedEvents(events:Array<SongEventData>, songLengthInMs:Int):Void
+  {
+    for (event in events)
+    {
+      addEvent(event, songLengthInMs, true);
+    }
+  }
+
   /**
    * Draws a note on the preview.
    * @param dir Note data.
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 4df53663c..0edba7357 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -188,17 +188,19 @@ class ChartEditorAudioHandler
           state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
           state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
           state.audioVisGroup.playerVis.detail = 1;
+          state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS);
 
           state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
           return true;
         case DAD:
           state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
           state.audioVisGroup.addOpponentVis(vocalTrack);
-          state.audioVisGroup.opponentVis.x = 405;
+          state.audioVisGroup.opponentVis.x = 435;
 
           state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
           state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
           state.audioVisGroup.opponentVis.detail = 1;
+          state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS);
 
           state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
 
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx
index f7105d2f7..62f1f4cbc 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx
@@ -2,6 +2,10 @@ package funkin.ui.debug.charting.handlers;
 
 import funkin.util.PlatformUtil;
 
+/**
+ * Handles modifying the shortcut text of menu items based on the current platform.
+ * On MacOS, `Ctrl`, `Alt`, and `Shift` are replaced with `⌘` (or `^`), `⌥`, and `⇧`, respectively.
+ */
 @:access(funkin.ui.debug.charting.ChartEditorState)
 class ChartEditorShortcutHandler
 {
@@ -18,7 +22,8 @@ class ChartEditorShortcutHandler
     state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
     state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
 
-    state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
+    state.menubarItemSelectAllNotes.shortcutText = ctrlOrCmd('A');
+    state.menubarItemSelectAllEvents.shortcutText = ctrlOrCmd(alt('A'));
     state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
     state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
     state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index f17c3d91e..dec199e98 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1,6 +1,5 @@
 package funkin.ui.freeplay;
 
-import funkin.input.Controls;
 import flash.text.TextField;
 import flixel.addons.display.FlxGridOverlay;
 import flixel.addons.transition.FlxTransitionableState;
@@ -23,33 +22,35 @@ import flixel.tweens.FlxTween;
 import flixel.util.FlxColor;
 import flixel.util.FlxSpriteUtil;
 import flixel.util.FlxTimer;
-import funkin.input.Controls.Control;
 import funkin.data.level.LevelRegistry;
 import funkin.data.song.SongRegistry;
 import funkin.graphics.adobeanimate.FlxAtlasSprite;
 import funkin.graphics.shaders.AngleMask;
 import funkin.graphics.shaders.HSVShader;
 import funkin.graphics.shaders.PureColor;
-import funkin.util.MathUtil;
 import funkin.graphics.shaders.StrokeShader;
+import funkin.input.Controls;
+import funkin.input.Controls.Control;
 import funkin.play.components.HealthIcon;
 import funkin.play.PlayState;
 import funkin.play.PlayStatePlaylist;
 import funkin.play.song.Song;
 import funkin.save.Save;
 import funkin.save.Save.SaveScoreData;
+import funkin.ui.AtlasText;
 import funkin.ui.freeplay.BGScrollingText;
 import funkin.ui.freeplay.DifficultyStars;
 import funkin.ui.freeplay.DJBoyfriend;
 import funkin.ui.freeplay.FreeplayScore;
 import funkin.ui.freeplay.LetterSort;
 import funkin.ui.freeplay.SongMenuItem;
+import funkin.ui.mainmenu.MainMenuState;
 import funkin.ui.MusicBeatState;
 import funkin.ui.MusicBeatSubState;
-import funkin.ui.mainmenu.MainMenuState;
 import funkin.ui.transition.LoadingState;
 import funkin.ui.transition.StickerSubState;
 import funkin.util.MathUtil;
+import funkin.util.MathUtil;
 import lime.app.Future;
 import lime.utils.Assets;
 
@@ -64,7 +65,7 @@ class FreeplayState extends MusicBeatSubState
   var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
 
   var fp:FreeplayScore;
-  var txtCompletion:FlxText;
+  var txtCompletion:AtlasText;
   var lerpCompletion:Float = 0;
   var intendedCompletion:Float = 0;
   var lerpScore:Float = 0;
@@ -87,6 +88,8 @@ class FreeplayState extends MusicBeatSubState
   var grpCapsules:FlxTypedGroup<SongMenuItem>;
   var curCapsule:SongMenuItem;
   var curPlaying:Bool = false;
+  var ostName:FlxText;
+  var difficultyStars:DifficultyStars;
 
   var dj:DJBoyfriend;
 
@@ -150,15 +153,10 @@ class FreeplayState extends MusicBeatSubState
       for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
       {
         var song:Song = SongRegistry.instance.fetchEntry(songId);
-        var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY);
 
-        var songName = songBaseDifficulty.songName;
-        var songOpponent = songBaseDifficulty.characters.opponent;
-        var songDifficulties = song.listDifficulties();
+        songs.push(new FreeplaySongData(levelId, songId, song));
 
-        songs.push(new FreeplaySongData(songId, songName, levelId, songOpponent, songDifficulties));
-
-        for (difficulty in songDifficulties)
+        for (difficulty in song.listDifficulties())
         {
           diffIdsTotal.pushUnique(difficulty);
         }
@@ -334,6 +332,8 @@ class FreeplayState extends MusicBeatSubState
       if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
     }
 
+    // NOTE: This is an AtlasSprite because we use an animation to bring it into view.
+    // TODO: Add the ability to select the album graphic.
     var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
     albumArt.visible = false;
     add(albumArt);
@@ -347,7 +347,7 @@ class FreeplayState extends MusicBeatSubState
 
     var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
     var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
-    var difficultyStars:DifficultyStars = new DifficultyStars(140, 39);
+    difficultyStars = new DifficultyStars(140, 39);
 
     difficultyStars.stars.visible = false;
     albumTitle.visible = false;
@@ -382,11 +382,16 @@ class FreeplayState extends MusicBeatSubState
     add(overhangStuff);
     FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
 
-    var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48);
+    var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48);
     fnfFreeplay.font = "VCR OSD Mono";
     fnfFreeplay.visible = false;
 
-    exitMovers.set([overhangStuff, fnfFreeplay],
+    ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48);
+    ostName.font = "VCR OSD Mono";
+    ostName.alignment = RIGHT;
+    ostName.visible = false;
+
+    exitMovers.set([overhangStuff, fnfFreeplay, ostName],
       {
         y: -overhangStuff.height,
         x: 0,
@@ -397,8 +402,9 @@ class FreeplayState extends MusicBeatSubState
     var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
     fnfFreeplay.shader = sillyStroke;
     add(fnfFreeplay);
+    add(ostName);
 
-    var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70);
+    var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
     fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
     fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
     fnfHighscoreSpr.visible = false;
@@ -415,8 +421,10 @@ class FreeplayState extends MusicBeatSubState
     fp.visible = false;
     add(fp);
 
-    txtCompletion = new FlxText(1200, 77, 0, "0", 32);
-    txtCompletion.font = "VCR OSD Mono";
+    var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
+    add(clearBoxSprite);
+
+    txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR);
     txtCompletion.visible = false;
     add(txtCompletion);
 
@@ -485,6 +493,7 @@ class FreeplayState extends MusicBeatSubState
       new FlxTimer().start(1 / 24, function(handShit) {
         fnfHighscoreSpr.visible = true;
         fnfFreeplay.visible = true;
+        ostName.visible = true;
         fp.visible = true;
         fp.updateScore(0);
 
@@ -674,9 +683,32 @@ class FreeplayState extends MusicBeatSubState
     lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2);
     lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
 
+    if (Math.isNaN(lerpScore))
+    {
+      lerpScore = intendedScore;
+    }
+
+    if (Math.isNaN(lerpCompletion))
+    {
+      lerpCompletion = intendedCompletion;
+    }
+
     fp.updateScore(Std.int(lerpScore));
 
-    txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
+    txtCompletion.text = '${Math.floor(lerpCompletion * 100)}';
+
+    // Right align the completion percentage
+    switch (txtCompletion.text.length)
+    {
+      case 3:
+        txtCompletion.x = 1185 - 10;
+      case 2:
+        txtCompletion.x = 1185;
+      case 1:
+        txtCompletion.x = 1185 + 24;
+      default:
+        txtCompletion.x = 1185;
+    }
 
     handleInputs(elapsed);
   }
@@ -913,6 +945,11 @@ class FreeplayState extends MusicBeatSubState
       intendedCompletion = 0.0;
     }
 
+    if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion))
+    {
+      intendedCompletion = 0;
+    }
+
     grpDifficulties.group.forEach(function(diffSprite) {
       diffSprite.visible = false;
     });
@@ -938,6 +975,27 @@ class FreeplayState extends MusicBeatSubState
         }
       }
     }
+
+    if (change != 0)
+    {
+      // Update the song capsules to reflect the new difficulty info.
+      for (songCapsule in grpCapsules.members)
+      {
+        if (songCapsule == null) continue;
+        if (songCapsule.songData != null)
+        {
+          songCapsule.songData.currentDifficulty = currentDifficulty;
+          songCapsule.init(null, null, songCapsule.songData);
+        }
+        else
+        {
+          songCapsule.init(null, null, null);
+        }
+      }
+    }
+
+    // Set the difficulty star count on the right.
+    difficultyStars.difficulty = daSong.songRating;
   }
 
   // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
@@ -1046,6 +1104,10 @@ class FreeplayState extends MusicBeatSubState
     {
       currentDifficulty = rememberedDifficulty;
     }
+
+    // Set the difficulty star count on the right.
+    var daSong = songs[curSelected];
+    difficultyStars.difficulty = daSong?.songRating ?? 0;
   }
 
   function changeSelection(change:Int = 0)
@@ -1176,19 +1238,47 @@ class FreeplaySongData
 {
   public var isFav:Bool = false;
 
-  public var songId:String = "";
-  public var songName:String = "";
-  public var levelId:String = "";
-  public var songCharacter:String = "";
-  public var songDifficulties:Array<String> = [];
+  var song:Song;
 
-  public function new(songId:String, songName:String, levelId:String, songCharacter:String, songDifficulties:Array<String>)
+  public var levelId(default, null):String = "";
+  public var songId(default, null):String = "";
+
+  public var songDifficulties(default, null):Array<String> = [];
+
+  public var songName(default, null):String = "";
+  public var songCharacter(default, null):String = "";
+  public var songRating(default, null):Int = 0;
+
+  public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
+
+  function set_currentDifficulty(value:String):String
+  {
+    if (currentDifficulty == value) return value;
+
+    currentDifficulty = value;
+    updateValues();
+    return value;
+  }
+
+  public function new(levelId:String, songId:String, song:Song)
   {
-    this.songId = songId;
-    this.songName = songName;
     this.levelId = levelId;
-    this.songCharacter = songCharacter;
-    this.songDifficulties = songDifficulties;
+    this.songId = songId;
+    this.song = song;
+
+    updateValues();
+  }
+
+  function updateValues():Void
+  {
+    this.songDifficulties = song.listDifficulties();
+    if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+
+    var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty);
+    if (songDifficulty == null) return;
+    this.songName = songDifficulty.songName;
+    this.songCharacter = songDifficulty.characters.opponent;
+    this.songRating = songDifficulty.difficultyRating;
   }
 }
 
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index 4e0772dfe..06d113468 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -35,11 +35,6 @@ class SongMenuItem extends FlxSpriteGroup
 
   var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
 
-  // lol...
-  var diffRanks:Array<String> = [
-    "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15"
-  ];
-
   public var targetPos:FlxPoint = new FlxPoint();
   public var doLerp:Bool = false;
   public var doJumpIn:Bool = false;
@@ -47,10 +42,12 @@ class SongMenuItem extends FlxSpriteGroup
   public var doJumpOut:Bool = false;
 
   public var onConfirm:Void->Void;
-  public var diffGrayscale:Grayscale;
+  public var grayscaleShader:Grayscale;
 
   public var hsvShader(default, set):HSVShader;
 
+  var diffRatingSprite:FlxSprite;
+
   public function new(x:Float, y:Float)
   {
     super(x, y);
@@ -75,26 +72,30 @@ class SongMenuItem extends FlxSpriteGroup
     add(ranking);
     grpHide.add(ranking);
 
-    diffGrayscale = new Grayscale(1);
-
-    var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks)));
-    diffRank.shader = diffGrayscale;
-    diffRank.visible = false;
-    add(diffRank);
-    diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y);
-    grpHide.add(diffRank);
-
     switch (rank)
     {
       case "perfect":
         ranking.x -= 10;
     }
 
+    grayscaleShader = new Grayscale(1);
+
+    diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00"));
+    diffRatingSprite.shader = grayscaleShader;
+    diffRatingSprite.visible = false;
+    add(diffRatingSprite);
+    diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
+    grpHide.add(diffRatingSprite);
+
     songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
     add(songText);
     grpHide.add(songText);
 
-    pixelIcon = new FlxSprite(155, 15);
+    // TODO: Use value from metadata instead of random.
+    updateDifficultyRating(FlxG.random.int(0, 15));
+
+    pixelIcon = new FlxSprite(160, 35);
+
     pixelIcon.makeGraphic(32, 32, 0x00000000);
     pixelIcon.antialiasing = false;
     pixelIcon.active = false;
@@ -113,6 +114,12 @@ class SongMenuItem extends FlxSpriteGroup
     setVisibleGrp(false);
   }
 
+  function updateDifficultyRating(newRating:Int)
+  {
+    var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
+    diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
+  }
+
   function set_hsvShader(value:HSVShader):HSVShader
   {
     this.hsvShader = value;
@@ -149,16 +156,17 @@ class SongMenuItem extends FlxSpriteGroup
     updateSelected();
   }
 
-  public function init(x:Float, y:Float, songData:Null<FreeplaySongData>)
+  public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>)
   {
-    this.x = x;
-    this.y = y;
+    if (x != null) this.x = x;
+    if (y != null) this.y = y;
     this.songData = songData;
 
     // Update capsule text.
     songText.text = songData?.songName ?? 'Random';
     // Update capsule character.
     if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
+    updateDifficultyRating(songData?.songRating ?? 0);
     // Update opacity, offsets, etc.
     updateSelected();
   }
@@ -200,7 +208,14 @@ class SongMenuItem extends FlxSpriteGroup
 
     pixelIcon.loadGraphic(Paths.image(charPath));
     pixelIcon.scale.x = pixelIcon.scale.y = 2;
-    pixelIcon.origin.x = 100;
+
+    switch (char)
+    {
+      case "parents-christmas":
+        pixelIcon.origin.x = 140;
+      default:
+        pixelIcon.origin.x = 100;
+    }
     // pixelIcon.origin.x = capsule.origin.x;
     // pixelIcon.offset.x -= pixelIcon.origin.x;
   }
@@ -336,7 +351,7 @@ class SongMenuItem extends FlxSpriteGroup
 
   function updateSelected():Void
   {
-    diffGrayscale.setAmount(this.selected ? 0 : 0.8);
+    grayscaleShader.setAmount(this.selected ? 0 : 0.8);
     songText.alpha = this.selected ? 1 : 0.6;
     songText.blurredText.visible = this.selected ? true : false;
     capsule.offset.x = this.selected ? 0 : -5;
diff --git a/source/funkin/util/macro/HaxelibVersions.hx b/source/funkin/util/macro/HaxelibVersions.hx
index f0317c397..1a4699bba 100644
--- a/source/funkin/util/macro/HaxelibVersions.hx
+++ b/source/funkin/util/macro/HaxelibVersions.hx
@@ -11,12 +11,12 @@ class HaxelibVersions
     #else
     // `#if display` is used for code completion. In this case returning an
     // empty string is good enough; We don't want to call functions on every hint.
-    var commitHash:String = "";
-    return macro $v{commitHashSplice};
+    var commitHash:Array<String> = [];
+    return macro $v{commitHash};
     #end
   }
 
-  #if (debug && macro)
+  #if (macro)
   static function readHmmData():hmm.HmmConfig
   {
     return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow();
diff --git a/source/funkin/util/tools/Int64Tools.hx b/source/funkin/util/tools/Int64Tools.hx
index 75448b36f..d53fa315d 100644
--- a/source/funkin/util/tools/Int64Tools.hx
+++ b/source/funkin/util/tools/Int64Tools.hx
@@ -1,32 +1,42 @@
 package funkin.util.tools;
 
+import haxe.Int64;
+
 /**
- * @see https://github.com/fponticelli/thx.core/blob/master/src/thx/Int64s.hx
+ * Why `haxe.Int64` doesn't have a built-in `toFloat` function is beyond me.
  */
 class Int64Tools
 {
-  static var min = haxe.Int64.make(0x80000000, 0);
-  static var one = haxe.Int64.make(0, 1);
-  static var two = haxe.Int64.ofInt(2);
-  static var zero = haxe.Int64.make(0, 0);
-  static var ten = haxe.Int64.ofInt(10);
+  private inline static var MAX_32_PRECISION:Float = 4294967296.0;
 
-  public static function toFloat(i:haxe.Int64):Float
+  public static function fromFloat(f:Float):Int64
   {
-    var isNegative = false;
-    if (i < 0)
+    var h = Std.int(f / MAX_32_PRECISION);
+    var l = Std.int(f);
+    return Int64.make(h, l);
+  }
+
+  public static function toFloat(i:Int64):Float
+  {
+    var f:Float = Int64.getLow(i);
+    if (f < 0) f += MAX_32_PRECISION;
+    return (Int64.getHigh(i) * MAX_32_PRECISION + f);
+  }
+
+  public static function isToIntSafe(i:Int64):Bool
+  {
+    return i.high != i.low >> 31;
+  }
+
+  public static function toIntSafe(i:Int64):Int
+  {
+    try
     {
-      if (i < min) return -9223372036854775808.0; // most -ve value can't be made +ve
-      isNegative = true;
-      i = -i;
+      return Int64.toInt(i);
     }
-    var multiplier = 1.0, ret = 0.0;
-    for (_ in 0...64)
+    catch (e:Dynamic)
     {
-      if (haxe.Int64.and(i, one) != zero) ret += multiplier;
-      multiplier *= 2.0;
-      i = haxe.Int64.shr(i, 1);
+      throw 'Could not represent value "${Int64.toStr(i)}" as an integer.';
     }
-    return (isNegative ? -1 : 1) * ret;
   }
 }