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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

From 4e47bfe68b056656c24355c2ff90042db3a62744 Mon Sep 17 00:00:00 2001
From: Keoiki <55053690+Keoiki@users.noreply.github.com>
Date: Tue, 21 May 2024 23:52:40 +0300
Subject: [PATCH 11/30] Fix Note Styles in PlayState & Chart Editor

---
 source/funkin/play/PlayState.hx                            | 7 +------
 .../debug/charting/toolboxes/ChartEditorMetadataToolbox.hx | 2 ++
 2 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 43dd485cf..dc07e1910 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1730,12 +1730,7 @@ class PlayState extends MusicBeatSubState
    */
   function initStrumlines():Void
   {
-    var noteStyleId:String = switch (currentStageId)
-    {
-      case 'school': 'pixel';
-      case 'schoolEvil': 'pixel';
-      default: Constants.DEFAULT_NOTE_STYLE;
-    }
+    var noteStyleId:String = currentChart.noteStyle;
     var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
     if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
 
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index f85307c64..98e263aaf 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -104,6 +104,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
       if (event.data?.id == null) return;
       chartEditorState.currentSongNoteStyle = event.data.id;
     };
+    var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, chartEditorState.currentSongMetadata.playData.noteStyle);
+    inputNoteStyle.value = startingValueNoteStyle;
 
     inputBPM.onChange = function(event:UIEvent) {
       if (event.value == null || event.value <= 0) return;

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

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

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

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

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

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

From 5e130eeffcecf2746b222930f2d801faed5a4c64 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Thu, 23 May 2024 18:18:37 +0200
Subject: [PATCH 14/30] Add Winning support to Legacy Health Icons

---
 source/funkin/play/components/HealthIcon.hx | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 957daa43c..ded00f378 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -373,6 +373,10 @@ class HealthIcon extends FunkinSprite
     // Don't flip BF's icon here! That's done later.
     this.animation.add(Idle, [0], 0, false, false);
     this.animation.add(Losing, [1], 0, false, false);
+    if (animation.numFrames >= 3)
+    {
+      this.animation.add(Winning, [2], 0, false, false);
+    }
   }
 
   function correctCharacterId(charId:Null<String>):String

From 28bf46022aa41bb0eeebcc32ae0fee88c9696884 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 23 May 2024 14:37:50 -0400
Subject: [PATCH 15/30] add simple PR auto labelling action

---
 .github/labeler.yml           | 12 ++++++++++++
 .github/workflows/labeler.yml | 14 ++++++++++++++
 2 files changed, 26 insertions(+)
 create mode 100644 .github/labeler.yml
 create mode 100644 .github/workflows/labeler.yml

diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 000000000..e8e490865
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,12 @@
+# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
+Documentation:
+- changed-files:
+  - any-glob-to-any-file:
+  - any-glob-to-any-file:
+      - docs/*
+      - '**/*.md'
+
+# Adds Haxe tag to PR's changing haxe code files
+Haxe:
+- changed-files:
+  - any-glob-to-any-file: '**/*.hx'
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 000000000..195b22b7c
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,14 @@
+name: "Pull Request Labeler"
+on:
+- pull_request
+
+jobs:
+  labeler:
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/labeler@v5
+      with:
+        sync-labels: true

From 5d866cb1867c1675d370cd06f805e08a9e307ea7 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 23 May 2024 14:53:25 -0400
Subject: [PATCH 16/30] pr target

---
 .github/workflows/labeler.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 195b22b7c..0bcc420d3 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -1,6 +1,6 @@
 name: "Pull Request Labeler"
 on:
-- pull_request
+- pull_request_target
 
 jobs:
   labeler:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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