diff --git a/.gitmodules b/.gitmodules
index be5e0aaa8..ad8099e60 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
 [submodule "assets"]
 	path = assets
-	url = https://github.com/FunkinCrew/funkin.assets
+	url = https://github.com/FunkinCrew/Funkin-assets-secret
 [submodule "art"]
 	path = art
-	url = https://github.com/FunkinCrew/funkin.art
+	url = https://github.com/FunkinCrew/Funkin-art-secret
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b8fdb64d1..6dc1dc008 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -3,13 +3,13 @@
   "configurations": [
     {
       // Launch in native/CPP on Windows/OSX/Linux
-      "name": "Lime",
+      "name": "Lime Build+Debug",
       "type": "lime",
       "request": "launch"
     },
     {
-      // Launch in native/CPP on Windows/OSX/Linux (without compiling)
-      "name": "Debug",
+      // Launch in native/CPP on Windows/OSX/Linux
+      "name": "Lime Debug (No Build)",
       "type": "lime",
       "request": "launch",
       "preLaunchTask": null
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a8a67245b..26fe0b042 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -155,6 +155,11 @@
       "target": "hl",
       "args": ["-debug", "-DDIALOGUE"]
     },
+    {
+      "label": "Windows / Debug (Results Screen Test)",
+      "target": "windows",
+      "args": ["-debug", "-DRESULTS"]
+    },
     {
       "label": "Windows / Debug (Straight to Chart Editor)",
       "target": "windows",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a852ca82d..10bbfe5f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@ All notable changes 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).
 
+## [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.
+- 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.
+### Changed
+- Tweaked the charts for several songs:
+  - Winter Horrorland
+  - Stress
+  - Lit Up
+- 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!)
+- 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 pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
+- Fixed a bug where the Chart Editor would crash when losing (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!)
+
 ## [0.3.3] - 2024-05-14
 ### Changed
 - Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
diff --git a/Project.xml b/Project.xml
index 24cdac270..f19a19e8c 100644
--- a/Project.xml
+++ b/Project.xml
@@ -128,6 +128,7 @@
 
 
 	<haxelib name="json2object" /> <!-- JSON parsing -->
+	<haxelib name="thx.core" /> <!-- General utility library, "the lodash of Haxe" -->
 	<haxelib name="thx.semver" /> <!-- Version string handling -->
 
 	<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
diff --git a/README.md b/README.md
index 4c6fd9e84..7b7032a20 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Full credits can be found in-game, or wherever the credits.json file is.
 
 ## Programming
 - [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
-- [MasterEric](https://twitter.com/EliteMasterEric) - Programmer
+- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
 - [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
 - [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
 - Our contributors on GitHub
diff --git a/assets b/assets
index 783f22e74..371cce1fd 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 783f22e741c85223da7f3f815b28fc4c6f240cbc
+Subproject commit 371cce1fdc44914ddc3a5327e996cece4e676715
diff --git a/example_mods/introMod/_polymod_meta.json b/example_mods/introMod/_polymod_meta.json
index e0b03f1cd..4dc0cd804 100644
--- a/example_mods/introMod/_polymod_meta.json
+++ b/example_mods/introMod/_polymod_meta.json
@@ -3,7 +3,7 @@
   "description": "An introductory mod.",
   "contributors": [
     {
-      "name": "MasterEric"
+      "name": "EliteMasterEric"
     }
   ],
   "api_version": "0.1.0",
diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json
index 4c0f177f9..0a2ed042c 100644
--- a/example_mods/testing123/_polymod_meta.json
+++ b/example_mods/testing123/_polymod_meta.json
@@ -3,7 +3,7 @@
   "description": "Newgrounds? More like OLDGROUNDS lol.",
   "contributors": [
     {
-      "name": "MasterEric"
+      "name": "EliteMasterEric"
     }
   ],
   "api_version": "0.1.0",
diff --git a/hmm.json b/hmm.json
index 288aa80b8..716754b59 100644
--- a/hmm.json
+++ b/hmm.json
@@ -153,7 +153,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
+      "ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 00d34fadb..a945c10c5 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -214,6 +214,30 @@ class InitState extends FlxState
     #elseif STAGEBUILD
     // -DSTAGEBUILD
     FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
+    #elseif RESULTS
+    // -DRESULTS
+    FlxG.switchState(() -> new funkin.play.ResultState(
+      {
+        storyMode: false,
+        title: "CUM SONG",
+        isNewHighscore: true,
+        scoreData:
+          {
+            score: 1_234_567,
+            tallies:
+              {
+                sick: 130,
+                good: 25,
+                bad: 69,
+                shit: 69,
+                missed: 69,
+                combo: 69,
+                maxCombo: 69,
+                totalNotesHit: 140,
+                totalNotes: 200 // 0,
+              }
+          },
+      }));
     #elseif ANIMDEBUG
     // -DANIMDEBUG
     FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx
index 9616dfe18..77e44bd1d 100644
--- a/source/funkin/api/newgrounds/NGUnsafe.hx
+++ b/source/funkin/api/newgrounds/NGUnsafe.hx
@@ -1,9 +1,5 @@
 package funkin.api.newgrounds;
 
-import flixel.util.FlxSignal;
-import flixel.util.FlxTimer;
-import lime.app.Application;
-import openfl.display.Stage;
 #if newgrounds
 import io.newgrounds.NG;
 import io.newgrounds.NGLite;
diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx
index c1f8ad3ba..3f5fc078a 100644
--- a/source/funkin/api/newgrounds/NGio.hx
+++ b/source/funkin/api/newgrounds/NGio.hx
@@ -2,19 +2,11 @@ package funkin.api.newgrounds;
 
 #if newgrounds
 import flixel.util.FlxSignal;
-import flixel.util.FlxTimer;
 import io.newgrounds.NG;
 import io.newgrounds.NGLite;
-import io.newgrounds.components.ScoreBoardComponent.Period;
 import io.newgrounds.objects.Error;
-import io.newgrounds.objects.Medal;
 import io.newgrounds.objects.Score;
-import io.newgrounds.objects.ScoreBoard;
-import io.newgrounds.objects.events.Response;
-import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
-import io.newgrounds.objects.events.Result.GetVersionResult;
 import lime.app.Application;
-import openfl.display.Stage;
 #end
 
 /**
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index df05cc3ef..939b17f28 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -11,10 +11,9 @@ import funkin.audio.waveform.WaveformDataParser;
 import funkin.data.song.SongData.SongMusicData;
 import funkin.data.song.SongRegistry;
 import funkin.util.tools.ICloneable;
-import openfl.Assets;
 import openfl.media.SoundMixer;
+
 #if (openfl >= "8.0.0")
-import openfl.utils.AssetType;
 #end
 
 /**
diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx
index 5037ee1d0..9a1e0e0c1 100644
--- a/source/funkin/audio/VoicesGroup.hx
+++ b/source/funkin/audio/VoicesGroup.hx
@@ -1,9 +1,7 @@
 package funkin.audio;
 
-import funkin.audio.FunkinSound;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import funkin.audio.waveform.WaveformData;
-import funkin.audio.waveform.WaveformDataParser;
 
 class VoicesGroup extends SoundGroup
 {
diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx
index ca77dd58a..b94f20b38 100644
--- a/source/funkin/audio/visualize/ABotVis.hx
+++ b/source/funkin/audio/visualize/ABotVis.hx
@@ -1,13 +1,9 @@
 package funkin.audio.visualize;
 
-import funkin.audio.visualize.dsp.FFT;
 import flixel.FlxSprite;
-import flixel.addons.plugin.taskManager.FlxTask;
 import flixel.graphics.frames.FlxAtlasFrames;
 import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
-import flixel.math.FlxMath;
 import flixel.sound.FlxSound;
-import funkin.util.MathUtil;
 import funkin.vis.dsp.SpectralAnalyzer;
 import funkin.vis.audioclip.frontends.LimeAudioClip;
 
diff --git a/source/funkin/audio/visualize/PolygonVisGroup.hx b/source/funkin/audio/visualize/PolygonVisGroup.hx
index cc68f4ae0..bff845796 100644
--- a/source/funkin/audio/visualize/PolygonVisGroup.hx
+++ b/source/funkin/audio/visualize/PolygonVisGroup.hx
@@ -1,6 +1,5 @@
 package funkin.audio.visualize;
 
-import funkin.audio.visualize.PolygonSpectogram;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.sound.FlxSound;
 
diff --git a/source/funkin/audio/visualize/SpectogramSprite.hx b/source/funkin/audio/visualize/SpectogramSprite.hx
index 636c0726a..615e80d95 100644
--- a/source/funkin/audio/visualize/SpectogramSprite.hx
+++ b/source/funkin/audio/visualize/SpectogramSprite.hx
@@ -8,8 +8,6 @@ import flixel.sound.FlxSound;
 import flixel.util.FlxColor;
 import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
 import funkin.audio.visualize.VisShit.CurAudioInfo;
-import funkin.audio.visualize.dsp.FFT;
-import lime.system.ThreadPool;
 import lime.utils.Int16Array;
 
 using Lambda;
@@ -38,8 +36,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
     lengthOfShit = amnt;
 
     regenLineShit();
-
-    // makeGraphic(200, 200, FlxColor.BLACK);
   }
 
   public function regenLineShit():Void
@@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
   {
     checkAndSetBuffer();
 
-    // vis.checkAndSetBuffer();
-
     if (setBuffer)
     {
       var samplesToGen:Int = Std.int(sampleRate * seconds);
@@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
           // a value between 10hz and 100Khz
           var hzPicker:Float = Math.pow(10, powedShit);
 
-          // var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, startingSample, startingSample + samplesToGen));
           var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1));
 
           group.members[i].x = prevLine.x;
@@ -211,8 +204,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
           var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y);
 
           // dont draw a line until i figure out a nicer way to view da spikes and shit idk lol!
-          // group.members[i].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1));
-          // group.members[i].angle = line.degrees;
         }
       }
     }
@@ -261,9 +252,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
 
           group.members[Std.int(remappedSample)].x = prevLine.x;
           group.members[Std.int(remappedSample)].y = prevLine.y;
-          // group.members[0].y = prevLine.y;
-
-          // FlxSpriteUtil.drawLine(this, prevLine.x, prevLine.y, width * remappedSample, left * height / 2 + height / 2);
           prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
           prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y;
 
diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx
index 204ced1e1..ba235fe89 100644
--- a/source/funkin/audio/visualize/VisShit.hx
+++ b/source/funkin/audio/visualize/VisShit.hx
@@ -3,7 +3,6 @@ package funkin.audio.visualize;
 import flixel.math.FlxMath;
 import flixel.sound.FlxSound;
 import funkin.audio.visualize.dsp.FFT;
-import lime.system.ThreadPool;
 import lime.utils.Int16Array;
 import funkin.util.MathUtil;
 
@@ -73,9 +72,6 @@ class VisShit
 
       freqOutput.push([]);
 
-      // if (FlxG.keys.justPressed.M)
-      // trace(FFT.rfft(chunk).map(z -> z.scale(1 / fs).magnitude));
-
       // find spectral peaks and their instantaneous frequencies
       for (k => s in freqs)
       {
@@ -91,7 +87,6 @@ class VisShit
         if (freq < maxFreq) freqOutput[indexOfArray].push(power);
         //
       }
-      // haxe.Log.trace("", null);
 
       indexOfArray++;
       // move to next (overlapping) chunk
diff --git a/source/funkin/audio/visualize/dsp/FFT.hx b/source/funkin/audio/visualize/dsp/FFT.hx
index dc75acb81..40ee9cb8c 100644
--- a/source/funkin/audio/visualize/dsp/FFT.hx
+++ b/source/funkin/audio/visualize/dsp/FFT.hx
@@ -1,7 +1,5 @@
 package funkin.audio.visualize.dsp;
 
-import funkin.audio.visualize.dsp.Complex;
-
 using funkin.audio.visualize.dsp.OffsetArray;
 using funkin.audio.visualize.dsp.Signal;
 
diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx
index 1f649b472..a939f91bf 100644
--- a/source/funkin/audio/waveform/WaveformData.hx
+++ b/source/funkin/audio/waveform/WaveformData.hx
@@ -1,7 +1,5 @@
 package funkin.audio.waveform;
 
-import funkin.util.MathUtil;
-
 @:nullSafety
 class WaveformData
 {
diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx
index 32ced2fbd..8eaba8117 100644
--- a/source/funkin/audio/waveform/WaveformSprite.hx
+++ b/source/funkin/audio/waveform/WaveformSprite.hx
@@ -1,7 +1,5 @@
 package funkin.audio.waveform;
 
-import funkin.audio.waveform.WaveformData;
-import funkin.audio.waveform.WaveformDataParser;
 import funkin.graphics.rendering.MeshRender;
 import flixel.util.FlxColor;
 
diff --git a/source/funkin/data/dialogue/conversation/ConversationData.hx b/source/funkin/data/dialogue/conversation/ConversationData.hx
index 30e3f451b..650519836 100644
--- a/source/funkin/data/dialogue/conversation/ConversationData.hx
+++ b/source/funkin/data/dialogue/conversation/ConversationData.hx
@@ -1,7 +1,5 @@
 package funkin.data.dialogue.conversation;
 
-import funkin.data.animation.AnimationData;
-
 /**
  * A type definition for the data for a specific conversation.
  * It includes things like what dialogue boxes to use, what text to display, and what animations to play.
diff --git a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
index ca072897f..fad1e43ad 100644
--- a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
+++ b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
@@ -1,7 +1,6 @@
 package funkin.data.dialogue.conversation;
 
 import funkin.play.cutscene.dialogue.Conversation;
-import funkin.data.dialogue.conversation.ConversationData;
 import funkin.play.cutscene.dialogue.ScriptedConversation;
 
 class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 250de99cb..c8431be33 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -11,6 +11,7 @@ import flixel.system.debug.watch.Tracker;
 // These are great.
 using Lambda;
 using StringTools;
+using thx.Arrays;
 using funkin.util.tools.ArraySortTools;
 using funkin.util.tools.ArrayTools;
 using funkin.util.tools.FloatTools;
diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx
index 548e4edfa..345791eef 100644
--- a/source/funkin/input/Controls.hx
+++ b/source/funkin/input/Controls.hx
@@ -715,7 +715,7 @@ class Controls extends FlxActionSet
           case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
           case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
           case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
-          case Control.FULLSCREEN: return [FlxKey.F];
+          case Control.FULLSCREEN: return [FlxKey.F11]; // We use F for other things LOL.
 
         }
       case Duo(true):
@@ -997,7 +997,7 @@ class Controls extends FlxActionSet
     for (control in Control.createAll())
     {
       var inputs:Array<Int> = Reflect.field(data, control.getName());
-      inputs = inputs.unique();
+      inputs = inputs.distinct();
       if (inputs != null)
       {
         if (inputs.length == 0) {
@@ -1050,7 +1050,7 @@ class Controls extends FlxActionSet
       if (inputs.length == 0) {
         inputs = [FlxKey.NONE];
       } else {
-        inputs = inputs.unique();
+        inputs = inputs.distinct();
       }
 
       Reflect.setField(data, control.getName(), inputs);
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 43dd485cf..a95166e21 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2809,6 +2809,7 @@ class PlayState extends MusicBeatSubState
     deathCounter = 0;
 
     var isNewHighscore = false;
+    var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, currentDifficulty);
 
     if (currentSong != null && currentSong.validScore)
     {
@@ -2828,7 +2829,6 @@ class PlayState extends MusicBeatSubState
               totalNotesHit: Highscore.tallies.totalNotesHit,
               totalNotes: Highscore.tallies.totalNotes,
             },
-          accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
         };
 
       // adds current song data into the tallies for the level (story levels)
@@ -2865,7 +2865,7 @@ class PlayState extends MusicBeatSubState
               score: PlayStatePlaylist.campaignScore,
               tallies:
                 {
-                  // TODO: Sum up the values for the whole level!
+                  // TODO: Sum up the values for the whole week!
                   sick: 0,
                   good: 0,
                   bad: 0,
@@ -2876,7 +2876,6 @@ class PlayState extends MusicBeatSubState
                   totalNotesHit: 0,
                   totalNotes: 0,
                 },
-              accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
             };
 
           if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
@@ -2962,11 +2961,11 @@ class PlayState extends MusicBeatSubState
       {
         if (rightGoddamnNow)
         {
-          moveToResultsScreen(isNewHighscore);
+          moveToResultsScreen(isNewHighscore, prevScoreData);
         }
         else
         {
-          zoomIntoResultsScreen(isNewHighscore);
+          zoomIntoResultsScreen(isNewHighscore, prevScoreData);
         }
       }
     }
@@ -3040,7 +3039,7 @@ class PlayState extends MusicBeatSubState
   /**
    * Play the camera zoom animation and then move to the results screen once it's done.
    */
-  function zoomIntoResultsScreen(isNewHighscore:Bool):Void
+  function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
   {
     trace('WENT TO RESULTS SCREEN!');
 
@@ -3080,7 +3079,7 @@ class PlayState extends MusicBeatSubState
     FlxTween.tween(camHUD, {alpha: 0}, 0.6,
       {
         onComplete: function(_) {
-          moveToResultsScreen(isNewHighscore);
+          moveToResultsScreen(isNewHighscore, prevScoreData);
         }
       });
 
@@ -3113,7 +3112,7 @@ class PlayState extends MusicBeatSubState
   /**
    * Move to the results screen right goddamn now.
    */
-  function moveToResultsScreen(isNewHighscore:Bool):Void
+  function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
   {
     persistentUpdate = false;
     vocals.stop();
@@ -3125,6 +3124,8 @@ class PlayState extends MusicBeatSubState
       {
         storyMode: PlayStatePlaylist.isStoryMode,
         title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+        prevScoreData: prevScoreData,
+        difficultyId: currentDifficulty,
         scoreData:
           {
             score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
@@ -3140,7 +3141,6 @@ class PlayState extends MusicBeatSubState
                 totalNotesHit: talliesToUse.totalNotesHit,
                 totalNotes: talliesToUse.totalNotes,
               },
-            accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
           },
         isNewHighscore: isNewHighscore
       });
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 56dd1e80f..ee7c8eade 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -12,6 +12,8 @@ import funkin.ui.MusicBeatSubState;
 import flixel.math.FlxRect;
 import flixel.text.FlxBitmapText;
 import funkin.ui.freeplay.FreeplayScore;
+import flixel.text.FlxText;
+import flixel.util.FlxColor;
 import flixel.tweens.FlxEase;
 import funkin.ui.freeplay.FreeplayState;
 import flixel.tweens.FlxTween;
@@ -22,153 +24,196 @@ import funkin.save.Save;
 import funkin.save.Save.SaveScoreData;
 import funkin.graphics.shaders.LeftMaskShader;
 import funkin.play.components.TallyCounter;
+import funkin.play.components.ClearPercentCounter;
 
 /**
  * The state for the results screen after a song or week is finished.
  */
+@:nullSafety
 class ResultState extends MusicBeatSubState
 {
   final params:ResultsStateParams;
 
-  var resultsVariation:ResultVariations;
-  var songName:FlxBitmapText;
-  var difficulty:FlxSprite;
+  final rank:ResultRank;
+  final songName:FlxBitmapText;
+  final difficulty:FlxSprite;
+  final clearPercentSmall:ClearPercentCounter;
 
-  var maskShaderSongName:LeftMaskShader = new LeftMaskShader();
-  var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
+  final maskShaderSongName:LeftMaskShader = new LeftMaskShader();
+  final maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
+
+  final resultsAnim:FunkinSprite;
+  final ratingsPopin:FunkinSprite;
+  final scorePopin:FunkinSprite;
+
+  final bgFlash:FlxSprite;
+
+  final highscoreNew:FlxSprite;
+  final score:ResultScore;
+
+  var bfPerfect:Null<FlxAtlasSprite> = null;
+  var bfExcellent:Null<FlxAtlasSprite> = null;
+  var bfGreat:Null<FlxAtlasSprite> = null;
+  var bfGood:Null<FlxSprite> = null;
+  var gfGood:Null<FlxSprite> = null;
+  var bfShit:Null<FlxAtlasSprite> = null;
 
   public function new(params:ResultsStateParams)
   {
     super();
 
     this.params = params;
-  }
 
-  override function create():Void
-  {
-    /*
-      if (params.scoreData.sick == params.scoreData.totalNotesHit
-        && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
-      else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
-        resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
-      else
-        resultsVariation = NORMAL;
-     */
-    resultsVariation = NORMAL;
+    rank = calculateRank(params);
+    // rank = SHIT;
 
-    FunkinSound.playMusic('results$resultsVariation',
-      {
-        startingVolume: 1.0,
-        overrideExisting: true,
-        restartTrack: true,
-        loop: resultsVariation != SHIT
-      });
-
-    // Reset the camera zoom on the results screen.
-    FlxG.camera.zoom = 1.0;
-
-    // TEMP-ish, just used to sorta "cache" the 3000x3000 image!
-    var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
-    add(cacheBullShit);
-
-    var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
-    add(dumb);
-
-    var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
-    bg.scrollFactor.set();
-    add(bg);
-
-    var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
-    bgFlash.scrollFactor.set();
-    bgFlash.visible = false;
-    add(bgFlash);
-
-    // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
-    // bfGfExcellent.visible = false;
-    // add(bfGfExcellent);
-    //
-    // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
-    // bfPerfect.visible = false;
-    // add(bfPerfect);
-    //
-    // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
-    // bfSHIT.visible = false;
-    // add(bfSHIT);
-    //
-    // bfGfExcellent.anim.onComplete = () -> {
-    // bfGfExcellent.anim.curFrame = 28;
-    // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
-    // };
-    //
-    // bfPerfect.anim.onComplete = () -> {
-    //  bfPerfect.anim.curFrame = 136;
-    //  bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
-    // };
-    //
-    // bfSHIT.anim.onComplete = () -> {
-    //  bfSHIT.anim.curFrame = 150;
-    //  bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
-    // };
-
-    var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
-    gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
-    gf.visible = false;
-    gf.animation.finishCallback = _ -> {
-      gf.animation.play('clap', true, false, 9);
-    };
-    add(gf);
-
-    var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
-    boyfriend.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
-    boyfriend.visible = false;
-    boyfriend.animation.finishCallback = function(_) {
-      boyfriend.animation.play('fall', true, false, 14);
-    };
-
-    add(boyfriend);
-
-    var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
-    soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
-    soundSystem.visible = false;
-    new FlxTimer().start(0.4, _ -> {
-      soundSystem.animation.play("idle");
-      soundSystem.visible = true;
-    });
-    add(soundSystem);
-
-    difficulty = new FlxSprite(555);
-
-    var diffSpr:String = switch (PlayState.instance.currentDifficulty)
-    {
-      case 'easy':
-        'difEasy';
-      case 'normal':
-        'difNormal';
-      case 'hard':
-        'difHard';
-      case 'erect':
-        'difErect';
-      case 'nightmare':
-        'difNightmare';
-      case _:
-        'difNormal';
-    }
-
-    difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
-    add(difficulty);
+    // We build a lot of this stuff in the constructor, then place it in create().
+    // This prevents having to do `null` checks everywhere.
 
     var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
     songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
     songName.text = params.title;
     songName.letterSpacing = -15;
     songName.angle = -4.4;
+    songName.zIndex = 1000;
+
+    difficulty = new FlxSprite(555);
+    difficulty.zIndex = 1000;
+
+    clearPercentSmall = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, 100, true);
+    clearPercentSmall.zIndex = 1000;
+    clearPercentSmall.visible = false;
+
+    bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
+
+    resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
+
+    ratingsPopin = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
+
+    scorePopin = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
+
+    highscoreNew = new FlxSprite(310, 570);
+
+    score = new ResultScore(35, 305, 10, params.scoreData.score);
+  }
+
+  override function create():Void
+  {
+    // 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;
+    add(bg);
+
+    bgFlash.scrollFactor.set();
+    bgFlash.visible = false;
+    bgFlash.zIndex = 20;
+    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, _ -> {
+      soundSystem.animation.play("idle");
+      soundSystem.visible = true;
+    });
+    soundSystem.zIndex = 1100;
+    add(soundSystem);
+
+    switch (rank)
+    {
+      case PERFECT | PERFECT_GOLD:
+        bfPerfect = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared"));
+        bfPerfect.visible = false;
+        bfPerfect.zIndex = 500;
+        add(bfPerfect);
+
+        bfPerfect.anim.onComplete = () -> {
+          if (bfPerfect != null)
+          {
+            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.visible = false;
+        bfExcellent.zIndex = 500;
+        add(bfExcellent);
+
+        bfExcellent.onAnimationFinish.add((animName) -> {
+          if (bfExcellent != null)
+          {
+            bfExcellent.playAnimation('Loop Start');
+          }
+        });
+
+      case GREAT:
+        bfGreat = new FlxAtlasSprite(640, 200, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT", "shared"));
+        bfGreat.visible = false;
+        bfGreat.zIndex = 500;
+        add(bfGreat);
+
+        bfGreat.onAnimationFinish.add((animName) -> {
+          if (bfGreat != null)
+          {
+            bfGreat.playAnimation('Loop Start');
+          }
+        });
+
+      case GOOD:
+        gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD');
+        gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
+        gfGood.visible = false;
+        gfGood.zIndex = 500;
+        gfGood.animation.finishCallback = _ -> {
+          if (gfGood != null)
+          {
+            gfGood.animation.play('clap', true, false, 9);
+          }
+        };
+        add(gfGood);
+
+        bfGood = FunkinSprite.createSparrow(640, -200, 'resultScreen/results-bf/resultsGOOD/resultBoyfriendGOOD');
+        bfGood.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
+        bfGood.visible = false;
+        bfGood.zIndex = 501;
+        bfGood.animation.finishCallback = function(_) {
+          if (bfGood != null)
+          {
+            bfGood.animation.play('fall', true, false, 14);
+          }
+        };
+        add(bfGood);
+
+      case SHIT:
+        bfShit = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/results-bf/resultsSHIT", "shared"));
+        bfShit.visible = false;
+        bfShit.zIndex = 500;
+        add(bfShit);
+        bfShit.onAnimationFinish.add((animName) -> {
+          if (bfShit != null)
+          {
+            bfShit.playAnimation('Loop Start');
+          }
+        });
+    }
+
+    var diffSpr:String = 'dif${params?.difficultyId ?? 'Normal'}';
+    difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
+    add(difficulty);
+
     add(songName);
 
     var angleRad = songName.angle * Math.PI / 180;
     speedOfTween.x = -1.0 * Math.cos(angleRad);
     speedOfTween.y = -1.0 * Math.sin(angleRad);
 
-    timerThenSongName();
+    timerThenSongName(1.0, false);
 
     songName.shader = maskShaderSongName;
     difficulty.shader = maskShaderDifficulty;
@@ -178,35 +223,53 @@ 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, startDelay: 0.5});
+    FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut});
+    blackTopBar.zIndex = 1010;
     add(blackTopBar);
 
-    var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
     resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false);
-    resultsAnim.animation.play("result");
+    resultsAnim.visible = false;
+    resultsAnim.zIndex = 1200;
     add(resultsAnim);
+    new FlxTimer().start(0.3, _ -> {
+      resultsAnim.visible = true;
+      resultsAnim.animation.play("result");
+    });
 
-    var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
     ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
     ratingsPopin.visible = false;
+    ratingsPopin.zIndex = 1200;
     add(ratingsPopin);
+    new FlxTimer().start(1.0, _ -> {
+      ratingsPopin.visible = true;
+      ratingsPopin.animation.play("idle");
+    });
 
-    var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
     scorePopin.animation.addByPrefix("score", "tally score", 24, false);
     scorePopin.visible = false;
+    scorePopin.zIndex = 1200;
     add(scorePopin);
+    new FlxTimer().start(1.0, _ -> {
+      scorePopin.visible = true;
+      scorePopin.animation.play("score");
+      scorePopin.animation.finishCallback = anim -> {
+        score.visible = true;
+        score.animateNumbers();
+      };
+    });
 
-    var highscoreNew:FlxSprite = new FlxSprite(310, 570);
     highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
     highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
     highscoreNew.visible = false;
     highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
     highscoreNew.updateHitbox();
+    highscoreNew.zIndex = 1200;
     add(highscoreNew);
 
     var hStuf:Int = 50;
 
     var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
+    ratingGrp.zIndex = 1200;
     add(ratingGrp);
 
     /**
@@ -236,32 +299,115 @@ class ResultState extends MusicBeatSubState
     var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6);
     ratingGrp.add(tallyMissed);
 
-    var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
     score.visible = false;
+    score.zIndex = 1200;
     add(score);
 
     for (ind => rating in ratingGrp.members)
     {
       rating.visible = false;
-      new FlxTimer().start((0.3 * ind) + 0.55, _ -> {
+      new FlxTimer().start((0.3 * ind) + 1.20, _ -> {
         rating.visible = true;
         FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut});
       });
     }
 
-    new FlxTimer().start(0.5, _ -> {
-      ratingsPopin.animation.play("idle");
-      ratingsPopin.visible = true;
+    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;
+      }
+    };
+
+    refresh();
+
+    super.create();
+  }
+
+  var rankTallyTimer:Null<FlxTimer> = null;
+  var clearPercentTarget:Int = 100;
+  var clearPercentLerp:Int = 0;
+
+  function startRankTallySequence():Void
+  {
+    var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100;
+    clearPercentTarget = Math.floor(clearPercentFloat);
+    // Prevent off-by-one errors.
+
+    clearPercentLerp = Std.int(Math.max(0, clearPercentTarget - 36));
+
+    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,
+      {
+        ease: FlxEase.quartOut,
+        onUpdate: _ -> {
+          // Only play the tick sound if the number increased.
+          if (clearPercentLerp != clearPercentCounter.curNumber)
+          {
+            clearPercentLerp = clearPercentCounter.curNumber;
+            FunkinSound.playOnce(Paths.sound('scrollMenu'));
+          }
+        },
+        onComplete: _ -> {
+          // 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;
+
+          clearPercentCounter.flash(true);
+          new FlxTimer().start(0.4, _ -> {
+            clearPercentCounter.flash(false);
+          });
+
+          displayRankText();
+
+          new FlxTimer().start(2.0, _ -> {
+            FlxTween.tween(clearPercentCounter, {alpha: 0}, 0.5,
+              {
+                startDelay: 0.5,
+                ease: FlxEase.quartOut,
+                onComplete: _ -> {
+                  remove(clearPercentCounter);
+                }
+              });
+
+            afterRankTallySequence();
+          });
+        }
+      });
+    clearPercentCounter.zIndex = 450;
+    add(clearPercentCounter);
+
+    if (ratingsPopin == null)
+    {
+      trace("Could not build ratingsPopin!");
+    }
+    else
+    {
+      // ratingsPopin.animation.play("idle");
+      // ratingsPopin.visible = true;
 
       ratingsPopin.animation.finishCallback = anim -> {
-        scorePopin.animation.play("score");
-        scorePopin.animation.finishCallback = anim -> {
-          score.visible = true;
-          score.animateNumbers();
-        };
-        scorePopin.visible = true;
+        // scorePopin.animation.play("score");
 
-        if (params.isNewHighscore)
+        // scorePopin.visible = true;
+
+        if (params.isNewHighscore ?? false)
         {
           highscoreNew.visible = true;
           highscoreNew.animation.play("new");
@@ -272,47 +418,128 @@ class ResultState extends MusicBeatSubState
           highscoreNew.visible = false;
         }
       };
+    }
 
-      switch (resultsVariation)
+    refresh();
+  }
+
+  function displayRankText():Void
+  {
+    var rankTextVert:FunkinSprite = FunkinSprite.create(FlxG.width - 64, 100, rank.getVerTextAsset());
+    rankTextVert.zIndex = 2000;
+    add(rankTextVert);
+
+    for (i in 0...10)
+    {
+      var rankTextBack:FunkinSprite = FunkinSprite.create(FlxG.width / 2 - 80, 50, rank.getHorTextAsset());
+      rankTextBack.y += (rankTextBack.height * i / 2) + 10;
+      rankTextBack.zIndex = 100;
+      add(rankTextBack);
+    }
+
+    refresh();
+  }
+
+  function afterRankTallySequence():Void
+  {
+    showSmallClearPercent();
+
+    FunkinSound.playMusic(rank.getMusicPath(),
       {
-        // case SHIT:
-        // bfSHIT.visible = true;
-        // bfSHIT.playAnimation("");
+        startingVolume: 1.0,
+        overrideExisting: true,
+        restartTrack: true,
+        loop: rank.shouldMusicLoop()
+      });
 
-        case NORMAL:
-          boyfriend.animation.play('fall');
-          boyfriend.visible = true;
-
-          new FlxTimer().start((1 / 24) * 12, _ -> {
-            bgFlash.visible = true;
-            FlxTween.tween(bgFlash, {alpha: 0}, 0.4);
-            new FlxTimer().start((1 / 24) * 2, _ ->
-              {
-                // bgFlash.alpha = 0.5;
-
-                // bgFlash.visible = false;
-              });
+    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:
+        if (bfPerfect == null)
+        {
+          trace("Could not build PERFECT animation!");
+        }
+        else
+        {
+          bfPerfect.visible = true;
+          bfPerfect.playAnimation('');
+        }
+
+      case EXCELLENT:
+        if (bfExcellent == null)
+        {
+          trace("Could not build EXCELLENT animation!");
+        }
+        else
+        {
+          bfExcellent.visible = true;
+          bfExcellent.playAnimation('Intro');
+        }
+
+      case GREAT:
+        if (bfGreat == null)
+        {
+          trace("Could not build GREAT animation!");
+        }
+        else
+        {
+          bfGreat.visible = true;
+          bfGreat.playAnimation('Intro');
+        }
+
+      case SHIT:
+        if (bfShit == null)
+        {
+          trace("Could not build SHIT animation!");
+        }
+        else
+        {
+          bfShit.visible = true;
+          bfShit.playAnimation('Intro');
+        }
+
+      case GOOD:
+        if (bfGood == null)
+        {
+          trace("Could not build GOOD animation!");
+        }
+        else
+        {
+          bfGood.animation.play('fall');
+          bfGood.visible = true;
 
           new FlxTimer().start((1 / 24) * 22, _ -> {
             // plays about 22 frames (at 24fps timing) after bf spawns in
-            gf.animation.play('clap', true);
-            gf.visible = true;
+            if (gfGood != null)
+            {
+              gfGood.animation.play('clap', true);
+              gfGood.visible = true;
+            }
+            else
+            {
+              trace("Could not build GOOD animation!");
+            }
           });
-        // case PERFECT:
-        //          bfPerfect.visible = true;
-        //          bfPerfect.playAnimation("");
-
-        // bfGfExcellent.visible = true;
-        // bfGfExcellent.playAnimation("");
-        default:
-      }
-    });
-
-    super.create();
+        }
+      default:
+    }
   }
 
-  function timerThenSongName():Void
+  function timerThenSongName(timerLength:Float = 3.0, autoScroll:Bool = true):Void
   {
     movingSongStuff = false;
 
@@ -323,21 +550,47 @@ class ResultState extends MusicBeatSubState
     difficulty.y = -difficulty.height;
     FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8});
 
+    if (clearPercentSmall != null)
+    {
+      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});
+    }
+
     songName.y = -songName.height;
     var fuckedupnumber = (10) * (songName.text.length / 15);
-    FlxTween.tween(songName, {y: diffYTween - 35 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9});
-    songName.x = (difficulty.x + difficulty.width) + 20;
+    FlxTween.tween(songName, {y: diffYTween - 25 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9});
+    songName.x = clearPercentSmall.x + clearPercentSmall.width - 30;
 
-    new FlxTimer().start(3, _ -> {
+    new FlxTimer().start(timerLength, _ -> {
       var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y);
 
       speedOfTween.set(0, 0);
       FlxTween.tween(speedOfTween, {x: tempSpeed.x, y: tempSpeed.y}, 0.7, {ease: FlxEase.quadIn});
 
-      movingSongStuff = true;
+      movingSongStuff = (autoScroll);
     });
   }
 
+  function showSmallClearPercent():Void
+  {
+    if (clearPercentSmall != null)
+    {
+      add(clearPercentSmall);
+      clearPercentSmall.visible = true;
+      clearPercentSmall.flash(true);
+      new FlxTimer().start(0.4, _ -> {
+        clearPercentSmall.flash(false);
+      });
+
+      clearPercentSmall.curNumber = clearPercentTarget;
+      clearPercentSmall.zIndex = 1000;
+      refresh();
+    }
+
+    movingSongStuff = true;
+  }
+
   var movingSongStuff:Bool = false;
   var speedOfTween:FlxPoint = FlxPoint.get(-1, 1);
 
@@ -345,11 +598,9 @@ class ResultState extends MusicBeatSubState
   {
     super.draw();
 
-    if (songName != null)
-    {
-      songName.clipRect = FlxRect.get(Math.max(0, 540 - songName.x), 0, FlxG.width, songName.height);
-      // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
-    }
+    songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height);
+
+    // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
 
     // if (songName != null && songName.frame != null)
     // maskShaderSongName.frameUV = songName.frame.uv;
@@ -364,8 +615,10 @@ class ResultState extends MusicBeatSubState
     {
       songName.x += speedOfTween.x;
       difficulty.x += speedOfTween.x;
+      clearPercentSmall.x += speedOfTween.x;
       songName.y += speedOfTween.y;
       difficulty.y += speedOfTween.y;
+      clearPercentSmall.y += speedOfTween.y;
 
       if (songName.x + songName.width < 100)
       {
@@ -401,14 +654,135 @@ class ResultState extends MusicBeatSubState
 
     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 ResultVariations(String)
+enum abstract ResultRank(String)
 {
+  var PERFECT_GOLD;
   var PERFECT;
   var EXCELLENT;
-  var NORMAL;
+  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 =
@@ -426,10 +800,21 @@ typedef ResultsStateParams =
   /**
    * Whether the displayed score is a new highscore
    */
-  var isNewHighscore:Bool;
+  var ?isNewHighscore:Bool;
+
+  /**
+   * The difficulty ID of the song/week we just played.
+   * @default Normal
+   */
+  var ?difficultyId:String;
 
   /**
    * The score, accuracy, and judgements.
    */
   var scoreData:SaveScoreData;
+
+  /**
+   * The previous score data, used for rank comparision.
+   */
+  var ?prevScoreData:SaveScoreData;
 };
diff --git a/source/funkin/play/components/ClearPercentCounter.hx b/source/funkin/play/components/ClearPercentCounter.hx
new file mode 100644
index 000000000..d296b0b0b
--- /dev/null
+++ b/source/funkin/play/components/ClearPercentCounter.hx
@@ -0,0 +1,137 @@
+package funkin.play.components;
+
+import funkin.graphics.FunkinSprite;
+import funkin.graphics.shaders.PureColor;
+import flixel.FlxSprite;
+import flixel.group.FlxGroup.FlxTypedGroup;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
+import flixel.math.FlxMath;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import flixel.text.FlxText.FlxTextAlign;
+import funkin.util.MathUtil;
+import flixel.util.FlxColor;
+
+/**
+ * Numerical counters used to display the clear percent.
+ */
+class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
+{
+  public var curNumber(default, set):Int = 0;
+
+  var numberChanged:Bool = false;
+
+  function set_curNumber(val:Int):Int
+  {
+    numberChanged = true;
+    return curNumber = val;
+  }
+
+  var small:Bool = false;
+  var flashShader:PureColor;
+
+  public function new(x:Float, y:Float, startingNumber:Int = 0, small:Bool = false)
+  {
+    super(x, y);
+
+    flashShader = new PureColor(FlxColor.WHITE);
+    flashShader.colorSet = true;
+
+    curNumber = startingNumber;
+
+    this.small = small;
+
+    var clearPercentText:FunkinSprite = FunkinSprite.create(0, 0, 'resultScreen/clearPercent/clearPercentText${small ? 'Small' : ''}');
+    clearPercentText.x = small ? 40 : 0;
+    add(clearPercentText);
+
+    drawNumbers();
+  }
+
+  /**
+   * Make the counter flash turn white or stop being all white.
+   * @param enabled Whether the counter should be white.
+   */
+  public function flash(enabled:Bool):Void
+  {
+    for (member in members)
+    {
+      member.shader = enabled ? flashShader : null;
+    }
+  }
+
+  var tmr:Float = 0;
+
+  override function update(elapsed:Float)
+  {
+    super.update(elapsed);
+
+    if (numberChanged) drawNumbers();
+  }
+
+  function drawNumbers()
+  {
+    var seperatedScore:Array<Int> = [];
+    var tempCombo:Int = Math.round(curNumber);
+
+    while (tempCombo != 0)
+    {
+      seperatedScore.push(tempCombo % 10);
+      tempCombo = Math.floor(tempCombo / 10);
+    }
+
+    if (seperatedScore.length == 0) seperatedScore.push(0);
+
+    seperatedScore.reverse();
+
+    for (ind => num in seperatedScore)
+    {
+      var digitIndex = 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;
+      var digitSize = small ? 32 : 72;
+      var digitHeightOffset = small ? -4 : 0;
+
+      var xPos = (digitIndex - 1 + digitOffset) * (digitSize * this.scale.x);
+      xPos += small ? -24 : 0;
+      var yPos = (digitIndex - 1 + digitOffset) * (digitHeightOffset * this.scale.y);
+      yPos += small ? 0 : 72;
+
+      if (digitIndex >= members.length)
+      {
+        // Three digits = LLR because the 1 and 0 won't be the same anyway.
+        var variant:Bool = (seperatedScore.length == 3) ? (digitIndex >= 2) : (digitIndex >= 1);
+        // 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);
+        add(numb);
+      }
+      else
+      {
+        members[digitIndex].animation.play(Std.string(num));
+        // Reset the position of the number
+        members[digitIndex].x = xPos + this.x;
+        members[digitIndex].y = yPos + this.y;
+      }
+    }
+  }
+}
+
+class ClearPercentNumber extends FlxSprite
+{
+  public function new(x:Float, y:Float, digit:Int, variant:Bool, small:Bool)
+  {
+    super(x, y);
+
+    frames = Paths.getSparrowAtlas('resultScreen/clearPercent/clearPercentNumber${small ? 'Small' : variant ? 'Right' : 'Left'}');
+
+    for (i in 0...10)
+    {
+      animation.addByPrefix('$i', 'number $i 0', 24, false);
+    }
+
+    animation.play('$digit');
+    updateHitbox();
+  }
+}
diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 957daa43c..2d7099e8a 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -24,7 +24,7 @@ import funkin.util.MathUtil;
  *     - i.e. `PlayState.instance.iconP1.playAnimation("losing")`
  *   - Scripts can also utilize all functionality that a normal FlxSprite would have access to, such as adding supplimental animations.
  *     - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);`
- * @author MasterEric
+ * @author EliteMasterEric
  */
 @:nullSafety
 class HealthIcon extends FunkinSprite
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 95e0668be..07d4ab69b 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -406,7 +406,7 @@ class Strumline extends FlxSpriteGroup
 
         if (Preferences.downscroll)
         {
-          holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
+          holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
         }
         else
         {
@@ -435,7 +435,7 @@ class Strumline extends FlxSpriteGroup
 
         if (Preferences.downscroll)
         {
-          holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
+          holdNote.y = this.y - INITIAL_OFFSET - holdNote.height + STRUMLINE_SIZE / 2;
         }
         else
         {
@@ -450,7 +450,7 @@ class Strumline extends FlxSpriteGroup
 
         if (Preferences.downscroll)
         {
-          holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
+          holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
         }
         else
         {
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index e71ae3213..53408fb34 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -399,6 +399,27 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return null;
   }
 
+  /**
+   * Given that this character is selected in the Freeplay menu,
+   * which variations should be available?
+   * @param charId The character ID to query.
+   * @return An array of available variations.
+   */
+  public function getVariationsByCharId(?charId:String):Array<String>
+  {
+    if (charId == null) charId = Constants.DEFAULT_CHARACTER;
+
+    if (variations.contains(charId))
+    {
+      return [charId];
+    }
+    else
+    {
+      // TODO: How to exclude character variations while keeping other custom variations?
+      return variations;
+    }
+  }
+
   /**
    * List all the difficulties in this song.
    *
@@ -418,12 +439,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     // so we have to map it to the actual difficulty names.
     // We also filter out difficulties that don't match the variation or that don't exist.
 
-    var diffFiltered:Array<String> = difficulties.keys().array().map(function(diffId:String):Null<String> {
-      var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
-      if (difficulty == null) return null;
-      if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
-      return difficulty.difficulty;
-    }).nonNull().unique();
+    var diffFiltered:Array<String> = difficulties.keys()
+      .array()
+      .map(function(diffId:String):Null<String> {
+        var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
+        if (difficulty == null) return null;
+        if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
+        return difficulty.difficulty;
+      })
+      .filterNull()
+      .distinct();
 
     diffFiltered = diffFiltered.filter(function(diffId:String):Bool {
       if (showHidden) return true;
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index acbe59edd..934d6a4aa 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -14,8 +14,7 @@ import funkin.util.SerializerUtil;
 @:nullSafety
 class Save
 {
-  // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
-  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3";
+  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.4";
   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.
@@ -53,7 +52,8 @@ class Save
   public function new(?data:RawSaveData)
   {
     if (data == null) this.data = Save.getDefault();
-    else this.data = data;
+    else
+      this.data = data;
   }
 
   public static function getDefault():RawSaveData
@@ -77,6 +77,9 @@ class Save
           levels: [],
           songs: [],
         },
+
+      favoriteSongs: [],
+
       options:
         {
           // Reasonable defaults.
@@ -554,6 +557,35 @@ class Save
     return false;
   }
 
+  public function isSongFavorited(id:String):Bool
+  {
+    if (data.favoriteSongs == null)
+    {
+      data.favoriteSongs = [];
+      flush();
+    };
+
+    return data.favoriteSongs.contains(id);
+  }
+
+  public function favoriteSong(id:String):Void
+  {
+    if (!isSongFavorited(id))
+    {
+      data.favoriteSongs.push(id);
+      flush();
+    }
+  }
+
+  public function unfavoriteSong(id:String):Void
+  {
+    if (isSongFavorited(id))
+    {
+      data.favoriteSongs.remove(id);
+      flush();
+    }
+  }
+
   public function getControls(playerId:Int, inputType:Device):Null<SaveControlsData>
   {
     switch (inputType)
@@ -714,6 +746,7 @@ class Save
 
 /**
  * An anonymous structure containingg all the user's save data.
+ * Isn't stored with JSON, stored with some sort of Haxe built-in serialization?
  */
 typedef RawSaveData =
 {
@@ -724,8 +757,6 @@ typedef RawSaveData =
   /**
    * A semantic versioning string for the save data format.
    */
-  @:jcustomparse(funkin.data.DataParse.semverVersion)
-  @:jcustomwrite(funkin.data.DataWrite.semverVersion)
   var version:Version;
 
   var api:SaveApiData;
@@ -740,6 +771,12 @@ typedef RawSaveData =
    */
   var options:SaveDataOptions;
 
+  /**
+   * The user's favorited songs in the Freeplay menu,
+   * as a list of song IDs.
+   */
+  var favoriteSongs:Array<String>;
+
   var mods:SaveDataMods;
 
   /**
@@ -809,11 +846,6 @@ typedef SaveScoreData =
    * The count of each judgement hit.
    */
   var tallies:SaveScoreTallyData;
-
-  /**
-   * The accuracy percentage.
-   */
-  var accuracy:Float;
 }
 
 typedef SaveScoreTallyData =
diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md
index 3fa9839d1..7c9094f2d 100644
--- a/source/funkin/save/changelog.md
+++ b/source/funkin/save/changelog.md
@@ -5,6 +5,9 @@ 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.4] - 2024-05-21
+### Added
+- `favoriteSongs:Array<String>` to `Save`
 
 ## [2.0.3] - 2024-01-09
 ### Added
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 3ed59e726..4fa9dd6b3 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -3,7 +3,6 @@ package funkin.save.migrator;
 import funkin.save.Save;
 import funkin.save.migrator.RawSaveData_v1_0_0;
 import thx.semver.Version;
-import funkin.util.StructureUtil;
 import funkin.util.VersionUtil;
 
 @:nullSafety
@@ -24,16 +23,20 @@ class SaveDataMigrator
     }
     else
     {
+      // Sometimes the Haxe serializer has issues with the version so we fix it here.
+      version = VersionUtil.repairVersion(version);
       if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
       {
-        // Simply import the structured data.
-        var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData));
+        // Import the structured data.
+        var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
+        var save:Save = new Save(saveDataWithDefaults);
         return save;
       }
       else
       {
-        trace('[SAVE] Invalid save data version! Returning blank data.');
-        trace(inputData);
+        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);
         return new Save(Save.getDefault());
       }
     }
@@ -118,7 +121,7 @@ class SaveDataMigrator
     var scoreDataEasy:SaveScoreData =
       {
         score: inputSaveData.songScores.get('${levelId}-easy') ?? 0,
-        accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
+        // accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
         tallies:
           {
             sick: 0,
@@ -137,7 +140,7 @@ class SaveDataMigrator
     var scoreDataNormal:SaveScoreData =
       {
         score: inputSaveData.songScores.get('${levelId}') ?? 0,
-        accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
+        // accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
         tallies:
           {
             sick: 0,
@@ -156,7 +159,7 @@ class SaveDataMigrator
     var scoreDataHard:SaveScoreData =
       {
         score: inputSaveData.songScores.get('${levelId}-hard') ?? 0,
-        accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
+        // accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
         tallies:
           {
             sick: 0,
@@ -178,7 +181,6 @@ class SaveDataMigrator
     var scoreDataEasy:SaveScoreData =
       {
         score: 0,
-        accuracy: 0,
         tallies:
           {
             sick: 0,
@@ -196,14 +198,13 @@ class SaveDataMigrator
     for (songId in songIds)
     {
       scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0));
-      scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
+      // scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
     }
     result.setSongScore(songIds[0], 'easy', scoreDataEasy);
 
     var scoreDataNormal:SaveScoreData =
       {
         score: 0,
-        accuracy: 0,
         tallies:
           {
             sick: 0,
@@ -221,14 +222,13 @@ class SaveDataMigrator
     for (songId in songIds)
     {
       scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0));
-      scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
+      // scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
     }
     result.setSongScore(songIds[0], 'normal', scoreDataNormal);
 
     var scoreDataHard:SaveScoreData =
       {
         score: 0,
-        accuracy: 0,
         tallies:
           {
             sick: 0,
@@ -246,7 +246,7 @@ class SaveDataMigrator
     for (songId in songIds)
     {
       scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0));
-      scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
+      // scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
     }
     result.setSongScore(songIds[0], 'hard', scoreDataHard);
   }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index d426abaaf..219412fd9 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -137,7 +137,7 @@ using Lambda;
  *
  * Some functionality is split into handler classes to help maintain my sanity.
  *
- * @author MasterEric
+ * @author EliteMasterEric
  */
 // @:nullSafety
 
diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx
index 35facf131..50f4a432c 100644
--- a/source/funkin/ui/freeplay/AlbumRoll.hx
+++ b/source/funkin/ui/freeplay/AlbumRoll.hx
@@ -38,7 +38,7 @@ class AlbumRoll extends FlxSpriteGroup
 
   var newAlbumArt:FlxAtlasSprite;
 
-  // var difficultyStars:DifficultyStars;
+  var difficultyStars:DifficultyStars;
   var _exitMovers:Null<FreeplayState.ExitMoverData>;
 
   var albumData:Album;
@@ -65,9 +65,9 @@ class AlbumRoll extends FlxSpriteGroup
 
     add(newAlbumArt);
 
-    // difficultyStars = new DifficultyStars(140, 39);
-    // difficultyStars.stars.visible = false;
-    // add(difficultyStars);
+    difficultyStars = new DifficultyStars(140, 39);
+    difficultyStars.stars.visible = false;
+    add(difficultyStars);
   }
 
   function onAlbumFinish(animName:String):Void
@@ -86,9 +86,14 @@ class AlbumRoll extends FlxSpriteGroup
   {
     if (albumId == null)
     {
-      // difficultyStars.stars.visible = false;
+      this.visible = false;
+      difficultyStars.stars.visible = false;
       return;
     }
+    else
+    {
+      this.visible = true;
+    }
 
     albumData = AlbumRegistry.instance.fetchEntry(albumId);
 
@@ -144,10 +149,10 @@ class AlbumRoll extends FlxSpriteGroup
     newAlbumArt.visible = true;
     newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
 
-    // difficultyStars.stars.visible = false;
+    difficultyStars.stars.visible = false;
     new FlxTimer().start(0.75, function(_) {
       // showTitle();
-      // showStars();
+      showStars();
     });
   }
 
@@ -156,16 +161,17 @@ class AlbumRoll extends FlxSpriteGroup
     newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
   }
 
-  // public function setDifficultyStars(?difficulty:Int):Void
-  // {
-  //   if (difficulty == null) return;
-  //   difficultyStars.difficulty = difficulty;
-  // }
-  // /**
-  //  * Make the album stars visible.
-  //  */
-  // public function showStars():Void
-  // {
-  //   difficultyStars.stars.visible = false; // true;
-  // }
+  public function setDifficultyStars(?difficulty:Int):Void
+  {
+    if (difficulty == null) return;
+    difficultyStars.difficulty = difficulty;
+  }
+
+  /**
+   * Make the album stars visible.
+   */
+  public function showStars():Void
+  {
+    difficultyStars.stars.visible = true; // true;
+  }
 }
diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx
new file mode 100644
index 000000000..51526bcbe
--- /dev/null
+++ b/source/funkin/ui/freeplay/DifficultyStars.hx
@@ -0,0 +1,106 @@
+package funkin.ui.freeplay;
+
+import flixel.group.FlxSpriteGroup;
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
+import funkin.graphics.shaders.HSVShader;
+
+class DifficultyStars extends FlxSpriteGroup
+{
+  /**
+   * Internal handler var for difficulty... ranges from 0... to 15
+   * 0 is 1 star... 15 is 0 stars!
+   */
+  var curDifficulty(default, set):Int = 0;
+
+  /**
+   * Range between 0 and 15
+   */
+  public var difficulty(default, set):Int = 1;
+
+  public var stars:FlxAtlasSprite;
+
+  var flames:FreeplayFlames;
+
+  var hsvShader:HSVShader;
+
+  public function new(x:Float, y:Float)
+  {
+    super(x, y);
+
+    hsvShader = new HSVShader();
+
+    flames = new FreeplayFlames(0, 0);
+    add(flames);
+
+    stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars"));
+    stars.anim.play("diff stars");
+    add(stars);
+
+    stars.shader = hsvShader;
+
+    for (memb in flames.members)
+      memb.shader = hsvShader;
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // "loops" the current animation
+    // for clarity, the animation file looks like
+    // frame : stars
+    // 0-99: 1 star
+    // 100-199: 2 stars
+    // ......
+    // 1300-1499: 15 stars
+    // 1500 : 0 stars
+    if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100)
+    {
+      stars.anim.play("diff stars", true, false, curDifficulty * 100);
+    }
+  }
+
+  function set_difficulty(value:Int):Int
+  {
+    difficulty = value;
+
+    if (difficulty <= 0)
+    {
+      difficulty = 0;
+      curDifficulty = 15;
+    }
+    else if (difficulty <= 15)
+    {
+      difficulty = value;
+      curDifficulty = difficulty - 1;
+    }
+    else
+    {
+      difficulty = 15;
+      curDifficulty = difficulty - 1;
+    }
+
+    if (difficulty > 10) flames.flameCount = difficulty - 10;
+    else
+      flames.flameCount = 0;
+
+    return difficulty;
+  }
+
+  function set_curDifficulty(value:Int):Int
+  {
+    curDifficulty = value;
+    if (curDifficulty == 15)
+    {
+      stars.anim.play("diff stars", true, false, 1500);
+      stars.anim.pause();
+    }
+    else
+    {
+      stars.anim.curFrame = Std.int(curDifficulty * 100);
+      stars.anim.play("diff stars", true, false, curDifficulty * 100);
+    }
+
+    return curDifficulty;
+  }
+}
diff --git a/source/funkin/ui/freeplay/FreeplayFlames.hx b/source/funkin/ui/freeplay/FreeplayFlames.hx
index c20d85898..f6b6f5c3d 100644
--- a/source/funkin/ui/freeplay/FreeplayFlames.hx
+++ b/source/funkin/ui/freeplay/FreeplayFlames.hx
@@ -50,8 +50,19 @@ class FreeplayFlames extends FlxSpriteGroup
     }
   }
 
+  var timers:Array<FlxTimer> = [];
+
   function set_flameCount(value:Int):Int
   {
+    // Stop all existing timers.
+    // This fixes a bug where quickly switching difficulties would show flames.
+    for (timer in timers)
+    {
+      timer.active = false;
+      timer.destroy();
+      timers.remove(timer);
+    }
+
     this.flameCount = value;
     var visibleCount:Int = 0;
     for (i in 0...5)
@@ -62,10 +73,18 @@ class FreeplayFlames extends FlxSpriteGroup
       {
         if (!flame.visible)
         {
-          new FlxTimer().start(flameTimer * visibleCount, function(_) {
+          var nextTimer:FlxTimer = new FlxTimer().start(flameTimer * visibleCount, function(currentTimer:FlxTimer) {
+            if (i >= this.flameCount)
+            {
+              trace('EARLY EXIT');
+              return;
+            }
+            timers.remove(currentTimer);
             flame.animation.play("flame", true);
             flame.visible = true;
           });
+          timers.push(nextTimer);
+
           visibleCount++;
         }
       }
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 1c7926f62..c02199dcf 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -120,8 +120,6 @@ class FreeplayState extends MusicBeatSubState
   var curCapsule:SongMenuItem;
   var curPlaying:Bool = false;
 
-  var displayedVariations:Array<String>;
-
   var dj:DJBoyfriend;
 
   var ostName:FlxText;
@@ -184,10 +182,6 @@ class FreeplayState extends MusicBeatSubState
     // Add a null entry that represents the RANDOM option
     songs.push(null);
 
-    // TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later.
-    // Default character (BF) shows default and Erect variations. Pico shows only Pico variations.
-    displayedVariations = (currentCharacter == 'bf') ? [Constants.DEFAULT_VARIATION, 'erect'] : [currentCharacter];
-
     // programmatically adds the songs via LevelRegistry and SongRegistry
     for (levelId in LevelRegistry.instance.listSortedLevelIds())
     {
@@ -195,7 +189,8 @@ class FreeplayState extends MusicBeatSubState
       {
         var song:Song = SongRegistry.instance.fetchEntry(songId);
 
-        // Only display songs which actually have available charts for the current character.
+        // 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);
         if (availableDifficultiesForSong.length == 0) continue;
 
@@ -488,10 +483,6 @@ class FreeplayState extends MusicBeatSubState
 
       albumRoll.playIntro();
 
-      new FlxTimer().start(0.75, function(_) {
-        // albumRoll.showTitle();
-      });
-
       FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
 
       diffSelLeft.visible = true;
@@ -708,8 +699,8 @@ class FreeplayState extends MusicBeatSubState
       if (targetSong != null)
       {
         var realShit:Int = curSelected;
-        targetSong.isFav = !targetSong.isFav;
-        if (targetSong.isFav)
+        var isFav = targetSong.toggleFavorite();
+        if (isFav)
         {
           FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
             {
@@ -1021,7 +1012,7 @@ class FreeplayState extends MusicBeatSubState
     {
       var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty);
       intendedScore = songScore?.score ?? 0;
-      intendedCompletion = songScore?.accuracy ?? 0.0;
+      intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
       rememberedDifficulty = currentDifficulty;
     }
     else
@@ -1086,6 +1077,9 @@ class FreeplayState extends MusicBeatSubState
       albumRoll.albumId = newAlbumId;
       albumRoll.skipIntro();
     }
+
+    // Set difficulty star count.
+    albumRoll.setDifficultyStars(daSong?.difficultyRating);
   }
 
   // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
@@ -1216,7 +1210,7 @@ class FreeplayState extends MusicBeatSubState
     {
       var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
       intendedScore = songScore?.score ?? 0;
-      intendedCompletion = songScore?.accuracy ?? 0.0;
+      intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
       diffIdsCurrent = daSongCapsule.songData.songDifficulties;
       rememberedSongId = daSongCapsule.songData.songId;
       changeDiff();
@@ -1397,11 +1391,12 @@ class FreeplaySongData
 
   public var songName(default, null):String = '';
   public var songCharacter(default, null):String = '';
-  public var songRating(default, null):Int = 0;
+  public var difficultyRating(default, null):Int = 0;
   public var albumId(default, null):Null<String> = null;
 
   public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
-  public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
+
+  var displayedVariations:Array<String> = [Constants.DEFAULT_VARIATION];
 
   function set_currentDifficulty(value:String):String
   {
@@ -1417,11 +1412,32 @@ class FreeplaySongData
     this.levelId = levelId;
     this.songId = songId;
     this.song = song;
+
+    this.isFav = Save.instance.isSongFavorited(songId);
+
     if (displayedVariations != null) this.displayedVariations = displayedVariations;
 
     updateValues(displayedVariations);
   }
 
+  /**
+   * Toggle whether or not the song is favorited, then flush to save data.
+   * @return Whether or not the song is now favorited.
+   */
+  public function toggleFavorite():Bool
+  {
+    isFav = !isFav;
+    if (isFav)
+    {
+      Save.instance.favoriteSong(this.songId);
+    }
+    else
+    {
+      Save.instance.unfavoriteSong(this.songId);
+    }
+    return isFav;
+  }
+
   function updateValues(variations:Array<String>):Void
   {
     this.songDifficulties = song.listDifficulties(variations, false, false);
@@ -1431,7 +1447,7 @@ class FreeplaySongData
     if (songDifficulty == null) return;
     this.songName = songDifficulty.songName;
     this.songCharacter = songDifficulty.characters.opponent;
-    this.songRating = songDifficulty.difficultyRating;
+    this.difficultyRating = songDifficulty.difficultyRating;
     if (songDifficulty.album == null)
     {
       FlxG.log.warn('No album for: ${songDifficulty.songName}');
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index f6d85e56e..cf9b52482 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -168,7 +168,7 @@ class SongMenuItem extends FlxSpriteGroup
     songText.text = songData?.songName ?? 'Random';
     // Update capsule character.
     if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
-    updateDifficultyRating(songData?.songRating ?? 0);
+    updateDifficultyRating(songData?.difficultyRating ?? 0);
     // Update opacity, offsets, etc.
     updateSelected();
   }
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index 7a21a6e8f..fc2a8c7d7 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -351,8 +351,7 @@ class MainMenuState extends MusicBeatState
               maxCombo: 0,
               totalNotesHit: 0,
               totalNotes: 0,
-            },
-          accuracy: 0,
+            }
         });
     }
     #end
diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx
index ffc756e1c..5a3efc36a 100644
--- a/source/funkin/ui/story/LevelProp.hx
+++ b/source/funkin/ui/story/LevelProp.hx
@@ -13,11 +13,10 @@ class LevelProp extends Bopper
     // Only reset the prop if the asset path has changed.
     if (propData == null || value?.assetPath != propData?.assetPath)
     {
-      this.visible = (value != null);
-      this.propData = value;
-      danceEvery = this.propData?.danceEvery ?? 0;
       applyData();
     }
+    this.visible = (value != null);
+    danceEvery = this.propData?.danceEvery ?? 0;
 
     return this.propData;
   }
diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index 95c378b24..bc26ad97a 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -57,8 +57,7 @@ class LoadingState extends MusicBeatSubState
     funkay.scrollFactor.set();
     funkay.screenCenter();
 
-    loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2);
-    loadBar.screenCenter(X);
+    loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(0, 10, 0xFFff16d2);
     add(loadBar);
 
     initSongsManifest().onComplete(function(lib) {
@@ -163,8 +162,15 @@ class LoadingState extends MusicBeatSubState
       targetShit = FlxMath.remapToRange(callbacks.numRemaining / callbacks.length, 1, 0, 0, 1);
 
       var lerpWidth:Int = Std.int(FlxMath.lerp(loadBar.width, FlxG.width * targetShit, 0.2));
-      loadBar.setGraphicSize(lerpWidth, loadBar.height);
-      loadBar.updateHitbox();
+      // this if-check prevents the setGraphicSize function
+      // from setting the width of the loadBar to the height of the loadBar
+      // this is a behaviour that is implemented in the setGraphicSize function
+      // if the width parameter is equal to 0
+      if (lerpWidth > 0)
+      {
+        loadBar.setGraphicSize(lerpWidth, loadBar.height);
+        loadBar.updateHitbox();
+      }
       FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length);
     }
 
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index c50f17697..2f3b570b3 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -455,6 +455,17 @@ class Constants
   public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true;
   public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true;
 
+  // % Sick
+  public static final RANK_PERFECT_PLAT_THRESHOLD:Float = 1.0; // % Sick
+  public static final RANK_PERFECT_GOLD_THRESHOLD:Float = 0.85; // % Sick
+
+  // % 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_GOOD_THRESHOLD:Float = 0.60;
+
+  // public static final RANK_SHIT_THRESHOLD:Float = 0.00;
   /**
    * FILE EXTENSIONS
    */
diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx
deleted file mode 100644
index 2f0c3818a..000000000
--- a/source/funkin/util/StructureUtil.hx
+++ /dev/null
@@ -1,136 +0,0 @@
-package funkin.util;
-
-import funkin.util.tools.MapTools;
-import haxe.DynamicAccess;
-
-/**
- * Utilities for working with anonymous structures.
- */
-class StructureUtil
-{
-  /**
-   * Merge two structures, with the second overwriting the first.
-   * Performs a SHALLOW clone, where child structures are not merged.
-   * @param a The base structure.
-   * @param b The new structure.
-   * @return The merged structure.
-   */
-  public static function merge(a:Dynamic, b:Dynamic):Dynamic
-  {
-    var result:DynamicAccess<Dynamic> = Reflect.copy(a);
-
-    for (field in Reflect.fields(b))
-    {
-      result.set(field, Reflect.field(b, field));
-    }
-
-    return result;
-  }
-
-  public static function toMap(a:Dynamic):haxe.ds.Map<String, Dynamic>
-  {
-    var result:haxe.ds.Map<String, Dynamic> = [];
-
-    for (field in Reflect.fields(a))
-    {
-      result.set(field, Reflect.field(a, field));
-    }
-
-    return result;
-  }
-
-  public static function isMap(a:Dynamic):Bool
-  {
-    return Std.isOfType(a, haxe.Constraints.IMap);
-  }
-
-  public static function isObject(a:Dynamic):Bool
-  {
-    switch (Type.typeof(a))
-    {
-      case TObject:
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  public static function isPrimitive(a:Dynamic):Bool
-  {
-    switch (Type.typeof(a))
-    {
-      case TInt | TFloat | TBool:
-        return true;
-      case TClass(c):
-        return false;
-      case TEnum(e):
-        return false;
-      case TObject:
-        return false;
-      case TFunction:
-        return false;
-      case TNull:
-        return true;
-      case TUnknown:
-        return false;
-      default:
-        return false;
-    }
-  }
-
-  /**
-   * Merge two structures, with the second overwriting the first.
-   * Performs a DEEP clone, where child structures are also merged recursively.
-   * @param a The base structure.
-   * @param b The new structure.
-   * @return The merged structure.
-   */
-  public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic
-  {
-    if (a == null) return b;
-    if (b == null) return null;
-    if (isPrimitive(a) && isPrimitive(b)) return b;
-    if (isMap(b))
-    {
-      if (isMap(a))
-      {
-        return MapTools.merge(a, b);
-      }
-      else
-      {
-        return StructureUtil.toMap(a).merge(b);
-      }
-    }
-    if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b;
-    if (Std.isOfType(b, haxe.ds.StringMap))
-    {
-      if (Std.isOfType(a, haxe.ds.StringMap))
-      {
-        return MapTools.merge(a, b);
-      }
-      else
-      {
-        return StructureUtil.toMap(a).merge(b);
-      }
-    }
-
-    var result:DynamicAccess<Dynamic> = Reflect.copy(a);
-
-    for (field in Reflect.fields(b))
-    {
-      if (Reflect.isObject(b))
-      {
-        // Note that isObject also returns true for class instances,
-        // but we just assume that's not a problem here.
-        result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field)));
-      }
-      else
-      {
-        // If we're here, b[field] is a primitive.
-        result.set(field, Reflect.field(b, field));
-      }
-    }
-
-    return result;
-  }
-}
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index 247ba19db..18d7eafa6 100644
--- a/source/funkin/util/VersionUtil.hx
+++ b/source/funkin/util/VersionUtil.hx
@@ -32,6 +32,25 @@ class VersionUtil
     }
   }
 
+  public static function repairVersion(version:thx.semver.Version):thx.semver.Version
+  {
+    var versionData:thx.semver.Version.SemVer = version;
+
+    if (thx.Types.isAnonymousObject(versionData.version))
+    {
+      // This is bad! versionData.version should be an array!
+      versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]];
+
+      var fixedVersion:thx.semver.Version = versionData;
+      return fixedVersion;
+    }
+    else
+    {
+      // No need for repair.
+      return version;
+    }
+  }
+
   /**
    * Checks that a given verison number satisisfies a given version rule.
    * Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.
diff --git a/source/funkin/util/macro/InlineMacro.hx b/source/funkin/util/macro/InlineMacro.hx
index b0e7ed184..c40257409 100644
--- a/source/funkin/util/macro/InlineMacro.hx
+++ b/source/funkin/util/macro/InlineMacro.hx
@@ -23,7 +23,7 @@ class InlineMacro
     var fields:Array<haxe.macro.Expr.Field> = haxe.macro.Context.getBuildFields();
 
     // Find the field with the given name.
-    var targetField:Null<haxe.macro.Expr.Field> = fields.find(function(f) return f.name == field
+    var targetField:Null<haxe.macro.Expr.Field> = thx.Arrays.find(fields, function(f) return f.name == field
       && (MacroUtil.isFieldStatic(f) == isStatic));
 
     // If the field was not found, throw an error.
diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx
index caf8e8aab..0fe245e3a 100644
--- a/source/funkin/util/tools/ArrayTools.hx
+++ b/source/funkin/util/tools/ArrayTools.hx
@@ -5,72 +5,6 @@ package funkin.util.tools;
  */
 class ArrayTools
 {
-  /**
-   * Returns a copy of the array with all duplicate elements removed.
-   * @param array The array to remove duplicates from.
-   * @return A copy of the array with all duplicate elements removed.
-   */
-  public static function unique<T>(array:Array<T>):Array<T>
-  {
-    var result:Array<T> = [];
-    for (element in array)
-    {
-      if (!result.contains(element))
-      {
-        result.push(element);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Returns a copy of the array with all `null` elements removed.
-   * @param array The array to remove `null` elements from.
-   * @return A copy of the array with all `null` elements removed.
-   */
-  public static function nonNull<T>(array:Array<Null<T>>):Array<T>
-  {
-    var result:Array<T> = [];
-    for (element in array)
-    {
-      if (element != null)
-      {
-        result.push(element);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Return the first element of the array that satisfies the predicate, or null if none do.
-   * @param input The array to search
-   * @param predicate The predicate to call
-   * @return The result
-   */
-  public static function find<T>(input:Array<T>, predicate:T->Bool):Null<T>
-  {
-    for (element in input)
-    {
-      if (predicate(element)) return element;
-    }
-    return null;
-  }
-
-  /**
-   * Return the index of the first element of the array that satisfies the predicate, or `-1` if none do.
-   * @param input The array to search
-   * @param predicate The predicate to call
-   * @return The index of the result
-   */
-  public static function findIndex<T>(input:Array<T>, predicate:T->Bool):Int
-  {
-    for (index in 0...input.length)
-    {
-      if (predicate(input[index])) return index;
-    }
-    return -1;
-  }
-
   /*
    * Push an element to the array if it is not already present.
    * @param input The array to push to
diff --git a/tests/unit/assets/shared/images/arrows.png b/tests/unit/assets/shared/images/arrows.png
deleted file mode 100644
index a44368432..000000000
Binary files a/tests/unit/assets/shared/images/arrows.png and /dev/null differ
diff --git a/tests/unit/assets/shared/images/arrows.xml b/tests/unit/assets/shared/images/arrows.xml
deleted file mode 100644
index 96a73a388..000000000
--- a/tests/unit/assets/shared/images/arrows.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<TextureAtlas imagePath="arrows.png">
-	<SubTexture name="staticLeft0001" x="0" y="0" width="17" height="17" />
-	<SubTexture name="staticDown0001" x="17" y="0" width="17" height="17" />
-	<SubTexture name="staticUp0001" x="34" y="0" width="17" height="17" />
-	<SubTexture name="staticRight0001" x="51" y="0" width="17" height="17" />
-	<SubTexture name="noteLeft0001" x="0" y="17" width="17" height="17" />
-	<SubTexture name="noteDown0001" x="17" y="17" width="17" height="17" />
-	<SubTexture name="noteUp0001" x="34" y="17" width="17" height="17" />
-	<SubTexture name="noteRight0001" x="51" y="17" width="17" height="17" />
-	<SubTexture name="pressedLeft0001" x="0" y="17" width="17" height="17" />
-	<SubTexture name="pressedDown0001" x="17" y="17" width="17" height="17" />
-	<SubTexture name="pressedUp0001" x="34" y="17" width="17" height="17" />
-	<SubTexture name="pressedRight0001" x="51" y="17" width="17" height="17" />
-	<SubTexture name="pressedLeft0002" x="0" y="34" width="17" height="17" />
-	<SubTexture name="pressedDown0002" x="17" y="34" width="17" height="17" />
-	<SubTexture name="pressedUp0002" x="34" y="34" width="17" height="17" />
-	<SubTexture name="pressedRight0002" x="51" y="34" width="17" height="17" />
-	<SubTexture name="confirmLeft0001" x="0" y="51" width="17" height="17" />
-	<SubTexture name="confirmDown0001" x="17" y="51" width="17" height="17" />
-	<SubTexture name="confirmUp0001" x="34" y="51" width="17" height="17" />
-	<SubTexture name="confirmRight0001" x="51" y="51" width="17" height="17" />
-	<SubTexture name="confirmLeft0002" x="0" y="68" width="17" height="17" />
-	<SubTexture name="confirmDown0002" x="17" y="68" width="17" height="17" />
-	<SubTexture name="confirmUp0002" x="34" y="68" width="17" height="17" />
-	<SubTexture name="confirmRight0002" x="51" y="68" width="17" height="17" />
-</TextureAtlas>