From 92b84168e1007342997bf0f7ee4619c4ad09e25f Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 20 May 2024 02:56:57 +0200
Subject: [PATCH 01/47] Add camOther to fix zooms on pause and stickers

---
 source/funkin/play/PlayState.hx | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 43dd485cf..20b9d3661 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -491,9 +491,9 @@ class PlayState extends MusicBeatSubState
   public var camGame:FlxCamera;
 
   /**
-   * The camera which contains, and controls visibility of, a video cutscene.
+   * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
    */
-  public var camCutscene:FlxCamera;
+  public var camOther:FlxCamera;
 
   /**
    * The combo popups. Includes the real-time combo counter and the rating.
@@ -960,7 +960,7 @@ class PlayState extends MusicBeatSubState
 
           FlxTransitionableState.skipNextTransIn = true;
           FlxTransitionableState.skipNextTransOut = true;
-          pauseSubState.camera = camHUD;
+          pauseSubState.camera = camOther;
           openSubState(pauseSubState);
           // boyfriendPos.put(); // TODO: Why is this here?
         }
@@ -1501,12 +1501,12 @@ class PlayState extends MusicBeatSubState
     camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
     camHUD = new FlxCamera();
     camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
-    camCutscene = new FlxCamera();
-    camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera.
+    camOther = new FlxCamera();
+    camOther.bgColor.alpha = 0; // Show the game scene behind the camera.
 
     FlxG.cameras.reset(camGame);
     FlxG.cameras.add(camHUD, false);
-    FlxG.cameras.add(camCutscene, false);
+    FlxG.cameras.add(camOther, false);
 
     // Configure camera follow point.
     if (previousCameraFollowPoint != null)
@@ -1900,7 +1900,6 @@ class PlayState extends MusicBeatSubState
     if (!result) return;
 
     isInCutscene = false;
-    camCutscene.visible = false;
 
     // TODO: Maybe tween in the camera after any cutscenes.
     camHUD.visible = true;
@@ -1919,7 +1918,7 @@ class PlayState extends MusicBeatSubState
     if (!currentConversation.alive) currentConversation.revive();
 
     currentConversation.completeCallback = onConversationComplete;
-    currentConversation.cameras = [camCutscene];
+    currentConversation.cameras = [camOther];
     currentConversation.zIndex = 1000;
     add(currentConversation);
     refresh();
@@ -2751,7 +2750,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camCutscene;
+        pauseSubState.camera = camOther;
         openSubState(pauseSubState);
       }
     }
@@ -2767,7 +2766,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camCutscene;
+        pauseSubState.camera = camOther;
         openSubState(pauseSubState);
       }
     }

From 0a6f1abd33d8cb474dcd8efe81380e5736538fc8 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 20 May 2024 23:52:45 +0200
Subject: [PATCH 02/47] Fix references to camCutscene

---
 source/funkin/play/cutscene/VideoCutscene.hx | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 01a492a77..2177114aa 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -81,12 +81,11 @@ class VideoCutscene
     // Trigger the cutscene. Don't play the song in the background.
     PlayState.instance.isInCutscene = true;
     PlayState.instance.camHUD.visible = false;
-    PlayState.instance.camCutscene.visible = true;
 
     // Display a black screen to hide the game while the video is playing.
     blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
     blackScreen.scrollFactor.set(0, 0);
-    blackScreen.cameras = [PlayState.instance.camCutscene];
+    blackScreen.cameras = [PlayState.instance.camOther];
     PlayState.instance.add(blackScreen);
 
     VideoCutscene.cutsceneType = cutsceneType;
@@ -120,7 +119,7 @@ class VideoCutscene
 
       vid.finishCallback = finishVideo.bind(0.5);
 
-      vid.cameras = [PlayState.instance.camCutscene];
+      vid.cameras = [PlayState.instance.camOther];
 
       PlayState.instance.add(vid);
 
@@ -147,7 +146,7 @@ class VideoCutscene
       vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
       vid.autoPause = false;
 
-      vid.cameras = [PlayState.instance.camCutscene];
+      vid.cameras = [PlayState.instance.camOther];
 
       PlayState.instance.add(vid);
 
@@ -305,7 +304,6 @@ class VideoCutscene
     vid = null;
     #end
 
-    PlayState.instance.camCutscene.visible = true;
     PlayState.instance.camHUD.visible = true;
 
     FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,

From 47cbd4b62038cbd07079b7c1d6ee88c73d54d724 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 21 May 2024 19:11:10 -0400
Subject: [PATCH 03/47] Update compiling guide with more troubleshooting

---
 docs/COMPILING.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 61 insertions(+), 1 deletion(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index 07df6367f..b08fc528e 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -21,4 +21,64 @@
 
 # Troubleshooting
 
-- During the cloning process, you may experience an error along the lines of `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)` due to poor connectivity. A common fix is to run ` git config --global http.postBuffer 4096M`.
+While performing the process of compilation, you may experience one of the following issues:
+
+## PolymodHandler: extra field coreAssetRedirect
+
+```
+Installing funkin.vis from https://github.com/FunkinCrew/funkVis branch: 98c9db09f0bbfedfe67a84538a5814aaef80bdea
+Error: std@sys_remove_dir
+Execution error: command "haxelib --never git funkin.vis https://github.com/FunkinCrew/funkVis 98c9db09f0bbfedfe67a84538a5814aaef80bdea" failed with status: 1 in cwd
+```
+
+If you receive this error, you are on an outdated version of Polymod.
+
+To solve, you should try reinstalling Polymod:
+
+```
+haxelib run hmm reinstall --force polymod
+```
+
+You can also try deleting your `.haxelib` folder in your Funkin' project, then reinstalling all your Haxelibs to prevent any other errors:
+
+```
+rm -rf ./.haxelib
+haxelib run hmm reinstall --force
+```
+
+## PolymodHandler: Couldn't find a match for this asset library: (vlc)
+
+```
+source/funkin/modding/PolymodErrorHandler.hx:84: [ERROR] Your Lime/OpenFL configuration is using custom asset libraries, and you provided frameworkParams in Polymod.init(), but we couldn't find a match for this asset library: (vlc)
+source/funkin/modding/PolymodHandler.hx:158: An error occurred! Failed when loading mods!
+source/funkin/util/logging/CrashHandler.hx:62: Error while handling crash: Null Object Reference
+```
+
+This error is specific to Linux targets. If you receive this error, you are on an outdated verison of hxCodec.
+
+To solve, you should try reinstalling hxCodec:
+
+```
+haxelib run hmm reinstall --force hxCodec
+```
+
+You can also try deleting your `.haxelib` folder in your Funkin' project, then reinstalling all your Haxelibs to prevent any other errors:
+
+```
+rm -rf ./.haxelib
+haxelib run hmm reinstall --force
+```
+
+## Git: stream 0 was not closed cleanly: PROTOCOL_ERROR
+
+```
+error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
+```
+
+If you receive this error while cloning, you may be experiencing issues with your network connection.
+
+To solve, you should try modifying your git configuration before cloning again:
+
+```
+git config --global http.postBuffer 4096M
+```

From ade4aeb3f7c49115591d69f739b41857886edbbc Mon Sep 17 00:00:00 2001
From: Karim Akra <144803230+KarimAkra@users.noreply.github.com>
Date: Sun, 9 Jun 2024 01:24:02 +0300
Subject: [PATCH 04/47] get Float instead of Int in cpp (Float is 64-bit in so
 it works easier than Int64)

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

diff --git a/source/funkin/util/MemoryUtil.hx b/source/funkin/util/MemoryUtil.hx
index f5935ed67..18fd41472 100644
--- a/source/funkin/util/MemoryUtil.hx
+++ b/source/funkin/util/MemoryUtil.hx
@@ -48,11 +48,11 @@ class MemoryUtil
    * Calculate the total memory usage of the program, in bytes.
    * @return Int
    */
-  public static function getMemoryUsed():Int
+  public static function getMemoryUsed():#if cpp Float #else Int #end
   {
     #if cpp
     // There is also Gc.MEM_INFO_RESERVED, MEM_INFO_CURRENT, and MEM_INFO_LARGE.
-    return cpp.vm.Gc.memInfo(cpp.vm.Gc.MEM_INFO_USAGE);
+    return cpp.vm.Gc.memInfo64(cpp.vm.Gc.MEM_INFO_USAGE);
     #else
     return openfl.system.System.totalMemory;
     #end

From 7904a6a20ff76364f6342578ec7d6849e4b098a0 Mon Sep 17 00:00:00 2001
From: Karim Akra <144803230+KarimAkra@users.noreply.github.com>
Date: Sun, 9 Jun 2024 01:25:21 +0300
Subject: [PATCH 05/47] use Math.fround instead of Math.round

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

diff --git a/source/funkin/ui/debug/MemoryCounter.hx b/source/funkin/ui/debug/MemoryCounter.hx
index b25b55645..50421f398 100644
--- a/source/funkin/ui/debug/MemoryCounter.hx
+++ b/source/funkin/ui/debug/MemoryCounter.hx
@@ -36,7 +36,7 @@ class MemoryCounter extends TextField
   @:noCompletion
   #if !flash override #end function __enterFrame(deltaTime:Float):Void
   {
-    var mem:Float = Math.round(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
+    var mem:Float = Math.fround(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
 
     if (mem > memPeak) memPeak = mem;
 

From 73982fbd606051fd6e0f2bf9e78ca174fdab0d88 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Sun, 9 Jun 2024 00:29:55 +0200
Subject: [PATCH 06/47] Fix Stack Overflow if song doesn't have "normal"
 difficulty

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

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 5e07fb396..0ef268d31 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -2046,6 +2046,8 @@ class FreeplaySongData
 
   function set_currentDifficulty(value:String):String
   {
+    if (currentDifficulty == value) return value;
+
     currentDifficulty = value;
     updateValues(displayedVariations);
     return value;

From 4997bc27fd0473b5b6f3996ac7150b0240cb62c0 Mon Sep 17 00:00:00 2001
From: Karim Akra <144803230+KarimAkra@users.noreply.github.com>
Date: Sun, 9 Jun 2024 12:47:17 +0300
Subject: [PATCH 07/47] remove the library strip

---
 source/funkin/audio/FunkinSound.hx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index 4f61e70c2..be5cc6931 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -491,8 +491,10 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
 
     // split the path and get only after first :
-    // we are bypassing the openfl/lime asset library fuss
+    // we are bypassing the openfl/lime asset library fuss on web only
+    #if web
     path = Paths.stripLibrary(path);
+    #end
 
     var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
 

From 9618cd2128323889d19246fb519b56494a9d20c7 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 10 Jun 2024 16:53:32 +0200
Subject: [PATCH 08/47] Fix chart reset when charting and pressing chart key

---
 source/funkin/play/PlayState.hx | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index b3d0a9f8a..1e22e98af 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2284,7 +2284,7 @@ class PlayState extends MusicBeatSubState
           health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
           songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
         }
-        
+
         // Make sure the player keeps singing while the note is held by the bot.
         if (isBotPlayMode && currentStage != null && currentStage.getBoyfriend() != null && currentStage.getBoyfriend().isSinging())
         {
@@ -2612,10 +2612,18 @@ class PlayState extends MusicBeatSubState
     {
       disableKeys = true;
       persistentUpdate = false;
-      FlxG.switchState(() -> new ChartEditorState(
-        {
-          targetSongId: currentSong.id,
-        }));
+      if (isChartingMode)
+      {
+        FlxG.sound.music?.pause();
+        this.close();
+      }
+      else
+      {
+        FlxG.switchState(() -> new ChartEditorState(
+          {
+            targetSongId: currentSong.id,
+          }));
+      }
     }
     #end
 

From fb71f9087d546854d4b44311682d84dcdd44fd32 Mon Sep 17 00:00:00 2001
From: NotHyper-474 <survivaltemer@gmail.com>
Date: Mon, 10 Jun 2024 23:12:28 -0300
Subject: [PATCH 09/47] Fix trace

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

diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index bc26ad97a..0f2ce1076 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -346,7 +346,7 @@ class LoadingState extends MusicBeatSubState
         return 'Done precaching ${path}';
       }, true);
 
-      trace("Queued ${path} for precaching");
+      trace('Queued ${path} for precaching');
       // FunkinSprite.cacheTexture(path);
     }
 

From b7eaa238e01ba0003ebd53cf71ecb2930c6c7b76 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Wed, 12 Jun 2024 00:34:04 +0200
Subject: [PATCH 10/47] Revert camCutscene rename

---
 source/funkin/play/PlayState.hx              | 16 ++++++++--------
 source/funkin/play/cutscene/VideoCutscene.hx |  6 +++---
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 20b9d3661..b88fef188 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -493,7 +493,7 @@ class PlayState extends MusicBeatSubState
   /**
    * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
    */
-  public var camOther:FlxCamera;
+  public var camCutscene:FlxCamera;
 
   /**
    * The combo popups. Includes the real-time combo counter and the rating.
@@ -960,7 +960,7 @@ class PlayState extends MusicBeatSubState
 
           FlxTransitionableState.skipNextTransIn = true;
           FlxTransitionableState.skipNextTransOut = true;
-          pauseSubState.camera = camOther;
+          pauseSubState.camera = camCutscene;
           openSubState(pauseSubState);
           // boyfriendPos.put(); // TODO: Why is this here?
         }
@@ -1501,12 +1501,12 @@ class PlayState extends MusicBeatSubState
     camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
     camHUD = new FlxCamera();
     camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
-    camOther = new FlxCamera();
-    camOther.bgColor.alpha = 0; // Show the game scene behind the camera.
+    camCutscene = new FlxCamera();
+    camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera.
 
     FlxG.cameras.reset(camGame);
     FlxG.cameras.add(camHUD, false);
-    FlxG.cameras.add(camOther, false);
+    FlxG.cameras.add(camCutscene, false);
 
     // Configure camera follow point.
     if (previousCameraFollowPoint != null)
@@ -1918,7 +1918,7 @@ class PlayState extends MusicBeatSubState
     if (!currentConversation.alive) currentConversation.revive();
 
     currentConversation.completeCallback = onConversationComplete;
-    currentConversation.cameras = [camOther];
+    currentConversation.cameras = [camCutscene];
     currentConversation.zIndex = 1000;
     add(currentConversation);
     refresh();
@@ -2750,7 +2750,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camOther;
+        pauseSubState.camera = camCutscene;
         openSubState(pauseSubState);
       }
     }
@@ -2766,7 +2766,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camOther;
+        pauseSubState.camera = camCutscene;
         openSubState(pauseSubState);
       }
     }
diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 2177114aa..7612c3ab6 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -85,7 +85,7 @@ class VideoCutscene
     // Display a black screen to hide the game while the video is playing.
     blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
     blackScreen.scrollFactor.set(0, 0);
-    blackScreen.cameras = [PlayState.instance.camOther];
+    blackScreen.cameras = [PlayState.instance.camCutscene];
     PlayState.instance.add(blackScreen);
 
     VideoCutscene.cutsceneType = cutsceneType;
@@ -119,7 +119,7 @@ class VideoCutscene
 
       vid.finishCallback = finishVideo.bind(0.5);
 
-      vid.cameras = [PlayState.instance.camOther];
+      vid.cameras = [PlayState.instance.camCutscene];
 
       PlayState.instance.add(vid);
 
@@ -146,7 +146,7 @@ class VideoCutscene
       vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
       vid.autoPause = false;
 
-      vid.cameras = [PlayState.instance.camOther];
+      vid.cameras = [PlayState.instance.camCutscene];
 
       PlayState.instance.add(vid);
 

From ba4677857aefbd10def4e03b3def59756deccdba Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Sat, 15 Jun 2024 20:23:55 +0200
Subject: [PATCH 11/47] Few animation editor bugfixes

---
 hmm.json                                      |  2 +-
 .../ui/debug/anim/DebugBoundingState.hx       | 23 ++++++++++++-------
 2 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/hmm.json b/hmm.json
index 68e0c5cb0..d217740db 100644
--- a/hmm.json
+++ b/hmm.json
@@ -30,7 +30,7 @@
       "name": "flixel-ui",
       "type": "git",
       "dir": null,
-      "ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
+      "ref": "d0afed7293c71ffdb1184751317fc709b44c9056",
       "url": "https://github.com/HaxeFlixel/flixel-ui"
     },
     {
diff --git a/source/funkin/ui/debug/anim/DebugBoundingState.hx b/source/funkin/ui/debug/anim/DebugBoundingState.hx
index 04784a5b7..d82bcc612 100644
--- a/source/funkin/ui/debug/anim/DebugBoundingState.hx
+++ b/source/funkin/ui/debug/anim/DebugBoundingState.hx
@@ -1,6 +1,7 @@
 package funkin.ui.debug.anim;
 
 import flixel.addons.display.FlxGridOverlay;
+import flixel.addons.display.FlxBackdrop;
 import flixel.addons.ui.FlxInputText;
 import flixel.addons.ui.FlxUIDropDownMenu;
 import flixel.FlxCamera;
@@ -55,7 +56,7 @@ class DebugBoundingState extends FlxState
     TODAY'S TO-DO
     - Cleaner UI
    */
-  var bg:FlxSprite;
+  var bg:FlxBackdrop;
   var fileInfo:FlxText;
 
   var txtGrp:FlxGroup;
@@ -80,7 +81,7 @@ class DebugBoundingState extends FlxState
   {
     // get the screen position, according to the HUD camera, temp default to FlxG.camera juuust in case?
     var hudMousePos:FlxPoint = FlxG.mouse.getScreenPosition(hudCam ?? FlxG.camera);
-    return Screen.instance.hasSolidComponentUnderPoint(hudMousePos.x, hudMousePos.y);
+    return Screen.instance.hasSolidComponentUnderPoint(hudMousePos.x, hudMousePos.y) || FlxG.mouse.overlaps(animDropDownMenu, hudCam);
   }
 
   override function create()
@@ -103,10 +104,7 @@ class DebugBoundingState extends FlxState
     hudCam = new FlxCamera();
     hudCam.bgColor.alpha = 0;
 
-    bg = FlxGridOverlay.create(10, 10);
-    // bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.GREEN);
-
-    bg.scrollFactor.set();
+    bg = new FlxBackdrop(FlxGridOverlay.createGrid(10, 10, FlxG.width, FlxG.height, true, 0xffe7e6e6, 0xffd9d5d5));
     add(bg);
 
     // we are setting this as the default draw camera only temporarily, to trick haxeui
@@ -289,16 +287,20 @@ class DebugBoundingState extends FlxState
 
   public var mouseOffset:FlxPoint = FlxPoint.get(0, 0);
   public var oldPos:FlxPoint = FlxPoint.get(0, 0);
+  public var movingCharacter:Bool = false;
 
   function mouseOffsetMovement()
   {
     if (swagChar != null)
     {
-      if (FlxG.mouse.justPressed)
+      if (FlxG.mouse.justPressed && !haxeUIFocused)
       {
+        movingCharacter = true;
         mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]);
       }
 
+      if (!movingCharacter) return;
+
       if (FlxG.mouse.pressed)
       {
         swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1];
@@ -307,6 +309,11 @@ class DebugBoundingState extends FlxState
 
         txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
       }
+
+      if (FlxG.mouse.justReleased)
+      {
+        movingCharacter = false;
+      }
     }
   }
 
@@ -373,7 +380,7 @@ class DebugBoundingState extends FlxState
         offsetView.visible = true;
         offsetView.active = true;
         offsetControls();
-        if (!haxeUIFocused) mouseOffsetMovement();
+        mouseOffsetMovement();
     }
 
     if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible;

From 250ec840efa17b323f69748271982659ad61cc06 Mon Sep 17 00:00:00 2001
From: MaybeMaru <97055307+MaybeMaru@users.noreply.github.com>
Date: Tue, 18 Jun 2024 15:11:00 +0200
Subject: [PATCH 12/47] Add missing constants

---
 source/funkin/play/notes/Strumline.hx    | 2 +-
 source/funkin/play/notes/SustainTrail.hx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 0e4b6645f..3c114b5e0 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -37,7 +37,7 @@ class Strumline extends FlxSpriteGroup
 
   static function get_RENDER_DISTANCE_MS():Float
   {
-    return FlxG.height / 0.45;
+    return FlxG.height / Constants.PIXELS_PER_MS;
   }
 
   /**
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index b358d7f03..f6d43b33f 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -160,7 +160,7 @@ class SustainTrail extends FlxSprite
    */
   public static inline function sustainHeight(susLength:Float, scroll:Float)
   {
-    return (susLength * 0.45 * scroll);
+    return (susLength * Constants.PIXELS_PER_MS * scroll);
   }
 
   function set_sustainLength(s:Float):Float

From 73efd963b9e1a98c77fdb6a2a8f57517d08f16d1 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Thu, 20 Jun 2024 07:24:36 +0200
Subject: [PATCH 13/47] Fix crash after pressing F5 and coming back from
 stickers

---
 source/funkin/ui/freeplay/FreeplayState.hx | 2 +-
 source/funkin/ui/story/StoryMenuState.hx   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 0caaf4591..949aa4bfe 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -214,7 +214,7 @@ class FreeplayState extends MusicBeatSubState
       prepForNewRank = true;
     }
 
-    if (stickers != null)
+    if (stickers?.members != null)
     {
       stickerSubState = stickers;
     }
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 06a83ab4d..7707850ce 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -113,7 +113,7 @@ class StoryMenuState extends MusicBeatState
   {
     super();
 
-    if (stickers != null)
+    if (stickers?.members != null)
     {
       stickerSubState = stickers;
     }

From dd7a894b518e6cf6467ad73ee15e277c244217a3 Mon Sep 17 00:00:00 2001
From: Pixel <146671762+JVNpixels@users.noreply.github.com>
Date: Fri, 21 Jun 2024 08:42:51 -0700
Subject: [PATCH 14/47] Update StoryMenuState.hx

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

diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 06a83ab4d..8623d8f39 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -336,6 +336,24 @@ class StoryMenuState extends MusicBeatState
           changeDifficulty(0);
         }
 
+        #if !html5
+        if (FlxG.mouse.wheel != 0)
+        {
+          changeLevel(-Math.round(FlxG.mouse.wheel));
+        }
+        #else
+        if (FlxG.mouse.wheel < 0)
+        {
+          changeLevel(-Math.round(FlxG.mouse.wheel / 8));
+        }
+        else if (FlxG.mouse.wheel > 0)
+        {
+          changeLevel(-Math.round(FlxG.mouse.wheel / 8));
+        }
+        #end
+
+        // HTML and NON HTML builds mouse fix.
+
         // TODO: Querying UI_RIGHT_P (justPressed) after UI_RIGHT always returns false. Fix it!
         if (controls.UI_RIGHT_P)
         {

From 0589624d117d52f3ec6224af26b4041205db612c Mon Sep 17 00:00:00 2001
From: Pixel <146671762+JVNpixels@users.noreply.github.com>
Date: Fri, 21 Jun 2024 22:46:07 -0700
Subject: [PATCH 15/47] Update source/funkin/ui/story/StoryMenuState.hx

Co-authored-by: gamerbross <55158797+gamerbross@users.noreply.github.com>
---
 source/funkin/ui/story/StoryMenuState.hx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 8623d8f39..5f21a9a88 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -352,7 +352,6 @@ class StoryMenuState extends MusicBeatState
         }
         #end
 
-        // HTML and NON HTML builds mouse fix.
 
         // TODO: Querying UI_RIGHT_P (justPressed) after UI_RIGHT always returns false. Fix it!
         if (controls.UI_RIGHT_P)

From be38ae6c0003ac63aea38f490b66e07b10f0bd6e Mon Sep 17 00:00:00 2001
From: Pixel <146671762+JVNpixels@users.noreply.github.com>
Date: Sat, 22 Jun 2024 19:23:40 -0700
Subject: [PATCH 16/47] Extra newline removal.

Removes the extra line in code.
---
 source/funkin/ui/story/StoryMenuState.hx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 5f21a9a88..90786b3f6 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -352,7 +352,6 @@ class StoryMenuState extends MusicBeatState
         }
         #end
 
-
         // TODO: Querying UI_RIGHT_P (justPressed) after UI_RIGHT always returns false. Fix it!
         if (controls.UI_RIGHT_P)
         {

From ef2885ed0e0b68ddf2e534eef1b5f6269c6700d2 Mon Sep 17 00:00:00 2001
From: AbnormalPoof <userabnormal89@gmail.com>
Date: Mon, 24 Jun 2024 03:55:59 -0500
Subject: [PATCH 17/47] video cutscene autopause magic

---
 source/funkin/play/PlayState.hx              | 16 ++++++++++++++--
 source/funkin/play/cutscene/VideoCutscene.hx |  2 +-
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f55cef388..de8492882 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1301,12 +1301,18 @@ class PlayState extends MusicBeatSubState
     super.closeSubState();
   }
 
-  #if discord_rpc
   /**
    * Function called when the game window gains focus.
    */
   public override function onFocus():Void
   {
+    if (VideoCutscene.isPlaying() && FlxG.autoPause && isGamePaused) VideoCutscene.pauseVideo();
+    #if html5
+    else
+      VideoCutscene.resumeVideo();
+    #end
+
+    #if discord_rpc
     if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
     {
       if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
@@ -1318,6 +1324,7 @@ class PlayState extends MusicBeatSubState
       else
         DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
     }
+    #end
 
     super.onFocus();
   }
@@ -1327,12 +1334,17 @@ class PlayState extends MusicBeatSubState
    */
   public override function onFocusLost():Void
   {
+    #if html5
+    if (FlxG.autoPause) VideoCutscene.pauseVideo();
+    #end
+
+    #if discord_rpc
     if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText,
       currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
+    #end
 
     super.onFocusLost();
   }
-  #end
 
   /**
    * Removes any references to the current stage, then clears the stage cache,
diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 01a492a77..abbcd4f54 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -145,7 +145,7 @@ class VideoCutscene
     {
       vid.zIndex = 0;
       vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
-      vid.autoPause = false;
+      vid.autoPause = FlxG.autoPause;
 
       vid.cameras = [PlayState.instance.camCutscene];
 

From 91bc63004abdd1a669ac6779d83fde5c88b641bd Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Thu, 27 Jun 2024 21:00:25 +0200
Subject: [PATCH 18/47] FunkinSound.playOnce return sound

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

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index c70f195d2..8d2ad9a32 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -533,11 +533,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
    * Play a sound effect once, then destroy it.
    * @param key
    * @param volume
-   * @return static function construct():FunkinSound
+   * @return A `FunkinSound` object, or `null` if the sound could not be loaded.
    */
-  public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Void
+  public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
   {
     var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
+    return result;
   }
 
   /**

From 02dd7e7264ab8c2d9f2136a1e07ff7e0e41e6904 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Thu, 27 Jun 2024 21:05:01 +0200
Subject: [PATCH 19/47] Implement danceEvery for Characters

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

diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 4ef86c6a9..f9e1f00f2 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -180,6 +180,7 @@ class BaseCharacter extends Bopper
     {
       this.characterName = _data.name;
       this.name = _data.name;
+      this.danceEvery = _data.danceEvery;
       this.singTimeSteps = _data.singTime;
       this.globalOffsets = _data.offsets;
       this.flipX = _data.flipX;

From 09c8a2600ee2d36a6ab23cfd4e68b7227bc15a90 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Sat, 29 Jun 2024 22:54:25 +0200
Subject: [PATCH 20/47] Fix null safety compiling

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

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index 8d2ad9a32..a0175ad4d 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -535,9 +535,9 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
    * @param volume
    * @return A `FunkinSound` object, or `null` if the sound could not be loaded.
    */
-  public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
+  public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Null<FunkinSound>
   {
-    var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
+    var result:Null<FunkinSound> = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
     return result;
   }
 

From 488fc6888f7d8588866ddcc1310c434365eae2d0 Mon Sep 17 00:00:00 2001
From: FlooferLand! <76737186+FlooferLand@users.noreply.github.com>
Date: Mon, 1 Jul 2024 14:42:50 +0300
Subject: [PATCH 21/47] Added new settings items

---
 source/funkin/ui/options/MenuItemEnums.hx     |  10 ++
 source/funkin/ui/options/PreferencesMenu.hx   | 149 +++++++++++-------
 .../options/items/CheckboxPreferenceItem.hx   |  49 ++++++
 .../ui/options/items/EnumPreferenceItem.hx    |  84 ++++++++++
 .../ui/options/items/NumberPreferenceItem.hx  | 136 ++++++++++++++++
 5 files changed, 372 insertions(+), 56 deletions(-)
 create mode 100644 source/funkin/ui/options/MenuItemEnums.hx
 create mode 100644 source/funkin/ui/options/items/CheckboxPreferenceItem.hx
 create mode 100644 source/funkin/ui/options/items/EnumPreferenceItem.hx
 create mode 100644 source/funkin/ui/options/items/NumberPreferenceItem.hx

diff --git a/source/funkin/ui/options/MenuItemEnums.hx b/source/funkin/ui/options/MenuItemEnums.hx
new file mode 100644
index 000000000..4513a92af
--- /dev/null
+++ b/source/funkin/ui/options/MenuItemEnums.hx
@@ -0,0 +1,10 @@
+package funkin.ui.options;
+
+// Add enums for use with `EnumPreferenceItem` here!
+/* Example:
+  class MyOptionEnum
+  {
+    public static inline var YuhUh = "true";  // "true" is the value's ID
+    public static inline var NuhUh = "false";
+  }
+ */
diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx
index 783aef0ba..5fbefceed 100644
--- a/source/funkin/ui/options/PreferencesMenu.hx
+++ b/source/funkin/ui/options/PreferencesMenu.hx
@@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont;
 import funkin.ui.options.OptionsState.Page;
 import funkin.graphics.FunkinCamera;
 import funkin.ui.TextMenuList.TextMenuItem;
+import funkin.audio.FunkinSound;
+import funkin.ui.options.MenuItemEnums;
+import funkin.ui.options.items.CheckboxPreferenceItem;
+import funkin.ui.options.items.NumberPreferenceItem;
+import funkin.ui.options.items.EnumPreferenceItem;
 
 class PreferencesMenu extends Page
 {
@@ -69,11 +74,51 @@ class PreferencesMenu extends Page
     }, Preferences.autoPause);
   }
 
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // Indent the selected item.
+    items.forEach(function(daItem:TextMenuItem) {
+      var thyOffset:Int = 0;
+
+      // Initializing thy text width (if thou text present)
+      var thyTextWidth:Int = 0;
+      if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth();
+      else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth();
+
+      if (thyTextWidth != 0)
+      {
+        // Magic number because of the weird offset thats being added by default
+        thyOffset += thyTextWidth - 75;
+      }
+
+      if (items.selectedItem == daItem)
+      {
+        thyOffset += 150;
+      }
+      else
+      {
+        thyOffset += 120;
+      }
+
+      daItem.x = thyOffset;
+    });
+  }
+
+  // - Preference item creation methods -
+  // Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside.
+
+  /**
+   * Creates a pref item that works with booleans
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   */
   function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
   {
     var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
 
-    items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
+    items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
       var value = !checkbox.currentValue;
       onChange(value);
       checkbox.currentValue = value;
@@ -82,62 +127,54 @@ class PreferencesMenu extends Page
     preferenceItems.add(checkbox);
   }
 
-  override function update(elapsed:Float)
+  /**
+   * Creates a pref item that works with general numbers
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   * @param min Minimum value (example: 0)
+   * @param max Maximum value (example: 10)
+   * @param step The value to increment/decrement by (default = 0.1)
+   * @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12)
+   */
+  function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
+      step:Float = 0.1, precision:Int):Void
   {
-    super.update(elapsed);
+    var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter);
+    items.addItem(prefName, item);
+    preferenceItems.add(item.lefthandText);
+  }
 
-    // Indent the selected item.
-    // TODO: Only do this on menu change?
-    items.forEach(function(daItem:TextMenuItem) {
-      if (items.selectedItem == daItem) daItem.x = 150;
-      else
-        daItem.x = 120;
-    });
-  }
-}
-
-class CheckboxPreferenceItem extends FlxSprite
-{
-  public var currentValue(default, set):Bool;
-
-  public function new(x:Float, y:Float, defaultValue:Bool = false)
-  {
-    super(x, y);
-
-    frames = Paths.getSparrowAtlas('checkboxThingie');
-    animation.addByPrefix('static', 'Check Box unselected', 24, false);
-    animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
-
-    setGraphicSize(Std.int(width * 0.7));
-    updateHitbox();
-
-    this.currentValue = defaultValue;
-  }
-
-  override function update(elapsed:Float)
-  {
-    super.update(elapsed);
-
-    switch (animation.curAnim.name)
-    {
-      case 'static':
-        offset.set();
-      case 'checked':
-        offset.set(17, 70);
-    }
-  }
-
-  function set_currentValue(value:Bool):Bool
-  {
-    if (value)
-    {
-      animation.play('checked', true);
-    }
-    else
-    {
-      animation.play('static');
-    }
-
-    return currentValue = value;
+  /**
+   * Creates a pref item that works with number percentages
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   * @param min Minimum value (default = 0)
+   * @param max Maximum value (default = 100)
+   */
+  function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void
+  {
+    var newCallback = function(value:Float) {
+      onChange(Std.int(value));
+    };
+    var formatter = function(value:Float) {
+      return '${value}%';
+    };
+    var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter);
+    items.addItem(prefName, item);
+    preferenceItems.add(item.lefthandText);
+  }
+
+  /**
+   * Creates a pref item that works with enums
+   * @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   */
+  function createPrefItemEnum(prefName:String, prefDesc:String, values:Map<String, String>, onChange:String->Void, defaultValue:String):Void
+  {
+    var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange);
+    items.addItem(prefName, item);
+    preferenceItems.add(item.lefthandText);
   }
 }
diff --git a/source/funkin/ui/options/items/CheckboxPreferenceItem.hx b/source/funkin/ui/options/items/CheckboxPreferenceItem.hx
new file mode 100644
index 000000000..88c4fb6b0
--- /dev/null
+++ b/source/funkin/ui/options/items/CheckboxPreferenceItem.hx
@@ -0,0 +1,49 @@
+package funkin.ui.options.items;
+
+import flixel.FlxSprite.FlxSprite;
+
+class CheckboxPreferenceItem extends FlxSprite
+{
+  public var currentValue(default, set):Bool;
+
+  public function new(x:Float, y:Float, defaultValue:Bool = false)
+  {
+    super(x, y);
+
+    frames = Paths.getSparrowAtlas('checkboxThingie');
+    animation.addByPrefix('static', 'Check Box unselected', 24, false);
+    animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
+
+    setGraphicSize(Std.int(width * 0.7));
+    updateHitbox();
+
+    this.currentValue = defaultValue;
+  }
+
+  override function update(elapsed:Float)
+  {
+    super.update(elapsed);
+
+    switch (animation.curAnim.name)
+    {
+      case 'static':
+        offset.set();
+      case 'checked':
+        offset.set(17, 70);
+    }
+  }
+
+  function set_currentValue(value:Bool):Bool
+  {
+    if (value)
+    {
+      animation.play('checked', true);
+    }
+    else
+    {
+      animation.play('static');
+    }
+
+    return currentValue = value;
+  }
+}
diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx
new file mode 100644
index 000000000..02a273353
--- /dev/null
+++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx
@@ -0,0 +1,84 @@
+package funkin.ui.options.items;
+
+import funkin.ui.TextMenuList;
+import funkin.ui.AtlasText;
+import funkin.input.Controls;
+import funkin.ui.options.MenuItemEnums;
+import haxe.EnumTools;
+
+/**
+ * Preference item that allows the player to pick a value from an enum (list of values)
+ */
+class EnumPreferenceItem extends TextMenuItem
+{
+  function controls():Controls
+  {
+    return PlayerSettings.player1.controls;
+  }
+
+  public var lefthandText:AtlasText;
+
+  public var currentValue:String;
+  public var onChangeCallback:Null<String->Void>;
+  public var map:Map<String, String>;
+  public var keys:Array<String> = [];
+
+  var index = 0;
+
+  public function new(x:Float, y:Float, name:String, map:Map<String, String>, defaultValue:String, ?callback:String->Void)
+  {
+    super(x, y, name, function() {
+      callback(this.currentValue);
+    });
+
+    updateHitbox();
+
+    this.map = map;
+    this.currentValue = defaultValue;
+    this.onChangeCallback = callback;
+
+    var i:Int = 0;
+    for (key in map.keys())
+    {
+      this.keys.push(key);
+      if (this.currentValue == key) index = i;
+      i += 1;
+    }
+
+    lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // var fancyTextFancyColor:Color;
+    if (selected)
+    {
+      var shouldDecrease:Bool = controls().UI_LEFT_P;
+      var shouldIncrease:Bool = controls().UI_RIGHT_P;
+
+      if (shouldDecrease) index -= 1;
+      if (shouldIncrease) index += 1;
+
+      if (index > keys.length - 1) index = 0;
+      if (index < 0) index = keys.length - 1;
+
+      currentValue = keys[index];
+      if (onChangeCallback != null && (shouldIncrease || shouldDecrease))
+      {
+        onChangeCallback(currentValue);
+      }
+    }
+
+    lefthandText.text = formatted(currentValue);
+  }
+
+  function formatted(value:String):String
+  {
+    // FIXME: Can't add arrows around the text because the font doesn't support < >
+    // var leftArrow:String = selected ? '<' : '';
+    // var rightArrow:String = selected ? '>' : '';
+    return '${map.get(value) ?? value}';
+  }
+}
diff --git a/source/funkin/ui/options/items/NumberPreferenceItem.hx b/source/funkin/ui/options/items/NumberPreferenceItem.hx
new file mode 100644
index 000000000..f3cd3cd46
--- /dev/null
+++ b/source/funkin/ui/options/items/NumberPreferenceItem.hx
@@ -0,0 +1,136 @@
+package funkin.ui.options.items;
+
+import funkin.ui.TextMenuList;
+import funkin.ui.AtlasText;
+import funkin.input.Controls;
+
+/**
+ * Preference item that allows the player to pick a value between min and max
+ */
+class NumberPreferenceItem extends TextMenuItem
+{
+  function controls():Controls
+  {
+    return PlayerSettings.player1.controls;
+  }
+
+  // Widgets
+  public var lefthandText:AtlasText;
+
+  // Constants
+  static final HOLD_DELAY:Float = 0.3; // seconds
+  static final CHANGE_RATE:Float = 0.08; // seconds
+
+  // Constructor-initialized variables
+  public var currentValue:Float;
+  public var min:Float;
+  public var max:Float;
+  public var step:Float;
+  public var precision:Int;
+  public var onChangeCallback:Null<Float->Void>;
+  public var valueFormatter:Null<Float->String>;
+
+  // Variables
+  var holdDelayTimer:Float = HOLD_DELAY; // seconds
+  var changeRateTimer:Float = 0.0; // seconds
+
+  /**
+   * @param min Minimum value (example: 0)
+   * @param max Maximum value (example: 100)
+   * @param step The value to increment/decrement by (example: 10)
+   * @param callback Will get called every time the user changes the setting; use this to apply/save the setting.
+   * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks
+   */
+  public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void,
+      ?valueFormatter:Float->String):Void
+  {
+    super(x, y, name, function() {
+      callback(this.currentValue);
+    });
+    lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
+
+    updateHitbox();
+
+    this.currentValue = defaultValue;
+    this.min = min;
+    this.max = max;
+    this.step = step;
+    this.precision = precision;
+    this.onChangeCallback = callback;
+    this.valueFormatter = valueFormatter;
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // var fancyTextFancyColor:Color;
+    if (selected)
+    {
+      holdDelayTimer -= elapsed;
+      if (holdDelayTimer <= 0.0)
+      {
+        changeRateTimer -= elapsed;
+      }
+
+      var jpLeft:Bool = controls().UI_LEFT_P;
+      var jpRight:Bool = controls().UI_RIGHT_P;
+
+      if (jpLeft || jpRight)
+      {
+        holdDelayTimer = HOLD_DELAY;
+        changeRateTimer = 0.0;
+      }
+
+      var shouldDecrease:Bool = jpLeft;
+      var shouldIncrease:Bool = jpRight;
+
+      if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
+      {
+        shouldDecrease = true;
+        changeRateTimer = CHANGE_RATE;
+      }
+      else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
+      {
+        shouldIncrease = true;
+        changeRateTimer = CHANGE_RATE;
+      }
+
+      // Actually increasing/decreasing the value
+      if (shouldDecrease)
+      {
+        var isBelowMin:Bool = currentValue - step < min;
+        currentValue = (currentValue - step).clamp(min, max);
+        if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue);
+      }
+      else if (shouldIncrease)
+      {
+        var isAboveMax:Bool = currentValue + step > max;
+        currentValue = (currentValue + step).clamp(min, max);
+        if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue);
+      }
+    }
+
+    lefthandText.text = formatted(currentValue);
+  }
+
+  /** Turns the float into a string */
+  function formatted(value:Float):String
+  {
+    var float:Float = toFixed(value);
+    if (valueFormatter != null)
+    {
+      return valueFormatter(float);
+    }
+    else
+    {
+      return '${float}';
+    }
+  }
+
+  function toFixed(value:Float):Float
+  {
+    var multiplier:Float = Math.pow(10, precision);
+    return Math.floor(value * multiplier) / multiplier;
+  }
+}

From be5e0aafeb68b366db11227f72befb99d4e7b2f4 Mon Sep 17 00:00:00 2001
From: FlooferLand! <76737186+FlooferLand@users.noreply.github.com>
Date: Mon, 1 Jul 2024 14:44:23 +0300
Subject: [PATCH 22/47] Added getWidth

---
 source/funkin/ui/AtlasText.hx | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx
index 186d87c2a..ef74abc1e 100644
--- a/source/funkin/ui/AtlasText.hx
+++ b/source/funkin/ui/AtlasText.hx
@@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
     }
   }
 
+  public function getWidth():Int
+  {
+    var width = 0;
+    for (char in this.text.split(""))
+    {
+      switch (char)
+      {
+        case " ":
+          {
+            width += 40;
+          }
+        case "\n":
+          {}
+        case char:
+          {
+            var sprite = new AtlasChar(atlas, char);
+            sprite.revive();
+            sprite.char = char;
+            sprite.alpha = 1;
+            width += Std.int(sprite.width);
+          }
+      }
+    }
+    return width;
+  }
+
   override function toString()
   {
     return "InputItem, " + FlxStringUtil.getDebugString([

From f82f0d3b05f7ec03c4d80fe7a8abdf01667c0973 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 8 Jul 2024 03:17:28 +0200
Subject: [PATCH 23/47] Remove SustainTrail forced alpha

---
 source/funkin/play/notes/Strumline.hx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 0e4b6645f..5784586ec 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -598,7 +598,6 @@ class Strumline extends FlxSpriteGroup
     {
       note.holdNoteSprite.hitNote = true;
       note.holdNoteSprite.missedNote = false;
-      note.holdNoteSprite.alpha = 1.0;
 
       note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
     }

From 717894300d6041e1a24b10682122d9feffb7d18a Mon Sep 17 00:00:00 2001
From: AppleHair <eytanzury@gmail.com>
Date: Tue, 9 Jul 2024 19:59:31 +0300
Subject: [PATCH 24/47] [BUGFIX] Fixed `cancelMenu` sound not playing after
 switching state.

---
 source/funkin/ui/mainmenu/MainMenuState.hx | 2 +-
 source/funkin/ui/options/OptionsState.hx   | 2 +-
 source/funkin/ui/story/StoryMenuState.hx   | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index d09536eea..a2bdd23f8 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -409,8 +409,8 @@ class MainMenuState extends MusicBeatState
 
     if (controls.BACK && menuItems.enabled && !menuItems.busy)
     {
-      FunkinSound.playOnce(Paths.sound('cancelMenu'));
       FlxG.switchState(() -> new TitleState());
+      FunkinSound.playOnce(Paths.sound('cancelMenu'));
     }
   }
 }
diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx
index 40308d96b..a2301e6a3 100644
--- a/source/funkin/ui/options/OptionsState.hx
+++ b/source/funkin/ui/options/OptionsState.hx
@@ -145,8 +145,8 @@ class Page extends FlxGroup
   {
     if (canExit && controls.BACK)
     {
-      FunkinSound.playOnce(Paths.sound('cancelMenu'));
       exit();
+      FunkinSound.playOnce(Paths.sound('cancelMenu'));
     }
   }
 
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 06a83ab4d..04d021256 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -374,9 +374,9 @@ class StoryMenuState extends MusicBeatState
 
     if (controls.BACK && !exitingMenu && !selectedLevel)
     {
-      FunkinSound.playOnce(Paths.sound('cancelMenu'));
       exitingMenu = true;
       FlxG.switchState(() -> new MainMenuState());
+      FunkinSound.playOnce(Paths.sound('cancelMenu'));
     }
   }
 

From 481f7ab61a6e206bc3aadfd2754cdc299116c087 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Wed, 10 Jul 2024 00:23:06 +0200
Subject: [PATCH 25/47] Fix F5 chart not reloading

---
 source/funkin/play/PlayState.hx               | 59 ++-----------------
 source/funkin/ui/MusicBeatState.hx            |  7 +--
 source/funkin/ui/MusicBeatSubState.hx         |  5 +-
 .../util/plugins/ReloadAssetsDebugPlugin.hx   | 14 ++++-
 4 files changed, 17 insertions(+), 68 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f55cef388..8b6753035 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1335,64 +1335,13 @@ class PlayState extends MusicBeatSubState
   #end
 
   /**
-   * Removes any references to the current stage, then clears the stage cache,
-   * then reloads all the stages.
-   *
-   * This is useful for when you want to edit a stage without reloading the whole game.
-   * Reloading works on both the JSON and the HXC, if applicable.
-   *
    * Call this by pressing F5 on a debug build.
    */
-  override function debug_refreshModules():Void
+  override function reloadAssets():Void
   {
-    // Prevent further gameplay updates, which will try to reference dead objects.
-    criticalFailure = true;
-
-    // Remove the current stage. If the stage gets deleted while it's still in use,
-    // it'll probably crash the game or something.
-    if (this.currentStage != null)
-    {
-      remove(currentStage);
-      var event:ScriptEvent = new ScriptEvent(DESTROY, false);
-      ScriptEventDispatcher.callEvent(currentStage, event);
-      currentStage = null;
-    }
-
-    if (!overrideMusic)
-    {
-      // Stop the instrumental.
-      if (FlxG.sound.music != null)
-      {
-        FlxG.sound.music.destroy();
-        FlxG.sound.music = null;
-      }
-
-      // Stop the vocals.
-      if (vocals != null && vocals.exists)
-      {
-        vocals.destroy();
-        vocals = null;
-      }
-    }
-    else
-    {
-      // Stop the instrumental.
-      if (FlxG.sound.music != null)
-      {
-        FlxG.sound.music.stop();
-      }
-
-      // Stop the vocals.
-      if (vocals != null && vocals.exists)
-      {
-        vocals.stop();
-      }
-    }
-
-    super.debug_refreshModules();
-
-    var event:ScriptEvent = new ScriptEvent(CREATE, false);
-    ScriptEventDispatcher.callEvent(currentSong, event);
+    funkin.modding.PolymodHandler.forceReloadAssets();
+    lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
+    LoadingState.loadPlayState(lastParams);
   }
 
   override function stepHit():Bool
diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx
index 92169df75..8668b64c1 100644
--- a/source/funkin/ui/MusicBeatState.hx
+++ b/source/funkin/ui/MusicBeatState.hx
@@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
   {
     // Emergency exit button.
     if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
-
-    // This can now be used in EVERY STATE YAY!
-    if (FlxG.keys.justPressed.F5) debug_refreshModules();
   }
 
   override function update(elapsed:Float)
@@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
     ModuleHandler.callEvent(event);
   }
 
-  function debug_refreshModules()
+  function reloadAssets()
   {
     PolymodHandler.forceReloadAssets();
 
-    this.destroy();
-
     // Create a new instance of the current state, so old data is cleared.
     FlxG.resetState();
   }
diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx
index 9035d12ff..5c40b37bc 100644
--- a/source/funkin/ui/MusicBeatSubState.hx
+++ b/source/funkin/ui/MusicBeatSubState.hx
@@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
     // Emergency exit button.
     if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
 
-    // This can now be used in EVERY STATE YAY!
-    if (FlxG.keys.justPressed.F5) debug_refreshModules();
-
     // Display Conductor info in the watch window.
     FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
     Conductor.watchQuick(conductorInUse);
@@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
     dispatchEvent(new UpdateScriptEvent(elapsed));
   }
 
-  function debug_refreshModules()
+  function reloadAssets()
   {
     PolymodHandler.forceReloadAssets();
 
diff --git a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
index f69609531..0e1e238ac 100644
--- a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
+++ b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
@@ -1,6 +1,9 @@
 package funkin.util.plugins;
 
+import flixel.FlxG;
 import flixel.FlxBasic;
+import funkin.ui.MusicBeatState;
+import funkin.ui.MusicBeatSubState;
 
 /**
  * A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
@@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic
     if (FlxG.keys.justPressed.F5)
     #end
     {
-      funkin.modding.PolymodHandler.forceReloadAssets();
+      var state:Dynamic = FlxG.state;
+      if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets();
+      else
+      {
+        funkin.modding.PolymodHandler.forceReloadAssets();
 
-      // Create a new instance of the current state, so old data is cleared.
-      FlxG.resetState();
+        // Create a new instance of the current state, so old data is cleared.
+        FlxG.resetState();
+      }
     }
   }
 

From 9ad2bb35f9b7f73b690a08b421fecd6a1eab0a8d Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Wed, 10 Jul 2024 01:26:16 +0200
Subject: [PATCH 26/47] ChartEditor Live Input Code Refactor + 6 key fix

---
 .../ui/debug/charting/ChartEditorState.hx     | 57 ++++++-------------
 1 file changed, 18 insertions(+), 39 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index f72cca77f..5e7493840 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -282,6 +282,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
 
+  /**
+   * A map of the keys for every live input style.
+   */
+  public static final LIVE_INPUT_KEYS:Map<ChartEditorLiveInputStyle, Array<FlxKey>> = [
+    NumberKeys => [
+      FIVE, SIX, SEVEN, EIGHT,
+       ONE, TWO, THREE,  FOUR
+    ],
+    WASDKeys => [
+      LEFT, DOWN, UP, RIGHT,
+         A,    S,  W,     D
+    ],
+    None => []
+  ];
+
   /**
    * INSTANCE DATA
    */
@@ -5129,46 +5144,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   function handlePlayhead():Void
   {
     // Place notes at the playhead with the keyboard.
-    switch (currentLiveInputStyle)
+    for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle])
     {
-      case ChartEditorLiveInputStyle.WASDKeys:
-        if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
-        if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
-        if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
-        if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
-        if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
-        if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
-        if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
-        if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
-
-        if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
-        if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
-        if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
-        if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
-        if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
-        if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
-        if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
-        if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
-      case ChartEditorLiveInputStyle.NumberKeys:
-        // Flipped because Dad is on the left but represents data 0-3.
-        if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
-        if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
-        if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
-        if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
-        if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
-        if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
-        if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
-        if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
-
-        if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
-        if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
-        if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
-        if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
-        if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
-        if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
-        if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
-      case ChartEditorLiveInputStyle.None:
-        // Do nothing.
+      if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note)
+      else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note);
     }
 
     // Place events at playhead.

From 2fb55ba3edf4e84a46dc6e451922af0a2ca63326 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Thu, 11 Jul 2024 06:59:22 +0200
Subject: [PATCH 27/47] Fix default health icon not used

---
 source/funkin/play/components/HealthIcon.hx | 29 +++++++--------------
 1 file changed, 9 insertions(+), 20 deletions(-)

diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 2442b0dc5..682334db3 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -33,7 +33,7 @@ class HealthIcon extends FunkinSprite
    * The character this icon is representing.
    * Setting this variable will automatically update the graphic.
    */
-  public var characterId(default, set):Null<String>;
+  public var characterId(default, set):String = Constants.DEFAULT_HEALTH_ICON;
 
   /**
    * Whether this health icon should automatically update its state based on the character's health.
@@ -116,7 +116,7 @@ class HealthIcon extends FunkinSprite
    */
   static final POSITION_OFFSET:Int = 26;
 
-  public function new(char:String = 'bf', playerId:Int = 0)
+  public function new(char:Null<String>, playerId:Int = 0)
   {
     super(0, 0);
     this.playerId = playerId;
@@ -127,7 +127,7 @@ class HealthIcon extends FunkinSprite
     initTargetSize();
   }
 
-  function set_characterId(value:Null<String>):Null<String>
+  function set_characterId(value:Null<String>):String
   {
     if (value == characterId) return value;
 
@@ -380,20 +380,9 @@ class HealthIcon extends FunkinSprite
     }
   }
 
-  function correctCharacterId(charId:Null<String>):String
+  function iconExists(charId:String):Bool
   {
-    if (charId == null)
-    {
-      return Constants.DEFAULT_HEALTH_ICON;
-    }
-
-    if (!Assets.exists(Paths.image('icons/icon-$charId')))
-    {
-      FlxG.log.warn('No icon for character: $charId : using default placeholder face instead!');
-      return Constants.DEFAULT_HEALTH_ICON;
-    }
-
-    return charId;
+    return Assets.exists(Paths.image('icons/icon-$charId'));
   }
 
   function isNewSpritesheet(charId:String):Bool
@@ -403,11 +392,11 @@ class HealthIcon extends FunkinSprite
 
   function loadCharacter(charId:Null<String>):Void
   {
-    if (charId == null || correctCharacterId(charId) != charId)
+    if (charId == null || !iconExists(charId))
     {
-      // This will recursively trigger loadCharacter to be called again.
-      characterId = correctCharacterId(charId);
-      return;
+      FlxG.log.warn('No icon for character: $charId : using default placeholder face instead!');
+      characterId = Constants.DEFAULT_HEALTH_ICON;
+      charId = characterId;
     }
 
     isLegacyStyle = !isNewSpritesheet(charId);

From a12b0e6ec36c45f0ec2c1516826cdfde2b8899e6 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Thu, 11 Jul 2024 18:10:45 +0300
Subject: [PATCH 28/47] Add HEY! song events to Tutorial

---
 source/funkin/play/PlayState.hx | 12 +-----------
 1 file changed, 1 insertion(+), 11 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f55cef388..ff55a79fd 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1489,7 +1489,7 @@ class PlayState extends MusicBeatSubState
     if (opponentStrumline != null) opponentStrumline.onBeatHit();
 
     // Make the characters dance on the beat
-    danceOnBeat();
+    //danceOnBeat();
 
     return true;
   }
@@ -1509,16 +1509,6 @@ class PlayState extends MusicBeatSubState
   function danceOnBeat():Void
   {
     if (currentStage == null) return;
-
-    // TODO: Add HEY! song events to Tutorial.
-    if (Conductor.instance.currentBeat % 16 == 15
-      && currentStage.getDad().characterId == 'gf'
-      && Conductor.instance.currentBeat > 16
-      && Conductor.instance.currentBeat < 48)
-    {
-      currentStage.getBoyfriend().playAnimation('hey', true);
-      currentStage.getDad().playAnimation('cheer', true);
-    }
   }
 
   /**

From 47a2cf4e5db9a36fb39e49500d85d2aca3961a06 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Thu, 11 Jul 2024 18:12:50 +0300
Subject: [PATCH 29/47] Add HEY! song events to Tutorial

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

diff --git a/assets b/assets
index 2e1594ee4..9ef18cde0 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 2e1594ee4c04c7148628bae471bdd061c9deb6b7
+Subproject commit 9ef18cde0b9802295118b922b76b851e96af2fbf

From 2993e17278f0162b3b86e9c7cf26d8d22cabf93c Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Sat, 13 Jul 2024 22:45:58 +0300
Subject: [PATCH 30/47] custom popups and countdowns yay!!!

---
 assets                                      |   2 +-
 source/funkin/play/Countdown.hx             | 154 ++++++++++----------
 source/funkin/play/PlayState.hx             |   6 +-
 source/funkin/play/components/PopUpStuff.hx |  58 ++++++--
 4 files changed, 124 insertions(+), 96 deletions(-)

diff --git a/assets b/assets
index 2e1594ee4..514a987ee 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 2e1594ee4c04c7148628bae471bdd061c9deb6b7
+Subproject commit 514a987ee57827b097ed035a1c2fdd1377a53b17
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 10636afdf..7686e5b75 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -10,6 +10,7 @@ import funkin.modding.events.ScriptEvent;
 import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
 import flixel.util.FlxTimer;
 import funkin.audio.FunkinSound;
+import openfl.utils.Assets;
 
 class Countdown
 {
@@ -18,6 +19,22 @@ class Countdown
    */
   public static var countdownStep(default, null):CountdownStep = BEFORE;
 
+  /**
+   * Which alternate countdown sound effect to use.
+   * You can set this via script.
+   * For example, in Week 6 it is `-pixel`.
+   */
+  public static var soundSuffix:String = '';
+
+  /**
+   * Which alternate graphic on countdown to use.
+   * You can set this via script.
+   * For example, in Week 6 it is `-pixel`.
+   */
+  public static var graphicSuffix:String = '';
+
+
+
   /**
    * The currently running countdown. This will be null if there is no countdown running.
    */
@@ -29,7 +46,7 @@ class Countdown
    * This will automatically stop and restart the countdown if it is already running.
    * @returns `false` if the countdown was cancelled by a script.
    */
-  public static function performCountdown(isPixelStyle:Bool):Bool
+  public static function performCountdown():Bool
   {
     countdownStep = BEFORE;
     var cancelled:Bool = propagateCountdownEvent(countdownStep);
@@ -64,10 +81,10 @@ class Countdown
       // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
       // Countdown graphic.
-      showCountdownGraphic(countdownStep, isPixelStyle);
+      showCountdownGraphic(countdownStep, graphicSuffix.toLowerCase().contains('pixel'));
 
       // Countdown sound.
-      playCountdownSound(countdownStep, isPixelStyle);
+      playCountdownSound(countdownStep);
 
       // Event handling bullshit.
       var cancelled:Bool = propagateCountdownEvent(countdownStep);
@@ -176,52 +193,30 @@ class Countdown
   }
 
   /**
-   * Retrieves the graphic to use for this step of the countdown.
-   * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
-   *
-   * This is public so modules can do lol funny shit.
+   * Reset the countdown configuration to the default.
    */
-  public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void
+  public static function reset()
+  {
+    soundSuffix = '';
+    graphicSuffix = '';
+  }
+
+  /**
+   * Retrieves the graphic to use for this step of the countdown.
+   */
+  public static function showCountdownGraphic(index:CountdownStep, isGraphicPixel:Bool):Void
   {
     var spritePath:String = null;
-
-    if (isPixelStyle)
-    {
-      switch (index)
-      {
-        case TWO:
-          spritePath = 'weeb/pixelUI/ready-pixel';
-        case ONE:
-          spritePath = 'weeb/pixelUI/set-pixel';
-        case GO:
-          spritePath = 'weeb/pixelUI/date-pixel';
-        default:
-          // null
-      }
-    }
-    else
-    {
-      switch (index)
-      {
-        case TWO:
-          spritePath = 'ready';
-        case ONE:
-          spritePath = 'set';
-        case GO:
-          spritePath = 'go';
-        default:
-          // null
-      }
-    }
+    spritePath = resolveGraphicPath(graphicSuffix, index);
 
     if (spritePath == null) return;
 
     var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
     countdownSprite.scrollFactor.set(0, 0);
 
-    if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
+    if (isGraphicPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
 
-    countdownSprite.antialiasing = !isPixelStyle;
+    countdownSprite.antialiasing = !isGraphicPixel;
 
     countdownSprite.updateHitbox();
     countdownSprite.screenCenter();
@@ -238,52 +233,55 @@ class Countdown
     PlayState.instance.add(countdownSprite);
   }
 
+  static function resolveGraphicPath(suffix:String, index:CountdownStep):Null<String>
+  {
+    var basePath:String = 'ui/countdown/';
+    var indexString:String = null;
+    switch (index)
+    {
+      case TWO:
+        indexString = 'ready';
+      case ONE:
+        indexString = 'set';
+      case GO:
+        indexString = 'go';
+      default:
+        // null
+    }
+    basePath += indexString;
+    var spritePath:String = basePath + suffix;
+    while (!Assets.exists(Paths.image(spritePath)) && suffix.length > 0)
+    {
+      suffix = suffix.split('-').slice(0, -1).join('-');
+      spritePath = basePath + suffix;
+    }
+    if (!Assets.exists(Paths.image(spritePath))) return null;
+    trace('Resolved sprite path: ' + Paths.image(spritePath));
+    return spritePath;
+  }
+
   /**
    * Retrieves the sound file to use for this step of the countdown.
-   * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
-   *
-   * This is public so modules can do lol funny shit.
    */
-  public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void
+  public static function playCountdownSound(index:CountdownStep):Void
   {
-    var soundPath:String = null;
+    FunkinSound.playOnce(resolveSoundPath(soundSuffix, index), Constants.COUNTDOWN_VOLUME);
+  }
 
-    if (isPixelStyle)
+  static function resolveSoundPath(suffix:String, step:CountdownStep):Null<String>
+  {
+    var basePath:String = 'gameplay/countdown/intro';
+    if (step != CountdownStep.BEFORE || step != CountdownStep.AFTER) basePath += step;
+
+    var soundPath:String = Paths.sound(basePath + suffix);
+    while (!Assets.exists(soundPath) && suffix.length > 0)
     {
-      switch (index)
-      {
-        case THREE:
-          soundPath = 'intro3-pixel';
-        case TWO:
-          soundPath = 'intro2-pixel';
-        case ONE:
-          soundPath = 'intro1-pixel';
-        case GO:
-          soundPath = 'introGo-pixel';
-        default:
-          // null
-      }
+      suffix = suffix.split('-').slice(0, -1).join('-');
+      soundPath = Paths.sound(basePath + suffix);
     }
-    else
-    {
-      switch (index)
-      {
-        case THREE:
-          soundPath = 'intro3';
-        case TWO:
-          soundPath = 'intro2';
-        case ONE:
-          soundPath = 'intro1';
-        case GO:
-          soundPath = 'introGo';
-        default:
-          // null
-      }
-    }
-
-    if (soundPath == null) return;
-
-    FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
+    if (!Assets.exists(soundPath)) return null;
+    trace('Resolved sound path: ' + soundPath);
+    return soundPath;
   }
 
   public static function decrement(step:CountdownStep):CountdownStep
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f55cef388..bf401ece2 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -899,7 +899,7 @@ class PlayState extends MusicBeatSubState
       health = Constants.HEALTH_STARTING;
       songScore = 0;
       Highscore.tallies.combo = 0;
-      Countdown.performCountdown(currentStageId.startsWith('school'));
+      Countdown.performCountdown();
 
       needsReset = false;
     }
@@ -1920,7 +1920,7 @@ class PlayState extends MusicBeatSubState
   public function startCountdown():Void
   {
     // If Countdown.performCountdown returns false, then the countdown was canceled by a script.
-    var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
+    var result:Bool = Countdown.performCountdown();
     if (!result) return;
 
     isInCutscene = false;
@@ -3061,6 +3061,8 @@ class PlayState extends MusicBeatSubState
 
     GameOverSubState.reset();
     PauseSubState.reset();
+    Countdown.reset();
+    PopUpStuff.reset();
 
     // Clear the static reference to this state.
     instance = null;
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index b7e206e97..ddf24d24b 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -7,25 +7,52 @@ import flixel.util.FlxDirection;
 import funkin.graphics.FunkinSprite;
 import funkin.play.PlayState;
 import funkin.util.TimerUtil;
+import openfl.utils.Assets;
 
 class PopUpStuff extends FlxTypedGroup<FlxSprite>
 {
   public var offsets:Array<Int> = [0, 0];
 
+  /**
+   * Which alternate graphic on popup to use.
+   * You can set this via script.
+   * For example, in Week 6 it is `-pixel`.
+   */
+  public static var graphicSuffix:String = '';
+
   override public function new()
   {
     super();
   }
 
+  static function resolveGraphicPath(suffix:String, index:String):Null<String>
+  {
+    var folder:String;
+    if (suffix != '')
+      folder = suffix.substring(0, suffix.indexOf("-")) + suffix.substring(suffix.indexOf("-") + 1);
+    else
+      folder = 'normal';
+    var basePath:String = 'gameplay/popup/$folder/$index';
+    var spritePath:String = basePath + suffix;
+    trace(spritePath);
+    while (!Assets.exists(Paths.image(spritePath)) && suffix.length > 0)
+    {
+      suffix = suffix.split('-').slice(0, -1).join('-');
+      spritePath = basePath + suffix;
+    }
+    if (!Assets.exists(Paths.image(spritePath))) return null;
+    return spritePath;
+  }
+
   public function displayRating(daRating:String)
   {
     var perfStart:Float = TimerUtil.start();
 
     if (daRating == null) daRating = "good";
 
-    var ratingPath:String = daRating;
+    var ratingPath:String = resolveGraphicPath(graphicSuffix, daRating);
 
-    if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
+    //if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
 
     var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath);
     rating.scrollFactor.set(0.2, 0.2);
@@ -40,7 +67,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     add(rating);
 
-    if (PlayState.instance.currentStageId.startsWith('school'))
+    if (graphicSuffix.toLowerCase().contains('pixel'))
     {
       rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
       rating.antialiasing = false;
@@ -73,15 +100,8 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     if (combo == null) combo = 0;
 
-    var pixelShitPart1:String = "";
-    var pixelShitPart2:String = '';
-
-    if (PlayState.instance.currentStageId.startsWith('school'))
-    {
-      pixelShitPart1 = 'weeb/pixelUI/';
-      pixelShitPart2 = '-pixel';
-    }
-    var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
+    var comboPath:String = resolveGraphicPath(graphicSuffix, Std.string(combo));
+    var comboSpr:FunkinSprite = FunkinSprite.create(comboPath);
     comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1];
     comboSpr.x = (FlxG.width * 0.507) + offsets[0];
     // comboSpr.x -= FlxG.camera.scroll.x * 0.2;
@@ -92,7 +112,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     // add(comboSpr);
 
-    if (PlayState.instance.currentStageId.startsWith('school'))
+    if (graphicSuffix.toLowerCase().contains('pixel'))
     {
       comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7));
       comboSpr.antialiasing = false;
@@ -129,9 +149,9 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
     var daLoop:Int = 1;
     for (i in seperatedScore)
     {
-      var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2);
+      var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, resolveGraphicPath(graphicSuffix, 'num' + Std.int(i)));
 
-      if (PlayState.instance.currentStageId.startsWith('school'))
+      if (graphicSuffix.toLowerCase().contains('pixel'))
       {
         numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 0.7));
         numScore.antialiasing = false;
@@ -166,4 +186,12 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     return combo;
   }
+
+  /**
+   * Reset the popup configuration to the default.
+   */
+  public static function reset()
+  {
+    graphicSuffix = '';
+  }
 }

From 2547fbb8f4033f815d3d8d5cef9eb9ddc02c2832 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Sat, 13 Jul 2024 23:08:16 +0300
Subject: [PATCH 31/47] remove trace

---
 assets                                      | 2 +-
 source/funkin/play/components/PopUpStuff.hx | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/assets b/assets
index 514a987ee..e414ee618 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 514a987ee57827b097ed035a1c2fdd1377a53b17
+Subproject commit e414ee618b4fa577dc3c3df4e156488e91c6348d
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index ddf24d24b..0a4d6b019 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -34,7 +34,6 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
       folder = 'normal';
     var basePath:String = 'gameplay/popup/$folder/$index';
     var spritePath:String = basePath + suffix;
-    trace(spritePath);
     while (!Assets.exists(Paths.image(spritePath)) && suffix.length > 0)
     {
       suffix = suffix.split('-').slice(0, -1).join('-');

From 9d3c043f8ab462973a1e760336581dee89326e42 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Sat, 13 Jul 2024 23:29:26 +0300
Subject: [PATCH 32/47] cache textures at their new locations!

---
 source/funkin/ui/transition/LoadingState.hx | 33 ++++++++++++++-------
 1 file changed, 22 insertions(+), 11 deletions(-)

diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index bc26ad97a..d0b8cfbe8 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -291,17 +291,28 @@ class LoadingState extends MusicBeatSubState
     FunkinSprite.preparePurgeCache();
     FunkinSprite.cacheTexture(Paths.image('healthBar'));
     FunkinSprite.cacheTexture(Paths.image('menuDesat'));
-    FunkinSprite.cacheTexture(Paths.image('combo'));
-    FunkinSprite.cacheTexture(Paths.image('num0'));
-    FunkinSprite.cacheTexture(Paths.image('num1'));
-    FunkinSprite.cacheTexture(Paths.image('num2'));
-    FunkinSprite.cacheTexture(Paths.image('num3'));
-    FunkinSprite.cacheTexture(Paths.image('num4'));
-    FunkinSprite.cacheTexture(Paths.image('num5'));
-    FunkinSprite.cacheTexture(Paths.image('num6'));
-    FunkinSprite.cacheTexture(Paths.image('num7'));
-    FunkinSprite.cacheTexture(Paths.image('num8'));
-    FunkinSprite.cacheTexture(Paths.image('num9'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/combo'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num0'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num1'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num2'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num3'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num4'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num5'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num6'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num7'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num8'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num9'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/combo-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num0-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num1-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num2-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num3-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num4-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num5-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num6-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num7-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num8-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num9-pixel'));
     FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));

From aed100df476df55d38e820cc86f643ed62117284 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Sat, 13 Jul 2024 23:39:56 +0300
Subject: [PATCH 33/47] how the hell did I miss these?

---
 source/funkin/ui/transition/LoadingState.hx | 21 ++++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)

diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index d0b8cfbe8..a571126fe 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -317,13 +317,20 @@ class LoadingState extends MusicBeatSubState
     FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
-    FunkinSprite.cacheTexture(Paths.image('ready', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('set', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('go', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('sick', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('good', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('bad', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('shit', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/ready', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/set', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/go', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/ready-pixel', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/set-pixel', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/go-pixel', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/sick'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/good'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/bad'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/shit'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/sick-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/good-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/bad-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/shit-pixel'));
     FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
 
     // List all image assets in the level's library.

From f8232911cb114e867448f10c134dc63b202b1e48 Mon Sep 17 00:00:00 2001
From: 7oltan <87986834+7oltan@users.noreply.github.com>
Date: Tue, 16 Jul 2024 12:52:51 +0300
Subject: [PATCH 34/47] FIX CHARACTER PATHS ISSUE

---
 source/funkin/play/character/AnimateAtlasCharacter.hx | 2 +-
 source/funkin/play/character/MultiSparrowCharacter.hx | 4 ++--
 source/funkin/play/character/PackerCharacter.hx       | 2 +-
 source/funkin/play/character/SparrowCharacter.hx      | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx
index ed58b92b5..5b126db6c 100644
--- a/source/funkin/play/character/AnimateAtlasCharacter.hx
+++ b/source/funkin/play/character/AnimateAtlasCharacter.hx
@@ -131,7 +131,7 @@ class AnimateAtlasCharacter extends BaseCharacter
   {
     trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
 
-    var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath, 'shared'));
+    var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath));
 
     sprite.onAnimationFinish.removeAll();
     sprite.onAnimationFinish.add(this.onAnimationFinished);
diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx
index 48c5afb58..48a0ec632 100644
--- a/source/funkin/play/character/MultiSparrowCharacter.hx
+++ b/source/funkin/play/character/MultiSparrowCharacter.hx
@@ -60,7 +60,7 @@ class MultiSparrowCharacter extends BaseCharacter
       }
     }
 
-    var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared');
+    var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath);
 
     if (texture == null)
     {
@@ -74,7 +74,7 @@ class MultiSparrowCharacter extends BaseCharacter
 
     for (asset in assetList)
     {
-      var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared');
+      var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset);
       // If we don't do this, the unused textures will be removed as soon as they're loaded.
 
       if (subTexture == null)
diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx
index 2bfac800a..b7dda424f 100644
--- a/source/funkin/play/character/PackerCharacter.hx
+++ b/source/funkin/play/character/PackerCharacter.hx
@@ -30,7 +30,7 @@ class PackerCharacter extends BaseCharacter
   {
     trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
 
-    var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared');
+    var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath);
     if (tex == null)
     {
       trace('Could not load Packer sprite: ${_data.assetPath}');
diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx
index a36aed84d..90a695ec3 100644
--- a/source/funkin/play/character/SparrowCharacter.hx
+++ b/source/funkin/play/character/SparrowCharacter.hx
@@ -33,7 +33,7 @@ class SparrowCharacter extends BaseCharacter
   {
     trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
 
-    var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
+    var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
     if (tex == null)
     {
       trace('Could not load Sparrow sprite: ${_data.assetPath}');

From f8a0627fd270745ba102e8c6a4b0a223e8cf9d3e Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Tue, 16 Jul 2024 20:53:12 +0300
Subject: [PATCH 35/47] goodbye scripts, hello notestyles!

---
 assets                                      |   2 +-
 source/funkin/play/Countdown.hx             | 101 +++++++++++---------
 source/funkin/play/components/PopUpStuff.hx |  75 +++++++--------
 source/funkin/ui/transition/LoadingState.hx |  73 +++++++-------
 source/funkin/util/Constants.hx             |   5 +
 5 files changed, 132 insertions(+), 124 deletions(-)

diff --git a/assets b/assets
index e414ee618..ca4d97554 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit e414ee618b4fa577dc3c3df4e156488e91c6348d
+Subproject commit ca4d97554f9e3853fda25733f21a5fa108c0b902
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 7686e5b75..b97451fa1 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -11,6 +11,8 @@ import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
 import flixel.util.FlxTimer;
 import funkin.audio.FunkinSound;
 import openfl.utils.Assets;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
 
 class Countdown
 {
@@ -20,20 +22,13 @@ class Countdown
   public static var countdownStep(default, null):CountdownStep = BEFORE;
 
   /**
-   * Which alternate countdown sound effect to use.
-   * You can set this via script.
-   * For example, in Week 6 it is `-pixel`.
+   * Which alternate graphic/sound on countdown to use.
+   * This is set via the current notestyle.
+   * For example, in Week 6 it is `pixel`.
    */
-  public static var soundSuffix:String = '';
-
-  /**
-   * Which alternate graphic on countdown to use.
-   * You can set this via script.
-   * For example, in Week 6 it is `-pixel`.
-   */
-  public static var graphicSuffix:String = '';
-
+  static var noteStyle:NoteStyle;
 
+  static var isPixel:Bool = false;
 
   /**
    * The currently running countdown. This will be null if there is no countdown running.
@@ -64,6 +59,11 @@ class Countdown
     // @:privateAccess
     // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
+    var fetchedNoteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(PlayState.instance.currentChart.noteStyle);
+    if (fetchedNoteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
+    else noteStyle = fetchedNoteStyle;
+    if (noteStyle._data.assets.note.isPixel) isPixel = true;
+
     // The timer function gets called based on the beat of the song.
     countdownTimer = new FlxTimer();
 
@@ -81,10 +81,10 @@ class Countdown
       // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
       // Countdown graphic.
-      showCountdownGraphic(countdownStep, graphicSuffix.toLowerCase().contains('pixel'));
+      showCountdownGraphic(countdownStep, noteStyle, isPixel);
 
       // Countdown sound.
-      playCountdownSound(countdownStep);
+      playCountdownSound(countdownStep, noteStyle);
 
       // Event handling bullshit.
       var cancelled:Bool = propagateCountdownEvent(countdownStep);
@@ -197,17 +197,31 @@ class Countdown
    */
   public static function reset()
   {
-    soundSuffix = '';
-    graphicSuffix = '';
+    noteStyle = NoteStyleRegistry.instance.fetchDefault();
+    isPixel = false;
   }
 
   /**
    * Retrieves the graphic to use for this step of the countdown.
    */
-  public static function showCountdownGraphic(index:CountdownStep, isGraphicPixel:Bool):Void
+  public static function showCountdownGraphic(index:CountdownStep, noteStyle:NoteStyle, isGraphicPixel:Bool):Void
   {
+    var indexString:String = null;
+    switch (index)
+    {
+      case TWO:
+        indexString = 'ready';
+      case ONE:
+        indexString = 'set';
+      case GO:
+        indexString = 'go';
+      default:
+        // null
+    }
+    if (indexString == null) return;
+
     var spritePath:String = null;
-    spritePath = resolveGraphicPath(graphicSuffix, index);
+    spritePath = resolveGraphicPath(noteStyle, indexString);
 
     if (spritePath == null) return;
 
@@ -215,12 +229,15 @@ class Countdown
     countdownSprite.scrollFactor.set(0, 0);
 
     if (isGraphicPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
+    else countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.7));
 
     countdownSprite.antialiasing = !isGraphicPixel;
 
     countdownSprite.updateHitbox();
     countdownSprite.screenCenter();
 
+    countdownSprite.cameras = [PlayState.instance.camHUD];
+
     // Fade sprite in, then out, then destroy it.
     FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
       {
@@ -233,29 +250,18 @@ class Countdown
     PlayState.instance.add(countdownSprite);
   }
 
-  static function resolveGraphicPath(suffix:String, index:CountdownStep):Null<String>
+  static function resolveGraphicPath(noteStyle:NoteStyle, index:String):Null<String>
   {
     var basePath:String = 'ui/countdown/';
-    var indexString:String = null;
-    switch (index)
+
+    var spritePath:String = basePath + noteStyle.id + '/$index';
+    // If nothing is found, revert it to default notestyle skin
+    if (!Assets.exists(Paths.image(spritePath)))
     {
-      case TWO:
-        indexString = 'ready';
-      case ONE:
-        indexString = 'set';
-      case GO:
-        indexString = 'go';
-      default:
-        // null
+      if (!isPixel) spritePath = basePath + Constants.DEFAULT_NOTE_STYLE + '/$index';
+      else spritePath = basePath + Constants.DEFAULT_PIXEL_NOTE_STYLE + '/$index';
     }
-    basePath += indexString;
-    var spritePath:String = basePath + suffix;
-    while (!Assets.exists(Paths.image(spritePath)) && suffix.length > 0)
-    {
-      suffix = suffix.split('-').slice(0, -1).join('-');
-      spritePath = basePath + suffix;
-    }
-    if (!Assets.exists(Paths.image(spritePath))) return null;
+
     trace('Resolved sprite path: ' + Paths.image(spritePath));
     return spritePath;
   }
@@ -263,23 +269,24 @@ class Countdown
   /**
    * Retrieves the sound file to use for this step of the countdown.
    */
-  public static function playCountdownSound(index:CountdownStep):Void
+  public static function playCountdownSound(step:CountdownStep, noteStyle:NoteStyle):Void
   {
-    FunkinSound.playOnce(resolveSoundPath(soundSuffix, index), Constants.COUNTDOWN_VOLUME);
+    return FunkinSound.playOnce(Paths.sound(resolveSoundPath(noteStyle, step)), Constants.COUNTDOWN_VOLUME);
   }
 
-  static function resolveSoundPath(suffix:String, step:CountdownStep):Null<String>
+  static function resolveSoundPath(noteStyle:NoteStyle, step:CountdownStep):Null<String>
   {
-    var basePath:String = 'gameplay/countdown/intro';
-    if (step != CountdownStep.BEFORE || step != CountdownStep.AFTER) basePath += step;
+    if (step == CountdownStep.BEFORE || step == CountdownStep.AFTER) return null;
+    var basePath:String = 'gameplay/countdown/';
 
-    var soundPath:String = Paths.sound(basePath + suffix);
-    while (!Assets.exists(soundPath) && suffix.length > 0)
+    var soundPath:String = basePath + noteStyle.id + '/intro$step';
+    // If nothing is found, revert it to default notestyle sound
+    if (!Assets.exists(Paths.sound(soundPath)))
     {
-      suffix = suffix.split('-').slice(0, -1).join('-');
-      soundPath = Paths.sound(basePath + suffix);
+      if (!isPixel) soundPath = basePath + Constants.DEFAULT_NOTE_STYLE + '/intro$step';
+      else soundPath = basePath + Constants.DEFAULT_PIXEL_NOTE_STYLE + '/intro$step';
     }
-    if (!Assets.exists(soundPath)) return null;
+
     trace('Resolved sound path: ' + soundPath);
     return soundPath;
   }
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index 0a4d6b019..eb59a9922 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -8,6 +8,8 @@ import funkin.graphics.FunkinSprite;
 import funkin.play.PlayState;
 import funkin.util.TimerUtil;
 import openfl.utils.Assets;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
 
 class PopUpStuff extends FlxTypedGroup<FlxSprite>
 {
@@ -15,31 +17,35 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
   /**
    * Which alternate graphic on popup to use.
-   * You can set this via script.
-   * For example, in Week 6 it is `-pixel`.
+   * This is set via the current notestyle.
+   * For example, in Week 6 it is `pixel`.
    */
-  public static var graphicSuffix:String = '';
+  static var noteStyle:NoteStyle;
+
+  static var isPixel:Bool = false;
 
   override public function new()
   {
     super();
+
+    var fetchedNoteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(PlayState.instance.currentChart.noteStyle);
+    if (fetchedNoteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
+    else noteStyle = fetchedNoteStyle;
+    if (noteStyle._data.assets.note.isPixel) isPixel = true;
   }
 
-  static function resolveGraphicPath(suffix:String, index:String):Null<String>
+  static function resolveGraphicPath(noteStyle:NoteStyle, index:String):Null<String>
   {
-    var folder:String;
-    if (suffix != '')
-      folder = suffix.substring(0, suffix.indexOf("-")) + suffix.substring(suffix.indexOf("-") + 1);
-    else
-      folder = 'normal';
-    var basePath:String = 'gameplay/popup/$folder/$index';
-    var spritePath:String = basePath + suffix;
-    while (!Assets.exists(Paths.image(spritePath)) && suffix.length > 0)
+    var basePath:String = 'ui/popup/';
+
+    var spritePath:String = basePath + noteStyle.id + '/$index';
+    // If nothing is found, revert it to default notestyle skin
+    if (!Assets.exists(Paths.image(spritePath)))
     {
-      suffix = suffix.split('-').slice(0, -1).join('-');
-      spritePath = basePath + suffix;
+      if (!isPixel) spritePath = basePath + Constants.DEFAULT_NOTE_STYLE + '/$index';
+      else spritePath = basePath + Constants.DEFAULT_PIXEL_NOTE_STYLE + '/$index';
     }
-    if (!Assets.exists(Paths.image(spritePath))) return null;
+
     return spritePath;
   }
 
@@ -49,7 +55,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     if (daRating == null) daRating = "good";
 
-    var ratingPath:String = resolveGraphicPath(graphicSuffix, daRating);
+    var ratingPath:String = resolveGraphicPath(noteStyle, daRating);
 
     //if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
 
@@ -66,7 +72,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     add(rating);
 
-    if (graphicSuffix.toLowerCase().contains('pixel'))
+    if (isPixel)
     {
       rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
       rating.antialiasing = false;
@@ -99,7 +105,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     if (combo == null) combo = 0;
 
-    var comboPath:String = resolveGraphicPath(graphicSuffix, Std.string(combo));
+    var comboPath:String = resolveGraphicPath(noteStyle, 'combo');
     var comboSpr:FunkinSprite = FunkinSprite.create(comboPath);
     comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1];
     comboSpr.x = (FlxG.width * 0.507) + offsets[0];
@@ -111,16 +117,10 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     // add(comboSpr);
 
-    if (graphicSuffix.toLowerCase().contains('pixel'))
-    {
-      comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7));
-      comboSpr.antialiasing = false;
-    }
-    else
-    {
-      comboSpr.setGraphicSize(Std.int(comboSpr.width * 0.7));
-      comboSpr.antialiasing = true;
-    }
+    if (isPixel) comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7));
+    else comboSpr.setGraphicSize(Std.int(comboSpr.width * 0.7));
+
+    comboSpr.antialiasing = !isPixel;
     comboSpr.updateHitbox();
 
     FlxTween.tween(comboSpr, {alpha: 0}, 0.2,
@@ -148,18 +148,12 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
     var daLoop:Int = 1;
     for (i in seperatedScore)
     {
-      var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, resolveGraphicPath(graphicSuffix, 'num' + Std.int(i)));
+      var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, resolveGraphicPath(noteStyle, 'num' + Std.int(i)));
 
-      if (graphicSuffix.toLowerCase().contains('pixel'))
-      {
-        numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 0.7));
-        numScore.antialiasing = false;
-      }
-      else
-      {
-        numScore.setGraphicSize(Std.int(numScore.width * 0.45));
-        numScore.antialiasing = true;
-      }
+      if (isPixel) numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 0.7));
+      else numScore.setGraphicSize(Std.int(numScore.width * 0.45));
+
+      numScore.antialiasing = !isPixel;
       numScore.updateHitbox();
 
       numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90;
@@ -191,6 +185,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
    */
   public static function reset()
   {
-    graphicSuffix = '';
+    noteStyle = NoteStyleRegistry.instance.fetchDefault();
+    isPixel = false;
   }
 }
diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index a571126fe..e50032d65 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -291,46 +291,47 @@ class LoadingState extends MusicBeatSubState
     FunkinSprite.preparePurgeCache();
     FunkinSprite.cacheTexture(Paths.image('healthBar'));
     FunkinSprite.cacheTexture(Paths.image('menuDesat'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/combo'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num0'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num1'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num2'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num3'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num4'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num5'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num6'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num7'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num8'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/num9'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/combo-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num0-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num1-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num2-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num3-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num4-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num5-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num6-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num7-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num8-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/num9-pixel'));
+    // Lord have mercy on me and this caching -anysad
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/combo'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num0'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num1'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num2'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num3'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num4'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num5'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num6'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num7'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num8'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num9'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/combo'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num0'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num1'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num2'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num3'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num4'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num5'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num6'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9'));
     FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
-    FunkinSprite.cacheTexture(Paths.image('ui/countdown/ready', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('ui/countdown/set', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('ui/countdown/go', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('ui/countdown/ready-pixel', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('ui/countdown/set-pixel', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('ui/countdown/go-pixel', 'shared'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/sick'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/good'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/bad'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/normal/shit'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/sick-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/good-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/bad-pixel'));
-    FunkinSprite.cacheTexture(Paths.image('gameplay/popup/pixel/shit-pixel'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/ready', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/set', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/go', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/ready', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/set', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/go', 'shared'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/sick'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/good'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/bad'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/shit'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/sick'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/good'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad'));
+    FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/shit'));
     FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
 
     // List all image assets in the level's library.
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index 1e0978839..79b0e05c5 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -258,6 +258,11 @@ class Constants
    */
   public static final DEFAULT_NOTE_STYLE:String = 'funkin';
 
+  /**
+   * The default pixel note style for songs.
+   */
+  public static final DEFAULT_PIXEL_NOTE_STYLE:String = 'pixel';
+
   /**
    * The default album for songs in Freeplay.
    */

From 5e1d25e791a14f6b301730043607e14b53f36e8d Mon Sep 17 00:00:00 2001
From: AppleHair <95587502+AppleHair@users.noreply.github.com>
Date: Tue, 16 Jul 2024 21:17:53 +0300
Subject: [PATCH 36/47] [BUGFIX] Prevented infinite recursion on freeplay when
 a song doesn't have the `normal` difficulty

Any attempt to create a song without the `normal` difficulty will result in a silent crash when opening freeplay. After some debugging, I discovered that the issue is caused by infinite recursion, which gets triggered at the start of `FreeplaySongData`'s `updateValues`.
---
 source/funkin/ui/freeplay/FreeplayState.hx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 0caaf4591..9873f3be6 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -2077,7 +2077,11 @@ class FreeplaySongData
   function updateValues(variations:Array<String>):Void
   {
     this.songDifficulties = song.listDifficulties(null, variations, false, false);
-    if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+    if (!this.songDifficulties.contains(currentDifficulty) && currentDifficulty != Constants.DEFAULT_DIFFICULTY)
+    {
+      currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+      return;
+    }
 
     var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations);
     if (songDifficulty == null) return;

From ee449819959ed51b5719f7ebd39493118c307eee Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Tue, 16 Jul 2024 22:02:05 +0300
Subject: [PATCH 37/47] you know what, make countdown sprites a little bigger

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

diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index b97451fa1..ccd478347 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -228,8 +228,8 @@ class Countdown
     var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
     countdownSprite.scrollFactor.set(0, 0);
 
-    if (isGraphicPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
-    else countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.7));
+    if (isGraphicPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE * 1.1));
+    else countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.8));
 
     countdownSprite.antialiasing = !isGraphicPixel;
 

From a93cd05aeb6c30c241c6c8f8211abb953bb65532 Mon Sep 17 00:00:00 2001
From: AppleHair <95587502+AppleHair@users.noreply.github.com>
Date: Tue, 16 Jul 2024 22:36:11 +0300
Subject: [PATCH 38/47] Added a comment

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

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 9873f3be6..51fda0677 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -2080,6 +2080,8 @@ class FreeplaySongData
     if (!this.songDifficulties.contains(currentDifficulty) && currentDifficulty != Constants.DEFAULT_DIFFICULTY)
     {
       currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+      // This method gets called again by the setter-method,
+      // so there's no need to continue.
       return;
     }
 

From 691da783fc41fc648b760833d4439c4368ef29a3 Mon Sep 17 00:00:00 2001
From: AppleHair <95587502+AppleHair@users.noreply.github.com>
Date: Wed, 17 Jul 2024 10:20:48 +0300
Subject: [PATCH 39/47] Updated to extend #2712

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

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 51fda0677..a22652586 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -2077,11 +2077,11 @@ class FreeplaySongData
   function updateValues(variations:Array<String>):Void
   {
     this.songDifficulties = song.listDifficulties(null, variations, false, false);
-    if (!this.songDifficulties.contains(currentDifficulty) && currentDifficulty != Constants.DEFAULT_DIFFICULTY)
+    if (!this.songDifficulties.contains(currentDifficulty))
     {
       currentDifficulty = Constants.DEFAULT_DIFFICULTY;
-      // This method gets called again by the setter-method,
-      // so there's no need to continue.
+      // This method gets called again by the setter-method
+      // or the difficulty didn't change, so there's no need to continue.
       return;
     }
 

From 4d81aa0fe696a1878d3678209e20c985e1631425 Mon Sep 17 00:00:00 2001
From: AppleHair <95587502+AppleHair@users.noreply.github.com>
Date: Wed, 17 Jul 2024 18:00:07 +0300
Subject: [PATCH 40/47] [BUGFIX] Ensure the variation used for the next song is
 valid.

When playing in story mode, `PlayState` assumes all the songs use the same variation, which is usually true, but not guaranteed. This PR makes `PlayState` use the `getFirstValidVariation` method to find the valid variation instead of using the `currentVariation` variable, which might not be valid.
---
 source/funkin/play/PlayState.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f55cef388..9b1cc2ed9 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2963,7 +2963,7 @@ class PlayState extends MusicBeatSubState
               {
                 targetSong: targetSong,
                 targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-                targetVariation: currentVariation,
+                targetVariation: targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty),
                 cameraFollowPoint: cameraFollowPoint.getPosition(),
               });
           });
@@ -2975,7 +2975,7 @@ class PlayState extends MusicBeatSubState
             {
               targetSong: targetSong,
               targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-              targetVariation: currentVariation,
+              targetVariation: targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty),
               cameraFollowPoint: cameraFollowPoint.getPosition(),
             });
         }

From ff1ab1eb4245092829701e322c1b4dbe27f80590 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Wed, 17 Jul 2024 23:19:18 +0300
Subject: [PATCH 41/47] welcome fallback note styles!

---
 source/funkin/play/Countdown.hx               | 53 ++++++++++++++-----
 source/funkin/play/components/PopUpStuff.hx   | 21 +++++++-
 .../funkin/play/notes/notestyle/NoteStyle.hx  |  2 +-
 3 files changed, 59 insertions(+), 17 deletions(-)

diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index ccd478347..25a896317 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -28,6 +28,8 @@ class Countdown
    */
   static var noteStyle:NoteStyle;
 
+  static var fallbackNoteStyle:Null<NoteStyle>;
+
   static var isPixel:Bool = false;
 
   /**
@@ -59,10 +61,7 @@ class Countdown
     // @:privateAccess
     // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
-    var fetchedNoteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(PlayState.instance.currentChart.noteStyle);
-    if (fetchedNoteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
-    else noteStyle = fetchedNoteStyle;
-    if (noteStyle._data.assets.note.isPixel) isPixel = true;
+    fetchNoteStyle();
 
     // The timer function gets called based on the beat of the song.
     countdownTimer = new FlxTimer();
@@ -81,7 +80,7 @@ class Countdown
       // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
       // Countdown graphic.
-      showCountdownGraphic(countdownStep, noteStyle, isPixel);
+      showCountdownGraphic(countdownStep, noteStyle);
 
       // Countdown sound.
       playCountdownSound(countdownStep, noteStyle);
@@ -201,10 +200,19 @@ class Countdown
     isPixel = false;
   }
 
+  static function fetchNoteStyle():Void
+  {
+    var fetchedNoteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(PlayState.instance.currentChart.noteStyle);
+    if (fetchedNoteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
+    else noteStyle = fetchedNoteStyle;
+    fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
+    isPixel = false;
+  }
+
   /**
    * Retrieves the graphic to use for this step of the countdown.
    */
-  public static function showCountdownGraphic(index:CountdownStep, noteStyle:NoteStyle, isGraphicPixel:Bool):Void
+  public static function showCountdownGraphic(index:CountdownStep, noteStyle:NoteStyle):Void
   {
     var indexString:String = null;
     switch (index)
@@ -228,16 +236,16 @@ class Countdown
     var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
     countdownSprite.scrollFactor.set(0, 0);
 
-    if (isGraphicPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE * 1.1));
+    if (isPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE * 1.1));
     else countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.8));
 
-    countdownSprite.antialiasing = !isGraphicPixel;
+    countdownSprite.antialiasing = !isPixel;
+
+    countdownSprite.cameras = [PlayState.instance.camHUD];
 
     countdownSprite.updateHitbox();
     countdownSprite.screenCenter();
 
-    countdownSprite.cameras = [PlayState.instance.camHUD];
-
     // Fade sprite in, then out, then destroy it.
     FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
       {
@@ -252,10 +260,18 @@ class Countdown
 
   static function resolveGraphicPath(noteStyle:NoteStyle, index:String):Null<String>
   {
+    fetchNoteStyle();
     var basePath:String = 'ui/countdown/';
-
     var spritePath:String = basePath + noteStyle.id + '/$index';
-    // If nothing is found, revert it to default notestyle skin
+
+    while (!Assets.exists(Paths.image(spritePath)) && fallbackNoteStyle != null) {
+      noteStyle = fallbackNoteStyle;
+      fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
+      spritePath = basePath + noteStyle.id + '/$index';
+    }
+    if (noteStyle.isHoldNotePixel()) isPixel = true;
+
+    // If ABSOLUTELY nothing is found, revert it to default notestyle skin
     if (!Assets.exists(Paths.image(spritePath)))
     {
       if (!isPixel) spritePath = basePath + Constants.DEFAULT_NOTE_STYLE + '/$index';
@@ -277,10 +293,19 @@ class Countdown
   static function resolveSoundPath(noteStyle:NoteStyle, step:CountdownStep):Null<String>
   {
     if (step == CountdownStep.BEFORE || step == CountdownStep.AFTER) return null;
+    fetchNoteStyle();
     var basePath:String = 'gameplay/countdown/';
-
     var soundPath:String = basePath + noteStyle.id + '/intro$step';
-    // If nothing is found, revert it to default notestyle sound
+
+    while (!Assets.exists(Paths.sound(soundPath)) && fallbackNoteStyle != null)
+    {
+      noteStyle = fallbackNoteStyle;
+      fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
+      soundPath = basePath + noteStyle.id + '/intro$step';
+    }
+    if (noteStyle.isHoldNotePixel()) isPixel = true;
+
+    // If ABSOLUTELY nothing is found, revert it to default notestyle sound
     if (!Assets.exists(Paths.sound(soundPath)))
     {
       if (!isPixel) soundPath = basePath + Constants.DEFAULT_NOTE_STYLE + '/intro$step';
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index eb59a9922..6c111c0db 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -22,23 +22,40 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
    */
   static var noteStyle:NoteStyle;
 
+  static var fallbackNoteStyle:Null<NoteStyle>;
+
   static var isPixel:Bool = false;
 
   override public function new()
   {
     super();
 
+    fetchNoteStyle();
+  }
+
+  static function fetchNoteStyle():Void
+  {
     var fetchedNoteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(PlayState.instance.currentChart.noteStyle);
     if (fetchedNoteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
     else noteStyle = fetchedNoteStyle;
-    if (noteStyle._data.assets.note.isPixel) isPixel = true;
+    fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
+    isPixel = false;
   }
 
   static function resolveGraphicPath(noteStyle:NoteStyle, index:String):Null<String>
   {
+    fetchNoteStyle();
     var basePath:String = 'ui/popup/';
-
     var spritePath:String = basePath + noteStyle.id + '/$index';
+
+    while (!Assets.exists(Paths.image(spritePath)) && fallbackNoteStyle != null)
+    {
+      noteStyle = fallbackNoteStyle;
+      fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
+      spritePath = basePath + noteStyle.id + '/$index';
+    }
+    if (noteStyle.isHoldNotePixel()) isPixel = true;
+
     // If nothing is found, revert it to default notestyle skin
     if (!Assets.exists(Paths.image(spritePath)))
     {
diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx
index d0cc09f6a..edfcff2ee 100644
--- a/source/funkin/play/notes/notestyle/NoteStyle.hx
+++ b/source/funkin/play/notes/notestyle/NoteStyle.hx
@@ -72,7 +72,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
    * Get the note style ID of the parent note style.
    * @return The string ID, or `null` if there is no parent.
    */
-  function getFallbackID():Null<String>
+  public function getFallbackID():Null<String>
   {
     return _data.fallback;
   }

From ba96b111960e2945c14eef0112368a30152d1e7c Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Thu, 18 Jul 2024 17:56:54 +0300
Subject: [PATCH 42/47] hopefully final polish?

---
 source/funkin/play/Countdown.hx             | 26 ++++++++++-----------
 source/funkin/play/components/PopUpStuff.hx | 10 ++++----
 2 files changed, 16 insertions(+), 20 deletions(-)

diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 25a896317..1a9605597 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -61,8 +61,6 @@ class Countdown
     // @:privateAccess
     // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
-    fetchNoteStyle();
-
     // The timer function gets called based on the beat of the song.
     countdownTimer = new FlxTimer();
 
@@ -80,10 +78,10 @@ class Countdown
       // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
 
       // Countdown graphic.
-      showCountdownGraphic(countdownStep, noteStyle);
+      showCountdownGraphic(countdownStep);
 
       // Countdown sound.
-      playCountdownSound(countdownStep, noteStyle);
+      playCountdownSound(countdownStep);
 
       // Event handling bullshit.
       var cancelled:Bool = propagateCountdownEvent(countdownStep);
@@ -212,7 +210,7 @@ class Countdown
   /**
    * Retrieves the graphic to use for this step of the countdown.
    */
-  public static function showCountdownGraphic(index:CountdownStep, noteStyle:NoteStyle):Void
+  public static function showCountdownGraphic(index:CountdownStep):Void
   {
     var indexString:String = null;
     switch (index)
@@ -229,25 +227,24 @@ class Countdown
     if (indexString == null) return;
 
     var spritePath:String = null;
-    spritePath = resolveGraphicPath(noteStyle, indexString);
+    spritePath = resolveGraphicPath(indexString);
 
     if (spritePath == null) return;
 
     var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
     countdownSprite.scrollFactor.set(0, 0);
 
-    if (isPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE * 1.1));
-    else countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.8));
+    if (isPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
+    else countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.7));
 
     countdownSprite.antialiasing = !isPixel;
 
     countdownSprite.cameras = [PlayState.instance.camHUD];
 
     countdownSprite.updateHitbox();
-    countdownSprite.screenCenter();
 
     // Fade sprite in, then out, then destroy it.
-    FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
+    FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
       {
         ease: FlxEase.cubeInOut,
         onComplete: function(twn:FlxTween) {
@@ -256,9 +253,10 @@ class Countdown
       });
 
     PlayState.instance.add(countdownSprite);
+    countdownSprite.screenCenter();
   }
 
-  static function resolveGraphicPath(noteStyle:NoteStyle, index:String):Null<String>
+  static function resolveGraphicPath(index:String):Null<String>
   {
     fetchNoteStyle();
     var basePath:String = 'ui/countdown/';
@@ -285,12 +283,12 @@ class Countdown
   /**
    * Retrieves the sound file to use for this step of the countdown.
    */
-  public static function playCountdownSound(step:CountdownStep, noteStyle:NoteStyle):Void
+  public static function playCountdownSound(step:CountdownStep):Void
   {
-    return FunkinSound.playOnce(Paths.sound(resolveSoundPath(noteStyle, step)), Constants.COUNTDOWN_VOLUME);
+    return FunkinSound.playOnce(Paths.sound(resolveSoundPath(step)), Constants.COUNTDOWN_VOLUME);
   }
 
-  static function resolveSoundPath(noteStyle:NoteStyle, step:CountdownStep):Null<String>
+  static function resolveSoundPath(step:CountdownStep):Null<String>
   {
     if (step == CountdownStep.BEFORE || step == CountdownStep.AFTER) return null;
     fetchNoteStyle();
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index 6c111c0db..402535878 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -29,8 +29,6 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
   override public function new()
   {
     super();
-
-    fetchNoteStyle();
   }
 
   static function fetchNoteStyle():Void
@@ -42,7 +40,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
     isPixel = false;
   }
 
-  static function resolveGraphicPath(noteStyle:NoteStyle, index:String):Null<String>
+  static function resolveGraphicPath(index:String):Null<String>
   {
     fetchNoteStyle();
     var basePath:String = 'ui/popup/';
@@ -72,7 +70,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     if (daRating == null) daRating = "good";
 
-    var ratingPath:String = resolveGraphicPath(noteStyle, daRating);
+    var ratingPath:String = resolveGraphicPath(daRating);
 
     //if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
 
@@ -122,7 +120,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
 
     if (combo == null) combo = 0;
 
-    var comboPath:String = resolveGraphicPath(noteStyle, 'combo');
+    var comboPath:String = resolveGraphicPath('combo');
     var comboSpr:FunkinSprite = FunkinSprite.create(comboPath);
     comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1];
     comboSpr.x = (FlxG.width * 0.507) + offsets[0];
@@ -165,7 +163,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
     var daLoop:Int = 1;
     for (i in seperatedScore)
     {
-      var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, resolveGraphicPath(noteStyle, 'num' + Std.int(i)));
+      var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, resolveGraphicPath('num' + Std.int(i)));
 
       if (isPixel) numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 0.7));
       else numScore.setGraphicSize(Std.int(numScore.width * 0.45));

From c8caeb42ded0be5292680db20c5cb99b2a37bf46 Mon Sep 17 00:00:00 2001
From: AppleHair <95587502+AppleHair@users.noreply.github.com>
Date: Tue, 23 Jul 2024 11:34:26 +0300
Subject: [PATCH 43/47] A fix requested by Eric

---
 source/funkin/play/PlayState.hx | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 9b1cc2ed9..20b8d784b 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2959,11 +2959,16 @@ class PlayState extends MusicBeatSubState
           FunkinSound.playOnce(Paths.sound('Lights_Shut_off'), function() {
             // no camFollow so it centers on horror tree
             var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
+            var targetVariation:String = currentVariation;
+            if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation))
+            {
+              targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION;
+            }
             LoadingState.loadPlayState(
               {
                 targetSong: targetSong,
                 targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-                targetVariation: targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty),
+                targetVariation: targetVariation,
                 cameraFollowPoint: cameraFollowPoint.getPosition(),
               });
           });
@@ -2971,11 +2976,16 @@ class PlayState extends MusicBeatSubState
         else
         {
           var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
+          var targetVariation:String = currentVariation;
+          if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation))
+          {
+            targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION;
+          }
           LoadingState.loadPlayState(
             {
               targetSong: targetSong,
               targetDifficulty: PlayStatePlaylist.campaignDifficulty,
-              targetVariation: targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty),
+              targetVariation: targetVariation,
               cameraFollowPoint: cameraFollowPoint.getPosition(),
             });
         }

From 8778ab1d0ec46556338e60fe0eb84c6d26064d7f Mon Sep 17 00:00:00 2001
From: Burgerballs <107233412+Burgerballs@users.noreply.github.com>
Date: Tue, 23 Jul 2024 18:09:39 +0100
Subject: [PATCH 44/47] Update PlayState.hx

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

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f55cef388..f82fd791e 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2033,7 +2033,7 @@ class PlayState extends MusicBeatSubState
 
     vocals.pause();
 
-    FlxG.sound.music.play(FlxG.sound.music.time);
+    FlxG.sound.music.play(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
 
     vocals.time = FlxG.sound.music.time;
     vocals.play(false, FlxG.sound.music.time);

From 964f6878c3fb8d13f4faf6723d299997edceaccf Mon Sep 17 00:00:00 2001
From: Burgerballs <107233412+Burgerballs@users.noreply.github.com>
Date: Tue, 23 Jul 2024 19:02:09 +0100
Subject: [PATCH 45/47] Update PlayState.hx

---
 source/funkin/play/PlayState.hx | 32 +++++++++++++++++---------------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f82fd791e..07e4f0b1d 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1404,17 +1404,6 @@ class PlayState extends MusicBeatSubState
 
     if (isGamePaused) return false;
 
-    if (!startingSong
-      && FlxG.sound.music != null
-      && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
-        || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
-    {
-      trace("VOCALS NEED RESYNC");
-      if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
-      trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
-      resyncVocals();
-    }
-
     if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
     if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
 
@@ -1436,6 +1425,17 @@ class PlayState extends MusicBeatSubState
       // activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
     }
 
+    if (!startingSong
+      && FlxG.sound.music != null
+      && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100
+        || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100))
+    {
+      trace("VOCALS NEED RESYNC");
+      if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
+      trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
+      resyncVocals();
+    }
+
     // Only bop camera if zoom level is below 135%
     if (Preferences.zoomCamera
       && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
@@ -2030,13 +2030,15 @@ class PlayState extends MusicBeatSubState
 
     // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
     if (!FlxG.sound.music.playing) return;
-
+    var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
+    FlxG.sound.music.pause();
     vocals.pause();
 
-    FlxG.sound.music.play(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
+    FlxG.sound.music.time = timeToPlayAt;
+    FlxG.sound.music.play(false, timeToPlayAt);
 
-    vocals.time = FlxG.sound.music.time;
-    vocals.play(false, FlxG.sound.music.time);
+    vocals.time = timeToPlayAt;
+    vocals.play(false, timeToPlayAt);
   }
 
   /**

From 16905c8210b0ada422d2552d3908f70cf9c6a2d3 Mon Sep 17 00:00:00 2001
From: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
Date: Sun, 8 Sep 2024 21:48:48 +0000
Subject: [PATCH 46/47] Update ScriptedFunkinSprite.hx

---
 source/funkin/modding/base/ScriptedFunkinSprite.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/modding/base/ScriptedFunkinSprite.hx b/source/funkin/modding/base/ScriptedFunkinSprite.hx
index dd8d15007..2ce84db3e 100644
--- a/source/funkin/modding/base/ScriptedFunkinSprite.hx
+++ b/source/funkin/modding/base/ScriptedFunkinSprite.hx
@@ -1,8 +1,8 @@
 package funkin.modding.base;
 
 /**
- * A script that can be tied to an FlxSprite.
- * Create a scripted class that extends FlxSprite to use this.
+ * A script that can be tied to a FunkinSprite.
+ * Create a scripted class that extends FunkinSprite to use this.
  */
 @:hscriptClass
 class ScriptedFunkinSprite extends funkin.graphics.FunkinSprite implements HScriptedClass {}

From f8d0ddef3456b31f729d9110f106b0594baaaf4a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 16 Sep 2024 18:17:07 -0400
Subject: [PATCH 47/47] Update troubleshooting guide

---
 docs/troubleshooting.md | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 3b93bab64..54ba396c4 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -1,4 +1,4 @@
-# Troubleshooting Common Issues
+# Troubleshooting Common Compilation Issues
 
 - Weird macro error with a very tall call stack: Restart Visual Studio Code
   - NOTE: This is caused by Polymod somewhere, and seems to only occur when there is another compile error somewhere in the program. There is a bounty up for it.
@@ -13,3 +13,11 @@
 
 - `LINK : fatal error LNK1201: error writing to program database ''; check for insufficient disk space, invalid path, or insufficient privilege`
   - This error occurs if the PDB file located in your `export` folder is in use or exceeds 4 GB. Try deleting the `export` folder and building again from scratch.
+
+- `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)`
+  - This error can happen during cloning as a result of poor network connectivity. A common fix is to run ` git config --global http.postBuffer 4096M` in your terminal.
+
+- Repository is missing an `assets` folder, or `assets` folder is empty.
+  - You did not clone the repository correctly! Copy the path to your `funkin` folder and run `cd the\path\you\copied`. Then follow the compilation guide starting from **Step 4**.
+
+- Other compilation issues may be caused by installing bad library versions. Try deleting the `.haxelib` folder and following the guide starting from **Step 5**.