From 1b6febf01c9a7826ec65004d3d4e46780792116f Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 18 Apr 2024 20:23:03 -0400
Subject: [PATCH 01/96] initial freeplay songs loading

---
 Project.xml                                |  1 +
 checkstyle.json                            |  2 +-
 hmm.json                                   |  9 +-
 source/funkin/Paths.hx                     | 20 ++++-
 source/funkin/audio/FunkinSound.hx         | 97 ++++++++++++++++++++--
 source/funkin/ui/freeplay/FreeplayState.hx | 23 +++--
 6 files changed, 127 insertions(+), 25 deletions(-)

diff --git a/Project.xml b/Project.xml
index fcfcfb9f3..87608bb88 100644
--- a/Project.xml
+++ b/Project.xml
@@ -126,6 +126,7 @@
 	<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
 	<haxelib name="funkin.vis"/>
 
+	<haxelib name="FlxPartialSound" /> <!-- Loading partial sound data -->
 
 	<haxelib name="json2object" /> <!-- JSON parsing -->
 	<haxelib name="thx.semver" /> <!-- Version string handling -->
diff --git a/checkstyle.json b/checkstyle.json
index dc89409da..41f0a7998 100644
--- a/checkstyle.json
+++ b/checkstyle.json
@@ -79,7 +79,7 @@
     {
       "props": {
         "ignoreExtern": true,
-        "format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
+        "format": "^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$",
         "tokens": ["INLINE", "NOTINLINE"]
       },
       "type": "ConstantName"
diff --git a/hmm.json b/hmm.json
index a6e4467a9..6b119c52f 100644
--- a/hmm.json
+++ b/hmm.json
@@ -1,5 +1,12 @@
 {
   "dependencies": [
+    {
+      "name": "FlxPartialSound",
+      "type": "git",
+      "dir": null,
+      "ref": "main",
+      "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
+    },
     {
       "name": "discord_rpc",
       "type": "git",
@@ -171,4 +178,4 @@
       "url": "https://github.com/FunkinCrew/thx.semver"
     }
   ]
-}
+}
\ No newline at end of file
diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx
index 54a4b7acf..b0a97c4fa 100644
--- a/source/funkin/Paths.hx
+++ b/source/funkin/Paths.hx
@@ -123,9 +123,17 @@ class Paths
     return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
   }
 
-  public static function inst(song:String, ?suffix:String = ''):String
+  /**
+   * Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/
+   * @param song name of the song to get instrumental for
+   * @param suffix any suffix to add to end of song name, used for `-erect` variants usually
+   * @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
+   * @return String
+   */
+  public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
   {
-    return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
+    var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
+    return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';
   }
 
   public static function image(key:String, ?library:String):String
@@ -153,3 +161,11 @@ class Paths
     return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
   }
 }
+
+enum abstract PathsFunction(String)
+{
+  var MUSIC;
+  var INST;
+  var VOICES;
+  var SOUND;
+}
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index df05cc3ef..728a06a32 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -11,7 +11,11 @@ import funkin.audio.waveform.WaveformDataParser;
 import funkin.data.song.SongData.SongMusicData;
 import funkin.data.song.SongRegistry;
 import funkin.util.tools.ICloneable;
+import funkin.util.flixel.sound.FlxPartialSound;
+import funkin.Paths.PathsFunction;
 import openfl.Assets;
+import lime.app.Future;
+import lime.app.Promise;
 import openfl.media.SoundMixer;
 #if (openfl >= "8.0.0")
 import openfl.utils.AssetType;
@@ -342,20 +346,52 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
         FlxG.log.warn('Tried and failed to find music metadata for $key');
       }
     }
-
-    var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
-    if (music != null)
+    var pathsFunction = params.pathsFunction ?? MUSIC;
+    var pathToUse = switch (pathsFunction)
     {
-      FlxG.sound.music = music;
+      case MUSIC: Paths.music('$key/$key');
+      case INST: Paths.inst('$key');
+      default: Paths.music('$key/$key');
+    }
 
-      // Prevent repeat update() and onFocus() calls.
-      FlxG.sound.list.remove(FlxG.sound.music);
+    var shouldLoadPartial = params.partialParams?.loadPartial ?? false;
 
-      return true;
+    if (shouldLoadPartial)
+    {
+      var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0, params.partialParams?.end ?? 1, params?.startingVolume ?? 1.0,
+        params.loop ?? true, false, true);
+
+      if (music != null)
+      {
+        music.onComplete(function(partialMusic:Null<FunkinSound>) {
+          @:nullSafety(Off)
+          FlxG.sound.music = partialMusic;
+          FlxG.sound.list.remove(FlxG.sound.music);
+        });
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
     else
     {
-      return false;
+      var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
+      if (music != null)
+      {
+        FlxG.sound.music = music;
+
+        // Prevent repeat update() and onFocus() calls.
+        FlxG.sound.list.remove(FlxG.sound.music);
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
   }
 
@@ -415,6 +451,36 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     return sound;
   }
 
+  /**
+   * Will load a section of a sound file, useful for Freeplay where we don't want to load all the bytes of a song
+   * @param path The path to the sound file
+   * @param start The start time of the sound file
+   * @param end The end time of the sound file
+   * @param volume Volume to start at
+   * @param looped Whether the sound file should loop
+   * @param autoDestroy Whether the sound file should be destroyed after it finishes playing
+   * @param autoPlay Whether the sound file should play immediately
+   * @return A FunkinSound object
+   */
+  public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false,
+      autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Future<Null<FunkinSound>>
+  {
+    var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
+
+    // split the path and get only after first :
+    // we are bypassing the openfl/lime asset library fuss
+    path = Paths.stripLibrary(path);
+
+    var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
+
+    soundRequest.onComplete(function(partialSound) {
+      var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
+      promise.complete(snd);
+    });
+
+    return promise.future;
+  }
+
   @:nullSafety(Off)
   public override function destroy():Void
   {
@@ -498,4 +564,19 @@ typedef FunkinSoundPlayMusicParams =
    * @default `true`
    */
   var ?mapTimeChanges:Bool;
+
+  /**
+   * Which Paths function to use to load a song
+   * @default `MUSIC`
+   */
+  var ?pathsFunction:PathsFunction;
+
+  var ?partialParams:PartialSoundParams;
+}
+
+typedef PartialSoundParams =
+{
+  var loadPartial:Bool;
+  var start:Float;
+  var end:Float;
 }
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 7b7543845..c290e6553 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1244,22 +1244,19 @@ class FreeplayState extends MusicBeatSubState
       else
       {
         // TODO: Stream the instrumental of the selected song?
-        var didReplace:Bool = FunkinSound.playMusic('freakyMenu',
+        FunkinSound.playMusic(daSongCapsule.songData.songId,
           {
-            startingVolume: 0.0,
+            startingVolume: 0.5,
             overrideExisting: true,
-            restartTrack: false
+            restartTrack: false,
+            pathsFunction: INST,
+            partialParams:
+              {
+                loadPartial: true,
+                start: 0,
+                end: 0.1
+              }
           });
-        if (didReplace)
-        {
-          FunkinSound.playMusic('freakyMenu',
-            {
-              startingVolume: 0.0,
-              overrideExisting: true,
-              restartTrack: false
-            });
-          FlxG.sound.music.fadeIn(2, 0, 0.8);
-        }
       }
       grpCapsules.members[curSelected].selected = true;
     }

From f2a06ad37b79c76566ab62f8244a84ac864155a0 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 9 May 2024 01:56:52 -0400
Subject: [PATCH 02/96] fading in and erect track loading

---
 source/funkin/audio/FunkinSound.hx         | 14 ++++++++++++--
 source/funkin/ui/freeplay/FreeplayState.hx |  9 +++++++--
 2 files changed, 19 insertions(+), 4 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index 728a06a32..5a49e29ee 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -347,10 +347,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
       }
     }
     var pathsFunction = params.pathsFunction ?? MUSIC;
+    var suffix = params.suffix ?? '';
     var pathToUse = switch (pathsFunction)
     {
       case MUSIC: Paths.music('$key/$key');
-      case INST: Paths.inst('$key');
+      case INST: Paths.inst('$key', suffix);
       default: Paths.music('$key/$key');
     }
 
@@ -359,7 +360,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     if (shouldLoadPartial)
     {
       var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0, params.partialParams?.end ?? 1, params?.startingVolume ?? 1.0,
-        params.loop ?? true, false, true);
+        params.loop ?? true, false, true, params.onComplete, params.onLoad);
 
       if (music != null)
       {
@@ -541,6 +542,12 @@ typedef FunkinSoundPlayMusicParams =
    */
   var ?startingVolume:Float;
 
+  /**
+   * The suffix of the music file to play. Usually for "-erect" tracks when loading an INST file
+   * @default ``
+   */
+  var ?suffix:String;
+
   /**
    * Whether to override music if a different track is already playing.
    * @default `false`
@@ -572,6 +579,9 @@ typedef FunkinSoundPlayMusicParams =
   var ?pathsFunction:PathsFunction;
 
   var ?partialParams:PartialSoundParams;
+
+  var ?onComplete:Void->Void;
+  var ?onLoad:Void->Void;
 }
 
 typedef PartialSoundParams =
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index c290e6553..d0183bf8e 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1243,19 +1243,24 @@ class FreeplayState extends MusicBeatSubState
       }
       else
       {
+        var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
         // TODO: Stream the instrumental of the selected song?
         FunkinSound.playMusic(daSongCapsule.songData.songId,
           {
-            startingVolume: 0.5,
+            startingVolume: 0.0,
             overrideExisting: true,
             restartTrack: false,
             pathsFunction: INST,
+            suffix: potentiallyErect,
             partialParams:
               {
                 loadPartial: true,
                 start: 0,
                 end: 0.1
-              }
+              },
+            onLoad: function() {
+              FlxG.sound.music.fadeIn(2, 0, 0.4);
+            }
           });
       }
       grpCapsules.members[curSelected].selected = true;

From c0485fd1a23f0f683b79935245bc89af18d15e4a Mon Sep 17 00:00:00 2001
From: gamerbross <blas333blas333blas@gmail.com>
Date: Sat, 11 May 2024 20:11:51 +0200
Subject: [PATCH 03/96] Fix Freeplay Crash when song is invalid

---
 source/funkin/ui/freeplay/FreeplayState.hx | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 7b7543845..a359010d3 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -29,6 +29,7 @@ import funkin.graphics.shaders.StrokeShader;
 import funkin.input.Controls;
 import funkin.play.PlayStatePlaylist;
 import funkin.play.song.Song;
+import funkin.ui.story.Level;
 import funkin.save.Save;
 import funkin.save.Save.SaveScoreData;
 import funkin.ui.AtlasText;
@@ -191,10 +192,24 @@ class FreeplayState extends MusicBeatSubState
     // programmatically adds the songs via LevelRegistry and SongRegistry
     for (levelId in LevelRegistry.instance.listSortedLevelIds())
     {
-      for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
+      var level:Level = LevelRegistry.instance.fetchEntry(levelId);
+
+      if (level == null)
+      {
+        trace('[WARN] Could not find level with id (${levelId})');
+        continue;
+      }
+
+      for (songId in level.getSongs())
       {
         var song:Song = SongRegistry.instance.fetchEntry(songId);
 
+        if (song == null)
+        {
+          trace('[WARN] Could not find song with id (${songId})');
+          continue;
+        }
+
         // Only display songs which actually have available charts for the current character.
         var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
         if (availableDifficultiesForSong.length == 0) continue;

From b10872e8e89b7e9ab404c57a26a7d8646b1921b9 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Fri, 17 May 2024 23:51:07 +0200
Subject: [PATCH 04/96] Fix TitleState late start + enter spam crash

---
 source/funkin/ui/title/TitleState.hx | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx
index 49bef5e4a..c9b3619e9 100644
--- a/source/funkin/ui/title/TitleState.hx
+++ b/source/funkin/ui/title/TitleState.hx
@@ -67,9 +67,11 @@ class TitleState extends MusicBeatState
     // DEBUG BULLSHIT
 
     // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
-    new FlxTimer().start(1, function(tmr:FlxTimer) {
+    if (!initialized) new FlxTimer().start(1, function(tmr:FlxTimer) {
       startIntro();
     });
+    else
+      startIntro();
   }
 
   function client_onMetaData(metaData:Dynamic)
@@ -118,7 +120,7 @@ class TitleState extends MusicBeatState
 
   function startIntro():Void
   {
-    playMenuMusic();
+    if (!initialized || FlxG.sound.music == null) playMenuMusic();
 
     persistentUpdate = true;
 
@@ -231,7 +233,7 @@ class TitleState extends MusicBeatState
         overrideExisting: true,
         restartTrack: true
       });
-    // Fade from 0.0 to 0.7 over 4 seconds
+    // Fade from 0.0 to 1 over 4 seconds
     if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
   }
 

From faf7a0643cd83bbf99ac99e302c6aaf2bcdebd30 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sun, 19 May 2024 01:47:36 -0400
Subject: [PATCH 05/96] Tinkered with ghost tapping some, leaving it off for
 now tho.

---
 source/funkin/play/PlayState.hx       | 24 ++++++++++++++----------
 source/funkin/play/notes/Strumline.hx | 14 ++++++++++++++
 2 files changed, 28 insertions(+), 10 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 44ad819c4..5b95c467c 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2298,8 +2298,6 @@ class PlayState extends MusicBeatSubState
     var notesInRange:Array<NoteSprite> = playerStrumline.getNotesMayHit();
     var holdNotesInRange:Array<SustainTrail> = playerStrumline.getHoldNotesHitOrMissed();
 
-    // If there are notes in range, pressing a key will cause a ghost miss.
-
     var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []];
 
     for (note in notesInRange)
@@ -2321,17 +2319,27 @@ class PlayState extends MusicBeatSubState
 
         // Play the strumline animation.
         playerStrumline.playPress(input.noteDirection);
+        trace('PENALTY Score: ${songScore}');
       }
-      else if (Constants.GHOST_TAPPING && (holdNotesInRange.length + notesInRange.length > 0) && notesInDirection.length == 0)
+      else if (Constants.GHOST_TAPPING && (!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
       {
-        // Pressed a wrong key with no notes nearby AND with notes in a different direction available.
+        // Pressed a wrong key with notes visible on-screen.
         // Perform a ghost miss (anti-spam).
         ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
 
         // Play the strumline animation.
         playerStrumline.playPress(input.noteDirection);
+        trace('PENALTY Score: ${songScore}');
       }
-      else if (notesInDirection.length > 0)
+      else if (notesInDirection.length == 0)
+      {
+        // Press a key with no penalty.
+
+        // Play the strumline animation.
+        playerStrumline.playPress(input.noteDirection);
+        trace('NO PENALTY Score: ${songScore}');
+      }
+      else
       {
         // Choose the first note, deprioritizing low priority notes.
         var targetNote:Null<NoteSprite> = notesInDirection.find((note) -> !note.lowPriority);
@@ -2341,17 +2349,13 @@ class PlayState extends MusicBeatSubState
         // Judge and hit the note.
         trace('Hit note! ${targetNote.noteData}');
         goodNoteHit(targetNote, input);
+        trace('Score: ${songScore}');
 
         notesInDirection.remove(targetNote);
 
         // Play the strumline animation.
         playerStrumline.playConfirm(input.noteDirection);
       }
-      else
-      {
-        // Play the strumline animation.
-        playerStrumline.playPress(input.noteDirection);
-      }
     }
 
     while (inputReleaseQueue.length > 0)
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 6a18f17d5..220b6723c 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -171,6 +171,20 @@ class Strumline extends FlxSpriteGroup
     updateNotes();
   }
 
+  /**
+   * Returns `true` if no notes are in range of the strumline and the player can spam without penalty.
+   */
+  public function mayGhostTap():Bool
+  {
+    // TODO: Refine this. Only querying "can be hit" is too tight but "is being rendered" is too loose.
+    // Also, if you just hit a note, there should be a (short) period where this is off so you can't spam.
+
+    // If there are any notes on screen, we can't ghost tap.
+    return notes.members.filter(function(note:NoteSprite) {
+      return note != null && note.alive && !note.hasBeenHit;
+    }).length == 0;
+  }
+
   /**
    * Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
    * @return An array of `NoteSprite` objects.

From 228ac66cc2e9966c0eae1f4bc050477ff9cba93f Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sun, 19 May 2024 01:48:51 -0400
Subject: [PATCH 06/96] Credit the song's charter in the pause menu.

---
 assets                                        |  2 +-
 source/funkin/data/song/SongData.hx           |  3 +
 source/funkin/data/song/SongRegistry.hx       |  2 +-
 .../data/song/importer/FNFLegacyImporter.hx   |  2 +-
 source/funkin/play/PauseSubState.hx           | 89 +++++++++++++++++--
 source/funkin/play/song/Song.hx               | 15 ++++
 .../ui/debug/charting/ChartEditorState.hx     |  2 +-
 .../toolboxes/ChartEditorMetadataToolbox.hx   | 16 ++++
 source/funkin/util/Constants.hx               |  5 ++
 9 files changed, 127 insertions(+), 9 deletions(-)

diff --git a/assets b/assets
index fd112e293..778e16705 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit fd112e293ee0f823ee98d5b8bd8a85e934f772f6
+Subproject commit 778e16705b30af85087f627594c22f4b5ba6141a
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 26380947a..bd25139a7 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -30,6 +30,9 @@ class SongMetadata implements ICloneable<SongMetadata>
   @:default("Unknown")
   public var artist:String;
 
+  @:optional
+  public var charter:Null<String> = null;
+
   @:optional
   @:default(96)
   public var divisions:Null<Int>; // Optional field
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index 277dcd9e1..a3305c4ec 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
    * Handle breaking changes by incrementing this value
    * and adding migration to the `migrateStageData()` function.
    */
-  public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2";
+  public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.3";
 
   public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
 
diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index ab2abda8e..fdfac7f72 100644
--- a/source/funkin/data/song/importer/FNFLegacyImporter.hx
+++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx
@@ -36,7 +36,7 @@ class FNFLegacyImporter
   {
     trace('Migrating song metadata from FNF Legacy.');
 
-    var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
+    var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
 
     var hadError:Bool = false;
 
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index fb9d9b4e2..c345871a9 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -101,6 +101,10 @@ class PauseSubState extends MusicBeatSubState
    */
   static final MUSIC_FINAL_VOLUME:Float = 0.75;
 
+  static final CHARTER_FADE_DELAY:Float = 15.0;
+
+  static final CHARTER_FADE_DURATION:Float = 0.75;
+
   /**
    * Defines which pause music to use.
    */
@@ -163,6 +167,12 @@ class PauseSubState extends MusicBeatSubState
    */
   var metadataDeaths:FlxText;
 
+  /**
+   * A text object which displays the current song's artist.
+   * Fades to the charter after a period before fading back.
+   */
+  var metadataArtist:FlxText;
+
   /**
    * The actual text objects for the menu entries.
    */
@@ -203,6 +213,8 @@ class PauseSubState extends MusicBeatSubState
     regenerateMenu();
 
     transitionIn();
+
+    startCharterTimer();
   }
 
   /**
@@ -222,6 +234,8 @@ class PauseSubState extends MusicBeatSubState
   public override function destroy():Void
   {
     super.destroy();
+    charterFadeTween.destroy();
+    charterFadeTween = null;
     pauseMusic.stop();
   }
 
@@ -270,16 +284,25 @@ class PauseSubState extends MusicBeatSubState
     metadata.scrollFactor.set(0, 0);
     add(metadata);
 
-    var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist');
+    var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name');
     metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     if (PlayState.instance?.currentChart != null)
     {
-      metadataSong.text = '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
+      metadataSong.text = '${PlayState.instance.currentChart.songName}';
     }
     metadataSong.scrollFactor.set(0, 0);
     metadata.add(metadataSong);
 
-    var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: ');
+    metadataArtist = new FlxText(20, metadataSong.y + 32, FlxG.width - 40, 'Artist: ${Constants.DEFAULT_ARTIST}');
+    metadataArtist.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+    if (PlayState.instance?.currentChart != null)
+    {
+      metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}';
+    }
+    metadataArtist.scrollFactor.set(0, 0);
+    metadata.add(metadataArtist);
+
+    var metadataDifficulty:FlxText = new FlxText(20, metadataArtist.y + 32, FlxG.width - 40, 'Difficulty: ');
     metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     if (PlayState.instance?.currentDifficulty != null)
     {
@@ -288,12 +311,12 @@ class PauseSubState extends MusicBeatSubState
     metadataDifficulty.scrollFactor.set(0, 0);
     metadata.add(metadataDifficulty);
 
-    metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
+    metadataDeaths = new FlxText(20, metadataDifficulty.y + 32, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
     metadataDeaths.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     metadataDeaths.scrollFactor.set(0, 0);
     metadata.add(metadataDeaths);
 
-    metadataPractice = new FlxText(20, 15 + 96, FlxG.width - 40, 'PRACTICE MODE');
+    metadataPractice = new FlxText(20, metadataDeaths.y + 32, FlxG.width - 40, 'PRACTICE MODE');
     metadataPractice.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
     metadataPractice.scrollFactor.set(0, 0);
@@ -302,6 +325,62 @@ class PauseSubState extends MusicBeatSubState
     updateMetadataText();
   }
 
+  var charterFadeTween:Null<FlxTween> = null;
+
+  function startCharterTimer():Void
+  {
+    charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION,
+      {
+        startDelay: CHARTER_FADE_DELAY,
+        ease: FlxEase.quartOut,
+        onComplete: (_) -> {
+          if (PlayState.instance?.currentChart != null)
+          {
+            metadataArtist.text = 'Charter: ${PlayState.instance.currentChart.charter ?? 'Unknown'}';
+          }
+          else
+          {
+            metadataArtist.text = 'Charter: ${Constants.DEFAULT_CHARTER}';
+          }
+
+          FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION,
+            {
+              ease: FlxEase.quartOut,
+              onComplete: (_) -> {
+                startArtistTimer();
+              }
+            });
+        }
+      });
+  }
+
+  function startArtistTimer():Void
+  {
+    charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION,
+      {
+        startDelay: CHARTER_FADE_DELAY,
+        ease: FlxEase.quartOut,
+        onComplete: (_) -> {
+          if (PlayState.instance?.currentChart != null)
+          {
+            metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}';
+          }
+          else
+          {
+            metadataArtist.text = 'Artist: ${Constants.DEFAULT_ARTIST}';
+          }
+
+          FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION,
+            {
+              ease: FlxEase.quartOut,
+              onComplete: (_) -> {
+                startCharterTimer();
+              }
+            });
+        }
+      });
+  }
+
   /**
    * Perform additional animations to transition the pause menu in when it is first displayed.
    */
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 23d8d2198..5da78e9df 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -120,6 +120,18 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return DEFAULT_ARTIST;
   }
 
+  /**
+   * The artist of the song.
+   */
+  public var charter(get, never):String;
+
+  function get_charter():String
+  {
+    if (_data != null) return _data?.charter ?? 'Unknown';
+    if (_metadata.size() > 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.charter ?? 'Unknown';
+    return Constants.DEFAULT_CHARTER;
+  }
+
   /**
    * @param id The ID of the song to load.
    * @param ignoreErrors If false, an exception will be thrown if the song data could not be loaded.
@@ -270,6 +282,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
 
         difficulty.songName = metadata.songName;
         difficulty.songArtist = metadata.artist;
+        difficulty.charter = metadata.charter ?? Constants.DEFAULT_CHARTER;
         difficulty.timeFormat = metadata.timeFormat;
         difficulty.divisions = metadata.divisions;
         difficulty.timeChanges = metadata.timeChanges;
@@ -334,6 +347,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
         {
           difficulty.songName = metadata.songName;
           difficulty.songArtist = metadata.artist;
+          difficulty.charter = metadata.charter ?? Constants.DEFAULT_CHARTER;
           difficulty.timeFormat = metadata.timeFormat;
           difficulty.divisions = metadata.divisions;
           difficulty.timeChanges = metadata.timeChanges;
@@ -586,6 +600,7 @@ class SongDifficulty
 
   public var songName:String = Constants.DEFAULT_SONGNAME;
   public var songArtist:String = Constants.DEFAULT_ARTIST;
+  public var charter:String = Constants.DEFAULT_CHARTER;
   public var timeFormat:SongTimeFormat = Constants.DEFAULT_TIMEFORMAT;
   public var divisions:Null<Int> = null;
   public var looped:Bool = false;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index a313981f4..980f5db4f 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1270,7 +1270,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     var result:Null<SongMetadata> = songMetadata.get(selectedVariation);
     if (result == null)
     {
-      result = new SongMetadata('DadBattle', 'Kawai Sprite', selectedVariation);
+      result = new SongMetadata('Default Song Name', Constants.DEFAULT_ARTIST, selectedVariation);
       songMetadata.set(selectedVariation, result);
     }
     return result;
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index f85307c64..80a421d80 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -29,6 +29,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
 {
   var inputSongName:TextField;
   var inputSongArtist:TextField;
+  var inputSongCharter:TextField;
   var inputStage:DropDown;
   var inputNoteStyle:DropDown;
   var buttonCharacterPlayer:Button;
@@ -89,6 +90,20 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
       }
     };
 
+    inputSongCharter.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.text != null && event.target.text != '';
+
+      if (valid)
+      {
+        inputSongCharter.removeClass('invalid-value');
+        chartEditorState.currentSongMetadata.charter = event.target.text;
+      }
+      else
+      {
+        chartEditorState.currentSongMetadata.charter = null;
+      }
+    };
+
     inputStage.onChange = function(event:UIEvent) {
       var valid:Bool = event.data != null && event.data.id != null;
 
@@ -176,6 +191,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
 
     inputSongName.value = chartEditorState.currentSongMetadata.songName;
     inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
+    inputSongCharter.value = chartEditorState.currentSongMetadata.charter;
     inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
     inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
     inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index 2f3b570b3..4e706c612 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -248,6 +248,11 @@ class Constants
    */
   public static final DEFAULT_ARTIST:String = 'Unknown';
 
+  /**
+   * The default charter for songs.
+   */
+  public static final DEFAULT_CHARTER:String = 'Unknown';
+
   /**
    * The default note style for songs.
    */

From 13595fca700d99c0a472687b20b1ba6170eec58f Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sun, 19 May 2024 01:49:06 -0400
Subject: [PATCH 07/96] Changelog entry for chart metadata

---
 source/funkin/data/song/CHANGELOG.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md
index 3cd3af070..4f1c66ade 100644
--- a/source/funkin/data/song/CHANGELOG.md
+++ b/source/funkin/data/song/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [2.2.3]
+### Added
+- Added `charter` field to denote authorship of a chart.
+
 ## [2.2.2]
 ### Added
 - Added `playData.previewStart` and `playData.previewEnd` fields to specify when in the song should the song's audio should be played as a preview in Freeplay.

From dcfc51cdcd53cd52b1b5ee34f0df6778afa1f2b9 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 20 May 2024 01:37:35 +0200
Subject: [PATCH 08/96] Fix Charting Sustain Trails Inverted

---
 .../ui/debug/charting/components/ChartEditorHoldNoteSprite.hx   | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index aeb6dd0e4..7c20358a4 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -36,6 +36,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
     zoom *= 0.7;
     zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
 
+    flipY = false;
+
     setup();
   }
 

From 9a18e3fde6ee779ca391dece4a0d94e79f584501 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 20 May 2024 01:38:52 +0200
Subject: [PATCH 09/96] Fix Charting Dragging Sustain Trails

---
 source/funkin/ui/debug/charting/ChartEditorState.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index b75cd8bf1..d426abaaf 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -4566,8 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             }
 
             gridGhostHoldNote.visible = true;
-            gridGhostHoldNote.noteData = gridGhostNote.noteData;
-            gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
+            gridGhostHoldNote.noteData = currentPlaceNoteData;
+            gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
 
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);

From 1050176b274014a17b7714ef75a26c8a8684cb46 Mon Sep 17 00:00:00 2001
From: richTrash21 <superboss865@gmail.com>
Date: Mon, 20 May 2024 23:52:48 +0400
Subject: [PATCH 10/96] main menu camera fix

---
 source/funkin/ui/debug/DebugMenuSubState.hx | 1 -
 source/funkin/ui/mainmenu/MainMenuState.hx  | 5 ++++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx
index 6d6e73e80..f8b1be9d2 100644
--- a/source/funkin/ui/debug/DebugMenuSubState.hx
+++ b/source/funkin/ui/debug/DebugMenuSubState.hx
@@ -62,7 +62,6 @@ class DebugMenuSubState extends MusicBeatSubState
     #if sys
     createItem("OPEN CRASH LOG FOLDER", openLogFolder);
     #end
-    FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y));
     FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500));
   }
 
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index 7a21a6e8f..9af4e299f 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -49,6 +49,8 @@ class MainMenuState extends MusicBeatState
     DiscordClient.changePresence("In the Menus", null);
     #end
 
+    FlxG.cameras.reset(new FunkinCamera('mainMenu'));
+
     transIn = FlxTransitionableState.defaultTransIn;
     transOut = FlxTransitionableState.defaultTransOut;
 
@@ -170,7 +172,6 @@ class MainMenuState extends MusicBeatState
 
   function resetCamStuff():Void
   {
-    FlxG.cameras.reset(new FunkinCamera('mainMenu'));
     FlxG.camera.follow(camFollow, null, 0.06);
     FlxG.camera.snapToTarget();
   }
@@ -329,6 +330,8 @@ class MainMenuState extends MusicBeatState
       persistentUpdate = false;
 
       FlxG.state.openSubState(new DebugMenuSubState());
+      // reset camera when debug menu is closed
+      subStateClosed.addOnce(_ -> resetCamStuff());
     }
     #end
 

From d84e832c6c9152abab106290dab72e5792fc6808 Mon Sep 17 00:00:00 2001
From: sector-a <82838084+sector-a@users.noreply.github.com>
Date: Wed, 22 May 2024 12:57:57 +0300
Subject: [PATCH 11/96] Make texts update on difficulty change in Story Menu

---
 source/funkin/ui/story/StoryMenuState.hx | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 0c2214529..820ac2ad1 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -466,6 +466,9 @@ class StoryMenuState extends MusicBeatState
       // Disable the funny music thing for now.
       // funnyMusicThing();
     }
+
+    updateText();
+    refresh();
   }
 
   final FADE_OUT_TIME:Float = 1.5;

From 9afc314a0d20f29d69beba70d91d23cc37922660 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 22 May 2024 15:20:53 -0400
Subject: [PATCH 12/96] Fix an issue with git modules

---
 .gitmodules | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/.gitmodules b/.gitmodules
index be5e0aaa8..2d5c11067 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,5 @@
 [submodule "assets"]
 	path = assets
-	url = https://github.com/FunkinCrew/funkin.assets
-[submodule "art"]
+	url = https://github.com/FunkinCrew/Funkin-Assets-secret
 	path = art
-	url = https://github.com/FunkinCrew/funkin.art
+	url = https://github.com/FunkinCrew/Funkin-Art-secret

From a2ee359e466746646c278d0adbef0c1fd08d21c1 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 23 May 2024 16:31:18 -0400
Subject: [PATCH 13/96] fix for songs overlapping each other on desktop

---
 source/funkin/audio/FunkinSound.hx | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index 5a49e29ee..aaddda9dc 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -364,10 +364,20 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
       if (music != null)
       {
+        for (future in partialQueue)
+        {
+          future = cast Future.withError("Music was overridden by another partial load");
+        }
+        partialQueue = [];
+        partialQueue.push(music);
+
+        @:nullSafety(Off)
         music.onComplete(function(partialMusic:Null<FunkinSound>) {
-          @:nullSafety(Off)
-          FlxG.sound.music = partialMusic;
-          FlxG.sound.list.remove(FlxG.sound.music);
+          if (partialQueue.pop() == music)
+          {
+            FlxG.sound.music = partialMusic;
+            FlxG.sound.list.remove(FlxG.sound.music);
+          }
         });
 
         return true;
@@ -396,6 +406,8 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     }
   }
 
+  static var partialQueue:Array<Future<Null<FunkinSound>>> = [];
+
   /**
    * Creates a new `FunkinSound` object synchronously.
    *
@@ -461,6 +473,8 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
    * @param looped Whether the sound file should loop
    * @param autoDestroy Whether the sound file should be destroyed after it finishes playing
    * @param autoPlay Whether the sound file should play immediately
+   * @param onComplete Callback when the sound finishes playing
+   * @param onLoad Callback when the sound finishes loading
    * @return A FunkinSound object
    */
   public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false,

From 0e920237940d894ffb9be77f67f4d73a8463d1f3 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 24 May 2024 00:21:45 -0400
Subject: [PATCH 14/96] Add credits for all charts + tweak charts for Guns, Lit
 Up, Winter Horrorland

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 778e16705..826be3bf1 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 778e16705b30af85087f627594c22f4b5ba6141a
+Subproject commit 826be3bf1e635e6b61c8c11bd3ece51f8a2b3061

From c8930b598025f6f9faff795aeb3280ae3480c6b6 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 24 May 2024 13:45:37 -0400
Subject: [PATCH 15/96] Attempt to repair local submodules

---
 .gitmodules | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitmodules b/.gitmodules
index 2d5c11067..452c0089b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,5 +1,6 @@
 [submodule "assets"]
 	path = assets
 	url = https://github.com/FunkinCrew/Funkin-Assets-secret
+[submodule "art"]
 	path = art
 	url = https://github.com/FunkinCrew/Funkin-Art-secret

From 98505e58eca59a084bdd5fc98765ee53aca7c926 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 24 May 2024 14:04:55 -0400
Subject: [PATCH 16/96] Take two at fixing submodules

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 66572f85d..52e007f5b 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa
+Subproject commit 52e007f5b682ee7b9d252edba78a88780510d32b

From dd3e241f0c08ae5239ba081c1022acc96e993abe Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 24 May 2024 16:44:12 -0400
Subject: [PATCH 17/96] Fix some merge conflicts

---
 source/funkin/input/Controls.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx
index 31551dec9..345791eef 100644
--- a/source/funkin/input/Controls.hx
+++ b/source/funkin/input/Controls.hx
@@ -997,7 +997,7 @@ class Controls extends FlxActionSet
     for (control in Control.createAll())
     {
       var inputs:Array<Int> = Reflect.field(data, control.getName());
-      inputs = inputs.unique();
+      inputs = inputs.distinct();
       if (inputs != null)
       {
         if (inputs.length == 0) {
@@ -1050,7 +1050,7 @@ class Controls extends FlxActionSet
       if (inputs.length == 0) {
         inputs = [FlxKey.NONE];
       } else {
-        inputs = inputs.unique();
+        inputs = inputs.distinct();
       }
 
       Reflect.setField(data, control.getName(), inputs);

From 07959d3e88898048cc1037a43310c20223cf18b1 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Sat, 25 May 2024 01:08:17 +0200
Subject: [PATCH 18/96] Fix Unscripted Stage Log Trace

---
 source/funkin/data/BaseRegistry.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 118516bec..2df3a87da 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -117,7 +117,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
         var entry:T = createEntry(entryId);
         if (entry != null)
         {
-          trace('  Loaded entry data: ${entry}');
+          trace('  Loaded entry data: ${entry.id}');
           entries.set(entry.id, entry);
         }
       }

From 4acfbfb76ddae10fae3cf848c1b7bbf4524baa55 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Sat, 25 May 2024 01:21:37 +0200
Subject: [PATCH 19/96] Fix Legacy Chart Importer wrong instrumental

---
 source/funkin/data/song/importer/FNFLegacyImporter.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index ab2abda8e..87f3f8136 100644
--- a/source/funkin/data/song/importer/FNFLegacyImporter.hx
+++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx
@@ -65,7 +65,7 @@ class FNFLegacyImporter
 
     songMetadata.timeChanges = rebuildTimeChanges(songData);
 
-    songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom');
+    songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
 
     return songMetadata;
   }

From cf61b9ef90810451c1a186c0388325f4d012bb9a Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Tue, 28 May 2024 14:51:16 +0200
Subject: [PATCH 20/96] Add toString to Stage + Revert "Fix Unscripted Stage
 Log Trace"

---
 source/funkin/data/BaseRegistry.hx | 2 +-
 source/funkin/play/stage/Stage.hx  | 5 +++++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 2df3a87da..118516bec 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -117,7 +117,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
         var entry:T = createEntry(entryId);
         if (entry != null)
         {
-          trace('  Loaded entry data: ${entry.id}');
+          trace('  Loaded entry data: ${entry}');
           entries.set(entry.id, entry);
         }
       }
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index eb9eb1810..a6a4293a0 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -852,6 +852,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
     }
   }
 
+  public override function toString():String
+  {
+    return 'Stage($id)';
+  }
+
   static function _fetchData(id:String):Null<StageData>
   {
     return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));

From 1ae30283a3a0d2a3bafb6d4da84d906764e80ed6 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 28 May 2024 15:34:09 -0400
Subject: [PATCH 21/96] re add the xmlns schema stuff

---
 Project.xml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/Project.xml b/Project.xml
index 24cdac270..dce45546f 100644
--- a/Project.xml
+++ b/Project.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<project>
+<project xmlns="http://lime.openfl.org/project/1.0.4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+xsi:schemaLocation="http://lime.openfl.org/project/1.0.4 http://lime.openfl.org/xsd/project-1.0.4.xsd">
 	<!-- _________________________ Application Settings _________________________ -->
 	<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.3" company="ninjamuffin99" />
 	<!--Switch Export with Unique ApplicationID and Icon-->
@@ -14,6 +15,7 @@
 
 	<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
 	<set name="SWF_VERSION" value="11.8" />
+	<
 	<!-- ____________________________ Window Settings ___________________________ -->
 	<!--These window settings apply to all targets-->
 	<window width="1280" height="720" fps="60" background="#000000" hardware="true" vsync="false" />
@@ -28,7 +30,7 @@
 	<set name="BUILD_DIR" value="export/debug" if="debug" />
 	<set name="BUILD_DIR" value="export/release" unless="debug" />
 	<set name="BUILD_DIR" value="export/32bit" if="32bit" />
-	<classpath name="source" />
+	<source path="source" />
 	<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
 	<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
 	<define name="PRELOAD_ALL" unless="web" />

From 01b6a11ddbbacc034f73b61451b78587b3536b7d Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 28 May 2024 22:57:01 -0400
Subject: [PATCH 22/96] flxpartialsound lock to current version

---
 hmm.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/hmm.json b/hmm.json
index 6b119c52f..5260d5229 100644
--- a/hmm.json
+++ b/hmm.json
@@ -4,7 +4,7 @@
       "name": "FlxPartialSound",
       "type": "git",
       "dir": null,
-      "ref": "main",
+      "ref": "8bb8ed50f520d9cd64a65414b119b8718924b93a",
       "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
     },
     {
@@ -178,4 +178,4 @@
       "url": "https://github.com/FunkinCrew/thx.semver"
     }
   ]
-}
\ No newline at end of file
+}

From 2d300039ae42988c570af0f36225c1732a4fb09c Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 28 May 2024 23:53:50 -0400
Subject: [PATCH 23/96] promises + error out partial sounds that attempt to
 load multiple times

---
 source/funkin/audio/FunkinSound.hx | 32 +++++++++++++++++-------------
 1 file changed, 18 insertions(+), 14 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index aaddda9dc..39a26aac1 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -360,24 +360,24 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     if (shouldLoadPartial)
     {
       var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0, params.partialParams?.end ?? 1, params?.startingVolume ?? 1.0,
-        params.loop ?? true, false, true, params.onComplete, params.onLoad);
+        params.loop ?? true, false, false, params.onComplete);
 
       if (music != null)
       {
-        for (future in partialQueue)
+        while (partialQueue.length > 0)
         {
-          future = cast Future.withError("Music was overridden by another partial load");
+          @:nullSafety(Off)
+          partialQueue.pop().error("Cancel loading partial sound");
         }
-        partialQueue = [];
+
         partialQueue.push(music);
 
         @:nullSafety(Off)
-        music.onComplete(function(partialMusic:Null<FunkinSound>) {
-          if (partialQueue.pop() == music)
-          {
-            FlxG.sound.music = partialMusic;
-            FlxG.sound.list.remove(FlxG.sound.music);
-          }
+        music.future.onComplete(function(partialMusic:Null<FunkinSound>) {
+          FlxG.sound.music = partialMusic;
+          FlxG.sound.list.remove(FlxG.sound.music);
+
+          if (params.onLoad != null) params.onLoad();
         });
 
         return true;
@@ -406,7 +406,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     }
   }
 
-  static var partialQueue:Array<Future<Null<FunkinSound>>> = [];
+  static var partialQueue:Array<Promise<Null<FunkinSound>>> = [];
 
   /**
    * Creates a new `FunkinSound` object synchronously.
@@ -478,7 +478,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
    * @return A FunkinSound object
    */
   public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false,
-      autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Future<Null<FunkinSound>>
+      autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Promise<Null<FunkinSound>>
   {
     var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
 
@@ -488,12 +488,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
     var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
 
-    soundRequest.onComplete(function(partialSound) {
+    promise.future.onError(function(e) {
+      soundRequest.error("Sound loading was errored or cancelled");
+    });
+
+    soundRequest.future.onComplete(function(partialSound) {
       var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
       promise.complete(snd);
     });
 
-    return promise.future;
+    return promise;
   }
 
   @:nullSafety(Off)

From 1f64c7fcc9757004925fdb641f5b9f19be248e5f Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 28 May 2024 23:54:23 -0400
Subject: [PATCH 24/96] update hmm flxpartialsound

---
 hmm.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hmm.json b/hmm.json
index 5260d5229..91d2f08bb 100644
--- a/hmm.json
+++ b/hmm.json
@@ -4,7 +4,7 @@
       "name": "FlxPartialSound",
       "type": "git",
       "dir": null,
-      "ref": "8bb8ed50f520d9cd64a65414b119b8718924b93a",
+      "ref": "44aa7eb",
       "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
     },
     {

From d97d77566e1e30800bb3c5f8ba973af38a8ded0b Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 29 May 2024 00:53:32 -0400
Subject: [PATCH 25/96] Make song score lerp faster

---
 source/funkin/ui/story/StoryMenuState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 820ac2ad1..c1a001e5d 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -306,7 +306,7 @@ class StoryMenuState extends MusicBeatState
   {
     Conductor.instance.update();
 
-    highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.5));
+    highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.25));
 
     scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}';
 

From ee4be810622f896fb1ff12e9f9ad03889e85f455 Mon Sep 17 00:00:00 2001
From: Your Name <survivaltemer@gmail.com>
Date: Wed, 29 May 2024 15:16:31 -0300
Subject: [PATCH 26/96] Now Preloader resets CWD

---
 source/funkin/ui/transition/preload/FunkinPreloader.hx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx
index b71af2b3b..9d2569588 100644
--- a/source/funkin/ui/transition/preload/FunkinPreloader.hx
+++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx
@@ -136,6 +136,8 @@ class FunkinPreloader extends FlxBasePreloader
     // We can't even call trace() yet, until Flixel loads.
     trace('Initializing custom preloader...');
 
+    funkin.util.CLIUtil.resetWorkingDir();
+
     this.siteLockTitleText = Constants.SITE_LOCK_TITLE;
     this.siteLockBodyText = Constants.SITE_LOCK_DESC;
   }

From fb752ddd7860248c208fc612a4630f4be2bcee1c Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 29 May 2024 17:05:20 -0400
Subject: [PATCH 27/96] remove random < in project.xml lol

---
 Project.xml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Project.xml b/Project.xml
index 16e4b9854..fcd29a25e 100644
--- a/Project.xml
+++ b/Project.xml
@@ -15,7 +15,6 @@ xsi:schemaLocation="http://lime.openfl.org/project/1.0.4 http://lime.openfl.org/
 
 	<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
 	<set name="SWF_VERSION" value="11.8" />
-	<
 	<!-- ____________________________ Window Settings ___________________________ -->
 	<!--These window settings apply to all targets-->
 	<window width="1280" height="720" fps="60" background="#000000" hardware="true" vsync="false" />

From 8d7591a796f9c0c392eb66c8e494d5f9a7a5e36a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 29 May 2024 21:43:54 -0400
Subject: [PATCH 28/96] Fix an issue where Story Menu props wouldn't render.

---
 source/funkin/ui/story/LevelProp.hx | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx
index 5a3efc36a..0547404a1 100644
--- a/source/funkin/ui/story/LevelProp.hx
+++ b/source/funkin/ui/story/LevelProp.hx
@@ -11,12 +11,15 @@ class LevelProp extends Bopper
   function set_propData(value:LevelPropData):LevelPropData
   {
     // Only reset the prop if the asset path has changed.
-    if (propData == null || value?.assetPath != propData?.assetPath)
+    if (propData == null || !(thx.Dynamics.equals(value, propData)))
     {
+      this.propData = value;
+
+      this.visible = this.propData != null;
+      danceEvery = this.propData?.danceEvery ?? 0;
+
       applyData();
     }
-    this.visible = (value != null);
-    danceEvery = this.propData?.danceEvery ?? 0;
 
     return this.propData;
   }

From 174c595837a63fef473ad191576e51862c240cc0 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 29 May 2024 22:49:57 -0400
Subject: [PATCH 29/96] Fix crash caused by improperly canceling a tween

---
 source/funkin/play/PauseSubState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index c345871a9..8c45fac65 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -234,7 +234,7 @@ class PauseSubState extends MusicBeatSubState
   public override function destroy():Void
   {
     super.destroy();
-    charterFadeTween.destroy();
+    charterFadeTween.cancel();
     charterFadeTween = null;
     pauseMusic.stop();
   }

From b7a828e7d89756a20c7415876446ddda2c1cbd84 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 30 May 2024 05:25:51 -0400
Subject: [PATCH 30/96] Make Freeplay use correct ranks, play the slam
 animation after Results, new Results music

---
 assets                                     |   2 +-
 source/funkin/InitState.hx                 |   4 +-
 source/funkin/play/PlayState.hx            |   3 +-
 source/funkin/play/ResultState.hx          | 226 ++++++---------------
 source/funkin/play/scoring/Scoring.hx      | 176 ++++++++++++++++
 source/funkin/save/Save.hx                 |  13 +-
 source/funkin/ui/freeplay/FreeplayState.hx | 107 +++++++---
 source/funkin/ui/freeplay/SongMenuItem.hx  |  99 +++++----
 8 files changed, 389 insertions(+), 241 deletions(-)

diff --git a/assets b/assets
index 8fea0bf1f..2719d3fc1 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 8fea0bf1fe07b6dd0efb8ecf46dc8091b0177007
+Subproject commit 2719d3fc1d8f5d0cbafae8d27141d6c471148482
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index a945c10c5..d0009f95b 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -220,6 +220,8 @@ class InitState extends FlxState
       {
         storyMode: false,
         title: "CUM SONG",
+        songId: "cum",
+        difficultyId: "hard",
         isNewHighscore: true,
         scoreData:
           {
@@ -227,7 +229,7 @@ class InitState extends FlxState
             tallies:
               {
                 sick: 130,
-                good: 25,
+                good: 70,
                 bad: 69,
                 shit: 69,
                 missed: 69,
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index a95166e21..e69a50b00 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -3123,9 +3123,10 @@ class PlayState extends MusicBeatSubState
     var res:ResultState = new ResultState(
       {
         storyMode: PlayStatePlaylist.isStoryMode,
+        songId: currentChart.song.id,
+        difficultyId: currentDifficulty,
         title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
         prevScoreData: prevScoreData,
-        difficultyId: currentDifficulty,
         scoreData:
           {
             score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index ee7c8eade..79880038d 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -21,6 +21,7 @@ import funkin.audio.FunkinSound;
 import flixel.util.FlxGradient;
 import flixel.util.FlxTimer;
 import funkin.save.Save;
+import funkin.play.scoring.Scoring;
 import funkin.save.Save.SaveScoreData;
 import funkin.graphics.shaders.LeftMaskShader;
 import funkin.play.components.TallyCounter;
@@ -34,7 +35,7 @@ class ResultState extends MusicBeatSubState
 {
   final params:ResultsStateParams;
 
-  final rank:ResultRank;
+  final rank:ScoringRank;
   final songName:FlxBitmapText;
   final difficulty:FlxSprite;
   final clearPercentSmall:ClearPercentCounter;
@@ -64,8 +65,7 @@ class ResultState extends MusicBeatSubState
 
     this.params = params;
 
-    rank = calculateRank(params);
-    // rank = SHIT;
+    rank = Scoring.calculateRank(params.scoreData) ?? SHIT;
 
     // We build a lot of this stuff in the constructor, then place it in create().
     // This prevents having to do `null` checks everywhere.
@@ -99,6 +99,8 @@ class ResultState extends MusicBeatSubState
 
   override function create():Void
   {
+    if (FlxG.sound.music != null) FlxG.sound.music.stop();
+
     // Reset the camera zoom on the results screen.
     FlxG.camera.zoom = 1.0;
 
@@ -327,6 +329,33 @@ class ResultState extends MusicBeatSubState
       }
     };
 
+    new FlxTimer().start(rank.getMusicDelay(), _ -> {
+      if (rank.hasMusicIntro())
+      {
+        // Play the intro music.
+        var introMusic:String = Paths.music(rank.getMusicPath() + '/' + rank.getMusicPath() + '-intro');
+        FunkinSound.load(introMusic, 1.0, false, true, true, () -> {
+          FunkinSound.playMusic(rank.getMusicPath(),
+            {
+              startingVolume: 1.0,
+              overrideExisting: true,
+              restartTrack: true,
+              loop: rank.shouldMusicLoop()
+            });
+        });
+      }
+      else
+      {
+        FunkinSound.playMusic(rank.getMusicPath(),
+          {
+            startingVolume: 1.0,
+            overrideExisting: true,
+            restartTrack: true,
+            loop: rank.shouldMusicLoop()
+          });
+      }
+    });
+
     refresh();
 
     super.create();
@@ -376,7 +405,8 @@ class ResultState extends MusicBeatSubState
 
           displayRankText();
 
-          new FlxTimer().start(2.0, _ -> {
+          // previously 2.0 seconds
+          new FlxTimer().start(0.25, _ -> {
             FlxTween.tween(clearPercentCounter, {alpha: 0}, 0.5,
               {
                 startDelay: 0.5,
@@ -444,28 +474,6 @@ class ResultState extends MusicBeatSubState
   {
     showSmallClearPercent();
 
-    FunkinSound.playMusic(rank.getMusicPath(),
-      {
-        startingVolume: 1.0,
-        overrideExisting: true,
-        restartTrack: true,
-        loop: rank.shouldMusicLoop()
-      });
-
-    FlxG.sound.music.onComplete = () -> {
-      if (rank == SHIT)
-      {
-        FunkinSound.playMusic('bluu',
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: true,
-            loop: true
-          });
-        FlxG.sound.music.fadeIn(10.0, 0.0, 1.0);
-      }
-    }
-
     switch (rank)
     {
       case PERFECT | PERFECT_GOLD:
@@ -478,7 +486,6 @@ class ResultState extends MusicBeatSubState
           bfPerfect.visible = true;
           bfPerfect.playAnimation('');
         }
-
       case EXCELLENT:
         if (bfExcellent == null)
         {
@@ -489,7 +496,6 @@ class ResultState extends MusicBeatSubState
           bfExcellent.visible = true;
           bfExcellent.playAnimation('Intro');
         }
-
       case GREAT:
         if (bfGreat == null)
         {
@@ -500,7 +506,6 @@ class ResultState extends MusicBeatSubState
           bfGreat.visible = true;
           bfGreat.playAnimation('Intro');
         }
-
       case SHIT:
         if (bfShit == null)
         {
@@ -511,7 +516,6 @@ class ResultState extends MusicBeatSubState
           bfShit.visible = true;
           bfShit.playAnimation('Intro');
         }
-
       case GOOD:
         if (bfGood == null)
         {
@@ -521,7 +525,6 @@ class ResultState extends MusicBeatSubState
         {
           bfGood.animation.play('fall');
           bfGood.visible = true;
-
           new FlxTimer().start((1 / 24) * 22, _ -> {
             // plays about 22 frames (at 24fps timing) after bf spawns in
             if (gfGood != null)
@@ -635,154 +638,39 @@ class ResultState extends MusicBeatSubState
 
     if (controls.PAUSE)
     {
-      FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
-      FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1,
-        {
-          onComplete: _ -> {
-            FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
-          }
-        });
+      if (FlxG.sound.music != null)
+      {
+        FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
+        FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1,
+          {
+            onComplete: _ -> {
+              FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
+            }
+          });
+      }
       if (params.storyMode)
       {
         openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
       }
       else
       {
-        openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
+        openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(
+          {
+            {
+              fromResults:
+                {
+                  oldRank: Scoring.calculateRank(params?.prevScoreData),
+                  newRank: rank,
+                  songId: params.songId,
+                  difficultyId: params.difficultyId
+                }
+            }
+          }, sticker)));
       }
     }
 
     super.update(elapsed);
   }
-
-  public static function calculateRank(params:ResultsStateParams):ResultRank
-  {
-    // Perfect (Platinum) is a Sick Full Clear
-    var isPerfectGold = params.scoreData.tallies.sick == params.scoreData.tallies.totalNotes;
-    if (isPerfectGold) return ResultRank.PERFECT_GOLD;
-
-    // Else, use the standard grades
-
-    // Grade % (only good and sick), 1.00 is a full combo
-    var grade = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes;
-    // Clear % (including bad and shit). 1.00 is a full clear but not a full combo
-    var clear = (params.scoreData.tallies.totalNotesHit) / params.scoreData.tallies.totalNotes;
-
-    if (grade == Constants.RANK_PERFECT_THRESHOLD)
-    {
-      return ResultRank.PERFECT;
-    }
-    else if (grade >= Constants.RANK_EXCELLENT_THRESHOLD)
-    {
-      return ResultRank.EXCELLENT;
-    }
-    else if (grade >= Constants.RANK_GREAT_THRESHOLD)
-    {
-      return ResultRank.GREAT;
-    }
-    else if (grade >= Constants.RANK_GOOD_THRESHOLD)
-    {
-      return ResultRank.GOOD;
-    }
-    else
-    {
-      return ResultRank.SHIT;
-    }
-  }
-}
-
-enum abstract ResultRank(String)
-{
-  var PERFECT_GOLD;
-  var PERFECT;
-  var EXCELLENT;
-  var GREAT;
-  var GOOD;
-  var SHIT;
-
-  public function getMusicPath():String
-  {
-    switch (abstract)
-    {
-      case PERFECT_GOLD:
-        return 'resultsPERFECT';
-      case PERFECT:
-        return 'resultsPERFECT';
-      case EXCELLENT:
-        return 'resultsNORMAL';
-      case GREAT:
-        return 'resultsNORMAL';
-      case GOOD:
-        return 'resultsNORMAL';
-      case SHIT:
-        return 'resultsSHIT';
-      default:
-        return 'resultsNORMAL';
-    }
-  }
-
-  public function shouldMusicLoop():Bool
-  {
-    switch (abstract)
-    {
-      case PERFECT_GOLD:
-        return true;
-      case PERFECT:
-        return true;
-      case EXCELLENT:
-        return true;
-      case GREAT:
-        return true;
-      case GOOD:
-        return true;
-      case SHIT:
-        return false;
-      default:
-        return false;
-    }
-  }
-
-  public function getHorTextAsset()
-  {
-    switch (abstract)
-    {
-      case PERFECT_GOLD:
-        return 'resultScreen/rankText/rankScrollPERFECT';
-      case PERFECT:
-        return 'resultScreen/rankText/rankScrollPERFECT';
-      case EXCELLENT:
-        return 'resultScreen/rankText/rankScrollEXCELLENT';
-      case GREAT:
-        return 'resultScreen/rankText/rankScrollGREAT';
-      case GOOD:
-        return 'resultScreen/rankText/rankScrollGOOD';
-      case SHIT:
-        return 'resultScreen/rankText/rankScrollLOSS';
-      default:
-        return 'resultScreen/rankText/rankScrollGOOD';
-    }
-  }
-
-  public function getVerTextAsset()
-  {
-    switch (abstract)
-    {
-      case PERFECT_GOLD:
-        return 'resultScreen/rankText/rankTextPERFECT';
-      case PERFECT:
-        return 'resultScreen/rankText/rankTextPERFECT';
-      case EXCELLENT:
-        return 'resultScreen/rankText/rankTextEXCELLENT';
-      case GREAT:
-        return 'resultScreen/rankText/rankTextGREAT';
-      case GOOD:
-        return 'resultScreen/rankText/rankTextGOOD';
-      case SHIT:
-        return 'resultScreen/rankText/rankTextLOSS';
-      default:
-        return 'resultScreen/rankText/rankTextGOOD';
-    }
-  }
 }
 
 typedef ResultsStateParams =
@@ -797,6 +685,8 @@ typedef ResultsStateParams =
    */
   var title:String;
 
+  var songId:String;
+
   /**
    * Whether the displayed score is a new highscore
    */
diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx
index 744091b44..6155ec879 100644
--- a/source/funkin/play/scoring/Scoring.hx
+++ b/source/funkin/play/scoring/Scoring.hx
@@ -1,5 +1,7 @@
 package funkin.play.scoring;
 
+import funkin.save.Save.SaveScoreData;
+
 /**
  * Which system to use when scoring and judging notes.
  */
@@ -344,4 +346,178 @@ class Scoring
       return 'miss';
     }
   }
+
+  public static function calculateRank(scoreData:Null<SaveScoreData>):Null<ScoringRank>
+  {
+    if (scoreData == null) return null;
+
+    // Perfect (Platinum) is a Sick Full Clear
+    var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes;
+    if (isPerfectGold) return ScoringRank.PERFECT_GOLD;
+
+    // Else, use the standard grades
+
+    // Grade % (only good and sick), 1.00 is a full combo
+    var grade = (scoreData.tallies.sick + scoreData.tallies.good) / scoreData.tallies.totalNotes;
+    // Clear % (including bad and shit). 1.00 is a full clear but not a full combo
+    var clear = (scoreData.tallies.totalNotesHit) / scoreData.tallies.totalNotes;
+
+    if (grade == Constants.RANK_PERFECT_THRESHOLD)
+    {
+      return ScoringRank.PERFECT;
+    }
+    else if (grade >= Constants.RANK_EXCELLENT_THRESHOLD)
+    {
+      return ScoringRank.EXCELLENT;
+    }
+    else if (grade >= Constants.RANK_GREAT_THRESHOLD)
+    {
+      return ScoringRank.GREAT;
+    }
+    else if (grade >= Constants.RANK_GOOD_THRESHOLD)
+    {
+      return ScoringRank.GOOD;
+    }
+    else
+    {
+      return ScoringRank.SHIT;
+    }
+  }
+}
+
+enum abstract ScoringRank(String)
+{
+  var PERFECT_GOLD;
+  var PERFECT;
+  var EXCELLENT;
+  var GREAT;
+  var GOOD;
+  var SHIT;
+
+  /**
+   * Delay in seconds
+   */
+  public function getMusicDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 5.0;
+      case EXCELLENT:
+        return 1.75;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getMusicPath():String
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'resultsPERFECT';
+      case PERFECT:
+        return 'resultsPERFECT';
+      case EXCELLENT:
+        return 'resultsEXCELLENT';
+      case GREAT:
+        return 'resultsNORMAL';
+      case GOOD:
+        return 'resultsNORMAL';
+      case SHIT:
+        return 'resultsSHIT';
+      default:
+        return 'resultsNORMAL';
+    }
+  }
+
+  public function hasMusicIntro():Bool
+  {
+    switch (abstract)
+    {
+      case EXCELLENT:
+        return true;
+      case SHIT:
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  public function getFreeplayRankIconAsset():Null<String>
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'PERFECTSICK';
+      case PERFECT:
+        return 'PERFECT';
+      case EXCELLENT:
+        return 'EXCELLENT';
+      case GREAT:
+        return 'GREAT';
+      case GOOD:
+        return 'GOOD';
+      case SHIT:
+        return 'LOSS';
+      default:
+        return null;
+    }
+  }
+
+  public function shouldMusicLoop():Bool
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT | EXCELLENT | GREAT | GOOD:
+        return true;
+      case SHIT:
+        return false;
+      default:
+        return false;
+    }
+  }
+
+  public function getHorTextAsset()
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'resultScreen/rankText/rankScrollPERFECT';
+      case PERFECT:
+        return 'resultScreen/rankText/rankScrollPERFECT';
+      case EXCELLENT:
+        return 'resultScreen/rankText/rankScrollEXCELLENT';
+      case GREAT:
+        return 'resultScreen/rankText/rankScrollGREAT';
+      case GOOD:
+        return 'resultScreen/rankText/rankScrollGOOD';
+      case SHIT:
+        return 'resultScreen/rankText/rankScrollLOSS';
+      default:
+        return 'resultScreen/rankText/rankScrollGOOD';
+    }
+  }
+
+  public function getVerTextAsset()
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'resultScreen/rankText/rankTextPERFECT';
+      case PERFECT:
+        return 'resultScreen/rankText/rankTextPERFECT';
+      case EXCELLENT:
+        return 'resultScreen/rankText/rankTextEXCELLENT';
+      case GREAT:
+        return 'resultScreen/rankText/rankTextGREAT';
+      case GOOD:
+        return 'resultScreen/rankText/rankTextGOOD';
+      case SHIT:
+        return 'resultScreen/rankText/rankTextLOSS';
+      default:
+        return 'resultScreen/rankText/rankTextGOOD';
+    }
+  }
 }
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index 934d6a4aa..7f25a8e01 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -1,15 +1,17 @@
 package funkin.save;
 
 import flixel.util.FlxSave;
-import funkin.save.migrator.SaveDataMigrator;
-import thx.semver.Version;
 import funkin.input.Controls.Device;
+import funkin.play.scoring.Scoring;
+import funkin.play.scoring.Scoring.ScoringRank;
 import funkin.save.migrator.RawSaveData_v1_0_0;
 import funkin.save.migrator.SaveDataMigrator;
+import funkin.save.migrator.SaveDataMigrator;
 import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
 import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
-import thx.semver.Version;
 import funkin.util.SerializerUtil;
+import thx.semver.Version;
+import thx.semver.Version;
 
 @:nullSafety
 class Save
@@ -492,6 +494,11 @@ class Save
     return song.get(difficultyId);
   }
 
+  public function getSongRank(songId:String, difficultyId:String = 'normal'):Null<ScoringRank>
+  {
+    return Scoring.calculateRank(getSongScore(songId, difficultyId));
+  }
+
   /**
    * Apply the score the user achieved for a given song on a given difficulty.
    */
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 530f28c33..904a2ca4a 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -34,6 +34,8 @@ import funkin.play.song.Song;
 import funkin.save.Save;
 import funkin.save.Save.SaveScoreData;
 import funkin.ui.AtlasText;
+import funkin.play.scoring.Scoring;
+import funkin.play.scoring.Scoring.ScoringRank;
 import funkin.ui.mainmenu.MainMenuState;
 import funkin.ui.MusicBeatSubState;
 import funkin.ui.transition.LoadingState;
@@ -49,6 +51,34 @@ import funkin.effects.IntervalShake;
 typedef FreeplayStateParams =
 {
   ?character:String,
+
+  ?fromResults:FromResultsParams,
+};
+
+/**
+ * A set of parameters for transitioning to the FreeplayState from the ResultsState.
+ */
+typedef FromResultsParams =
+{
+  /**
+   * The previous rank the song hand, if any. Null if it had no score before.
+   */
+  var ?oldRank:ScoringRank;
+
+  /**
+   * The new rank the song has.
+   */
+  var newRank:ScoringRank;
+
+  /**
+   * The song ID to play the animation on.
+   */
+  var songId:String;
+
+  /**
+   * The difficulty ID to play the animation on.
+   */
+  var difficultyId:String;
 };
 
 /**
@@ -160,10 +190,14 @@ class FreeplayState extends MusicBeatSubState
   var bgDad:FlxSprite;
   var cardGlow:FlxSprite;
 
+  var fromResultsParams:Null<FromResultsParams> = null;
+
   public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
   {
     currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
 
+    fromResultsParams = params?.fromResults;
+
     if (stickers != null)
     {
       stickerSubState = stickers;
@@ -587,6 +621,11 @@ class FreeplayState extends MusicBeatSubState
 
       cardGlow.visible = true;
       FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut});
+
+      if (fromResultsParams != null)
+      {
+        rankAnimStart(fromResultsParams);
+      }
     });
 
     generateSongList(null, false);
@@ -657,6 +696,12 @@ class FreeplayState extends MusicBeatSubState
     // If curSelected is 0, the result will be null and fall back to the rememberedSongId.
     rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId;
 
+    if (fromResultsParams != null)
+    {
+      rememberedSongId = fromResultsParams.songId;
+      rememberedDifficulty = fromResultsParams.difficultyId;
+    }
+
     for (cap in grpCapsules.members)
     {
       cap.songText.resetText();
@@ -763,8 +808,10 @@ class FreeplayState extends MusicBeatSubState
     return songsToFilter;
   }
 
-  function rankAnimStart()
+  function rankAnimStart(fromResults:Null<FromResultsParams>):Void
   {
+    busy = true;
+
     dj.fistPump();
     // rankCamera.fade(FlxColor.BLACK, 0.5, true);
     rankCamera.fade(0xFF000000, 0.5, true, null, true);
@@ -789,21 +836,21 @@ class FreeplayState extends MusicBeatSubState
 
     new FlxTimer().start(0.5, _ -> {
       grpCapsules.members[curSelected].doLerp = false;
-      rankDisplayNew();
+      rankDisplayNew(fromResults);
     });
   }
 
-  function rankDisplayNew()
+  function rankDisplayNew(fromResults:Null<FromResultsParams>):Void
   {
     grpCapsules.members[curSelected].ranking.alpha = 1;
     grpCapsules.members[curSelected].blurredRanking.alpha = 1;
 
     grpCapsules.members[curSelected].ranking.scale.set(20, 20);
     grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20);
-    // var tempr:Int = FlxG.random.int(0, 4);
 
-    // grpCapsules.members[curSelected].ranking.rank = tempr;
-    grpCapsules.members[curSelected].ranking.animation.play(grpCapsules.members[curSelected].ranking.animation.curAnim.name, true);
+    grpCapsules.members[curSelected].ranking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
+    // grpCapsules.members[curSelected].ranking.animation.curAnim.name, true);
+
     FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1);
 
     grpCapsules.members[curSelected].blurredRanking.animation.play(grpCapsules.members[curSelected].blurredRanking.animation.curAnim.name, true);
@@ -811,13 +858,13 @@ class FreeplayState extends MusicBeatSubState
 
     new FlxTimer().start(0.1, _ -> {
       trace(grpCapsules.members[curSelected].ranking.rank);
-      switch (grpCapsules.members[curSelected].tempr)
+      switch (fromResultsParams?.newRank)
       {
-        case 0:
+        case SHIT:
           FunkinSound.playOnce(Paths.sound('ranks/rankinbad'));
-        case 4:
+        case PERFECT:
           FunkinSound.playOnce(Paths.sound('ranks/rankinperfect'));
-        case 5:
+        case PERFECT_GOLD:
           FunkinSound.playOnce(Paths.sound('ranks/rankinperfect'));
         default:
           FunkinSound.playOnce(Paths.sound('ranks/rankinnormal'));
@@ -846,31 +893,31 @@ class FreeplayState extends MusicBeatSubState
     });
 
     new FlxTimer().start(0.6, _ -> {
-      rankAnimSlam();
+      rankAnimSlam(fromResults);
       // IntervalShake.shake(grpCapsules.members[curSelected].capsule, 0.3, 1 / 30, 0, 0.3, FlxEase.quartIn);
     });
   }
 
-  function rankAnimSlam()
+  function rankAnimSlam(fromResultsParams:Null<FromResultsParams>)
   {
     // FlxTween.tween(rankCamera, {"zoom": 1.9}, 0.5, {ease: FlxEase.backOut});
     FlxTween.tween(rankBg, {alpha: 0}, 0.5, {ease: FlxEase.expoIn});
 
     // FlxTween.tween(grpCapsules.members[curSelected], {angle: 5}, 0.5, {ease: FlxEase.backIn});
 
-    switch (grpCapsules.members[curSelected].tempr)
+    switch (fromResultsParams?.newRank)
     {
-      case 0:
+      case SHIT:
         FunkinSound.playOnce(Paths.sound('ranks/loss'));
-      case 1:
+      case GOOD:
         FunkinSound.playOnce(Paths.sound('ranks/good'));
-      case 2:
+      case GREAT:
         FunkinSound.playOnce(Paths.sound('ranks/great'));
-      case 3:
+      case EXCELLENT:
         FunkinSound.playOnce(Paths.sound('ranks/excellent'));
-      case 4:
+      case PERFECT:
         FunkinSound.playOnce(Paths.sound('ranks/perfect'));
-      case 5:
+      case PERFECT_GOLD:
         FunkinSound.playOnce(Paths.sound('ranks/perfect'));
       default:
         FunkinSound.playOnce(Paths.sound('ranks/loss'));
@@ -880,7 +927,7 @@ class FreeplayState extends MusicBeatSubState
     new FlxTimer().start(0.5, _ -> {
       funnyCam.shake(0.0045, 0.35);
 
-      if (grpCapsules.members[curSelected].tempr == 0)
+      if (fromResultsParams?.newRank == SHIT)
       {
         dj.pumpFistBad();
       }
@@ -915,6 +962,9 @@ class FreeplayState extends MusicBeatSubState
             IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12, 0, FlxEase.quadOut, function(_) {
               capsule.doLerp = true;
               capsule.cameras = [funnyCam];
+
+              // NOW we can interact with the menu
+              busy = false;
             }, null);
 
             // FlxTween.tween(capsule, {"targetPos.x": capsule.targetPos.x - 50}, 0.6,
@@ -981,7 +1031,10 @@ class FreeplayState extends MusicBeatSubState
   var spamTimer:Float = 0;
   var spamming:Bool = false;
 
-  var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
+  /**
+   * If true, disable interaction with the interface.
+   */
+  var busy:Bool = false;
 
   var originalPos:FlxPoint = new FlxPoint();
 
@@ -989,19 +1042,20 @@ class FreeplayState extends MusicBeatSubState
   {
     super.update(elapsed);
 
+    #if debug
     if (FlxG.keys.justPressed.T)
     {
-      rankAnimStart();
+      rankAnimStart(fromResultsParams);
     }
 
     if (FlxG.keys.justPressed.H)
     {
-      rankDisplayNew();
+      rankDisplayNew(fromResultsParams);
     }
 
     if (FlxG.keys.justPressed.G)
     {
-      rankAnimSlam();
+      rankAnimSlam(fromResultsParams);
     }
 
     if (FlxG.keys.justPressed.I)
@@ -1024,6 +1078,7 @@ class FreeplayState extends MusicBeatSubState
       confirmTextGlow.y += 1;
       trace(confirmTextGlow.x, confirmTextGlow.y);
     }
+    #end
 
     if (FlxG.keys.justPressed.F)
     {
@@ -1765,6 +1820,8 @@ class FreeplaySongData
 
   public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
 
+  public var scoringRank:Null<ScoringRank> = null;
+
   var displayedVariations:Array<String> = [Constants.DEFAULT_VARIATION];
 
   function set_currentDifficulty(value:String):String
@@ -1827,6 +1884,8 @@ class FreeplaySongData
     {
       this.albumId = songDifficulty.album;
     }
+
+    this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty);
   }
 }
 
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index 536a9cfe6..ad6ea386e 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -20,6 +20,7 @@ import funkin.graphics.FunkinSprite;
 import flixel.tweens.FlxEase;
 import flixel.tweens.FlxTween;
 import flixel.addons.effects.FlxTrail;
+import funkin.play.scoring.Scoring.ScoringRank;
 import flixel.util.FlxColor;
 
 class SongMenuItem extends FlxSpriteGroup
@@ -70,8 +71,6 @@ class SongMenuItem extends FlxSpriteGroup
 
   var impactThing:FunkinSprite;
 
-  public var tempr:Int;
-
   public function new(x:Float, y:Float)
   {
     super(x, y);
@@ -132,13 +131,10 @@ class SongMenuItem extends FlxSpriteGroup
     // doesn't get added, simply is here to help with visibility of things for the pop in!
     grpHide = new FlxGroup();
 
-    var rank:String = FlxG.random.getObject(ranks);
-
-    tempr = FlxG.random.int(0, 5);
-    ranking = new FreeplayRank(420, 41, tempr);
+    ranking = new FreeplayRank(420, 41);
     add(ranking);
 
-    blurredRanking = new FreeplayRank(ranking.x, ranking.y, tempr);
+    blurredRanking = new FreeplayRank(ranking.x, ranking.y);
     blurredRanking.shader = new GaussianBlurShader(1);
     add(blurredRanking);
     // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
@@ -344,19 +340,19 @@ class SongMenuItem extends FlxSpriteGroup
       });
     add(evilTrail);
 
-    switch (tempr)
+    switch (ranking.rank)
     {
-      case 0:
+      case SHIT:
         evilTrail.color = 0xFF6044FF;
-      case 1:
+      case GOOD:
         evilTrail.color = 0xFFEF8764;
-      case 2:
+      case GREAT:
         evilTrail.color = 0xFFEAF6FF;
-      case 3:
+      case EXCELLENT:
         evilTrail.color = 0xFFFDCB42;
-      case 4:
+      case PERFECT:
         evilTrail.color = 0xFFFF58B4;
-      case 5:
+      case PERFECT_GOLD:
         evilTrail.color = 0xFFFFB619;
     }
   }
@@ -393,6 +389,12 @@ class SongMenuItem extends FlxSpriteGroup
     // diffRatingSprite.visible = false;
   }
 
+  function updateScoringRank(newRank:Null<ScoringRank>):Void
+  {
+    this.ranking.rank = newRank;
+    this.blurredRanking.rank = newRank;
+  }
+
   function set_hsvShader(value:HSVShader):HSVShader
   {
     this.hsvShader = value;
@@ -441,6 +443,7 @@ class SongMenuItem extends FlxSpriteGroup
     if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
     updateBPM(Std.int(songData?.songStartingBpm) ?? 0);
     updateDifficultyRating(songData?.difficultyRating ?? 0);
+    updateScoringRank(songData?.scoringRank);
     // Update opacity, offsets, etc.
     updateSelected();
 
@@ -668,41 +671,53 @@ class SongMenuItem extends FlxSpriteGroup
 
 class FreeplayRank extends FlxSprite
 {
-  public var rank(default, set):Int = 0;
+  public var rank(default, set):Null<ScoringRank> = null;
 
-  var numToRank:Array<String> = ["LOSS", "GOOD", "GREAT", "EXCELLENT", "PERFECT", "PERFECTSICK"];
-
-  function set_rank(val):Int
+  function set_rank(val:Null<ScoringRank>):Null<ScoringRank>
   {
-    animation.play(numToRank[val], true, false);
+    rank = val;
 
-    centerOffsets(false);
-
-    switch (val)
+    if (rank == null)
     {
-      case 0:
-      // offset.x -= 1;
-      case 1:
-        // offset.x -= 1;
-        offset.y -= 8;
-      case 2:
-        // offset.x -= 1;
-        offset.y -= 8;
-      case 3:
-      // offset.y += 5;
-      case 4:
-      // offset.y += 5;
-      default:
-        centerOffsets(false);
+      this.visible = false;
     }
-    updateHitbox();
-    return val;
+    else
+    {
+      this.visible = true;
+
+      animation.play(val.getFreeplayRankIconAsset(), true, false);
+
+      centerOffsets(false);
+
+      switch (val)
+      {
+        case SHIT:
+        // offset.x -= 1;
+        case GOOD:
+          // offset.x -= 1;
+          offset.y -= 8;
+        case GREAT:
+          // offset.x -= 1;
+          offset.y -= 8;
+        case EXCELLENT:
+        // offset.y += 5;
+        case PERFECT:
+        // offset.y += 5;
+        case PERFECT_GOLD:
+        // offset.y += 5;
+        default:
+          centerOffsets(false);
+      }
+      updateHitbox();
+    }
+
+    return rank = val;
   }
 
-  public var baseY:Float = 0;
   public var baseX:Float = 0;
+  public var baseY:Float = 0;
 
-  public function new(x:Float, y:Float, ?initRank:Int = 0)
+  public function new(x:Float, y:Float)
   {
     super(x, y);
 
@@ -717,9 +732,7 @@ class FreeplayRank extends FlxSprite
 
     blend = BlendMode.ADD;
 
-    this.rank = initRank;
-
-    animation.play(numToRank[initRank], true);
+    this.rank = null;
 
     // setGraphicSize(Std.int(width * 0.9));
     scale.set(0.9, 0.9);

From 7a2f3c81a1d5cbb02c376256361eadb953cc9e6d Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 30 May 2024 21:11:17 -0400
Subject: [PATCH 31/96] fix the one random slice of pixels where the bg doesnt
 show... lol

---
 source/funkin/ui/title/TitleState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx
index c9b3619e9..c6dbcd505 100644
--- a/source/funkin/ui/title/TitleState.hx
+++ b/source/funkin/ui/title/TitleState.hx
@@ -124,7 +124,7 @@ class TitleState extends MusicBeatState
 
     persistentUpdate = true;
 
-    var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
+    var bg:FunkinSprite = new FunkinSprite(-1).makeSolidColor(FlxG.width + 2, FlxG.height, FlxColor.BLACK);
     bg.screenCenter();
     add(bg);
 

From dc33da904cc51d2ce1cf08aa3af1030863ae86f3 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 30 May 2024 23:03:27 -0400
Subject: [PATCH 32/96] assets submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 2719d3fc1..4bc0b35f6 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 2719d3fc1d8f5d0cbafae8d27141d6c471148482
+Subproject commit 4bc0b35f6c7aa22086b85b6a635c6f0511d277fe

From 7347b66ce47570b6098085945cd561d6fe0f2154 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 30 May 2024 23:07:41 -0400
Subject: [PATCH 33/96] removes trace() that lags freeplay

---
 source/funkin/ui/freeplay/SongMenuItem.hx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index ad6ea386e..75a4c07a3 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -264,8 +264,6 @@ class SongMenuItem extends FlxSpriteGroup
 
   function updateBPM(newBPM:Int):Void
   {
-    trace(newBPM);
-
     var shiftX:Float = 191;
     var tempShift:Float = 0;
 

From 40d1fd96a36bf566437d2dafdb716ffafee9e212 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 30 May 2024 23:27:00 -0400
Subject: [PATCH 34/96] art submod

---
 art | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/art b/art
index 66572f85d..faeba700c 160000
--- a/art
+++ b/art
@@ -1 +1 @@
-Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa
+Subproject commit faeba700c5526bd4fd57ccc927d875c82b9d3553

From 007ec95e8548892e70189e782081201624211827 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 30 May 2024 23:42:48 -0400
Subject: [PATCH 35/96] update difficulties

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 4bc0b35f6..11bcd1b79 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 4bc0b35f6c7aa22086b85b6a635c6f0511d277fe
+Subproject commit 11bcd1b79169df4f0aa46d72c867e960a287d28a

From 0d4f3cdc334c8593fe7b1042d1c2295adf69992e Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 01:42:41 -0400
Subject: [PATCH 36/96] Scrolling results text

---
 assets                            |  2 +-
 source/funkin/InitState.hx        |  4 +--
 source/funkin/play/ResultState.hx | 44 ++++++++++++++++++++++++++++---
 3 files changed, 43 insertions(+), 7 deletions(-)

diff --git a/assets b/assets
index 11bcd1b79..7a0d92d30 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 11bcd1b79169df4f0aa46d72c867e960a287d28a
+Subproject commit 7a0d92d3007de42c452b2ea97a917d8c8d114ee7
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index d0009f95b..c7a08d714 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -219,9 +219,9 @@ class InitState extends FlxState
     FlxG.switchState(() -> new funkin.play.ResultState(
       {
         storyMode: false,
-        title: "CUM SONG",
+        title: "Cum Song Erect by Kawai Sprite",
         songId: "cum",
-        difficultyId: "hard",
+        difficultyId: "nightmare",
         isNewHighscore: true,
         scoreData:
           {
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 79880038d..8b8c0aea3 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -15,8 +15,10 @@ import funkin.ui.freeplay.FreeplayScore;
 import flixel.text.FlxText;
 import flixel.util.FlxColor;
 import flixel.tweens.FlxEase;
+import funkin.graphics.FunkinCamera;
 import funkin.ui.freeplay.FreeplayState;
 import flixel.tweens.FlxTween;
+import flixel.addons.display.FlxBackdrop;
 import funkin.audio.FunkinSound;
 import flixel.util.FlxGradient;
 import flixel.util.FlxTimer;
@@ -59,6 +61,10 @@ class ResultState extends MusicBeatSubState
   var gfGood:Null<FlxSprite> = null;
   var bfShit:Null<FlxAtlasSprite> = null;
 
+  final cameraBG:FunkinCamera;
+  final cameraScroll:FunkinCamera;
+  final cameraEverything:FunkinCamera;
+
   public function new(params:ResultsStateParams)
   {
     super();
@@ -67,6 +73,10 @@ class ResultState extends MusicBeatSubState
 
     rank = Scoring.calculateRank(params.scoreData) ?? SHIT;
 
+    cameraBG = new FunkinCamera('resultsBG', 0, 0, FlxG.width, FlxG.height);
+    cameraScroll = new FunkinCamera('resultsScroll', 0, 0, FlxG.width, FlxG.height);
+    cameraEverything = new FunkinCamera('resultsEverything', 0, 0, FlxG.width, FlxG.height);
+
     // We build a lot of this stuff in the constructor, then place it in create().
     // This prevents having to do `null` checks everywhere.
 
@@ -101,17 +111,32 @@ class ResultState extends MusicBeatSubState
   {
     if (FlxG.sound.music != null) FlxG.sound.music.stop();
 
+    // We need multiple cameras so we can put one at an angle.
+    cameraScroll.angle = -3.8;
+
+    cameraBG.bgColor = FlxColor.MAGENTA;
+    cameraScroll.bgColor = FlxColor.TRANSPARENT;
+    cameraEverything.bgColor = FlxColor.TRANSPARENT;
+
+    FlxG.cameras.add(cameraBG, false);
+    FlxG.cameras.add(cameraScroll, false);
+    FlxG.cameras.add(cameraEverything, false);
+
+    FlxG.cameras.setDefaultDrawTarget(cameraEverything, true);
+
     // Reset the camera zoom on the results screen.
     FlxG.camera.zoom = 1.0;
 
     var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
     bg.scrollFactor.set();
     bg.zIndex = 10;
+    bg.cameras = [cameraBG];
     add(bg);
 
     bgFlash.scrollFactor.set();
     bgFlash.visible = false;
     bgFlash.zIndex = 20;
+    bgFlash.cameras = [cameraBG];
     add(bgFlash);
 
     // The sound system which falls into place behind the score text. Plays every time!
@@ -455,16 +480,27 @@ class ResultState extends MusicBeatSubState
 
   function displayRankText():Void
   {
-    var rankTextVert:FunkinSprite = FunkinSprite.create(FlxG.width - 64, 100, rank.getVerTextAsset());
-    rankTextVert.zIndex = 2000;
+    var rankTextVert:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getVerTextAsset()), Y, 0, 30);
+    rankTextVert.x = FlxG.width - 64;
+    rankTextVert.y = 100;
+    rankTextVert.zIndex = 990;
     add(rankTextVert);
 
+    // Scrolling.
+    rankTextVert.velocity.y = -50;
+
     for (i in 0...10)
     {
-      var rankTextBack:FunkinSprite = FunkinSprite.create(FlxG.width / 2 - 80, 50, rank.getHorTextAsset());
-      rankTextBack.y += (rankTextBack.height * i / 2) + 10;
+      var rankTextBack:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getHorTextAsset()), X, 10, 0);
+      rankTextBack.x = FlxG.width / 2 - 320;
+      rankTextBack.y = 50 + (150 * i / 2) + 10;
+      // rankTextBack.angle = -3.8;
       rankTextBack.zIndex = 100;
+      rankTextBack.cameras = [cameraScroll];
       add(rankTextBack);
+
+      // Scrolling.
+      rankTextBack.velocity.x = (i % 2 == 0) ? -10.0 : 10.0;
     }
 
     refresh();

From 9e0a99374691f8a3018dc547a49c7c400b9245a7 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 02:03:40 -0400
Subject: [PATCH 37/96] Disable song previews for mod songs rather than
 crashing the game

---
 source/funkin/audio/FunkinSound.hx | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index b94c6008c..7663c1305 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -377,7 +377,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
           FlxG.sound.music = partialMusic;
           FlxG.sound.list.remove(FlxG.sound.music);
 
-          if (params.onLoad != null) params.onLoad();
+          if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
         });
 
         return true;
@@ -488,14 +488,21 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
     var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
 
-    promise.future.onError(function(e) {
-      soundRequest.error("Sound loading was errored or cancelled");
-    });
+    if (soundRequest == null)
+    {
+      promise.complete(null);
+    }
+    else
+    {
+      promise.future.onError(function(e) {
+        soundRequest.error("Sound loading was errored or cancelled");
+      });
 
-    soundRequest.future.onComplete(function(partialSound) {
-      var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
-      promise.complete(snd);
-    });
+      soundRequest.future.onComplete(function(partialSound) {
+        var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
+        promise.complete(snd);
+      });
+    }
 
     return promise;
   }

From 2a8cdfaffae9dca58c0957f825f491ec89bd8070 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 02:30:42 -0400
Subject: [PATCH 38/96] Fix an extra crash.

---
 source/funkin/ui/freeplay/FreeplayState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 192c6e3ce..f0695e51e 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -830,7 +830,7 @@ class FreeplayState extends MusicBeatSubState
     dj.fistPump();
     // rankCamera.fade(FlxColor.BLACK, 0.5, true);
     rankCamera.fade(0xFF000000, 0.5, true, null, true);
-    FlxG.sound.music.volume = 0;
+    if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
     rankBg.alpha = 1;
 
     originalPos.x = grpCapsules.members[curSelected].x;

From 1e65cacb0efaa92991fd2290fe6f30b58e38ba7d Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Fri, 31 May 2024 10:39:06 +0100
Subject: [PATCH 39/96] fix funkinsound compile error

---
 source/funkin/audio/FunkinSound.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index b94c6008c..cf4a71189 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -359,7 +359,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
     if (shouldLoadPartial)
     {
-      var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0, params.partialParams?.end ?? 1, params?.startingVolume ?? 1.0,
+      var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0,
         params.loop ?? true, false, false, params.onComplete);
 
       if (music != null)

From 28444fd47814a4c3833310743d7d5ce1944ec9e9 Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Fri, 31 May 2024 10:39:53 +0100
Subject: [PATCH 40/96] rank tweaks, favourite changes + rank anim fixes

---
 source/funkin/play/ResultState.hx          |  61 +++++++++--
 source/funkin/play/scoring/Scoring.hx      |  58 ++++++++++-
 source/funkin/ui/credits/CreditsState.hx   |   3 +-
 source/funkin/ui/freeplay/AlbumRoll.hx     |   2 +-
 source/funkin/ui/freeplay/FreeplayState.hx | 112 +++++++++++++++++----
 source/funkin/ui/freeplay/SongMenuItem.hx  |   7 +-
 source/funkin/ui/mainmenu/MainMenuState.hx |  13 ++-
 7 files changed, 220 insertions(+), 36 deletions(-)

diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 79880038d..1ad19b41e 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -59,6 +59,8 @@ class ResultState extends MusicBeatSubState
   var gfGood:Null<FlxSprite> = null;
   var bfShit:Null<FlxAtlasSprite> = null;
 
+  var rankBg:FunkinSprite;
+
   public function new(params:ResultsStateParams)
   {
     super();
@@ -95,6 +97,8 @@ class ResultState extends MusicBeatSubState
     highscoreNew = new FlxSprite(310, 570);
 
     score = new ResultScore(35, 305, 10, params.scoreData.score);
+
+    rankBg = new FunkinSprite(0, 0);
   }
 
   override function create():Void
@@ -356,6 +360,12 @@ class ResultState extends MusicBeatSubState
       }
     });
 
+    rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xFF000000);
+    rankBg.zIndex = 99999;
+    add(rankBg);
+
+   rankBg.alpha = 0;
+
     refresh();
 
     super.create();
@@ -654,18 +664,47 @@ class ResultState extends MusicBeatSubState
       }
       else
       {
-        openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(
-          {
+        var rigged:Bool = true;
+        if (rank > Scoring.calculateRank(params?.prevScoreData))
+        //if (rigged)
+        {
+          trace('THE RANK IS Higher.....');
+
+          FlxTween.tween(rankBg, {alpha: 1}, 0.5,
             {
-              fromResults:
-                {
-                  oldRank: Scoring.calculateRank(params?.prevScoreData),
-                  newRank: rank,
-                  songId: params.songId,
-                  difficultyId: params.difficultyId
-                }
-            }
-          }, sticker)));
+              ease: FlxEase.expoOut,
+              onComplete: function(_) {
+                FlxG.switchState(FreeplayState.build(
+                  {
+                    {
+                      fromResults:
+                        {
+                          oldRank: Scoring.calculateRank(params?.prevScoreData),
+                          newRank: rank,
+                          songId: params.songId,
+                          difficultyId: params.difficultyId
+                        }
+                    }
+                  }));
+              }
+            });
+        }
+        else
+        {
+          trace('rank is lower...... and/or equal');
+          openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(
+            {
+              {
+                fromResults:
+                  {
+                    oldRank: null,
+                    newRank: rank,
+                    songId: params.songId,
+                    difficultyId: params.difficultyId
+                  }
+              }
+            }, sticker)));
+        }
       }
     }
 
diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx
index 6155ec879..f941f3f01 100644
--- a/source/funkin/play/scoring/Scoring.hx
+++ b/source/funkin/play/scoring/Scoring.hx
@@ -349,7 +349,7 @@ class Scoring
 
   public static function calculateRank(scoreData:Null<SaveScoreData>):Null<ScoringRank>
   {
-    if (scoreData == null) return null;
+    if (scoreData?.tallies.totalNotes == 0 || scoreData == null) return null;
 
     // Perfect (Platinum) is a Sick Full Clear
     var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes;
@@ -394,6 +394,62 @@ enum abstract ScoringRank(String)
   var GOOD;
   var SHIT;
 
+  @:op(A > B) static function compare(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
+  {
+    if (a != null && b == null) return true;
+    if (a == null || b == null) return false;
+
+    var temp1:Int = 0;
+    var temp2:Int = 0;
+
+    // temp 1
+    switch (a)
+    {
+      case PERFECT_GOLD:
+        temp1 = 5;
+      case PERFECT:
+        temp1 = 4;
+      case EXCELLENT:
+        temp1 = 3;
+      case GREAT:
+        temp1 = 2;
+      case GOOD:
+        temp1 = 1;
+      case SHIT:
+        temp1 = 0;
+      default:
+        temp1 = -1;
+    }
+
+    // temp 2
+    switch (b)
+    {
+      case PERFECT_GOLD:
+        temp2 = 5;
+      case PERFECT:
+        temp2 = 4;
+      case EXCELLENT:
+        temp2 = 3;
+      case GREAT:
+        temp2 = 2;
+      case GOOD:
+        temp2 = 1;
+      case SHIT:
+        temp2 = 0;
+      default:
+        temp2 = -1;
+    }
+
+    if (temp1 > temp2)
+    {
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
   /**
    * Delay in seconds
    */
diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx
index 6be1fecf7..44769e9b3 100644
--- a/source/funkin/ui/credits/CreditsState.hx
+++ b/source/funkin/ui/credits/CreditsState.hx
@@ -4,6 +4,7 @@ import flixel.text.FlxText;
 import flixel.util.FlxColor;
 import funkin.audio.FunkinSound;
 import flixel.FlxSprite;
+import funkin.ui.mainmenu.MainMenuState;
 import flixel.group.FlxSpriteGroup;
 
 /**
@@ -199,7 +200,7 @@ class CreditsState extends MusicBeatState
 
   function exit():Void
   {
-    FlxG.switchState(funkin.ui.mainmenu.MainMenuState.new);
+    FlxG.switchState(() -> new MainMenuState());
   }
 
   public override function destroy():Void
diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx
index 50f4a432c..20cd91379 100644
--- a/source/funkin/ui/freeplay/AlbumRoll.hx
+++ b/source/funkin/ui/freeplay/AlbumRoll.hx
@@ -131,7 +131,7 @@ class AlbumRoll extends FlxSpriteGroup
 
     if (exitMovers == null) return;
 
-    exitMovers.set([newAlbumArt],
+    exitMovers.set([newAlbumArt, difficultyStars],
       {
         x: FlxG.width,
         speed: 0.4,
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 192c6e3ce..186c84c33 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -193,12 +193,18 @@ class FreeplayState extends MusicBeatSubState
 
   var fromResultsParams:Null<FromResultsParams> = null;
 
+  var prepForNewRank:Bool = false;
+
   public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
   {
     currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
 
     fromResultsParams = params?.fromResults;
 
+    if(fromResultsParams?.oldRank != null){
+      prepForNewRank = true;
+    }
+
     if (stickers != null)
     {
       stickerSubState = stickers;
@@ -235,11 +241,14 @@ class FreeplayState extends MusicBeatSubState
     isDebug = true;
     #end
 
-    FunkinSound.playMusic('freakyMenu',
+    if (prepForNewRank == false)
+    {
+      FunkinSound.playMusic('freakyMenu',
       {
         overrideExisting: true,
         restartTrack: false
       });
+    }
 
     // Add a null entry that represents the RANDOM option
     songs.push(null);
@@ -637,7 +646,7 @@ class FreeplayState extends MusicBeatSubState
       cardGlow.visible = true;
       FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut});
 
-      if (fromResultsParams != null)
+      if (prepForNewRank == true)
       {
         rankAnimStart(fromResultsParams);
       }
@@ -667,6 +676,11 @@ class FreeplayState extends MusicBeatSubState
     FlxG.cameras.add(rankCamera, false);
     rankBg.cameras = [rankCamera];
     rankBg.alpha = 0;
+
+    if (prepForNewRank == true)
+    {
+      rankCamera.fade(0xFF000000, 0, false, null, true);
+    }
   }
 
   var currentFilter:SongFilter = null;
@@ -826,6 +840,7 @@ class FreeplayState extends MusicBeatSubState
   function rankAnimStart(fromResults:Null<FromResultsParams>):Void
   {
     busy = true;
+    // grpCapsules.members[curSelected].forcePosition();
 
     dj.fistPump();
     // rankCamera.fade(FlxColor.BLACK, 0.5, true);
@@ -833,8 +848,14 @@ class FreeplayState extends MusicBeatSubState
     FlxG.sound.music.volume = 0;
     rankBg.alpha = 1;
 
-    originalPos.x = grpCapsules.members[curSelected].x;
-    originalPos.y = grpCapsules.members[curSelected].y;
+    grpCapsules.members[curSelected].doLerp = false;
+
+    // originalPos.x = grpCapsules.members[curSelected].x;
+    // originalPos.y = grpCapsules.members[curSelected].y;
+
+    originalPos.x = 320.488;
+    originalPos.y = 235.6;
+    trace(originalPos);
 
     grpCapsules.members[curSelected].ranking.alpha = 0;
     grpCapsules.members[curSelected].blurredRanking.alpha = 0;
@@ -846,11 +867,13 @@ class FreeplayState extends MusicBeatSubState
     FlxTween.tween(funnyCam, {"zoom": 1.1}, 0.6, {ease: FlxEase.sineIn});
 
     grpCapsules.members[curSelected].cameras = [rankCamera];
-    grpCapsules.members[curSelected].targetPos.set((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2),
+    // grpCapsules.members[curSelected].targetPos.set((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2),
+    //  (FlxG.height / 2) - (grpCapsules.members[curSelected].height / 2));
+
+    grpCapsules.members[curSelected].setPosition((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2),
       (FlxG.height / 2) - (grpCapsules.members[curSelected].height / 2));
 
     new FlxTimer().start(0.5, _ -> {
-      grpCapsules.members[curSelected].doLerp = false;
       rankDisplayNew(fromResults);
     });
   }
@@ -1028,6 +1051,12 @@ class FreeplayState extends MusicBeatSubState
 
     new FlxTimer().start(2, _ -> {
       // dj.fistPump();
+      prepForNewRank = false;
+      FunkinSound.playMusic('freakyMenu',
+      {
+        overrideExisting: true,
+        restartTrack: false
+      });
       FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
     });
   }
@@ -1095,7 +1124,7 @@ class FreeplayState extends MusicBeatSubState
     }
     #end
 
-    if (FlxG.keys.justPressed.F)
+    if (FlxG.keys.justPressed.F && !busy)
     {
       var targetSong = grpCapsules.members[curSelected]?.songData;
       if (targetSong != null)
@@ -1104,24 +1133,45 @@ class FreeplayState extends MusicBeatSubState
         var isFav = targetSong.toggleFavorite();
         if (isFav)
         {
-          FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
+          grpCapsules.members[realShit].favIcon.visible = true;
+          grpCapsules.members[realShit].favIcon.animation.play('fav');
+          FunkinSound.playOnce(Paths.sound('fav'), 1);
+          busy = true;
+
+          grpCapsules.members[realShit].doLerp = false;
+          FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y - 5}, 0.1, {ease: FlxEase.expoOut});
+
+          FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y + 5}, 0.1,
             {
-              ease: FlxEase.elasticOut,
-              onComplete: _ -> {
-                grpCapsules.members[realShit].favIcon.visible = true;
-                grpCapsules.members[realShit].favIcon.animation.play('fav');
+              ease: FlxEase.expoIn,
+              startDelay: 0.1,
+              onComplete: function(_) {
+                grpCapsules.members[realShit].doLerp = true;
+                busy = false;
               }
             });
         }
         else
         {
-          grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
-          new FlxTimer().start((1 / 24) * 14, _ -> {
+          grpCapsules.members[realShit].favIcon.animation.play('fav', true, true, 9);
+          FunkinSound.playOnce(Paths.sound('unfav'), 1);
+          new FlxTimer().start(0.2, _ -> {
             grpCapsules.members[realShit].favIcon.visible = false;
           });
-          new FlxTimer().start((1 / 24) * 24, _ -> {
-            FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
-          });
+
+          busy = true;
+          grpCapsules.members[realShit].doLerp = false;
+          FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y + 5}, 0.1, {ease: FlxEase.expoOut});
+
+          FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y - 5}, 0.1,
+            {
+              ease: FlxEase.expoIn,
+              startDelay: 0.1,
+              onComplete: function(_) {
+                grpCapsules.members[realShit].doLerp = true;
+                busy = false;
+              }
+            });
         }
       }
     }
@@ -1325,6 +1375,24 @@ class FreeplayState extends MusicBeatSubState
 
       var longestTimer:Float = 0;
 
+      // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut});
+      FlxTween.color(pinkBack, 0.25, 0xFFFFD863, 0xFFFFD0D5, {ease: FlxEase.quadOut});
+
+      cardGlow.visible = true;
+      cardGlow.alpha = 1;
+      cardGlow.scale.set(1, 1);
+      FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut});
+
+      orangeBackShit.visible = false;
+      alsoOrangeLOL.visible = false;
+
+      moreWays.visible = false;
+      funnyScroll.visible = false;
+      txtNuts.visible = false;
+      funnyScroll2.visible = false;
+      moreWays2.visible = false;
+      funnyScroll3.visible = false;
+
       for (grpSpr in exitMovers.keys())
       {
         var moveData:MoveData = exitMovers.get(grpSpr);
@@ -1555,6 +1623,7 @@ class FreeplayState extends MusicBeatSubState
     FunkinSound.playOnce(Paths.sound('confirmMenu'));
     dj.confirm();
 
+    grpCapsules.members[curSelected].forcePosition();
     grpCapsules.members[curSelected].songText.flickerText();
 
     // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut});
@@ -1689,6 +1758,7 @@ class FreeplayState extends MusicBeatSubState
       }
       else
       {
+        if(prepForNewRank == false){
         var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
         // TODO: Stream the instrumental of the selected song?
         FunkinSound.playMusic(daSongCapsule.songData.songId,
@@ -1708,6 +1778,7 @@ class FreeplayState extends MusicBeatSubState
               FlxG.sound.music.fadeIn(2, 0, 0.4);
             }
           });
+        }
       }
       grpCapsules.members[curSelected].selected = true;
     }
@@ -1719,7 +1790,12 @@ class FreeplayState extends MusicBeatSubState
    */
   public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
   {
-    var result = new MainMenuState();
+    var result:MainMenuState;
+    if(params?.fromResults.oldRank != null){
+      result = new MainMenuState(true);
+    }else{
+      result = new MainMenuState(false);
+    }
 
     result.openSubState(new FreeplayState(params, stickers));
     result.persistentUpdate = false;
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index ad6ea386e..b52032bc3 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -371,7 +371,7 @@ class SongMenuItem extends FlxSpriteGroup
       switch (i)
       {
         case 0:
-          if (newRating > 10)
+          if (newRating < 10)
           {
             bigNumbers[i].digit = 0;
           }
@@ -677,7 +677,7 @@ class FreeplayRank extends FlxSprite
   {
     rank = val;
 
-    if (rank == null)
+    if (rank == null || val == null)
     {
       this.visible = false;
     }
@@ -687,6 +687,8 @@ class FreeplayRank extends FlxSprite
 
       animation.play(val.getFreeplayRankIconAsset(), true, false);
 
+      trace(val.getFreeplayRankIconAsset());
+
       centerOffsets(false);
 
       switch (val)
@@ -707,6 +709,7 @@ class FreeplayRank extends FlxSprite
         // offset.y += 5;
         default:
           centerOffsets(false);
+          this.visible = false;
       }
       updateHitbox();
     }
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index 22262006a..c504b3c3e 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -42,6 +42,15 @@ class MainMenuState extends MusicBeatState
   var magenta:FlxSprite;
   var camFollow:FlxObject;
 
+  var overrideMusic:Bool = false;
+
+  public function new(?_overrideMusic:Bool = false)
+  {
+    super();
+    overrideMusic = _overrideMusic;
+
+  }
+
   override function create():Void
   {
     #if discord_rpc
@@ -54,7 +63,7 @@ class MainMenuState extends MusicBeatState
     transIn = FlxTransitionableState.defaultTransIn;
     transOut = FlxTransitionableState.defaultTransOut;
 
-    playMenuMusic();
+    if(overrideMusic == false) playMenuMusic();
 
     // We want the state to always be able to begin with being able to accept inputs and show the anims of the menu items.
     persistentUpdate = true;
@@ -163,7 +172,7 @@ class MainMenuState extends MusicBeatState
 
   function playMenuMusic():Void
   {
-    FunkinSound.playMusic('freakyMenu',
+      FunkinSound.playMusic('freakyMenu',
       {
         overrideExisting: true,
         restartTrack: false

From 5d0db60810b37e3a076e3581ed2b33416ed1050c Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Fri, 31 May 2024 10:40:51 +0100
Subject: [PATCH 41/96] assets submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 4bc0b35f6..d64939d86 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 4bc0b35f6c7aa22086b85b6a635c6f0511d277fe
+Subproject commit d64939d866c70a831b76ed78930782b66871dcc2

From d89a898e6c3d33430002896bd601ec72c6a0faba Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Fri, 31 May 2024 17:16:26 -0400
Subject: [PATCH 42/96] make songs last longer on freeplay

---
 source/funkin/ui/freeplay/FreeplayState.hx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 192c6e3ce..71052a923 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -104,6 +104,7 @@ class FreeplayState extends MusicBeatSubState
 
   /**
    * For the audio preview, the duration of the fade-out effect.
+   *
    */
   public static final FADE_OUT_DURATION:Float = 0.25;
 
@@ -1690,7 +1691,6 @@ class FreeplayState extends MusicBeatSubState
       else
       {
         var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
-        // TODO: Stream the instrumental of the selected song?
         FunkinSound.playMusic(daSongCapsule.songData.songId,
           {
             startingVolume: 0.0,
@@ -1701,8 +1701,8 @@ class FreeplayState extends MusicBeatSubState
             partialParams:
               {
                 loadPartial: true,
-                start: 0,
-                end: 0.1
+                start: 0.05,
+                end: 0.25
               },
             onLoad: function() {
               FlxG.sound.music.fadeIn(2, 0, 0.4);

From 98eda8ef551ad533b29a5a1625ec613b78165c5e Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 18:09:07 -0400
Subject: [PATCH 43/96] Give timeFormat a default value (fixes the Stress
 issues!)

---
 source/funkin/data/song/SongData.hx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index bd25139a7..ca805e139 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -56,6 +56,8 @@ class SongMetadata implements ICloneable<SongMetadata>
   @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
   public var generatedBy:String;
 
+  @:optional
+  @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
   public var timeFormat:SongTimeFormat;
 
   public var timeChanges:Array<SongTimeChange>;
@@ -117,7 +119,7 @@ class SongMetadata implements ICloneable<SongMetadata>
   {
     var ignoreNullOptionals = true;
     var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
-    // I believe @:jignored should be iggnored by the writer?
+    // I believe @:jignored should be ignored by the writer?
     // var output = this.clone();
     // output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
     return writer.write(this, pretty ? '  ' : null);

From 8bf26322e9a405e3eb70508711b66a606cd444a6 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 18:09:30 -0400
Subject: [PATCH 44/96] Update `generatedBy` to the latest value when saving a
 chart.

---
 .../ChartEditorImportExportHandler.hx         | 25 +++++++++++++++----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index 0308cd871..a78eeae4c 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -384,17 +384,32 @@ class ChartEditorImportExportHandler
       if (variationId == '')
       {
         var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
-        if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
+        if (variationMetadata != null)
+        {
+          variationMetadata.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
+        }
         var variationChart:Null<SongChartData> = state.songChartData.get(variation);
-        if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
+        if (variationChart != null)
+        {
+          variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
+        }
       }
       else
       {
         var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
-        if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json',
-          variationMetadata.serialize()));
+        if (variationMetadata != null)
+        {
+          variationMetadata.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json', variationMetadata.serialize()));
+        }
         var variationChart:Null<SongChartData> = state.songChartData.get(variation);
-        if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize()));
+        if (variationChart != null)
+        {
+          variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize()));
+        }
       }
     }
 

From 06daa9d402a81bb45b5bf214595b4d0ac794fd4c Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 19:20:39 -0400
Subject: [PATCH 45/96] Increase Great threshold to 80%

---
 source/funkin/util/Constants.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index 4e706c612..1e0978839 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -467,7 +467,7 @@ class Constants
   // % Hit
   public static final RANK_PERFECT_THRESHOLD:Float = 1.00;
   public static final RANK_EXCELLENT_THRESHOLD:Float = 0.90;
-  public static final RANK_GREAT_THRESHOLD:Float = 0.75;
+  public static final RANK_GREAT_THRESHOLD:Float = 0.80;
   public static final RANK_GOOD_THRESHOLD:Float = 0.60;
 
   // public static final RANK_SHIT_THRESHOLD:Float = 0.00;

From 9088570b926eae749c5cc6822dbb01cd1d541f69 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 19:21:21 -0400
Subject: [PATCH 46/96] Make sure JSON data uses latest version/generatedBy
 when writing.

---
 source/funkin/data/song/SongData.hx           | 24 +++++++++++++++++++
 .../data/song/importer/ChartManifestData.hx   |  8 +++++++
 source/funkin/data/stage/StageData.hx         |  8 +++++++
 .../funkin/ui/credits/CreditsDataHandler.hx   |  2 +-
 .../ChartEditorImportExportHandler.hx         |  4 +++-
 5 files changed, 44 insertions(+), 2 deletions(-)

diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index ca805e139..769af8f08 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -117,6 +117,9 @@ class SongMetadata implements ICloneable<SongMetadata>
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var ignoreNullOptionals = true;
     var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
     // I believe @:jignored should be ignored by the writer?
@@ -125,6 +128,12 @@ class SongMetadata implements ICloneable<SongMetadata>
     return writer.write(this, pretty ? '  ' : null);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_METADATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   /**
    * Produces a string representation suitable for debugging.
    */
@@ -373,6 +382,12 @@ class SongMusicData implements ICloneable<SongMusicData>
     this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_MUSIC_DATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   public function clone():SongMusicData
   {
     var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
@@ -605,11 +620,20 @@ class SongChartData implements ICloneable<SongChartData>
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var ignoreNullOptionals = true;
     var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
     return writer.write(this, pretty ? '  ' : null);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_CHART_DATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   public function clone():SongChartData
   {
     // We have to manually perform the deep clone here because Map.deepClone() doesn't work.
diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx
index dd0d28479..04b5a1b69 100644
--- a/source/funkin/data/song/importer/ChartManifestData.hx
+++ b/source/funkin/data/song/importer/ChartManifestData.hx
@@ -61,10 +61,18 @@ class ChartManifestData
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var writer = new json2object.JsonWriter<ChartManifestData>();
     return writer.write(this, pretty ? '  ' : null);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = CHART_MANIFEST_DATA_VERSION;
+  }
+
   public static function deserialize(contents:String):Null<ChartManifestData>
   {
     var parser = new json2object.JsonParser<ChartManifestData>();
diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx
index 22b883c75..bebd86d02 100644
--- a/source/funkin/data/stage/StageData.hx
+++ b/source/funkin/data/stage/StageData.hx
@@ -58,9 +58,17 @@ class StageData
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var writer = new json2object.JsonWriter<StageData>();
     return writer.write(this, pretty ? '  ' : null);
   }
+
+  public function updateVersionToLatest():Void
+  {
+    this.version = StageRegistry.STAGE_DATA_VERSION;
+  }
 }
 
 typedef StageDataCharacters =
diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx
index 2240ec50e..844d0f4db 100644
--- a/source/funkin/ui/credits/CreditsDataHandler.hx
+++ b/source/funkin/ui/credits/CreditsDataHandler.hx
@@ -54,7 +54,7 @@ class CreditsDataHandler
           body: [
             {line: 'ninjamuffin99'},
             {line: 'PhantomArcade'},
-            {line: 'KawaiSprite'},
+            {line: 'Kawai Sprite'},
             {line: 'evilsk8r'},
           ]
         }
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index a78eeae4c..e84f7ec43 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -386,12 +386,14 @@ class ChartEditorImportExportHandler
         var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
         if (variationMetadata != null)
         {
+          variationMetadata.version = funkin.data.song.SongRegistry.SONG_METADATA_VERSION;
           variationMetadata.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
           zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
         }
         var variationChart:Null<SongChartData> = state.songChartData.get(variation);
         if (variationChart != null)
         {
+          variationChart.version = funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION;
           variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
           zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
         }
@@ -401,12 +403,12 @@ class ChartEditorImportExportHandler
         var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
         if (variationMetadata != null)
         {
-          variationMetadata.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
           zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json', variationMetadata.serialize()));
         }
         var variationChart:Null<SongChartData> = state.songChartData.get(variation);
         if (variationChart != null)
         {
+          variationChart.version = funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION;
           variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
           zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize()));
         }

From 074b1afc7659cf0923a03533994a812c9f39ef8d Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 20:16:23 -0400
Subject: [PATCH 47/96] Readd a reverted freeplay bugfix

---
 source/funkin/audio/FunkinSound.hx | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index b94c6008c..7663c1305 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -377,7 +377,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
           FlxG.sound.music = partialMusic;
           FlxG.sound.list.remove(FlxG.sound.music);
 
-          if (params.onLoad != null) params.onLoad();
+          if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
         });
 
         return true;
@@ -488,14 +488,21 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
     var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
 
-    promise.future.onError(function(e) {
-      soundRequest.error("Sound loading was errored or cancelled");
-    });
+    if (soundRequest == null)
+    {
+      promise.complete(null);
+    }
+    else
+    {
+      promise.future.onError(function(e) {
+        soundRequest.error("Sound loading was errored or cancelled");
+      });
 
-    soundRequest.future.onComplete(function(partialSound) {
-      var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
-      promise.complete(snd);
-    });
+      soundRequest.future.onComplete(function(partialSound) {
+        var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
+        promise.complete(snd);
+      });
+    }
 
     return promise;
   }

From e36fbfa72c79bf2ac793e1a78a80b0559c9cba64 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 20:16:37 -0400
Subject: [PATCH 48/96] Update the changelog

---
 CHANGELOG.md | 36 +++++++++++++++++++++++++++++-------
 1 file changed, 29 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10bbfe5f7..f5aefb885 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,31 +6,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [0.4.0] - 2024-05-??
 ### Added
-- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from
-- Improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
-- Reworked the Results screen, with additional animations and audio based on your performance.
+- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
+- Major visual improvements to the Results screen, with additional animations and audio based on your performance.
+- Major visual improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
+  - Freeplay now plays a preview of songs when you hover over them.
 - Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
   - You can see who charted a song from the Pause menu.
+- Added a new Scroll Speed chart event to change the note speed mid-song (thanks )
 ### Changed
 - Tweaked the charts for several songs:
+  - Monster
   - Winter Horrorland
   - Stress
   - Lit Up
+  - Tutorial (increased the note speed slightly)
+  - Senpai (increased the note speed)
+  - Thorns (increased the note speed slightly)
+- Favorite songs marked in Freeplay are now stored between sessions.
 - Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
+- Improved logic for NoteHitScriptEvents, allowing you to view the hit diff and modify whether a note hit is a combo break (thanks nebulazorua!)
 - Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
   - Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
 ### Fixed
+- Fixed a bug where the game would silently fail to load saves on HTML5
+- Fixed some bugs with the props on the Story Menu not bopping properly
+- Additional fixes to the Loading bar on HTML5 (thanks lemz1!)
+- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!)
+- Fixed a camera bug in the Main Menu (thanks richTrash21!)
+- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!)
+- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!)
 - Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
-- Fixed a bug where the Chart Editor would crash when losing (thanks gamerbross!)
+- Fixed a bug where the Chart Editor Playtest would crash when losing (thanks gamerbross!)
+- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay (thanks gamerbross!)
+- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!)
+- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!)
+- Improved debug logging for unscripted stages (thanks gamerbross!)
 - Made improvements to compiling documentation (thanks gedehari!)
 - Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!)
 - Optimized animation handling for characters (thanks richTrash21!)
+- Additional bug fixes and optimizations.
 
 ## [0.3.3] - 2024-05-14
 ### Changed
 - Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
 ### Fixed
-- Fix Web Loading Bar (thanks lemz1!)
+- Fixes to the Loading bar on HTML5 (thanks lemz1!)
 - Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
 - Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!)
 - Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!)
@@ -38,11 +58,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!)
 - Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!)
 - Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!)
-- Fix for a game over easter egg so you don't accidentally exit it when viewing
 - Fix a crash when querying FlxG.state in the crash handler
+- Fix for a game over easter egg so you don't accidentally exit it when viewing
 - Fix an issue where the Freeplay menu never displays 100% clear
+- Fix an issue where Weekend 1 Pico attempted to retrieve a missing asset.
+- Fix an issue where duplicate keybinds would be stoed, potentially causing a crash
 - Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!)
-- Hopefully fixed Freeplay crashes on AMD gpu's
+- Fix a crash on Freeplay found on AMD graphics cards
 
 ## [0.3.2] - 2024-05-03
 ### Added

From e08cdac35bfa3c57540d90d21118797d2e244fde Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Sat, 1 Jun 2024 01:16:52 +0100
Subject: [PATCH 49/96] score number shuffling

---
 source/funkin/play/ResultScore.hx | 80 +++++++++++++++++++++++++++++--
 1 file changed, 76 insertions(+), 4 deletions(-)

diff --git a/source/funkin/play/ResultScore.hx b/source/funkin/play/ResultScore.hx
index d5d5a6567..23e6c8d32 100644
--- a/source/funkin/play/ResultScore.hx
+++ b/source/funkin/play/ResultScore.hx
@@ -2,11 +2,16 @@ package funkin.play;
 
 import flixel.FlxSprite;
 import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxTimer;
+import flixel.tweens.FlxEase;
 
 class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 {
   public var scoreShit(default, set):Int = 0;
 
+  public var scoreStart:Int = 0;
+
   function set_scoreShit(val):Int
   {
     if (group == null || group.members == null) return val;
@@ -16,7 +21,8 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 
     while (dumbNumb > 0)
     {
-      group.members[loopNum].digit = dumbNumb % 10;
+      scoreStart += 1;
+      group.members[loopNum].finalDigit = dumbNumb % 10;
 
       // var funnyNum = group.members[loopNum];
       // prevNum = group.members[loopNum + 1];
@@ -44,9 +50,15 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 
   public function animateNumbers():Void
   {
-    for (i in group.members)
+    for (i in group.members.length-scoreStart...group.members.length)
     {
-      i.playAnim();
+     // if(i.finalDigit == 10) continue;
+
+      new FlxTimer().start((i-1)/24, _ -> {
+        group.members[i].finalDelay = scoreStart - (i-1);
+        group.members[i].playAnim();
+        group.members[i].shuffle();
+      });
     }
   }
 
@@ -71,12 +83,26 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 class ScoreNum extends FlxSprite
 {
   public var digit(default, set):Int = 10;
+  public var finalDigit(default, set):Int = 10;
+  public var glow:Bool = true;
+
+  function set_finalDigit(val):Int
+  {
+    animation.play('GONE', true, false, 0);
+
+    return finalDigit = val;
+  }
 
   function set_digit(val):Int
   {
     if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val])
     {
-      animation.play(numToString[val], true, false, 0);
+      if(glow){
+        animation.play(numToString[val], true, false, 0);
+        glow = false;
+      }else{
+        animation.play(numToString[val], true, false, 4);
+      }
       updateHitbox();
 
       switch (val)
@@ -107,6 +133,10 @@ class ScoreNum extends FlxSprite
     animation.play(numToString[digit], true, false, 0);
   }
 
+  public var shuffleTimer:FlxTimer;
+  public var finalTween:FlxTween;
+  public var finalDelay:Float = 0;
+
   public var baseY:Float = 0;
   public var baseX:Float = 0;
 
@@ -114,6 +144,47 @@ class ScoreNum extends FlxSprite
     "ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED"
   ];
 
+  function finishShuffleTween():Void{
+
+    var tweenFunction = function(x) {
+      var digitRounded = Math.floor(x);
+      //if(digitRounded == finalDigit) glow = true;
+      digit = digitRounded;
+    };
+
+    finalTween = FlxTween.num(0.0, finalDigit, 23/24, {
+      ease: FlxEase.quadOut,
+      onComplete: function (input) {
+        new FlxTimer().start((finalDelay)/24, _ -> {
+          animation.play(animation.curAnim.name, true, false, 0);
+        });
+        // fuck
+      }
+    }, tweenFunction);
+  }
+
+
+  function shuffleProgress(shuffleTimer:FlxTimer):Void
+  {
+    var tempDigit:Int = digit;
+    tempDigit += 1;
+    if(tempDigit > 9) tempDigit = 0;
+    if(tempDigit < 0) tempDigit = 0;
+    digit = tempDigit;
+
+    if (shuffleTimer.loops > 0 && shuffleTimer.loopsLeft == 0)
+    {
+      //digit = finalDigit;
+      finishShuffleTween();
+    }
+  }
+
+  public function shuffle():Void{
+    var duration:Float = 41/24;
+    var interval:Float = 1/24;
+    shuffleTimer = new FlxTimer().start(interval, shuffleProgress, Std.int(duration / interval));
+  }
+
   public function new(x:Float, y:Float)
   {
     super(x, y);
@@ -130,6 +201,7 @@ class ScoreNum extends FlxSprite
     }
 
     animation.addByPrefix('DISABLED', 'DISABLED', 24, false);
+    animation.addByPrefix('GONE', 'GONE', 24, false);
 
     this.digit = 10;
 

From 1cffc36ba8aa51b90523ff7fbe4464eca957cc29 Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Sat, 1 Jun 2024 01:17:11 +0100
Subject: [PATCH 50/96] results timing redo + fixed anims for all ranks

---
 source/funkin/play/ResultState.hx     | 280 +++++++++++++++++++++-----
 source/funkin/play/scoring/Scoring.hx |  70 ++++++-
 2 files changed, 293 insertions(+), 57 deletions(-)

diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 8b8c0aea3..8a7332a83 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -5,6 +5,7 @@ import funkin.ui.story.StoryMenuState;
 import funkin.graphics.adobeanimate.FlxAtlasSprite;
 import flixel.FlxSprite;
 import funkin.graphics.FunkinSprite;
+import flixel.effects.FlxFlicker;
 import flixel.graphics.frames.FlxBitmapFont;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.math.FlxPoint;
@@ -55,8 +56,10 @@ class ResultState extends MusicBeatSubState
   final score:ResultScore;
 
   var bfPerfect:Null<FlxAtlasSprite> = null;
+  var heartsPerfect:Null<FlxAtlasSprite> = null;
   var bfExcellent:Null<FlxAtlasSprite> = null;
   var bfGreat:Null<FlxAtlasSprite> = null;
+  var gfGreat:Null<FlxAtlasSprite> = null;
   var bfGood:Null<FlxSprite> = null;
   var gfGood:Null<FlxSprite> = null;
   var bfShit:Null<FlxAtlasSprite> = null;
@@ -94,15 +97,15 @@ class ResultState extends MusicBeatSubState
     clearPercentSmall.zIndex = 1000;
     clearPercentSmall.visible = false;
 
-    bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
+    bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFF1A6, 0xFFFFF1BE], 90);
 
     resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
 
-    ratingsPopin = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
+    ratingsPopin = FunkinSprite.createSparrow(-135, 135, "resultScreen/ratingsPopin");
 
-    scorePopin = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
+    scorePopin = FunkinSprite.createSparrow(-180, 515, "resultScreen/scorePopin");
 
-    highscoreNew = new FlxSprite(310, 570);
+    highscoreNew = new FlxSprite(44, 557);
 
     score = new ResultScore(35, 305, 10, params.scoreData.score);
   }
@@ -136,14 +139,14 @@ class ResultState extends MusicBeatSubState
     bgFlash.scrollFactor.set();
     bgFlash.visible = false;
     bgFlash.zIndex = 20;
-    bgFlash.cameras = [cameraBG];
+    //bgFlash.cameras = [cameraBG];
     add(bgFlash);
 
     // The sound system which falls into place behind the score text. Plays every time!
     var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
     soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
     soundSystem.visible = false;
-    new FlxTimer().start(0.3, _ -> {
+    new FlxTimer().start(8/24, _ -> {
       soundSystem.animation.play("idle");
       soundSystem.visible = true;
     });
@@ -153,7 +156,22 @@ class ResultState extends MusicBeatSubState
     switch (rank)
     {
       case PERFECT | PERFECT_GOLD:
-        bfPerfect = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared"));
+        heartsPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT/hearts", "shared"));
+        heartsPerfect.visible = false;
+        heartsPerfect.zIndex = 501;
+        add(heartsPerfect);
+
+        heartsPerfect.anim.onComplete = () -> {
+          if (heartsPerfect != null)
+          {
+            //bfPerfect.anim.curFrame = 137;
+            heartsPerfect.anim.curFrame = 43;
+            heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+
+        bfPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared"));
         bfPerfect.visible = false;
         bfPerfect.zIndex = 500;
         add(bfPerfect);
@@ -161,36 +179,57 @@ class ResultState extends MusicBeatSubState
         bfPerfect.anim.onComplete = () -> {
           if (bfPerfect != null)
           {
+            //bfPerfect.anim.curFrame = 137;
             bfPerfect.anim.curFrame = 137;
             bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
           }
         };
 
       case EXCELLENT:
-        bfExcellent = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared"));
+        bfExcellent = new FlxAtlasSprite(1329, 429, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared"));
         bfExcellent.visible = false;
         bfExcellent.zIndex = 500;
         add(bfExcellent);
 
-        bfExcellent.onAnimationFinish.add((animName) -> {
+        bfExcellent.anim.onComplete = () -> {
           if (bfExcellent != null)
           {
-            bfExcellent.playAnimation('Loop Start');
+            bfExcellent.anim.curFrame = 28;
+            bfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
           }
-        });
+        };
+
 
       case GREAT:
-        bfGreat = new FlxAtlasSprite(640, 200, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT", "shared"));
+        gfGreat = new FlxAtlasSprite(802, 331, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/gf", "shared"));
+        gfGreat.visible = false;
+        gfGreat.zIndex = 499;
+        add(gfGreat);
+
+        gfGreat.scale.set(0.93, 0.93);
+
+        gfGreat.anim.onComplete = () -> {
+          if (gfGreat != null)
+          {
+            gfGreat.anim.curFrame = 9;
+            gfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+        bfGreat = new FlxAtlasSprite(929, 363, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/bf", "shared"));
         bfGreat.visible = false;
         bfGreat.zIndex = 500;
         add(bfGreat);
 
-        bfGreat.onAnimationFinish.add((animName) -> {
+        bfGreat.scale.set(0.93, 0.93);
+
+        bfGreat.anim.onComplete = () -> {
           if (bfGreat != null)
           {
-            bfGreat.playAnimation('Loop Start');
+            bfGreat.anim.curFrame = 15;
+            bfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce!
           }
-        });
+        };
 
       case GOOD:
         gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD');
@@ -250,7 +289,7 @@ class ResultState extends MusicBeatSubState
 
     var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack"));
     blackTopBar.y = -blackTopBar.height;
-    FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut});
+    FlxTween.tween(blackTopBar, {y: 0}, 7/24, {ease: FlxEase.quartOut, startDelay: 3/24});
     blackTopBar.zIndex = 1010;
     add(blackTopBar);
 
@@ -258,7 +297,7 @@ class ResultState extends MusicBeatSubState
     resultsAnim.visible = false;
     resultsAnim.zIndex = 1200;
     add(resultsAnim);
-    new FlxTimer().start(0.3, _ -> {
+    new FlxTimer().start(6/24, _ -> {
       resultsAnim.visible = true;
       resultsAnim.animation.play("result");
     });
@@ -267,7 +306,7 @@ class ResultState extends MusicBeatSubState
     ratingsPopin.visible = false;
     ratingsPopin.zIndex = 1200;
     add(ratingsPopin);
-    new FlxTimer().start(1.0, _ -> {
+    new FlxTimer().start(21/24, _ -> {
       ratingsPopin.visible = true;
       ratingsPopin.animation.play("idle");
     });
@@ -276,23 +315,49 @@ class ResultState extends MusicBeatSubState
     scorePopin.visible = false;
     scorePopin.zIndex = 1200;
     add(scorePopin);
-    new FlxTimer().start(1.0, _ -> {
+    new FlxTimer().start(36/24, _ -> {
       scorePopin.visible = true;
       scorePopin.animation.play("score");
       scorePopin.animation.finishCallback = anim -> {
-        score.visible = true;
-        score.animateNumbers();
+
       };
     });
 
+    new FlxTimer().start(37/24, _ -> {
+      score.visible = true;
+      score.animateNumbers();
+      startRankTallySequence();
+    });
+
+    new FlxTimer().start(rank.getBFDelay(), _ -> {
+      afterRankTallySequence();
+    });
+
+    new FlxTimer().start(rank.getFlashDelay(), _ -> {
+      displayRankText();
+    });
+
     highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
-    highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
+    highscoreNew.animation.addByPrefix("new", "highscoreAnim0", 24, false);
     highscoreNew.visible = false;
-    highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
+    //highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
     highscoreNew.updateHitbox();
     highscoreNew.zIndex = 1200;
     add(highscoreNew);
 
+    new FlxTimer().start(rank.getHighscoreDelay(), _ -> {
+       if (params.isNewHighscore ?? false)
+        {
+          highscoreNew.visible = true;
+          highscoreNew.animation.play("new");
+          highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16);
+        }
+        else
+        {
+          highscoreNew.visible = false;
+        }
+    });
+
     var hStuf:Int = 50;
 
     var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
@@ -310,7 +375,10 @@ class ResultState extends MusicBeatSubState
     ratingGrp.add(maxCombo);
 
     hStuf += 2;
-    var extraYOffset:Float = 5;
+    var extraYOffset:Float = 7;
+
+    hStuf += 2;
+
     var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E);
     ratingGrp.add(tallySick);
 
@@ -339,20 +407,17 @@ class ResultState extends MusicBeatSubState
       });
     }
 
-    ratingsPopin.animation.finishCallback = anim -> {
-      startRankTallySequence();
 
-      if (params.isNewHighscore ?? false)
-      {
-        highscoreNew.visible = true;
-        highscoreNew.animation.play("new");
-        FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
-      }
-      else
-      {
-        highscoreNew.visible = false;
-      }
-    };
+      // if (params.isNewHighscore ?? false)
+      // {
+      //   highscoreNew.visible = true;
+      //   highscoreNew.animation.play("new");
+      //   //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
+      // }
+      // else
+      // {
+      //   highscoreNew.visible = false;
+      // }
 
     new FlxTimer().start(rank.getMusicDelay(), _ -> {
       if (rank.hasMusicIntro())
@@ -392,6 +457,8 @@ class ResultState extends MusicBeatSubState
 
   function startRankTallySequence():Void
   {
+    bgFlash.visible = true;
+    FlxTween.tween(bgFlash, {alpha: 0}, 5/24);
     var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100;
     clearPercentTarget = Math.floor(clearPercentFloat);
     // Prevent off-by-one errors.
@@ -400,8 +467,8 @@ class ResultState extends MusicBeatSubState
 
     trace('Clear percent target: ' + clearPercentFloat + ', round: ' + clearPercentTarget);
 
-    var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, clearPercentLerp);
-    FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 1.5,
+    var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 190, FlxG.height / 2 - 70, clearPercentLerp);
+    FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 58/24,
       {
         ease: FlxEase.quartOut,
         onUpdate: _ -> {
@@ -416,10 +483,6 @@ class ResultState extends MusicBeatSubState
           // Play confirm sound.
           FunkinSound.playOnce(Paths.sound('confirmMenu'));
 
-          // Flash background.
-          bgFlash.visible = true;
-          FlxTween.tween(bgFlash, {alpha: 0}, 0.4);
-
           // Just to be sure that the lerp didn't mess things up.
           clearPercentCounter.curNumber = clearPercentTarget;
 
@@ -428,7 +491,7 @@ class ResultState extends MusicBeatSubState
             clearPercentCounter.flash(false);
           });
 
-          displayRankText();
+          //displayRankText();
 
           // previously 2.0 seconds
           new FlxTimer().start(0.25, _ -> {
@@ -441,7 +504,7 @@ class ResultState extends MusicBeatSubState
                 }
               });
 
-            afterRankTallySequence();
+           //afterRankTallySequence();
           });
         }
       });
@@ -466,7 +529,6 @@ class ResultState extends MusicBeatSubState
         {
           highscoreNew.visible = true;
           highscoreNew.animation.play("new");
-          FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
         }
         else
         {
@@ -480,27 +542,36 @@ class ResultState extends MusicBeatSubState
 
   function displayRankText():Void
   {
+    bgFlash.visible = true;
+    bgFlash.alpha = 1;
+    FlxTween.tween(bgFlash, {alpha: 0}, 14/24);
+
     var rankTextVert:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getVerTextAsset()), Y, 0, 30);
-    rankTextVert.x = FlxG.width - 64;
+    rankTextVert.x = FlxG.width - 44;
     rankTextVert.y = 100;
     rankTextVert.zIndex = 990;
     add(rankTextVert);
 
-    // Scrolling.
-    rankTextVert.velocity.y = -50;
+    FlxFlicker.flicker(rankTextVert, 2/24 * 3, 2/24, true);
 
-    for (i in 0...10)
+    // Scrolling.
+    new FlxTimer().start(30/24, _ -> {
+        rankTextVert.velocity.y = -80;
+    });
+
+
+    for (i in 0...12)
     {
       var rankTextBack:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getHorTextAsset()), X, 10, 0);
       rankTextBack.x = FlxG.width / 2 - 320;
-      rankTextBack.y = 50 + (150 * i / 2) + 10;
+      rankTextBack.y = 50 + (135 * i / 2) + 10;
       // rankTextBack.angle = -3.8;
       rankTextBack.zIndex = 100;
       rankTextBack.cameras = [cameraScroll];
       add(rankTextBack);
 
       // Scrolling.
-      rankTextBack.velocity.x = (i % 2 == 0) ? -10.0 : 10.0;
+      rankTextBack.velocity.x = (i % 2 == 0) ? -7.0 : 7.0;
     }
 
     refresh();
@@ -508,6 +579,7 @@ class ResultState extends MusicBeatSubState
 
   function afterRankTallySequence():Void
   {
+
     showSmallClearPercent();
 
     switch (rank)
@@ -522,6 +594,17 @@ class ResultState extends MusicBeatSubState
           bfPerfect.visible = true;
           bfPerfect.playAnimation('');
         }
+        new FlxTimer().start(106 / 24, _ -> {
+        if (heartsPerfect == null)
+        {
+          trace("Could not build heartsPerfect animation!");
+        }
+        else
+        {
+          heartsPerfect.visible = true;
+          heartsPerfect.playAnimation('');
+        }
+        });
       case EXCELLENT:
         if (bfExcellent == null)
         {
@@ -530,7 +613,7 @@ class ResultState extends MusicBeatSubState
         else
         {
           bfExcellent.visible = true;
-          bfExcellent.playAnimation('Intro');
+          bfExcellent.playAnimation('');
         }
       case GREAT:
         if (bfGreat == null)
@@ -540,8 +623,20 @@ class ResultState extends MusicBeatSubState
         else
         {
           bfGreat.visible = true;
-          bfGreat.playAnimation('Intro');
+          bfGreat.playAnimation('');
         }
+
+        new FlxTimer().start(6 / 24, _ -> {
+        if (gfGreat == null)
+        {
+          trace("Could not build GREAT animation for gf!");
+        }
+        else
+        {
+          gfGreat.visible = true;
+          gfGreat.playAnimation('');
+        }
+        });
       case SHIT:
         if (bfShit == null)
         {
@@ -627,7 +722,9 @@ class ResultState extends MusicBeatSubState
       refresh();
     }
 
-    movingSongStuff = true;
+    new FlxTimer().start(2.5, _ -> {
+      movingSongStuff = true;
+    });
   }
 
   var movingSongStuff:Bool = false;
@@ -647,6 +744,79 @@ class ResultState extends MusicBeatSubState
 
   override function update(elapsed:Float):Void
   {
+    // if(FlxG.keys.justPressed.R){
+    //   FlxG.switchState(() -> new funkin.play.ResultState(
+    //   {
+    //     storyMode: false,
+    //     title: "Cum Song Erect by Kawai Sprite",
+    //     songId: "cum",
+    //     difficultyId: "nightmare",
+    //     isNewHighscore: true,
+    //     scoreData:
+    //       {
+    //         score: 1_234_567,
+    //         tallies:
+    //           {
+    //             sick: 200,
+    //             good: 0,
+    //             bad: 0,
+    //             shit: 0,
+    //             missed: 0,
+    //             combo: 0,
+    //             maxCombo: 69,
+    //             totalNotesHit: 200,
+    //             totalNotes: 200 // 0,
+    //           }
+    //       },
+    //   }));
+    // }
+
+    // if(heartsPerfect != null){
+    // if (FlxG.keys.justPressed.I)
+    // {
+    //   heartsPerfect.y -= 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // if (FlxG.keys.justPressed.J)
+    // {
+    //   heartsPerfect.x -= 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // if (FlxG.keys.justPressed.L)
+    // {
+    //   heartsPerfect.x += 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // if (FlxG.keys.justPressed.K)
+    // {
+    //   heartsPerfect.y += 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // }
+
+    // if(bfGreat != null){
+    // if (FlxG.keys.justPressed.W)
+    // {
+    //   bfGreat.y -= 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // if (FlxG.keys.justPressed.A)
+    // {
+    //   bfGreat.x -= 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // if (FlxG.keys.justPressed.D)
+    // {
+    //   bfGreat.x += 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // if (FlxG.keys.justPressed.S)
+    // {
+    //   bfGreat.y += 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // }
+
     // maskShaderSongName.swagSprX = songName.x;
     maskShaderDifficulty.swagSprX = difficulty.x;
 
diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx
index 6155ec879..04bd81cf8 100644
--- a/source/funkin/play/scoring/Scoring.hx
+++ b/source/funkin/play/scoring/Scoring.hx
@@ -403,9 +403,75 @@ enum abstract ScoringRank(String)
     {
       case PERFECT_GOLD | PERFECT:
         // return 2.5;
-        return 5.0;
+        return 95/24;
       case EXCELLENT:
-        return 1.75;
+        return 0;
+      case GREAT:
+        return 5/24;
+      case GOOD:
+        return 3/24;
+      case SHIT:
+        return 2/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getBFDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 95/24;
+      case EXCELLENT:
+        return 97/24;
+      case GREAT:
+        return 95/24;
+      case GOOD:
+        return 95/24;
+      case SHIT:
+        return 95/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getFlashDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 129/24;
+      case EXCELLENT:
+        return 122/24;
+      case GREAT:
+        return 109/24;
+      case GOOD:
+        return 107/24;
+      case SHIT:
+        return 186/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getHighscoreDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 140/24;
+      case EXCELLENT:
+        return 140/24;
+      case GREAT:
+        return 129/24;
+      case GOOD:
+        return 127/24;
+      case SHIT:
+        return 207/24;
       default:
         return 3.5;
     }

From b8920effdcae5b2155a5a9a25e365dd500f9c705 Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Sat, 1 Jun 2024 01:17:58 +0100
Subject: [PATCH 51/96] assets submod..

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 7a0d92d30..de73a8ec8 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 7a0d92d3007de42c452b2ea97a917d8c8d114ee7
+Subproject commit de73a8ec8b7462beedca5c2eb539aea38d4c7712

From fd28c91e75b949976bc074d0cd1d0cde5c97b6f4 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 21:21:46 -0400
Subject: [PATCH 52/96] Update assets submodule

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 7a0d92d30..59d376218 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 7a0d92d3007de42c452b2ea97a917d8c8d114ee7
+Subproject commit 59d376218d288ef3001de6ef78b8d6d7c5f52842

From 12acdcd9d9cb4cf1a65e5598f421ecca962f33a3 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 21:55:27 -0400
Subject: [PATCH 53/96] Fix a Results->Freeplay crash

---
 source/funkin/ui/freeplay/FreeplayState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 192c6e3ce..1bde92667 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -868,7 +868,7 @@ class FreeplayState extends MusicBeatSubState
 
     FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1);
 
-    grpCapsules.members[curSelected].blurredRanking.animation.play(grpCapsules.members[curSelected].blurredRanking.animation.curAnim.name, true);
+    grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
     FlxTween.tween(grpCapsules.members[curSelected].blurredRanking, {"scale.x": 1, "scale.y": 1}, 0.1);
 
     new FlxTimer().start(0.1, _ -> {

From 6851edc64b89ce960d34975af1cf2ce6699e5996 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 21:55:33 -0400
Subject: [PATCH 54/96] Add new charts

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 59d376218..0e5a66cb1 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 59d376218d288ef3001de6ef78b8d6d7c5f52842
+Subproject commit 0e5a66cb15229fde2c65503ba1267dfc7b4640b5

From e4eb9a7dc90f40ef667d78c027daecf58832819a Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 1 Jun 2024 16:23:22 -0400
Subject: [PATCH 55/96] assets submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 0e5a66cb1..3bfa4e3da 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 0e5a66cb15229fde2c65503ba1267dfc7b4640b5
+Subproject commit 3bfa4e3da87713ea651f60d4f898c283e5d86093

From bd7875e86a9a3aabe11d9e6679579540ac011189 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 1 Jun 2024 17:13:07 -0400
Subject: [PATCH 56/96] fix rank icons appearing when they shouldn't be

---
 source/funkin/play/scoring/Scoring.hx | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx
index 6155ec879..a6d92454b 100644
--- a/source/funkin/play/scoring/Scoring.hx
+++ b/source/funkin/play/scoring/Scoring.hx
@@ -351,6 +351,9 @@ class Scoring
   {
     if (scoreData == null) return null;
 
+    // we can return null here, meaning that the player hasn't actually played and finished the song (thus has no data)
+    if (scoreData.tallies.totalNotes == 0) return null;
+
     // Perfect (Platinum) is a Sick Full Clear
     var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes;
     if (isPerfectGold) return ScoringRank.PERFECT_GOLD;

From 63bd6f2ace9c093133f1c7e1c02de60ad969aa4a Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Sun, 2 Jun 2024 00:25:52 +0100
Subject: [PATCH 57/96] sparks nd more fixes.. i think

---
 source/funkin/play/ResultState.hx          |   9 +-
 source/funkin/ui/freeplay/FreeplayState.hx | 194 +++++++++++++++------
 source/funkin/ui/freeplay/SongMenuItem.hx  |  15 ++
 3 files changed, 164 insertions(+), 54 deletions(-)

diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 1ad19b41e..d38731e14 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -364,7 +364,7 @@ class ResultState extends MusicBeatSubState
     rankBg.zIndex = 99999;
     add(rankBg);
 
-   rankBg.alpha = 0;
+    rankBg.alpha = 0;
 
     refresh();
 
@@ -665,8 +665,7 @@ class ResultState extends MusicBeatSubState
       else
       {
         var rigged:Bool = true;
-        if (rank > Scoring.calculateRank(params?.prevScoreData))
-        //if (rigged)
+        if (rank > Scoring.calculateRank(params?.prevScoreData)) // if (rigged)
         {
           trace('THE RANK IS Higher.....');
 
@@ -682,7 +681,8 @@ class ResultState extends MusicBeatSubState
                           oldRank: Scoring.calculateRank(params?.prevScoreData),
                           newRank: rank,
                           songId: params.songId,
-                          difficultyId: params.difficultyId
+                          difficultyId: params.difficultyId,
+                          playRankAnim: true
                         }
                     }
                   }));
@@ -698,6 +698,7 @@ class ResultState extends MusicBeatSubState
                 fromResults:
                   {
                     oldRank: null,
+                    playRankAnim: false,
                     newRank: rank,
                     songId: params.songId,
                     difficultyId: params.difficultyId
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 186c84c33..e1d8eb81c 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -6,6 +6,7 @@ import flixel.addons.ui.FlxInputText;
 import flixel.FlxCamera;
 import flixel.FlxSprite;
 import flixel.group.FlxGroup;
+import funkin.graphics.shaders.GaussianBlurShader;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
 import flixel.input.touch.FlxTouch;
@@ -45,6 +46,7 @@ import funkin.util.MathUtil;
 import lime.utils.Assets;
 import flixel.tweens.misc.ShakeTween;
 import funkin.effects.IntervalShake;
+import funkin.ui.freeplay.SongMenuItem.FreeplayRank;
 
 /**
  * Parameters used to initialize the FreeplayState.
@@ -66,6 +68,11 @@ typedef FromResultsParams =
    */
   var ?oldRank:ScoringRank;
 
+  /**
+   * Whether or not to play the rank animation on returning to freeplay.
+   */
+  var playRankAnim:Bool;
+
   /**
    * The new rank the song has.
    */
@@ -201,7 +208,8 @@ class FreeplayState extends MusicBeatSubState
 
     fromResultsParams = params?.fromResults;
 
-    if(fromResultsParams?.oldRank != null){
+    if (fromResultsParams?.playRankAnim == true)
+    {
       prepForNewRank = true;
     }
 
@@ -244,10 +252,10 @@ class FreeplayState extends MusicBeatSubState
     if (prepForNewRank == false)
     {
       FunkinSound.playMusic('freakyMenu',
-      {
-        overrideExisting: true,
-        restartTrack: false
-      });
+        {
+          overrideExisting: true,
+          restartTrack: false
+        });
     }
 
     // Add a null entry that represents the RANDOM option
@@ -837,6 +845,9 @@ class FreeplayState extends MusicBeatSubState
     return songsToFilter;
   }
 
+  var sparks:FlxSprite;
+  var sparksADD:FlxSprite;
+
   function rankAnimStart(fromResults:Null<FromResultsParams>):Void
   {
     busy = true;
@@ -848,6 +859,49 @@ class FreeplayState extends MusicBeatSubState
     FlxG.sound.music.volume = 0;
     rankBg.alpha = 1;
 
+    if (fromResults?.oldRank != null)
+    {
+      grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank;
+      grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank;
+
+      sparks = new FlxSprite(0, 0);
+      sparks.frames = Paths.getSparrowAtlas('freeplay/sparks');
+      sparks.animation.addByPrefix('sparks', 'sparks', 24, false);
+      sparks.visible = false;
+      sparks.blend = BlendMode.ADD;
+      sparks.setPosition(517, 134);
+      sparks.scale.set(0.5, 0.5);
+      add(sparks);
+      sparks.cameras = [rankCamera];
+
+      sparksADD = new FlxSprite(0, 0);
+      sparksADD.visible = false;
+      sparksADD.frames = Paths.getSparrowAtlas('freeplay/sparksadd');
+      sparksADD.animation.addByPrefix('sparks add', 'sparks add', 24, false);
+      sparksADD.setPosition(498, 116);
+      sparksADD.blend = BlendMode.ADD;
+      sparksADD.scale.set(0.5, 0.5);
+      add(sparksADD);
+      sparksADD.cameras = [rankCamera];
+
+      switch (fromResults.oldRank)
+      {
+        case SHIT:
+          sparksADD.color = 0xFF6044FF;
+        case GOOD:
+          sparksADD.color = 0xFFEF8764;
+        case GREAT:
+          sparksADD.color = 0xFFEAF6FF;
+        case EXCELLENT:
+          sparksADD.color = 0xFFFDCB42;
+        case PERFECT:
+          sparksADD.color = 0xFFFF58B4;
+        case PERFECT_GOLD:
+          sparksADD.color = 0xFFFFB619;
+      }
+      // sparksADD.color = sparks.color;
+    }
+
     grpCapsules.members[curSelected].doLerp = false;
 
     // originalPos.x = grpCapsules.members[curSelected].x;
@@ -857,8 +911,8 @@ class FreeplayState extends MusicBeatSubState
     originalPos.y = 235.6;
     trace(originalPos);
 
-    grpCapsules.members[curSelected].ranking.alpha = 0;
-    grpCapsules.members[curSelected].blurredRanking.alpha = 0;
+    grpCapsules.members[curSelected].ranking.visible = false;
+    grpCapsules.members[curSelected].blurredRanking.visible = false;
 
     rankCamera.zoom = 1.85;
     FlxTween.tween(rankCamera, {"zoom": 1.8}, 0.6, {ease: FlxEase.sineIn});
@@ -880,9 +934,8 @@ class FreeplayState extends MusicBeatSubState
 
   function rankDisplayNew(fromResults:Null<FromResultsParams>):Void
   {
-    grpCapsules.members[curSelected].ranking.alpha = 1;
-    grpCapsules.members[curSelected].blurredRanking.alpha = 1;
-
+    grpCapsules.members[curSelected].ranking.visible = true;
+    grpCapsules.members[curSelected].blurredRanking.visible = true;
     grpCapsules.members[curSelected].ranking.scale.set(20, 20);
     grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20);
 
@@ -895,7 +948,23 @@ class FreeplayState extends MusicBeatSubState
     FlxTween.tween(grpCapsules.members[curSelected].blurredRanking, {"scale.x": 1, "scale.y": 1}, 0.1);
 
     new FlxTimer().start(0.1, _ -> {
-      trace(grpCapsules.members[curSelected].ranking.rank);
+      // trace(grpCapsules.members[curSelected].ranking.rank);
+      if (fromResults?.oldRank != null)
+      {
+        grpCapsules.members[curSelected].fakeRanking.visible = false;
+        grpCapsules.members[curSelected].fakeBlurredRanking.visible = false;
+
+        sparks.visible = true;
+        sparksADD.visible = true;
+        sparks.animation.play('sparks', true);
+        sparksADD.animation.play('sparks add', true);
+
+        sparks.animation.finishCallback = anim -> {
+          sparks.visible = false;
+          sparksADD.visible = false;
+        };
+      }
+
       switch (fromResultsParams?.newRank)
       {
         case SHIT:
@@ -1053,10 +1122,10 @@ class FreeplayState extends MusicBeatSubState
       // dj.fistPump();
       prepForNewRank = false;
       FunkinSound.playMusic('freakyMenu',
-      {
-        overrideExisting: true,
-        restartTrack: false
-      });
+        {
+          overrideExisting: true,
+          restartTrack: false
+        });
       FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
     });
   }
@@ -1092,35 +1161,56 @@ class FreeplayState extends MusicBeatSubState
       rankAnimStart(fromResultsParams);
     }
 
-    if (FlxG.keys.justPressed.H)
-    {
-      rankDisplayNew(fromResultsParams);
-    }
+    // if (FlxG.keys.justPressed.H)
+    // {
+    //   rankDisplayNew(fromResultsParams);
+    // }
+
+    // if (FlxG.keys.justPressed.G)
+    // {
+    //   rankAnimSlam(fromResultsParams);
+    // }
 
     if (FlxG.keys.justPressed.G)
     {
-      rankAnimSlam(fromResultsParams);
+      sparks.y -= 2;
+      trace(sparks.x, sparks.y);
+    }
+    if (FlxG.keys.justPressed.V)
+    {
+      sparks.x -= 2;
+      trace(sparks.x, sparks.y);
+    }
+    if (FlxG.keys.justPressed.N)
+    {
+      sparks.x += 2;
+      trace(sparks.x, sparks.y);
+    }
+    if (FlxG.keys.justPressed.B)
+    {
+      sparks.y += 2;
+      trace(sparks.x, sparks.y);
     }
 
     if (FlxG.keys.justPressed.I)
     {
-      confirmTextGlow.y -= 1;
-      trace(confirmTextGlow.x, confirmTextGlow.y);
+      sparksADD.y -= 2;
+      trace(sparksADD.x, sparksADD.y);
     }
     if (FlxG.keys.justPressed.J)
     {
-      confirmTextGlow.x -= 1;
-      trace(confirmTextGlow.x, confirmTextGlow.y);
+      sparksADD.x -= 2;
+      trace(sparksADD.x, sparksADD.y);
     }
     if (FlxG.keys.justPressed.L)
     {
-      confirmTextGlow.x += 1;
-      trace(confirmTextGlow.x, confirmTextGlow.y);
+      sparksADD.x += 2;
+      trace(sparksADD.x, sparksADD.y);
     }
     if (FlxG.keys.justPressed.K)
     {
-      confirmTextGlow.y += 1;
-      trace(confirmTextGlow.x, confirmTextGlow.y);
+      sparksADD.y += 2;
+      trace(sparksADD.x, sparksADD.y);
     }
     #end
 
@@ -1758,26 +1848,27 @@ class FreeplayState extends MusicBeatSubState
       }
       else
       {
-        if(prepForNewRank == false){
-        var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
-        // TODO: Stream the instrumental of the selected song?
-        FunkinSound.playMusic(daSongCapsule.songData.songId,
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: false,
-            pathsFunction: INST,
-            suffix: potentiallyErect,
-            partialParams:
-              {
-                loadPartial: true,
-                start: 0,
-                end: 0.1
-              },
-            onLoad: function() {
-              FlxG.sound.music.fadeIn(2, 0, 0.4);
-            }
-          });
+        if (prepForNewRank == false)
+        {
+          var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
+          // TODO: Stream the instrumental of the selected song?
+          FunkinSound.playMusic(daSongCapsule.songData.songId,
+            {
+              startingVolume: 0.0,
+              overrideExisting: true,
+              restartTrack: false,
+              pathsFunction: INST,
+              suffix: potentiallyErect,
+              partialParams:
+                {
+                  loadPartial: true,
+                  start: 0,
+                  end: 0.1
+                },
+              onLoad: function() {
+                FlxG.sound.music.fadeIn(2, 0, 0.4);
+              }
+            });
         }
       }
       grpCapsules.members[curSelected].selected = true;
@@ -1791,9 +1882,12 @@ class FreeplayState extends MusicBeatSubState
   public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
   {
     var result:MainMenuState;
-    if(params?.fromResults.oldRank != null){
+    if (params?.fromResults.playRankAnim == true)
+    {
       result = new MainMenuState(true);
-    }else{
+    }
+    else
+    {
       result = new MainMenuState(false);
     }
 
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index b52032bc3..3af75c105 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -39,9 +39,13 @@ class SongMenuItem extends FlxSpriteGroup
 
   public var songText:CapsuleText;
   public var favIcon:FlxSprite;
+
   public var ranking:FreeplayRank;
   public var blurredRanking:FreeplayRank;
 
+  public var fakeRanking:FreeplayRank;
+  public var fakeBlurredRanking:FreeplayRank;
+
   var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect", "perfectsick"];
 
   public var targetPos:FlxPoint = new FlxPoint();
@@ -131,12 +135,23 @@ class SongMenuItem extends FlxSpriteGroup
     // doesn't get added, simply is here to help with visibility of things for the pop in!
     grpHide = new FlxGroup();
 
+    fakeRanking = new FreeplayRank(420, 41);
+    add(fakeRanking);
+
+    fakeBlurredRanking = new FreeplayRank(fakeRanking.x, fakeRanking.y);
+    fakeBlurredRanking.shader = new GaussianBlurShader(1);
+    add(fakeBlurredRanking);
+
+    fakeRanking.visible = false;
+    fakeBlurredRanking.visible = false;
+
     ranking = new FreeplayRank(420, 41);
     add(ranking);
 
     blurredRanking = new FreeplayRank(ranking.x, ranking.y);
     blurredRanking.shader = new GaussianBlurShader(1);
     add(blurredRanking);
+
     // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
     // ranking.scale.x = ranking.scale.y = realScaled;
     // ranking.alpha = 0.75;

From b96fa51045f4cae5fa889eade75ac1f1c676034a Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Sun, 2 Jun 2024 00:26:13 +0100
Subject: [PATCH 58/96] assets submod!!

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index d64939d86..ad8a0a28a 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit d64939d866c70a831b76ed78930782b66871dcc2
+Subproject commit ad8a0a28addb3153c01bca2f8a2fdea05c5ac9ea

From 8248cffbafb1621b5c2811f01a02ee62bd3b48ee Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 1 Jun 2024 20:36:14 -0400
Subject: [PATCH 59/96] small bool cleanings, and maybe fixes

---
 source/funkin/ui/freeplay/FreeplayState.hx | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index e1d8eb81c..e97ab391b 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1121,12 +1121,6 @@ class FreeplayState extends MusicBeatSubState
     new FlxTimer().start(2, _ -> {
       // dj.fistPump();
       prepForNewRank = false;
-      FunkinSound.playMusic('freakyMenu',
-        {
-          overrideExisting: true,
-          restartTrack: false
-        });
-      FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
     });
   }
 
@@ -1527,6 +1521,7 @@ class FreeplayState extends MusicBeatSubState
               overrideExisting: true,
               restartTrack: false
             });
+          FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
           close();
         }
         else
@@ -1842,13 +1837,13 @@ class FreeplayState extends MusicBeatSubState
           {
             startingVolume: 0.0,
             overrideExisting: true,
-            restartTrack: true
+            restartTrack: false
           });
         FlxG.sound.music.fadeIn(2, 0, 0.8);
       }
       else
       {
-        if (prepForNewRank == false)
+        if (!prepForNewRank)
         {
           var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
           // TODO: Stream the instrumental of the selected song?
@@ -1882,14 +1877,9 @@ class FreeplayState extends MusicBeatSubState
   public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
   {
     var result:MainMenuState;
-    if (params?.fromResults.playRankAnim == true)
-    {
-      result = new MainMenuState(true);
-    }
+    if (params?.fromResults.playRankAnim) result = new MainMenuState(true);
     else
-    {
       result = new MainMenuState(false);
-    }
 
     result.openSubState(new FreeplayState(params, stickers));
     result.persistentUpdate = false;

From 7aee1f900af2d19df18252a72eb382326b878c2f Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 31 May 2024 21:55:27 -0400
Subject: [PATCH 60/96] Fix a Results->Freeplay crash

---
 source/funkin/ui/freeplay/FreeplayState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index e97ab391b..0dd520ee3 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -944,7 +944,7 @@ class FreeplayState extends MusicBeatSubState
 
     FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1);
 
-    grpCapsules.members[curSelected].blurredRanking.animation.play(grpCapsules.members[curSelected].blurredRanking.animation.curAnim.name, true);
+    grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
     FlxTween.tween(grpCapsules.members[curSelected].blurredRanking, {"scale.x": 1, "scale.y": 1}, 0.1);
 
     new FlxTimer().start(0.1, _ -> {

From 7c6c51ea71d8c8790a3ba742e0b7b5724ad33197 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 1 Jun 2024 22:36:47 -0400
Subject: [PATCH 61/96] results dupe fix

---
 source/funkin/ui/freeplay/FreeplayState.hx | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 6441c9cd5..218ecc2ab 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -712,12 +712,6 @@ class FreeplayState extends MusicBeatSubState
     // If curSelected is 0, the result will be null and fall back to the rememberedSongId.
     rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId;
 
-    if (fromResultsParams != null)
-    {
-      rememberedSongId = fromResultsParams.songId;
-      rememberedDifficulty = fromResultsParams.difficultyId;
-    }
-
     for (cap in grpCapsules.members)
     {
       cap.songText.resetText();
@@ -828,6 +822,14 @@ class FreeplayState extends MusicBeatSubState
   {
     busy = true;
 
+    if (fromResults != null)
+    {
+      rememberedSongId = fromResults.songId;
+      rememberedDifficulty = fromResults.difficultyId;
+      changeSelection();
+      changeDiff();
+    }
+
     dj.fistPump();
     // rankCamera.fade(FlxColor.BLACK, 0.5, true);
     rankCamera.fade(0xFF000000, 0.5, true, null, true);

From 51a44d481056b5846ed23c97af5f6547ff99ae76 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 1 Jun 2024 23:36:57 -0400
Subject: [PATCH 62/96] persistent draw false for results screen (fixes
 FUNK-256)

---
 source/funkin/play/PlayState.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 478a13269..af3281c4b 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -3144,7 +3144,7 @@ class PlayState extends MusicBeatSubState
           },
         isNewHighscore: isNewHighscore
       });
-    res.camera = camHUD;
+    this.persistentDraw = false;
     openSubState(res);
   }
 

From 08f3b1d95b99ce84907fcf74bc12a68c699dc8a8 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sun, 2 Jun 2024 00:15:10 -0400
Subject: [PATCH 63/96] anim fixes on results

---
 source/funkin/play/ResultState.hx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 8b8c0aea3..35976c359 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -123,6 +123,7 @@ class ResultState extends MusicBeatSubState
     FlxG.cameras.add(cameraEverything, false);
 
     FlxG.cameras.setDefaultDrawTarget(cameraEverything, true);
+    this.camera = cameraEverything;
 
     // Reset the camera zoom on the results screen.
     FlxG.camera.zoom = 1.0;

From f961ac8e4e55e4d9b9f4250b6c123869a6d1a96a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sun, 2 Jun 2024 02:11:06 -0400
Subject: [PATCH 64/96] Actually fix a crash bug on Freeplay menu when
 selecting a mod

---
 hmm.json | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/hmm.json b/hmm.json
index fe4e56dd9..f94b662ac 100644
--- a/hmm.json
+++ b/hmm.json
@@ -1,12 +1,5 @@
 {
   "dependencies": [
-    {
-      "name": "FlxPartialSound",
-      "type": "git",
-      "dir": null,
-      "ref": "44aa7eb",
-      "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
-    },
     {
       "name": "discord_rpc",
       "type": "git",
@@ -47,6 +40,13 @@
       "ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49",
       "url": "https://github.com/FunkinCrew/flxanimate"
     },
+    {
+      "name": "FlxPartialSound",
+      "type": "git",
+      "dir": null,
+      "ref": "f986332ba5ab02abd386ce662578baf04904604a",
+      "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
+    },
     {
       "name": "format",
       "type": "haxelib",

From 75621435ea870d80532a66cbf13f60bd361811e3 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sun, 2 Jun 2024 02:53:07 -0400
Subject: [PATCH 65/96] lil more polish to fav icon + clipping

---
 source/funkin/ui/freeplay/FreeplayState.hx | 11 ++++++-----
 source/funkin/ui/freeplay/SongMenuItem.hx  |  2 +-
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index d3471eeb1..494bc20f0 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -655,7 +655,7 @@ class FreeplayState extends MusicBeatSubState
       cardGlow.visible = true;
       FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut});
 
-      if (prepForNewRank == true)
+      if (prepForNewRank)
       {
         rankAnimStart(fromResultsParams);
       }
@@ -686,7 +686,7 @@ class FreeplayState extends MusicBeatSubState
     rankBg.cameras = [rankCamera];
     rankBg.alpha = 0;
 
-    if (prepForNewRank == true)
+    if (prepForNewRank)
     {
       rankCamera.fade(0xFF000000, 0, false, null, true);
     }
@@ -782,10 +782,8 @@ class FreeplayState extends MusicBeatSubState
       funnyMenu.hsvShader = hsvShader;
 
       funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45);
-
-      funnyMenu.forcePosition();
-
       funnyMenu.checkClip();
+      funnyMenu.forcePosition();
 
       grpCapsules.add(funnyMenu);
     }
@@ -1223,6 +1221,8 @@ class FreeplayState extends MusicBeatSubState
           grpCapsules.members[realShit].favIcon.visible = true;
           grpCapsules.members[realShit].favIcon.animation.play('fav');
           FunkinSound.playOnce(Paths.sound('fav'), 1);
+          grpCapsules.members[realShit].checkClip();
+          grpCapsules.members[realShit].selected = grpCapsules.members[realShit].selected; // set selected again, so it can run it's getter function to initialize movement
           busy = true;
 
           grpCapsules.members[realShit].doLerp = false;
@@ -1244,6 +1244,7 @@ class FreeplayState extends MusicBeatSubState
           FunkinSound.playOnce(Paths.sound('unfav'), 1);
           new FlxTimer().start(0.2, _ -> {
             grpCapsules.members[realShit].favIcon.visible = false;
+            grpCapsules.members[realShit].checkClip();
           });
 
           busy = true;
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index 41010f0b5..b9fef5a79 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -266,7 +266,7 @@ class SongMenuItem extends FlxSpriteGroup
     var clipType:Int = 0;
 
     if (ranking.visible == true) clipType += 1;
-    if (favIcon.visible == true) clipType += 1;
+    if (favIcon.visible == true) clipType = 2;
     switch (clipType)
     {
       case 2:

From 6596c47f473d935ca135b1e66c384d52007755b8 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 3 Jun 2024 14:03:30 -0400
Subject: [PATCH 66/96] fabs

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index d9ea5ebe5..f7c418c52 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit d9ea5ebe5e4db8584a8b1e1e16820b4d1527794c
+Subproject commit f7c418c52f38769daf56521ee801df699ae5435b

From e12a48b6fc2721086fa54b6deb3d7cd08130ae2b Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 3 Jun 2024 18:18:33 -0400
Subject: [PATCH 67/96] fabs code stuff and difficulty fixin

---
 assets                            |   2 +-
 source/funkin/play/ResultState.hx | 123 ++++++++++++++----------------
 2 files changed, 59 insertions(+), 66 deletions(-)

diff --git a/assets b/assets
index f7c418c52..3766c3b67 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit f7c418c52f38769daf56521ee801df699ae5435b
+Subproject commit 3766c3b6709f043e63d8eae66887159975891073
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 1740c0371..6f540687f 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -143,14 +143,14 @@ class ResultState extends MusicBeatSubState
     bgFlash.scrollFactor.set();
     bgFlash.visible = false;
     bgFlash.zIndex = 20;
-    //bgFlash.cameras = [cameraBG];
+    // bgFlash.cameras = [cameraBG];
     add(bgFlash);
 
     // The sound system which falls into place behind the score text. Plays every time!
     var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
     soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
     soundSystem.visible = false;
-    new FlxTimer().start(8/24, _ -> {
+    new FlxTimer().start(8 / 24, _ -> {
       soundSystem.animation.play("idle");
       soundSystem.visible = true;
     });
@@ -168,13 +168,12 @@ class ResultState extends MusicBeatSubState
         heartsPerfect.anim.onComplete = () -> {
           if (heartsPerfect != null)
           {
-            //bfPerfect.anim.curFrame = 137;
+            // bfPerfect.anim.curFrame = 137;
             heartsPerfect.anim.curFrame = 43;
             heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
           }
         };
 
-
         bfPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared"));
         bfPerfect.visible = false;
         bfPerfect.zIndex = 500;
@@ -183,7 +182,7 @@ class ResultState extends MusicBeatSubState
         bfPerfect.anim.onComplete = () -> {
           if (bfPerfect != null)
           {
-            //bfPerfect.anim.curFrame = 137;
+            // bfPerfect.anim.curFrame = 137;
             bfPerfect.anim.curFrame = 137;
             bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
           }
@@ -203,7 +202,6 @@ class ResultState extends MusicBeatSubState
           }
         };
 
-
       case GREAT:
         gfGreat = new FlxAtlasSprite(802, 331, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/gf", "shared"));
         gfGreat.visible = false;
@@ -273,7 +271,7 @@ class ResultState extends MusicBeatSubState
         });
     }
 
-    var diffSpr:String = 'dif${params?.difficultyId ?? 'Normal'}';
+    var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}';
     difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
     add(difficulty);
 
@@ -293,7 +291,7 @@ class ResultState extends MusicBeatSubState
 
     var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack"));
     blackTopBar.y = -blackTopBar.height;
-    FlxTween.tween(blackTopBar, {y: 0}, 7/24, {ease: FlxEase.quartOut, startDelay: 3/24});
+    FlxTween.tween(blackTopBar, {y: 0}, 7 / 24, {ease: FlxEase.quartOut, startDelay: 3 / 24});
     blackTopBar.zIndex = 1010;
     add(blackTopBar);
 
@@ -301,7 +299,7 @@ class ResultState extends MusicBeatSubState
     resultsAnim.visible = false;
     resultsAnim.zIndex = 1200;
     add(resultsAnim);
-    new FlxTimer().start(6/24, _ -> {
+    new FlxTimer().start(6 / 24, _ -> {
       resultsAnim.visible = true;
       resultsAnim.animation.play("result");
     });
@@ -310,7 +308,7 @@ class ResultState extends MusicBeatSubState
     ratingsPopin.visible = false;
     ratingsPopin.zIndex = 1200;
     add(ratingsPopin);
-    new FlxTimer().start(21/24, _ -> {
+    new FlxTimer().start(21 / 24, _ -> {
       ratingsPopin.visible = true;
       ratingsPopin.animation.play("idle");
     });
@@ -319,15 +317,13 @@ class ResultState extends MusicBeatSubState
     scorePopin.visible = false;
     scorePopin.zIndex = 1200;
     add(scorePopin);
-    new FlxTimer().start(36/24, _ -> {
+    new FlxTimer().start(36 / 24, _ -> {
       scorePopin.visible = true;
       scorePopin.animation.play("score");
-      scorePopin.animation.finishCallback = anim -> {
-
-      };
+      scorePopin.animation.finishCallback = anim -> {};
     });
 
-    new FlxTimer().start(37/24, _ -> {
+    new FlxTimer().start(37 / 24, _ -> {
       score.visible = true;
       score.animateNumbers();
       startRankTallySequence();
@@ -344,22 +340,22 @@ class ResultState extends MusicBeatSubState
     highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
     highscoreNew.animation.addByPrefix("new", "highscoreAnim0", 24, false);
     highscoreNew.visible = false;
-    //highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
+    // highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
     highscoreNew.updateHitbox();
     highscoreNew.zIndex = 1200;
     add(highscoreNew);
 
     new FlxTimer().start(rank.getHighscoreDelay(), _ -> {
-       if (params.isNewHighscore ?? false)
-        {
-          highscoreNew.visible = true;
-          highscoreNew.animation.play("new");
-          highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16);
-        }
-        else
-        {
-          highscoreNew.visible = false;
-        }
+      if (params.isNewHighscore ?? false)
+      {
+        highscoreNew.visible = true;
+        highscoreNew.animation.play("new");
+        highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16);
+      }
+      else
+      {
+        highscoreNew.visible = false;
+      }
     });
 
     var hStuf:Int = 50;
@@ -411,17 +407,16 @@ class ResultState extends MusicBeatSubState
       });
     }
 
-
-      // if (params.isNewHighscore ?? false)
-      // {
-      //   highscoreNew.visible = true;
-      //   highscoreNew.animation.play("new");
-      //   //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
-      // }
-      // else
-      // {
-      //   highscoreNew.visible = false;
-      // }
+    // if (params.isNewHighscore ?? false)
+    // {
+    //   highscoreNew.visible = true;
+    //   highscoreNew.animation.play("new");
+    //   //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
+    // }
+    // else
+    // {
+    //   highscoreNew.visible = false;
+    // }
 
     new FlxTimer().start(rank.getMusicDelay(), _ -> {
       if (rank.hasMusicIntro())
@@ -468,7 +463,7 @@ class ResultState extends MusicBeatSubState
   function startRankTallySequence():Void
   {
     bgFlash.visible = true;
-    FlxTween.tween(bgFlash, {alpha: 0}, 5/24);
+    FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24);
     var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100;
     clearPercentTarget = Math.floor(clearPercentFloat);
     // Prevent off-by-one errors.
@@ -478,7 +473,7 @@ class ResultState extends MusicBeatSubState
     trace('Clear percent target: ' + clearPercentFloat + ', round: ' + clearPercentTarget);
 
     var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 190, FlxG.height / 2 - 70, clearPercentLerp);
-    FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 58/24,
+    FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 58 / 24,
       {
         ease: FlxEase.quartOut,
         onUpdate: _ -> {
@@ -501,7 +496,7 @@ class ResultState extends MusicBeatSubState
             clearPercentCounter.flash(false);
           });
 
-          //displayRankText();
+          // displayRankText();
 
           // previously 2.0 seconds
           new FlxTimer().start(0.25, _ -> {
@@ -514,7 +509,7 @@ class ResultState extends MusicBeatSubState
                 }
               });
 
-           //afterRankTallySequence();
+            // afterRankTallySequence();
           });
         }
       });
@@ -554,7 +549,7 @@ class ResultState extends MusicBeatSubState
   {
     bgFlash.visible = true;
     bgFlash.alpha = 1;
-    FlxTween.tween(bgFlash, {alpha: 0}, 14/24);
+    FlxTween.tween(bgFlash, {alpha: 0}, 14 / 24);
 
     var rankTextVert:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getVerTextAsset()), Y, 0, 30);
     rankTextVert.x = FlxG.width - 44;
@@ -562,14 +557,13 @@ class ResultState extends MusicBeatSubState
     rankTextVert.zIndex = 990;
     add(rankTextVert);
 
-    FlxFlicker.flicker(rankTextVert, 2/24 * 3, 2/24, true);
+    FlxFlicker.flicker(rankTextVert, 2 / 24 * 3, 2 / 24, true);
 
     // Scrolling.
-    new FlxTimer().start(30/24, _ -> {
-        rankTextVert.velocity.y = -80;
+    new FlxTimer().start(30 / 24, _ -> {
+      rankTextVert.velocity.y = -80;
     });
 
-
     for (i in 0...12)
     {
       var rankTextBack:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getHorTextAsset()), X, 10, 0);
@@ -589,7 +583,6 @@ class ResultState extends MusicBeatSubState
 
   function afterRankTallySequence():Void
   {
-
     showSmallClearPercent();
 
     switch (rank)
@@ -605,15 +598,15 @@ class ResultState extends MusicBeatSubState
           bfPerfect.playAnimation('');
         }
         new FlxTimer().start(106 / 24, _ -> {
-        if (heartsPerfect == null)
-        {
-          trace("Could not build heartsPerfect animation!");
-        }
-        else
-        {
-          heartsPerfect.visible = true;
-          heartsPerfect.playAnimation('');
-        }
+          if (heartsPerfect == null)
+          {
+            trace("Could not build heartsPerfect animation!");
+          }
+          else
+          {
+            heartsPerfect.visible = true;
+            heartsPerfect.playAnimation('');
+          }
         });
       case EXCELLENT:
         if (bfExcellent == null)
@@ -637,15 +630,15 @@ class ResultState extends MusicBeatSubState
         }
 
         new FlxTimer().start(6 / 24, _ -> {
-        if (gfGreat == null)
-        {
-          trace("Could not build GREAT animation for gf!");
-        }
-        else
-        {
-          gfGreat.visible = true;
-          gfGreat.playAnimation('');
-        }
+          if (gfGreat == null)
+          {
+            trace("Could not build GREAT animation for gf!");
+          }
+          else
+          {
+            gfGreat.visible = true;
+            gfGreat.playAnimation('');
+          }
         });
       case SHIT:
         if (bfShit == null)

From cd85bcf22264b9527848a2d2963206b898ee8bd2 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 3 Jun 2024 18:32:46 -0400
Subject: [PATCH 68/96] assets submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 3766c3b67..1f3e2932e 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 3766c3b6709f043e63d8eae66887159975891073
+Subproject commit 1f3e2932ebc3395eb484e364605c233166052868

From bc21f0c6063453e10e102411c7c69018d978c29e Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 3 Jun 2024 19:39:37 -0400
Subject: [PATCH 69/96] Update assets submodule

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 1f3e2932e..4039bd018 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 1f3e2932ebc3395eb484e364605c233166052868
+Subproject commit 4039bd018d474994b44317b74cdd724b5f73b749

From c056c7276285b847e4ea969ccb3b38dd23fd60b6 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 4 Jun 2024 14:26:24 -0400
Subject: [PATCH 70/96] Implement advanced save data repair.

---
 source/funkin/save/Save.hx                    | 81 +++++++++++++++++++
 .../funkin/save/migrator/SaveDataMigrator.hx  |  1 +
 source/funkin/util/VersionUtil.hx             |  5 +-
 3 files changed, 86 insertions(+), 1 deletion(-)

diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index 7f25a8e01..7634c1f51 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -732,6 +732,87 @@ class Save
     }
   }
 
+  public static function archiveBadSaveData(data:Dynamic):Void
+  {
+    // We want to save this somewhere so we can try to recover it for the user in the future!
+
+    final RECOVERY_SLOT_START = 1000;
+
+    writeToAvailableSlot(RECOVERY_SLOT_START, data);
+  }
+
+  public static function debug_queryBadSaveData():Void
+  {
+    final RECOVERY_SLOT_START = 1000;
+    final RECOVERY_SLOT_END = 1100;
+    var firstBadSaveData = querySlotRange(RECOVERY_SLOT_START, RECOVERY_SLOT_END);
+    if (firstBadSaveData > 0)
+    {
+      trace('[SAVE] Found bad save data in slot ${firstBadSaveData}!');
+      trace('We should look into recovery...');
+
+      trace(haxe.Json.stringify(fetchFromSlotRaw(firstBadSaveData)));
+    }
+  }
+
+  static function fetchFromSlotRaw(slot:Int):Null<Dynamic>
+  {
+    var targetSaveData = new FlxSave();
+    targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    if (targetSaveData.isEmpty()) return null;
+    return targetSaveData.data;
+  }
+
+  static function writeToAvailableSlot(slot:Int, data:Dynamic):Void
+  {
+    trace('[SAVE] Finding slot to write data to (starting with ${slot})...');
+
+    var targetSaveData = new FlxSave();
+    targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    while (!targetSaveData.isEmpty())
+    {
+      // Keep trying to bind to slots until we find an empty slot.
+      trace('[SAVE] Slot ${slot} is taken, continuing...');
+      slot++;
+      targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    }
+
+    trace('[SAVE] Writing data to slot ${slot}...');
+    targetSaveData.mergeData(data, true);
+
+    trace('[SAVE] Data written to slot ${slot}!');
+  }
+
+  /**
+   * Return true if the given save slot is not empty.
+   * @param slot The slot number to check.
+   * @return Whether the slot is not empty.
+   */
+  static function querySlot(slot:Int):Bool
+  {
+    var targetSaveData = new FlxSave();
+    targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    return !targetSaveData.isEmpty();
+  }
+
+  /**
+   * Return true if any of the slots in the given range is not empty.
+   * @param start The starting slot number to check.
+   * @param end The ending slot number to check.
+   * @return The first slot in the range that is not empty, or `-1` if none are.
+   */
+  static function querySlotRange(start:Int, end:Int):Int
+  {
+    for (i in start...end)
+    {
+      if (querySlot(i))
+      {
+        return i;
+      }
+    }
+    return -1;
+  }
+
   static function fetchLegacySaveData():Null<RawSaveData_v1_0_0>
   {
     trace("[SAVE] Checking for legacy save data...");
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 4fa9dd6b3..b7d278cc6 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -36,6 +36,7 @@ class SaveDataMigrator
       {
         var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
         lime.app.Application.current.window.alert(message, "Save Data Failure");
+        Save.archiveBadSaveData(inputData);
         trace('[SAVE] ' + message);
         return new Save(Save.getDefault());
       }
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index 18d7eafa6..b84b66341 100644
--- a/source/funkin/util/VersionUtil.hx
+++ b/source/funkin/util/VersionUtil.hx
@@ -39,13 +39,16 @@ class VersionUtil
     if (thx.Types.isAnonymousObject(versionData.version))
     {
       // This is bad! versionData.version should be an array!
-      versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]];
+      trace('[SAVE] Version data repair required! (got ${versionData.version})');
+      var fixedVersionData = [versionData.version[0], versionData.version[1], versionData.version[2]];
+      versionData.version = fixedVersionData;
 
       var fixedVersion:thx.semver.Version = versionData;
       return fixedVersion;
     }
     else
     {
+      trace('[SAVE] Version data repair not required (got ${version})');
       // No need for repair.
       return version;
     }

From d4712a8ef7ebc60318e47b6c70faca646a9d0830 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 16:24:24 -0400
Subject: [PATCH 71/96] alphabetical filtering for freeplay non-all sort

---
 source/funkin/ui/freeplay/FreeplayState.hx | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 494bc20f0..fe64a1e6f 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -804,6 +804,13 @@ class FreeplayState extends MusicBeatSubState
    */
   public function sortSongs(songsToFilter:Array<FreeplaySongData>, songFilter:SongFilter):Array<FreeplaySongData>
   {
+    var filterAlphabetically = function(a:FreeplaySongData, b:FreeplaySongData):Int {
+      if (a?.songName.toLowerCase() < b?.songName.toLowerCase()) return -1;
+      else if (a?.songName.toLowerCase() > b?.songName.toLowerCase()) return 1;
+      else
+        return 0;
+    };
+
     switch (songFilter.filterType)
     {
       case REGEXP:
@@ -818,6 +825,8 @@ class FreeplayState extends MusicBeatSubState
           return filterRegexp.match(str.songName);
         });
 
+        songsToFilter.sort(filterAlphabetically);
+
       case STARTSWITH:
         // extra note: this is essentially a "search"
 
@@ -832,9 +841,13 @@ class FreeplayState extends MusicBeatSubState
           if (str == null) return true; // Random
           return str.isFav;
         });
+
+        songsToFilter.sort(filterAlphabetically);
+
       default:
         // return all on default
     }
+
     return songsToFilter;
   }
 

From 29a33c8815a1ca3e33a731f9d8d405a57041f102 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 16:38:21 -0400
Subject: [PATCH 72/96] modifies behaviour of how the difficulty stars appear

---
 source/funkin/ui/freeplay/AlbumRoll.hx       |  7 ++++---
 source/funkin/ui/freeplay/DifficultyStars.hx | 11 ++++++++---
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx
index 20cd91379..49c588722 100644
--- a/source/funkin/ui/freeplay/AlbumRoll.hx
+++ b/source/funkin/ui/freeplay/AlbumRoll.hx
@@ -66,7 +66,7 @@ class AlbumRoll extends FlxSpriteGroup
     add(newAlbumArt);
 
     difficultyStars = new DifficultyStars(140, 39);
-    difficultyStars.stars.visible = false;
+    difficultyStars.visible = false;
     add(difficultyStars);
   }
 
@@ -149,7 +149,7 @@ class AlbumRoll extends FlxSpriteGroup
     newAlbumArt.visible = true;
     newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
 
-    difficultyStars.stars.visible = false;
+    difficultyStars.visible = false;
     new FlxTimer().start(0.75, function(_) {
       // showTitle();
       showStars();
@@ -172,6 +172,7 @@ class AlbumRoll extends FlxSpriteGroup
    */
   public function showStars():Void
   {
-    difficultyStars.stars.visible = true; // true;
+    difficultyStars.visible = true; // true;
+    difficultyStars.flameCheck();
   }
 }
diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx
index 51526bcbe..e7a2b8888 100644
--- a/source/funkin/ui/freeplay/DifficultyStars.hx
+++ b/source/funkin/ui/freeplay/DifficultyStars.hx
@@ -19,7 +19,7 @@ class DifficultyStars extends FlxSpriteGroup
 
   public var stars:FlxAtlasSprite;
 
-  var flames:FreeplayFlames;
+  public var flames:FreeplayFlames;
 
   var hsvShader:HSVShader;
 
@@ -80,11 +80,16 @@ class DifficultyStars extends FlxSpriteGroup
       curDifficulty = difficulty - 1;
     }
 
+    flameCheck();
+
+    return difficulty;
+  }
+
+  public function flameCheck():Void
+  {
     if (difficulty > 10) flames.flameCount = difficulty - 10;
     else
       flames.flameCount = 0;
-
-    return difficulty;
   }
 
   function set_curDifficulty(value:Int):Int

From 84d4d044d647fa3460ca484526cb3282a0c51930 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 16:47:58 -0400
Subject: [PATCH 73/96] check clip width when text changes

---
 source/funkin/ui/freeplay/CapsuleText.hx  | 20 ++++++++++++++++----
 source/funkin/ui/freeplay/SongMenuItem.hx |  2 +-
 2 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/source/funkin/ui/freeplay/CapsuleText.hx b/source/funkin/ui/freeplay/CapsuleText.hx
index c3fd51d1f..c3bcdb09b 100644
--- a/source/funkin/ui/freeplay/CapsuleText.hx
+++ b/source/funkin/ui/freeplay/CapsuleText.hx
@@ -58,12 +58,24 @@ class CapsuleText extends FlxSpriteGroup
   function set_clipWidth(value:Int):Int
   {
     resetText();
-    if (whiteText.width > value)
+    checkClipWidth(value);
+    return clipWidth = value;
+  }
+
+  /**
+   * Checks if the text if it's too long, and clips if it is
+   * @param wid
+   */
+  function checkClipWidth(?wid:Int):Void
+  {
+    if (wid == null) wid = clipWidth;
+
+    if (whiteText.width > wid)
     {
       tooLong = true;
 
-      blurredText.clipRect = new FlxRect(0, 0, value, blurredText.height);
-      whiteText.clipRect = new FlxRect(0, 0, value, whiteText.height);
+      blurredText.clipRect = new FlxRect(0, 0, wid, blurredText.height);
+      whiteText.clipRect = new FlxRect(0, 0, wid, whiteText.height);
     }
     else
     {
@@ -72,7 +84,6 @@ class CapsuleText extends FlxSpriteGroup
       blurredText.clipRect = null;
       whiteText.clipRect = null;
     }
-    return clipWidth = value;
   }
 
   function set_text(value:String):String
@@ -86,6 +97,7 @@ class CapsuleText extends FlxSpriteGroup
 
     blurredText.text = value;
     whiteText.text = value;
+    checkClipWidth();
     whiteText.textField.filters = [
       new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM),
       // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW)
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index b9fef5a79..d40809fad 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -326,7 +326,7 @@ class SongMenuItem extends FlxSpriteGroup
 
   var evilTrail:FlxTrail;
 
-  public function fadeAnim()
+  public function fadeAnim():Void
   {
     impactThing = new FunkinSprite(0, 0);
     impactThing.frames = capsule.frames;

From 6a62f38c33a33c90c224bfc7ecbef0481e29c908 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 16:51:27 -0400
Subject: [PATCH 74/96] quicki fix for incorrect clip tweens

---
 source/funkin/ui/freeplay/SongMenuItem.hx | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index d40809fad..b861bca15 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -671,14 +671,9 @@ class SongMenuItem extends FlxSpriteGroup
     ranking.alpha = this.selected ? 1 : 0.7;
     ranking.color = this.selected ? 0xFFFFFFFF : 0xFFAAAAAA;
 
-    if (selected)
-    {
-      if (songText.tooLong == true) songText.initMove();
-    }
-    else
-    {
-      if (songText.tooLong == true) songText.resetText();
-    }
+    if (songText.tooLong) songText.resetText();
+
+    if (selected && songText.tooLong) songText.initMove();
   }
 }
 

From ae950c738214e20446cc8ded5b163544a5ee0280 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 4 Jun 2024 19:44:00 -0400
Subject: [PATCH 75/96] Finish save data repair (you should be able to transfer
 your save now)

---
 CHANGELOG.md                                   |  1 +
 source/funkin/save/Save.hx                     | 16 +++++++++-------
 source/funkin/save/changelog.md                |  4 ++++
 .../funkin/save/migrator/SaveDataMigrator.hx   |  6 +++---
 source/funkin/util/VersionUtil.hx              | 18 ++++++++++++++++--
 5 files changed, 33 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5aefb885..898978ca3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   - Senpai (increased the note speed)
   - Thorns (increased the note speed slightly)
 - Favorite songs marked in Freeplay are now stored between sessions.
+- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future.
 - Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
 - Improved logic for NoteHitScriptEvents, allowing you to view the hit diff and modify whether a note hit is a combo break (thanks nebulazorua!)
 - Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index 7634c1f51..2ff6b96cc 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -16,7 +16,7 @@ import thx.semver.Version;
 @:nullSafety
 class Save
 {
-  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.4";
+  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.5";
   public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
 
   // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@@ -56,6 +56,9 @@ class Save
     if (data == null) this.data = Save.getDefault();
     else
       this.data = data;
+
+    // Make sure the verison number is up to date before we flush.
+    this.data.version = Save.SAVE_DATA_VERSION;
   }
 
   public static function getDefault():RawSaveData
@@ -713,7 +716,6 @@ class Save
       {
         trace('[SAVE] Found legacy save data, converting...');
         var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
-        @:privateAccess
         FlxG.save.mergeData(gameSave.data, true);
       }
       else
@@ -725,20 +727,19 @@ class Save
     }
     else
     {
-      trace('[SAVE] Loaded save data.');
-      @:privateAccess
+      trace('[SAVE] Found existing save data.');
       var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
       FlxG.save.mergeData(gameSave.data, true);
     }
   }
 
-  public static function archiveBadSaveData(data:Dynamic):Void
+  public static function archiveBadSaveData(data:Dynamic):Int
   {
     // We want to save this somewhere so we can try to recover it for the user in the future!
 
     final RECOVERY_SLOT_START = 1000;
 
-    writeToAvailableSlot(RECOVERY_SLOT_START, data);
+    return writeToAvailableSlot(RECOVERY_SLOT_START, data);
   }
 
   public static function debug_queryBadSaveData():Void
@@ -763,7 +764,7 @@ class Save
     return targetSaveData.data;
   }
 
-  static function writeToAvailableSlot(slot:Int, data:Dynamic):Void
+  static function writeToAvailableSlot(slot:Int, data:Dynamic):Int
   {
     trace('[SAVE] Finding slot to write data to (starting with ${slot})...');
 
@@ -781,6 +782,7 @@ class Save
     targetSaveData.mergeData(data, true);
 
     trace('[SAVE] Data written to slot ${slot}!');
+    return slot;
   }
 
   /**
diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md
index 7c9094f2d..e3038373d 100644
--- a/source/funkin/save/changelog.md
+++ b/source/funkin/save/changelog.md
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [2.0.5] - 2024-05-21
+### Fixed
+- Resolved an issue where HTML5 wouldn't store the semantic version properly, causing the game to fail to load the save.
+
 ## [2.0.4] - 2024-05-21
 ### Added
 - `favoriteSongs:Array<String>` to `Save`
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index b7d278cc6..7a929322a 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -35,9 +35,9 @@ class SaveDataMigrator
       else
       {
         var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
-        lime.app.Application.current.window.alert(message, "Save Data Failure");
-        Save.archiveBadSaveData(inputData);
-        trace('[SAVE] ' + message);
+        var slot:Int = Save.archiveBadSaveData(inputData);
+        var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.';
+        lime.app.Application.current.window.alert(fullMessage, "Save Data Failure");
         return new Save(Save.getDefault());
       }
     }
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index b84b66341..832ce008a 100644
--- a/source/funkin/util/VersionUtil.hx
+++ b/source/funkin/util/VersionUtil.hx
@@ -23,6 +23,8 @@ class VersionUtil
   {
     try
     {
+      var versionRaw:thx.semver.Version.SemVer = version;
+      trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}');
       return version.satisfies(versionRule);
     }
     catch (e)
@@ -40,10 +42,22 @@ class VersionUtil
     {
       // This is bad! versionData.version should be an array!
       trace('[SAVE] Version data repair required! (got ${versionData.version})');
-      var fixedVersionData = [versionData.version[0], versionData.version[1], versionData.version[2]];
-      versionData.version = fixedVersionData;
+      // Turn the objects back into arrays.
+      // I'd use DynamicsT.values but IDK if it maintains order
+      versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]];
+
+      // This is so jank but it should work.
+      var buildData:Dynamic<String> = cast versionData.build;
+      var buildDataFixed:Array<thx.semver.Version.Identifier> = thx.Dynamics.DynamicsT.values(buildData)
+        .map(function(d:Dynamic) return StringId(d.toString()));
+      versionData.build = buildDataFixed;
+
+      var preData:Dynamic<String> = cast versionData.pre;
+      var preDataFixed:Array<thx.semver.Version.Identifier> = thx.Dynamics.DynamicsT.values(preData).map(function(d:Dynamic) return StringId(d.toString()));
+      versionData.pre = preDataFixed;
 
       var fixedVersion:thx.semver.Version = versionData;
+      trace('[SAVE] Fixed version: ${fixedVersion}');
       return fixedVersion;
     }
     else

From 97400ed5d8dc8867f7eb4dde01ed7b0832309054 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 22:26:35 -0400
Subject: [PATCH 76/96] santa sounds

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 4039bd018..b7d3772e7 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 4039bd018d474994b44317b74cdd724b5f73b749
+Subproject commit b7d3772e76ca1d05626201594acb7f4b996d4a80

From 8bdaf8f513c9a003f1622123dce1aeeb1d8ebab5 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 22:55:21 -0400
Subject: [PATCH 77/96] web rank sounds

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index b7d3772e7..f2202ff9d 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit b7d3772e76ca1d05626201594acb7f4b996d4a80
+Subproject commit f2202ff9d920743830cf2b47442040ad80f5ca05

From 66d86a6969b924cba3c17ae5505aea7dbddcec6f Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 23:07:08 -0400
Subject: [PATCH 78/96] 2hot death fix

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index f2202ff9d..613365d98 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit f2202ff9d920743830cf2b47442040ad80f5ca05
+Subproject commit 613365d98921346f0502a62d0a5f68fce6d27373

From 50990b15e100a18f2f3d2cd8074e09a8c70e3749 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 23:36:32 -0400
Subject: [PATCH 79/96] heart glow

---
 source/funkin/ui/freeplay/FreeplayState.hx |  6 ++++++
 source/funkin/ui/freeplay/SongMenuItem.hx  | 13 +++++++++++++
 2 files changed, 19 insertions(+)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index fe64a1e6f..4fc94c2d7 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -757,6 +757,7 @@ class FreeplayState extends MusicBeatSubState
     randomCapsule.alpha = 0;
     randomCapsule.songText.visible = false;
     randomCapsule.favIcon.visible = false;
+    randomCapsule.favIconBlurred.visible = false;
     randomCapsule.ranking.visible = false;
     randomCapsule.blurredRanking.visible = false;
     randomCapsule.initJumpIn(0, force);
@@ -779,6 +780,7 @@ class FreeplayState extends MusicBeatSubState
       funnyMenu.capsule.alpha = 0.5;
       funnyMenu.songText.visible = false;
       funnyMenu.favIcon.visible = tempSongs[i].isFav;
+      funnyMenu.favIconBlurred.visible = tempSongs[i].isFav;
       funnyMenu.hsvShader = hsvShader;
 
       funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45);
@@ -1232,7 +1234,9 @@ class FreeplayState extends MusicBeatSubState
         if (isFav)
         {
           grpCapsules.members[realShit].favIcon.visible = true;
+          grpCapsules.members[realShit].favIconBlurred.visible = true;
           grpCapsules.members[realShit].favIcon.animation.play('fav');
+          grpCapsules.members[realShit].favIconBlurred.animation.play('fav');
           FunkinSound.playOnce(Paths.sound('fav'), 1);
           grpCapsules.members[realShit].checkClip();
           grpCapsules.members[realShit].selected = grpCapsules.members[realShit].selected; // set selected again, so it can run it's getter function to initialize movement
@@ -1254,9 +1258,11 @@ class FreeplayState extends MusicBeatSubState
         else
         {
           grpCapsules.members[realShit].favIcon.animation.play('fav', true, true, 9);
+          grpCapsules.members[realShit].favIconBlurred.animation.play('fav', true, true, 9);
           FunkinSound.playOnce(Paths.sound('unfav'), 1);
           new FlxTimer().start(0.2, _ -> {
             grpCapsules.members[realShit].favIcon.visible = false;
+            grpCapsules.members[realShit].favIconBlurred.visible = false;
             grpCapsules.members[realShit].checkClip();
           });
 
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index b861bca15..fb91fbd1c 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -38,6 +38,7 @@ class SongMenuItem extends FlxSpriteGroup
   public var selected(default, set):Bool;
 
   public var songText:CapsuleText;
+  public var favIconBlurred:FlxSprite;
   public var favIcon:FlxSprite;
 
   public var ranking:FreeplayRank;
@@ -190,6 +191,16 @@ class SongMenuItem extends FlxSpriteGroup
     add(pixelIcon);
     grpHide.add(pixelIcon);
 
+    favIconBlurred = new FlxSprite(380, 40);
+    favIconBlurred.frames = Paths.getSparrowAtlas('freeplay/favHeart');
+    favIconBlurred.animation.addByPrefix('fav', 'favorite heart', 24, false);
+    favIconBlurred.animation.play('fav');
+    favIconBlurred.setGraphicSize(50, 50);
+    favIconBlurred.blend = BlendMode.ADD;
+    favIconBlurred.shader = new GaussianBlurShader(1.2);
+    favIconBlurred.visible = false;
+    add(favIconBlurred);
+
     favIcon = new FlxSprite(380, 40);
     favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart');
     favIcon.animation.addByPrefix('fav', 'favorite heart', 24, false);
@@ -669,6 +680,8 @@ class SongMenuItem extends FlxSpriteGroup
     capsule.offset.x = this.selected ? 0 : -5;
     capsule.animation.play(this.selected ? "selected" : "unselected");
     ranking.alpha = this.selected ? 1 : 0.7;
+    favIcon.alpha = this.selected ? 1 : 0.6;
+    favIconBlurred.alpha = this.selected ? 1 : 0;
     ranking.color = this.selected ? 0xFFFFFFFF : 0xFFAAAAAA;
 
     if (songText.tooLong) songText.resetText();

From 03bccee0542c363548b9cad25e1cbfd84e6fb0a3 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 4 Jun 2024 23:39:49 -0400
Subject: [PATCH 80/96] assets submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 613365d98..efb7a833a 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 613365d98921346f0502a62d0a5f68fce6d27373
+Subproject commit efb7a833a78ab9c3bd9fe36bb10a8adc6a87e30b

From 5dda95c69baffddfae35f7e5fdf1fdb84661a80f Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 5 Jun 2024 00:40:14 -0400
Subject: [PATCH 81/96] Fix some display issues with the clear percent in the
 results screen.

---
 source/funkin/InitState.hx                      |  2 +-
 .../play/components/ClearPercentCounter.hx      | 17 ++++++++++++-----
 2 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index c7a08d714..49b15ddf6 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -229,7 +229,7 @@ class InitState extends FlxState
             tallies:
               {
                 sick: 130,
-                good: 70,
+                good: 60,
                 bad: 69,
                 shit: 69,
                 missed: 69,
diff --git a/source/funkin/play/components/ClearPercentCounter.hx b/source/funkin/play/components/ClearPercentCounter.hx
index d296b0b0b..6af40019b 100644
--- a/source/funkin/play/components/ClearPercentCounter.hx
+++ b/source/funkin/play/components/ClearPercentCounter.hx
@@ -35,7 +35,7 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
     super(x, y);
 
     flashShader = new PureColor(FlxColor.WHITE);
-    flashShader.colorSet = true;
+    flashShader.colorSet = false;
 
     curNumber = startingNumber;
 
@@ -54,10 +54,7 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
    */
   public function flash(enabled:Bool):Void
   {
-    for (member in members)
-    {
-      member.shader = enabled ? flashShader : null;
-    }
+    flashShader.colorSet = enabled;
   }
 
   var tmr:Float = 0;
@@ -98,6 +95,7 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
       var yPos = (digitIndex - 1 + digitOffset) * (digitHeightOffset * this.scale.y);
       yPos += small ? 0 : 72;
 
+      trace('[COUNTER] Drawing digit ${num}');
       if (digitIndex >= members.length)
       {
         // Three digits = LLR because the 1 and 0 won't be the same anyway.
@@ -105,6 +103,8 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
         // var variant:Bool = (seperatedScore.length % 2 != 0) ? (digitIndex % 2 == 0) : (digitIndex % 2 == 1);
         var numb:ClearPercentNumber = new ClearPercentNumber(xPos, yPos, num, variant, this.small);
         numb.scale.set(this.scale.x, this.scale.y);
+        numb.shader = flashShader;
+        numb.visible = true;
         add(numb);
       }
       else
@@ -113,8 +113,15 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
         // Reset the position of the number
         members[digitIndex].x = xPos + this.x;
         members[digitIndex].y = yPos + this.y;
+        members[digitIndex].visible = true;
       }
     }
+    trace('[COUNTER] Members: ${members.length}, expected members: ${seperatedScore.length + 1}');
+    for (ind in (seperatedScore.length + 1)...(members.length))
+    {
+      trace('Hiding digit ${ind}');
+      members[ind].visible = false;
+    }
   }
 }
 

From 640c1cf236f2578795c1385ac2ffad72a4095228 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 5 Jun 2024 00:49:59 -0400
Subject: [PATCH 82/96] Dave say 1 minute to make Dad sneak up and 2 minutes to
 play the cartoons

---
 source/funkin/ui/freeplay/DJBoyfriend.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx
index 248526aaf..bd2f73e42 100644
--- a/source/funkin/ui/freeplay/DJBoyfriend.hx
+++ b/source/funkin/ui/freeplay/DJBoyfriend.hx
@@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
 
   var gotSpooked:Bool = false;
 
-  static final SPOOK_PERIOD:Float = 120.0;
-  static final TV_PERIOD:Float = 180.0;
+  static final SPOOK_PERIOD:Float = 60.0;
+  static final TV_PERIOD:Float = 120.0;
 
   // Time since dad last SPOOKED you.
   var timeSinceSpook:Float = 0;

From 9f3d80d1dbbefba5adc91aab84a0a25c91cb0394 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 5 Jun 2024 01:16:30 -0400
Subject: [PATCH 83/96] Cory made some chart tweaks. Plus some authorship
 changes.

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index efb7a833a..6af094050 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit efb7a833a78ab9c3bd9fe36bb10a8adc6a87e30b
+Subproject commit 6af0940504514701e3625b65d8332c27198e9198

From 3534ba6224d8dd950c9f599cedd15e597cc469ab Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 13:50:57 -0400
Subject: [PATCH 84/96] roses fix

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 6af094050..faef517a0 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 6af0940504514701e3625b65d8332c27198e9198
+Subproject commit faef517a068bb6821d828462504d7baebc91306f

From fa1fafba68be5b8abf0e7ad266b333a3d47c1875 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 14:13:06 -0400
Subject: [PATCH 85/96] less effin traces

---
 source/funkin/play/components/ClearPercentCounter.hx | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/source/funkin/play/components/ClearPercentCounter.hx b/source/funkin/play/components/ClearPercentCounter.hx
index 6af40019b..e3d3795d9 100644
--- a/source/funkin/play/components/ClearPercentCounter.hx
+++ b/source/funkin/play/components/ClearPercentCounter.hx
@@ -59,14 +59,14 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
 
   var tmr:Float = 0;
 
-  override function update(elapsed:Float)
+  override function update(elapsed:Float):Void
   {
     super.update(elapsed);
 
     if (numberChanged) drawNumbers();
   }
 
-  function drawNumbers()
+  function drawNumbers():Void
   {
     var seperatedScore:Array<Int> = [];
     var tempCombo:Int = Math.round(curNumber);
@@ -83,7 +83,7 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
 
     for (ind => num in seperatedScore)
     {
-      var digitIndex = ind + 1;
+      var digitIndex:Int = ind + 1;
       // If there's only one digit, move it to the right
       // If there's three digits, move them all to the left
       var digitOffset = (seperatedScore.length == 1) ? 1 : (seperatedScore.length == 3) ? -1 : 0;
@@ -95,7 +95,6 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
       var yPos = (digitIndex - 1 + digitOffset) * (digitHeightOffset * this.scale.y);
       yPos += small ? 0 : 72;
 
-      trace('[COUNTER] Drawing digit ${num}');
       if (digitIndex >= members.length)
       {
         // Three digits = LLR because the 1 and 0 won't be the same anyway.
@@ -116,10 +115,8 @@ class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
         members[digitIndex].visible = true;
       }
     }
-    trace('[COUNTER] Members: ${members.length}, expected members: ${seperatedScore.length + 1}');
     for (ind in (seperatedScore.length + 1)...(members.length))
     {
-      trace('Hiding digit ${ind}');
       members[ind].visible = false;
     }
   }

From 6e761ba156de336af2b567e14dfbfdc36c796129 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 14:47:29 -0400
Subject: [PATCH 86/96] hardcode the songname position

---
 source/funkin/play/ResultState.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 6f540687f..48fb3b04e 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -691,13 +691,13 @@ class ResultState extends MusicBeatSubState
     {
       clearPercentSmall.x = (difficulty.x + difficulty.width) + 60;
       clearPercentSmall.y = -clearPercentSmall.height;
-      FlxTween.tween(clearPercentSmall, {y: 122 - 5}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8});
+      FlxTween.tween(clearPercentSmall, {y: 122 - 5}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.85});
     }
 
     songName.y = -songName.height;
     var fuckedupnumber = (10) * (songName.text.length / 15);
     FlxTween.tween(songName, {y: diffYTween - 25 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9});
-    songName.x = clearPercentSmall.x + clearPercentSmall.width - 30;
+    songName.x = clearPercentSmall.x + 94;
 
     new FlxTimer().start(timerLength, _ -> {
       var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y);

From 5bcc0f9b25c8dc4d8942fa87fb085430efc99575 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 15:02:29 -0400
Subject: [PATCH 87/96] partial sound loading should be more resiliant

---
 source/funkin/audio/FunkinSound.hx         | 20 ++++++++++++++------
 source/funkin/ui/freeplay/FreeplayState.hx |  2 ++
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index 4cd0e7557..4f61e70c2 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -357,6 +357,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
     var shouldLoadPartial = params.partialParams?.loadPartial ?? false;
 
+    // even if we arent' trying to partial load a song, we want to error out any songs in progress,
+    // so we don't get overlapping music if someone were to load a new song while a partial one is loading!
+
+    emptyPartialQueue();
+
     if (shouldLoadPartial)
     {
       var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0,
@@ -364,12 +369,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 
       if (music != null)
       {
-        while (partialQueue.length > 0)
-        {
-          @:nullSafety(Off)
-          partialQueue.pop().error("Cancel loading partial sound");
-        }
-
         partialQueue.push(music);
 
         @:nullSafety(Off)
@@ -406,6 +405,15 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     }
   }
 
+  public static function emptyPartialQueue():Void
+  {
+    while (partialQueue.length > 0)
+    {
+      @:nullSafety(Off)
+      partialQueue.pop().error("Cancel loading partial sound");
+    }
+  }
+
   static var partialQueue:Array<Promise<Null<FunkinSound>>> = [];
 
   /**
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 4fc94c2d7..2ecbb5739 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1769,6 +1769,8 @@ class FreeplayState extends MusicBeatSubState
     funnyScroll3.visible = false;
 
     new FlxTimer().start(1, function(tmr:FlxTimer) {
+      FunkinSound.emptyPartialQueue();
+
       Paths.setCurrentLevel(cap.songData.levelId);
       LoadingState.loadPlayState(
         {

From c1bfc67f52d527bdeffa2a57c06a6dccbf105b0c Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 15:43:25 -0400
Subject: [PATCH 88/96] remove effin trace

---
 source/funkin/ui/freeplay/SongMenuItem.hx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index fb91fbd1c..a0fa0ae42 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -708,8 +708,6 @@ class FreeplayRank extends FlxSprite
 
       animation.play(val.getFreeplayRankIconAsset(), true, false);
 
-      trace(val.getFreeplayRankIconAsset());
-
       centerOffsets(false);
 
       switch (val)

From eaa63196b3fda9243784c3706610361da861e45e Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 17:10:53 -0400
Subject: [PATCH 89/96] fix for dadbattle appearing on freeplay

---
 source/funkin/play/song/Song.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index fd006c0f3..d85703721 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -378,7 +378,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
    */
   public function getDifficulty(?diffId:String, ?variation:String, ?variations:Array<String>):Null<SongDifficulty>
   {
-    if (diffId == null) diffId = listDifficulties(variation)[0];
+    if (diffId == null) diffId = listDifficulties(variation, variations)[0];
     if (variation == null) variation = Constants.DEFAULT_VARIATION;
     if (variations == null) variations = [variation];
 

From 8616722e6023729f354da0949d93a204ddd36dba Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 17:14:04 -0400
Subject: [PATCH 90/96] asset submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index faef517a0..56fe3a366 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit faef517a068bb6821d828462504d7baebc91306f
+Subproject commit 56fe3a3662bf5588b4f6cfec492efe5d4cfd600c

From 4311dd20744fac60909f8ecf923ef3533654505b Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 17:58:22 -0400
Subject: [PATCH 91/96] hopefully dadbattle fix lol

---
 source/funkin/ui/freeplay/FreeplayState.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 2ecbb5739..035ad2612 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -2024,7 +2024,7 @@ class FreeplaySongData
 
   function set_currentDifficulty(value:String):String
   {
-    if (currentDifficulty == value) return value;
+    // if (currentDifficulty == value) return value;
 
     currentDifficulty = value;
     updateValues(displayedVariations);
@@ -2064,7 +2064,7 @@ class FreeplaySongData
 
   function updateValues(variations:Array<String>):Void
   {
-    this.songDifficulties = song.listDifficulties(variations, false, false);
+    this.songDifficulties = song.listDifficulties(null, variations, false, false);
     if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
 
     var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);

From bc783a278a0ebd7e69ba9f81698346198bfb98d8 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 5 Jun 2024 18:21:57 -0400
Subject: [PATCH 92/96] rank clipping fix

---
 source/funkin/ui/freeplay/FreeplayState.hx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 035ad2612..a9721bd7c 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1640,6 +1640,7 @@ class FreeplayState extends MusicBeatSubState
         {
           songCapsule.songData.currentDifficulty = currentDifficulty;
           songCapsule.init(null, null, songCapsule.songData);
+          songCapsule.checkClip();
         }
         else
         {
@@ -2024,8 +2025,6 @@ class FreeplaySongData
 
   function set_currentDifficulty(value:String):String
   {
-    // if (currentDifficulty == value) return value;
-
     currentDifficulty = value;
     updateValues(displayedVariations);
     return value;

From e27e96a1cb3ce63a3fc75054b67757e6ba6c20c1 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 5 Jun 2024 20:19:00 -0400
Subject: [PATCH 93/96] Update assets submodule

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 56fe3a366..68f223f96 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 56fe3a3662bf5588b4f6cfec492efe5d4cfd600c
+Subproject commit 68f223f965c60a3874d7842ee2d448eee5afc87b

From 642f272bce4a4399936fe1114e0db6795c0c19dd Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Thu, 6 Jun 2024 01:49:33 +0100
Subject: [PATCH 94/96] freeplay polish + new text

---
 source/funkin/play/song/Song.hx            |  6 ++
 source/funkin/ui/freeplay/FreeplayState.hx | 78 +++++++++++++---------
 source/funkin/ui/freeplay/SongMenuItem.hx  | 37 +++++++++-
 3 files changed, 87 insertions(+), 34 deletions(-)

diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index d85703721..df3e343e2 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -91,6 +91,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return _metadata.keys().array();
   }
 
+  // this returns false so that any new song can override this and return true when needed
+  public function isSongNew(currentDifficulty:String):Bool
+  {
+    return false;
+  }
+
   /**
    * Set to false if the song was edited in the charter and should not be saved as a high score.
    */
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index a9721bd7c..607b7a353 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -859,6 +859,7 @@ class FreeplayState extends MusicBeatSubState
   function rankAnimStart(fromResults:Null<FromResultsParams>):Void
   {
     busy = true;
+    grpCapsules.members[curSelected].sparkle.alpha = 0;
     // grpCapsules.members[curSelected].forcePosition();
 
     if (fromResults != null)
@@ -1088,6 +1089,8 @@ class FreeplayState extends MusicBeatSubState
 
               // NOW we can interact with the menu
               busy = false;
+              grpCapsules.members[curSelected].sparkle.alpha = 0.7;
+              playCurSongPreview(capsule);
             }, null);
 
             // FlxTween.tween(capsule, {"targetPos.x": capsule.targetPos.x - 50}, 0.6,
@@ -1814,7 +1817,7 @@ class FreeplayState extends MusicBeatSubState
 
   function changeSelection(change:Int = 0):Void
   {
-    FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
+    if (!prepForNewRank) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
 
     var prevSelected:Int = curSelected;
 
@@ -1855,43 +1858,48 @@ class FreeplayState extends MusicBeatSubState
       if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure
     }
 
-    if (grpCapsules.countLiving() > 0)
+    if (grpCapsules.countLiving() > 0 && !prepForNewRank)
     {
-      if (curSelected == 0)
-      {
-        FunkinSound.playMusic('freeplayRandom',
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: false
-          });
-        FlxG.sound.music.fadeIn(2, 0, 0.8);
-      }
-      else
-      {
-        var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
-        FunkinSound.playMusic(daSongCapsule.songData.songId,
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: false,
-            pathsFunction: INST,
-            suffix: potentiallyErect,
-            partialParams:
-              {
-                loadPartial: true,
-                start: 0.05,
-                end: 0.25
-              },
-            onLoad: function() {
-              FlxG.sound.music.fadeIn(2, 0, 0.4);
-            }
-          });
-      }
+      playCurSongPreview(daSongCapsule);
       grpCapsules.members[curSelected].selected = true;
     }
   }
 
+  public function playCurSongPreview(daSongCapsule:SongMenuItem):Void
+  {
+    if (curSelected == 0)
+    {
+      FunkinSound.playMusic('freeplayRandom',
+        {
+          startingVolume: 0.0,
+          overrideExisting: true,
+          restartTrack: false
+        });
+      FlxG.sound.music.fadeIn(2, 0, 0.8);
+    }
+    else
+    {
+      var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
+      FunkinSound.playMusic(daSongCapsule.songData.songId,
+        {
+          startingVolume: 0.0,
+          overrideExisting: true,
+          restartTrack: false,
+          pathsFunction: INST,
+          suffix: potentiallyErect,
+          partialParams:
+            {
+              loadPartial: true,
+              start: 0.05,
+              end: 0.25
+            },
+          onLoad: function() {
+            FlxG.sound.music.fadeIn(2, 0, 0.4);
+          }
+        });
+    }
+  }
+
   /**
    * Build an instance of `FreeplayState` that is above the `MainMenuState`.
    * @return The MainMenuState with the FreeplayState as a substate.
@@ -2004,6 +2012,8 @@ class FreeplaySongData
    */
   public var isFav:Bool = false;
 
+  public var isNew:Bool = false;
+
   var song:Song;
 
   public var levelId(default, null):String = '';
@@ -2083,6 +2093,8 @@ class FreeplaySongData
     }
 
     this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty);
+
+    this.isNew = song.isSongNew(currentDifficulty);
   }
 }
 
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index a0fa0ae42..dc30b4345 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -21,6 +21,8 @@ import flixel.tweens.FlxEase;
 import flixel.tweens.FlxTween;
 import flixel.addons.effects.FlxTrail;
 import funkin.play.scoring.Scoring.ScoringRank;
+import funkin.save.Save;
+import funkin.save.Save.SaveScoreData;
 import flixel.util.FlxColor;
 
 class SongMenuItem extends FlxSpriteGroup
@@ -76,6 +78,10 @@ class SongMenuItem extends FlxSpriteGroup
 
   var impactThing:FunkinSprite;
 
+  public var sparkle:FlxSprite;
+
+  var sparkleTimer:FlxTimer;
+
   public function new(x:Float, y:Float)
   {
     super(x, y);
@@ -110,7 +116,7 @@ class SongMenuItem extends FlxSpriteGroup
     newText.animation.play('newAnim', true);
     newText.setGraphicSize(Std.int(newText.width * 0.9));
 
-    newText.visible = false;
+    // newText.visible = false;
 
     add(newText);
 
@@ -153,6 +159,18 @@ class SongMenuItem extends FlxSpriteGroup
     blurredRanking.shader = new GaussianBlurShader(1);
     add(blurredRanking);
 
+    sparkle = new FlxSprite(ranking.x, ranking.y);
+    sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle');
+    sparkle.animation.addByPrefix('sparkle', 'sparkle', 24, false);
+    sparkle.animation.play('sparkle', true);
+    sparkle.scale.set(0.8, 0.8);
+    sparkle.blend = BlendMode.ADD;
+
+    sparkle.visible = false;
+    sparkle.alpha = 0.7;
+
+    add(sparkle);
+
     // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
     // ranking.scale.x = ranking.scale.y = realScaled;
     // ranking.alpha = 0.75;
@@ -218,6 +236,13 @@ class SongMenuItem extends FlxSpriteGroup
     setVisibleGrp(false);
   }
 
+  function sparkleEffect(timer:FlxTimer):Void
+  {
+    sparkle.setPosition(FlxG.random.float(ranking.x - 20, ranking.x + 3), FlxG.random.float(ranking.y - 29, ranking.y + 4));
+    sparkle.animation.play('sparkle', true);
+    sparkleTimer = new FlxTimer().start(FlxG.random.float(1.2, 4.5), sparkleEffect);
+  }
+
   // no way to grab weeks rn, so this needs to be done :/
   // negative values mean weekends
   function checkWeek(name:String):Void
@@ -415,8 +440,17 @@ class SongMenuItem extends FlxSpriteGroup
 
   function updateScoringRank(newRank:Null<ScoringRank>):Void
   {
+    if (sparkleTimer != null) sparkleTimer.cancel();
+    sparkle.visible = false;
+
     this.ranking.rank = newRank;
     this.blurredRanking.rank = newRank;
+
+    if (newRank == PERFECT_GOLD)
+    {
+      sparkleTimer = new FlxTimer().start(1, sparkleEffect);
+      sparkle.visible = true;
+    }
   }
 
   function set_hsvShader(value:HSVShader):HSVShader
@@ -468,6 +502,7 @@ class SongMenuItem extends FlxSpriteGroup
     updateBPM(Std.int(songData?.songStartingBpm) ?? 0);
     updateDifficultyRating(songData?.difficultyRating ?? 0);
     updateScoringRank(songData?.scoringRank);
+    newText.visible = songData?.isNew;
     // Update opacity, offsets, etc.
     updateSelected();
 

From b7eed4c4d3a1c1b4331e254f7eea75ff334fc3f4 Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Thu, 6 Jun 2024 01:50:19 +0100
Subject: [PATCH 95/96] assets submoddd!!!!!

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 56fe3a366..36e395467 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 56fe3a3662bf5588b4f6cfec492efe5d4cfd600c
+Subproject commit 36e3954675428214ae6d3ce5123ad3bc9882ea8b

From e4647e9e2d754a8128a25dcc3d75e463fa8aed4c Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 6 Jun 2024 19:46:44 -0400
Subject: [PATCH 96/96] assets submod

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index 36e395467..0062c05d5 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 36e3954675428214ae6d3ce5123ad3bc9882ea8b
+Subproject commit 0062c05d559ae281ce39f8df3da6efb1f92ca808