diff --git a/.gitmodules b/.gitmodules
index ad8099e60..452c0089b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
 [submodule "assets"]
 	path = assets
-	url = https://github.com/FunkinCrew/Funkin-assets-secret
+	url = https://github.com/FunkinCrew/Funkin-Assets-secret
 [submodule "art"]
 	path = art
-	url = https://github.com/FunkinCrew/Funkin-art-secret
+	url = https://github.com/FunkinCrew/Funkin-Art-secret
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10bbfe5f7..898978ca3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,31 +6,52 @@ 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.
+- 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!)
   - 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 +59,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
diff --git a/Project.xml b/Project.xml
index f19a19e8c..fcd29a25e 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-->
@@ -28,7 +29,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" />
@@ -126,6 +127,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.core" /> <!-- General utility library, "the lodash of Haxe" -->
diff --git a/art b/art
index 66572f85d..faeba700c 160000
--- a/art
+++ b/art
@@ -1 +1 @@
-Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa
+Subproject commit faeba700c5526bd4fd57ccc927d875c82b9d3553
diff --git a/assets b/assets
index 8fea0bf1f..0062c05d5 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 8fea0bf1fe07b6dd0efb8ecf46dc8091b0177007
+Subproject commit 0062c05d559ae281ce39f8df3da6efb1f92ca808
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 716754b59..f94b662ac 100644
--- a/hmm.json
+++ b/hmm.json
@@ -40,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",
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index a945c10c5..49b15ddf6 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -219,7 +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: "nightmare",
         isNewHighscore: true,
         scoreData:
           {
@@ -227,7 +229,7 @@ class InitState extends FlxState
             tallies:
               {
                 sick: 130,
-                good: 25,
+                good: 60,
                 bad: 69,
                 shit: 69,
                 missed: 69,
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 939b17f28..4f61e70c2 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -11,6 +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")
@@ -341,23 +346,76 @@ 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 suffix = params.suffix ?? '';
+    var pathToUse = switch (pathsFunction)
     {
-      FlxG.sound.music = music;
+      case MUSIC: Paths.music('$key/$key');
+      case INST: Paths.inst('$key', suffix);
+      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;
+    // 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,
+        params.loop ?? true, false, false, params.onComplete);
+
+      if (music != null)
+      {
+        partialQueue.push(music);
+
+        @:nullSafety(Off)
+        music.future.onComplete(function(partialMusic:Null<FunkinSound>) {
+          FlxG.sound.music = partialMusic;
+          FlxG.sound.list.remove(FlxG.sound.music);
+
+          if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
+        });
+
+        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;
+      }
     }
   }
 
+  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>>> = [];
+
   /**
    * Creates a new `FunkinSound` object synchronously.
    *
@@ -414,6 +472,49 @@ 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
+   * @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,
+      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>>();
+
+    // 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);
+
+    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);
+      });
+    }
+
+    return promise;
+  }
+
   @:nullSafety(Off)
   public override function destroy():Void
   {
@@ -474,6 +575,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`
@@ -497,4 +604,22 @@ typedef FunkinSoundPlayMusicParams =
    * @default `true`
    */
   var ?mapTimeChanges:Bool;
+
+  /**
+   * Which Paths function to use to load a song
+   * @default `MUSIC`
+   */
+  var ?pathsFunction:PathsFunction;
+
+  var ?partialParams:PartialSoundParams;
+
+  var ?onComplete:Void->Void;
+  var ?onLoad:Void->Void;
+}
+
+typedef PartialSoundParams =
+{
+  var loadPartial:Bool;
+  var start:Float;
+  var end:Float;
 }
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.
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 26380947a..769af8f08 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
@@ -53,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>;
@@ -112,14 +117,23 @@ 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 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);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_METADATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   /**
    * Produces a string representation suitable for debugging.
    */
@@ -368,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);
@@ -600,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/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/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/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index ab2abda8e..acbb99342 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;
 
@@ -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;
   }
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/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index fb9d9b4e2..8c45fac65 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.cancel();
+    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/PlayState.hx b/source/funkin/play/PlayState.hx
index 6b0e917ca..af3281c4b 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2317,8 +2317,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)
@@ -2340,17 +2338,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);
@@ -2360,17 +2368,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)
@@ -3118,9 +3122,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,
@@ -3139,7 +3144,7 @@ class PlayState extends MusicBeatSubState
           },
         isNewHighscore: isNewHighscore
       });
-    res.camera = camHUD;
+    this.persistentDraw = false;
     openSubState(res);
   }
 
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;
 
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index ee7c8eade..48fb3b04e 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;
@@ -15,12 +16,15 @@ 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;
 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 +38,7 @@ class ResultState extends MusicBeatSubState
 {
   final params:ResultsStateParams;
 
-  final rank:ResultRank;
+  final rank:ScoringRank;
   final songName:FlxBitmapText;
   final difficulty:FlxSprite;
   final clearPercentSmall:ClearPercentCounter;
@@ -52,20 +56,30 @@ 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;
 
+  var rankBg:FunkinSprite;
+  final cameraBG:FunkinCamera;
+  final cameraScroll:FunkinCamera;
+  final cameraEverything:FunkinCamera;
+
   public function new(params:ResultsStateParams)
   {
     super();
 
     this.params = params;
 
-    rank = calculateRank(params);
-    // rank = SHIT;
+    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.
@@ -84,39 +98,59 @@ 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);
+
+    rankBg = new FunkinSprite(0, 0);
   }
 
   override function create():Void
   {
+    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);
+    this.camera = cameraEverything;
+
     // 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!
     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;
     });
@@ -126,7 +160,21 @@ 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);
@@ -134,36 +182,56 @@ 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');
@@ -203,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);
 
@@ -223,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}, 0.4, {ease: FlxEase.quartOut});
+    FlxTween.tween(blackTopBar, {y: 0}, 7 / 24, {ease: FlxEase.quartOut, startDelay: 3 / 24});
     blackTopBar.zIndex = 1010;
     add(blackTopBar);
 
@@ -231,7 +299,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");
     });
@@ -240,7 +308,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");
     });
@@ -249,23 +317,47 @@ 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();
-      };
+      scorePopin.animation.finishCallback = anim -> {};
+    });
+
+    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>();
@@ -283,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);
 
@@ -312,20 +407,49 @@ 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)
+    new FlxTimer().start(rank.getMusicDelay(), _ -> {
+      if (rank.hasMusicIntro())
       {
-        highscoreNew.visible = true;
-        highscoreNew.animation.play("new");
-        FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
+        // 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
       {
-        highscoreNew.visible = false;
+        FunkinSound.playMusic(rank.getMusicPath(),
+          {
+            startingVolume: 1.0,
+            overrideExisting: true,
+            restartTrack: true,
+            loop: rank.shouldMusicLoop()
+          });
       }
-    };
+    });
+
+    rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xFF000000);
+    rankBg.zIndex = 99999;
+    add(rankBg);
+
+    rankBg.alpha = 0;
 
     refresh();
 
@@ -338,6 +462,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.
@@ -346,8 +472,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: _ -> {
@@ -362,10 +488,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;
 
@@ -374,9 +496,10 @@ class ResultState extends MusicBeatSubState
             clearPercentCounter.flash(false);
           });
 
-          displayRankText();
+          // 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,
@@ -386,7 +509,7 @@ class ResultState extends MusicBeatSubState
                 }
               });
 
-            afterRankTallySequence();
+            // afterRankTallySequence();
           });
         }
       });
@@ -411,7 +534,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
         {
@@ -425,16 +547,35 @@ class ResultState extends MusicBeatSubState
 
   function displayRankText():Void
   {
-    var rankTextVert:FunkinSprite = FunkinSprite.create(FlxG.width - 64, 100, rank.getVerTextAsset());
-    rankTextVert.zIndex = 2000;
+    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 - 44;
+    rankTextVert.y = 100;
+    rankTextVert.zIndex = 990;
     add(rankTextVert);
 
-    for (i in 0...10)
+    FlxFlicker.flicker(rankTextVert, 2 / 24 * 3, 2 / 24, true);
+
+    // Scrolling.
+    new FlxTimer().start(30 / 24, _ -> {
+      rankTextVert.velocity.y = -80;
+    });
+
+    for (i in 0...12)
     {
-      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 + (135 * i / 2) + 10;
+      // rankTextBack.angle = -3.8;
       rankTextBack.zIndex = 100;
+      rankTextBack.cameras = [cameraScroll];
       add(rankTextBack);
+
+      // Scrolling.
+      rankTextBack.velocity.x = (i % 2 == 0) ? -7.0 : 7.0;
     }
 
     refresh();
@@ -444,28 +585,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 +597,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)
         {
@@ -487,9 +616,8 @@ class ResultState extends MusicBeatSubState
         else
         {
           bfExcellent.visible = true;
-          bfExcellent.playAnimation('Intro');
+          bfExcellent.playAnimation('');
         }
-
       case GREAT:
         if (bfGreat == null)
         {
@@ -498,9 +626,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)
         {
@@ -511,7 +650,6 @@ class ResultState extends MusicBeatSubState
           bfShit.visible = true;
           bfShit.playAnimation('Intro');
         }
-
       case GOOD:
         if (bfGood == null)
         {
@@ -521,7 +659,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)
@@ -554,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);
@@ -588,7 +725,9 @@ class ResultState extends MusicBeatSubState
       refresh();
     }
 
-    movingSongStuff = true;
+    new FlxTimer().start(2.5, _ -> {
+      movingSongStuff = true;
+    });
   }
 
   var movingSongStuff:Bool = false;
@@ -608,6 +747,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;
 
@@ -635,154 +847,69 @@ 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)));
+        var rigged:Bool = true;
+        if (rank > Scoring.calculateRank(params?.prevScoreData)) // if (rigged)
+        {
+          trace('THE RANK IS Higher.....');
+
+          FlxTween.tween(rankBg, {alpha: 1}, 0.5,
+            {
+              ease: FlxEase.expoOut,
+              onComplete: function(_) {
+                FlxG.switchState(FreeplayState.build(
+                  {
+                    {
+                      fromResults:
+                        {
+                          oldRank: Scoring.calculateRank(params?.prevScoreData),
+                          newRank: rank,
+                          songId: params.songId,
+                          difficultyId: params.difficultyId,
+                          playRankAnim: true
+                        }
+                    }
+                  }));
+              }
+            });
+        }
+        else
+        {
+          trace('rank is lower...... and/or equal');
+          openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(
+            {
+              {
+                fromResults:
+                  {
+                    oldRank: null,
+                    playRankAnim: false,
+                    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 +924,8 @@ typedef ResultsStateParams =
    */
   var title:String;
 
+  var songId:String;
+
   /**
    * Whether the displayed score is a new highscore
    */
diff --git a/source/funkin/play/components/ClearPercentCounter.hx b/source/funkin/play/components/ClearPercentCounter.hx
index d296b0b0b..e3d3795d9 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,22 +54,19 @@ 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;
 
-  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);
@@ -86,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;
@@ -105,6 +102,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 +112,13 @@ 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;
       }
     }
+    for (ind in (seperatedScore.length + 1)...(members.length))
+    {
+      members[ind].visible = false;
+    }
   }
 }
 
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 07d4ab69b..472c38bba 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.
diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx
index 744091b44..d6f71fc7e 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,303 @@ class Scoring
       return 'miss';
     }
   }
+
+  public static function calculateRank(scoreData:Null<SaveScoreData>):Null<ScoringRank>
+  {
+    if (scoreData?.tallies.totalNotes == 0 || 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;
+
+    // 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;
+
+  @: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
+   */
+  public function getMusicDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 95/24;
+      case EXCELLENT:
+        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;
+    }
+  }
+
+  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/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 53408fb34..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.
    */
@@ -120,6 +126,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 +288,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 +353,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;
@@ -364,7 +384,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];
 
@@ -590,6 +610,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/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index e9c99558d..4f8ab4434 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));
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index 934d6a4aa..2ff6b96cc 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -1,20 +1,22 @@
 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
 {
-  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.
@@ -54,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
@@ -492,6 +497,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.
    */
@@ -706,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
@@ -718,13 +727,94 @@ 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):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;
+
+    return 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):Int
+  {
+    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 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/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 4fa9dd6b3..7a929322a 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -35,8 +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");
-        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/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/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/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/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index a313981f4..260393fac 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;
@@ -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);
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();
   }
 
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index 0308cd871..e84f7ec43 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -384,17 +384,34 @@ 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.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) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
+        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()));
+        }
       }
       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)
+        {
+          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.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()));
+        }
       }
     }
 
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index 98e263aaf..c97e8142d 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;
 
@@ -178,6 +193,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/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx
index 50f4a432c..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);
   }
 
@@ -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,
@@ -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/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/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;
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
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 530f28c33..607b7a353 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;
@@ -31,9 +32,12 @@ 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;
+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;
@@ -42,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.
@@ -49,6 +54,39 @@ 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;
+
+  /**
+   * Whether or not to play the rank animation on returning to freeplay.
+   */
+  var playRankAnim:Bool;
+
+  /**
+   * 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;
 };
 
 /**
@@ -73,6 +111,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;
 
@@ -160,10 +199,21 @@ class FreeplayState extends MusicBeatSubState
   var bgDad:FlxSprite;
   var cardGlow:FlxSprite;
 
+  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?.playRankAnim == true)
+    {
+      prepForNewRank = true;
+    }
+
     if (stickers != null)
     {
       stickerSubState = stickers;
@@ -200,11 +250,14 @@ class FreeplayState extends MusicBeatSubState
     isDebug = true;
     #end
 
-    FunkinSound.playMusic('freakyMenu',
-      {
-        overrideExisting: true,
-        restartTrack: false
-      });
+    if (prepForNewRank == false)
+    {
+      FunkinSound.playMusic('freakyMenu',
+        {
+          overrideExisting: true,
+          restartTrack: false
+        });
+    }
 
     // Add a null entry that represents the RANDOM option
     songs.push(null);
@@ -212,10 +265,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 difficulties for the current character.
         var displayedVariations = song.getVariationsByCharId(currentCharacter);
         var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
@@ -587,6 +654,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 (prepForNewRank)
+      {
+        rankAnimStart(fromResultsParams);
+      }
     });
 
     generateSongList(null, false);
@@ -613,6 +685,11 @@ class FreeplayState extends MusicBeatSubState
     FlxG.cameras.add(rankCamera, false);
     rankBg.cameras = [rankCamera];
     rankBg.alpha = 0;
+
+    if (prepForNewRank)
+    {
+      rankCamera.fade(0xFF000000, 0, false, null, true);
+    }
   }
 
   var currentFilter:SongFilter = null;
@@ -680,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);
@@ -702,13 +780,12 @@ 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);
-
-      funnyMenu.forcePosition();
-
       funnyMenu.checkClip();
+      funnyMenu.forcePosition();
 
       grpCapsules.add(funnyMenu);
     }
@@ -729,6 +806,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:
@@ -743,6 +827,8 @@ class FreeplayState extends MusicBeatSubState
           return filterRegexp.match(str.songName);
         });
 
+        songsToFilter.sort(filterAlphabetically);
+
       case STARTSWITH:
         // extra note: this is essentially a "search"
 
@@ -757,25 +843,93 @@ class FreeplayState extends MusicBeatSubState
           if (str == null) return true; // Random
           return str.isFav;
         });
+
+        songsToFilter.sort(filterAlphabetically);
+
       default:
         // return all on default
     }
+
     return songsToFilter;
   }
 
-  function rankAnimStart()
+  var sparks:FlxSprite;
+  var sparksADD:FlxSprite;
+
+  function rankAnimStart(fromResults:Null<FromResultsParams>):Void
   {
+    busy = true;
+    grpCapsules.members[curSelected].sparkle.alpha = 0;
+    // grpCapsules.members[curSelected].forcePosition();
+
+    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);
-    FlxG.sound.music.volume = 0;
+    if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
     rankBg.alpha = 1;
 
-    originalPos.x = grpCapsules.members[curSelected].x;
-    originalPos.y = grpCapsules.members[curSelected].y;
+    if (fromResults?.oldRank != null)
+    {
+      grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank;
+      grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank;
 
-    grpCapsules.members[curSelected].ranking.alpha = 0;
-    grpCapsules.members[curSelected].blurredRanking.alpha = 0;
+      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;
+    // originalPos.y = grpCapsules.members[curSelected].y;
+
+    originalPos.x = 320.488;
+    originalPos.y = 235.6;
+    trace(originalPos);
+
+    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});
@@ -784,40 +938,57 @@ 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();
+      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.visible = true;
+    grpCapsules.members[curSelected].blurredRanking.visible = true;
     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);
+    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, _ -> {
-      trace(grpCapsules.members[curSelected].ranking.rank);
-      switch (grpCapsules.members[curSelected].tempr)
+      // trace(grpCapsules.members[curSelected].ranking.rank);
+      if (fromResults?.oldRank != null)
       {
-        case 0:
+        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:
           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 +1017,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 +1051,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 +1086,11 @@ 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;
+              grpCapsules.members[curSelected].sparkle.alpha = 0.7;
+              playCurSongPreview(capsule);
             }, null);
 
             // FlxTween.tween(capsule, {"targetPos.x": capsule.targetPos.x - 50}, 0.6,
@@ -963,7 +1139,7 @@ class FreeplayState extends MusicBeatSubState
 
     new FlxTimer().start(2, _ -> {
       // dj.fistPump();
-      FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
+      prepForNewRank = false;
     });
   }
 
@@ -981,7 +1157,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,43 +1168,66 @@ class FreeplayState extends MusicBeatSubState
   {
     super.update(elapsed);
 
+    #if debug
     if (FlxG.keys.justPressed.T)
     {
-      rankAnimStart();
+      rankAnimStart(fromResultsParams);
     }
 
-    if (FlxG.keys.justPressed.H)
-    {
-      rankDisplayNew();
-    }
+    // if (FlxG.keys.justPressed.H)
+    // {
+    //   rankDisplayNew(fromResultsParams);
+    // }
+
+    // if (FlxG.keys.justPressed.G)
+    // {
+    //   rankAnimSlam(fromResultsParams);
+    // }
 
     if (FlxG.keys.justPressed.G)
     {
-      rankAnimSlam();
+      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
 
-    if (FlxG.keys.justPressed.F)
+    if (FlxG.keys.justPressed.F && !busy)
     {
       var targetSong = grpCapsules.members[curSelected]?.songData;
       if (targetSong != null)
@@ -1034,24 +1236,52 @@ 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].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
+          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);
+          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();
           });
-          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;
+              }
+            });
         }
       }
     }
@@ -1255,6 +1485,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);
@@ -1299,6 +1547,7 @@ class FreeplayState extends MusicBeatSubState
               overrideExisting: true,
               restartTrack: false
             });
+          FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
           close();
         }
         else
@@ -1394,6 +1643,7 @@ class FreeplayState extends MusicBeatSubState
         {
           songCapsule.songData.currentDifficulty = currentDifficulty;
           songCapsule.init(null, null, songCapsule.songData);
+          songCapsule.checkClip();
         }
         else
         {
@@ -1485,6 +1735,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});
@@ -1522,6 +1773,8 @@ class FreeplayState extends MusicBeatSubState
     funnyScroll3.visible = false;
 
     new FlxTimer().start(1, function(tmr:FlxTimer) {
+      FunkinSound.emptyPartialQueue();
+
       Paths.setCurrentLevel(cap.songData.levelId);
       LoadingState.loadPlayState(
         {
@@ -1564,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;
 
@@ -1605,49 +1858,58 @@ 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: true
-          });
-        FlxG.sound.music.fadeIn(2, 0, 0.8);
-      }
-      else
-      {
-        // TODO: Stream the instrumental of the selected song?
-        var didReplace:Bool = FunkinSound.playMusic('freakyMenu',
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: false
-          });
-        if (didReplace)
-        {
-          FunkinSound.playMusic('freakyMenu',
-            {
-              startingVolume: 0.0,
-              overrideExisting: true,
-              restartTrack: false
-            });
-          FlxG.sound.music.fadeIn(2, 0, 0.8);
-        }
-      }
+      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.
    */
   public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
   {
-    var result = new MainMenuState();
+    var result:MainMenuState;
+    if (params?.fromResults.playRankAnim) result = new MainMenuState(true);
+    else
+      result = new MainMenuState(false);
 
     result.openSubState(new FreeplayState(params, stickers));
     result.persistentUpdate = false;
@@ -1750,6 +2012,8 @@ class FreeplaySongData
    */
   public var isFav:Bool = false;
 
+  public var isNew:Bool = false;
+
   var song:Song;
 
   public var levelId(default, null):String = '';
@@ -1765,12 +2029,12 @@ 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
   {
-    if (currentDifficulty == value) return value;
-
     currentDifficulty = value;
     updateValues(displayedVariations);
     return value;
@@ -1809,7 +2073,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);
@@ -1827,6 +2091,10 @@ class FreeplaySongData
     {
       this.albumId = songDifficulty.album;
     }
+
+    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 536a9cfe6..dc30b4345 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -20,6 +20,9 @@ import funkin.graphics.FunkinSprite;
 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
@@ -37,10 +40,15 @@ 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;
   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();
@@ -70,7 +78,9 @@ class SongMenuItem extends FlxSpriteGroup
 
   var impactThing:FunkinSprite;
 
-  public var tempr:Int;
+  public var sparkle:FlxSprite;
+
+  var sparkleTimer:FlxTimer;
 
   public function new(x:Float, y:Float)
   {
@@ -106,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);
 
@@ -132,15 +142,35 @@ 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);
+    fakeRanking = new FreeplayRank(420, 41);
+    add(fakeRanking);
 
-    tempr = FlxG.random.int(0, 5);
-    ranking = new FreeplayRank(420, 41, tempr);
+    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, tempr);
+    blurredRanking = new FreeplayRank(ranking.x, ranking.y);
     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;
@@ -179,6 +209,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);
@@ -196,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
@@ -255,7 +302,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:
@@ -268,8 +315,6 @@ class SongMenuItem extends FlxSpriteGroup
 
   function updateBPM(newBPM:Int):Void
   {
-    trace(newBPM);
-
     var shiftX:Float = 191;
     var tempShift:Float = 0;
 
@@ -317,7 +362,7 @@ class SongMenuItem extends FlxSpriteGroup
 
   var evilTrail:FlxTrail;
 
-  public function fadeAnim()
+  public function fadeAnim():Void
   {
     impactThing = new FunkinSprite(0, 0);
     impactThing.frames = capsule.frames;
@@ -344,19 +389,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;
     }
   }
@@ -375,7 +420,7 @@ class SongMenuItem extends FlxSpriteGroup
       switch (i)
       {
         case 0:
-          if (newRating > 10)
+          if (newRating < 10)
           {
             bigNumbers[i].digit = 0;
           }
@@ -393,6 +438,21 @@ class SongMenuItem extends FlxSpriteGroup
     // diffRatingSprite.visible = false;
   }
 
+  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
   {
     this.hsvShader = value;
@@ -441,6 +501,8 @@ class SongMenuItem extends FlxSpriteGroup
     if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
     updateBPM(Std.int(songData?.songStartingBpm) ?? 0);
     updateDifficultyRating(songData?.difficultyRating ?? 0);
+    updateScoringRank(songData?.scoringRank);
+    newText.visible = songData?.isNew;
     // Update opacity, offsets, etc.
     updateSelected();
 
@@ -653,56 +715,66 @@ 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 (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();
   }
 }
 
 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 || val == 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);
+          this.visible = 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 +789,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);
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index fc2a8c7d7..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
@@ -49,10 +58,12 @@ class MainMenuState extends MusicBeatState
     DiscordClient.changePresence("In the Menus", null);
     #end
 
+    FlxG.cameras.reset(new FunkinCamera('mainMenu'));
+
     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;
@@ -161,7 +172,7 @@ class MainMenuState extends MusicBeatState
 
   function playMenuMusic():Void
   {
-    FunkinSound.playMusic('freakyMenu',
+      FunkinSound.playMusic('freakyMenu',
       {
         overrideExisting: true,
         restartTrack: false
@@ -170,7 +181,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 +339,8 @@ class MainMenuState extends MusicBeatState
       persistentUpdate = false;
 
       FlxG.state.openSubState(new DebugMenuSubState());
+      // reset camera when debug menu is closed
+      subStateClosed.addOnce(_ -> resetCamStuff());
     }
     #end
 
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;
   }
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 0c2214529..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)}';
 
@@ -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;
diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx
index 49bef5e4a..c6dbcd505 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,11 +120,11 @@ class TitleState extends MusicBeatState
 
   function startIntro():Void
   {
-    playMenuMusic();
+    if (!initialized || FlxG.sound.music == null) playMenuMusic();
 
     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);
 
@@ -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);
   }
 
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;
   }
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index 2f3b570b3..1e0978839 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.
    */
@@ -462,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;
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index 18d7eafa6..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)
@@ -39,13 +41,28 @@ class VersionUtil
     if (thx.Types.isAnonymousObject(versionData.version))
     {
       // This is bad! versionData.version should be an array!
+      trace('[SAVE] Version data repair required! (got ${versionData.version})');
+      // 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
     {
+      trace('[SAVE] Version data repair not required (got ${version})');
       // No need for repair.
       return version;
     }