From be8f5699b5439af8314c1708ad4596ba0c86f7a8 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 11 Mar 2024 23:42:32 -0400
Subject: [PATCH] 2hot stutter actually fixed!

---
 assets                                        |  2 +-
 source/funkin/audio/FunkinSound.hx            | 64 ++++++++++++----
 source/funkin/data/song/SongRegistry.hx       |  7 ++
 source/funkin/play/PlayState.hx               |  3 +-
 source/funkin/ui/freeplay/FreeplayState.hx    | 28 ++++---
 source/funkin/ui/mainmenu/MainMenuState.hx    |  9 ++-
 source/funkin/ui/story/StoryMenuState.hx      | 13 +---
 source/funkin/ui/title/TitleState.hx          | 16 ++--
 source/funkin/ui/transition/LoadingState.hx   | 27 +++++++
 .../{tools/TimerTools.hx => TimerUtil.hx}     |  0
 source/funkin/util/logging/Perf.hx            | 76 +++++++++++++++++++
 source/funkin/util/tools/StringTools.hx       | 30 ++++++++
 12 files changed, 222 insertions(+), 53 deletions(-)
 rename source/funkin/util/{tools/TimerTools.hx => TimerUtil.hx} (100%)
 create mode 100644 source/funkin/util/logging/Perf.hx

diff --git a/assets b/assets
index 2d3db0cce..7e37fd971 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 2d3db0cce9bd06cf280bbf6a0b10e57982f32fc3
+Subproject commit 7e37fd971006140db30aa7b4746f4b94f2e5a613
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index a1e14d705..9efa6ed50 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -8,6 +8,8 @@ import flixel.sound.FlxSound;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.system.FlxAssets.FlxSoundAsset;
 import funkin.util.tools.ICloneable;
+import funkin.data.song.SongData.SongMusicData;
+import funkin.data.song.SongRegistry;
 import funkin.audio.waveform.WaveformData;
 import funkin.audio.waveform.WaveformDataParser;
 import flixel.math.FlxMath;
@@ -28,7 +30,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
   /**
    * Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
    */
-  static var cache(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
+  static var pool(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
 
   public var muted(default, set):Bool = false;
 
@@ -265,23 +267,55 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
   }
 
   /**
-   * Creates a new `FunkinSound` object.
+   * Creates a new `FunkinSound` object and loads it as the current music track.
    *
-   * @param   embeddedSound   The embedded sound resource you want to play.  To stream, use the optional URL parameter instead.
-   * @param   volume          How loud to play it (0 to 1).
-   * @param   looped          Whether to loop this sound.
-   * @param   group           The group to add this sound to.
-   * @param   autoDestroy     Whether to destroy this sound when it finishes playing.
+   * @param key The key of the music you want to play. Music should be at `music/<key>/<key>.ogg`.
+   * @param overrideExisting Whether to override music if it is already playing.
+   * @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with.
+   *   Data should be at `music/<key>/<key>-metadata.json`.
+   */
+  public static function playMusic(key:String, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void
+  {
+    if (!overrideExisting && FlxG.sound.music?.playing) return;
+
+    if (mapTimeChanges)
+    {
+      var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
+      // Will fall back and return null if the metadata doesn't exist or can't be parsed.
+      if (songMusicData != null)
+      {
+        Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
+      }
+      else
+      {
+        FlxG.log.warn('Tried and failed to find music metadata for $key');
+      }
+    }
+
+    FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'));
+
+    // Prevent repeat update() and onFocus() calls.
+    FlxG.sound.list.remove(FlxG.sound.music);
+  }
+
+  /**
+   * Creates a new `FunkinSound` object synchronously.
+   *
+   * @param embeddedSound   The embedded sound resource you want to play.  To stream, use the optional URL parameter instead.
+   * @param volume          How loud to play it (0 to 1).
+   * @param looped          Whether to loop this sound.
+   * @param group           The group to add this sound to.
+   * @param autoDestroy     Whether to destroy this sound when it finishes playing.
    *                          Leave this value set to `false` if you want to re-use this `FunkinSound` instance.
-   * @param   autoPlay        Whether to play the sound immediately or wait for a `play()` call.
-   * @param   onComplete      Called when the sound finished playing.
-   * @param   onLoad          Called when the sound finished loading.  Called immediately for succesfully loaded embedded sounds.
-   * @return  A `FunkinSound` object.
+   * @param autoPlay        Whether to play the sound immediately or wait for a `play()` call.
+   * @param onComplete      Called when the sound finished playing.
+   * @param onLoad          Called when the sound finished loading.  Called immediately for succesfully loaded embedded sounds.
+   * @return A `FunkinSound` object.
    */
   public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
       ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
   {
-    var sound:FunkinSound = cache.recycle(construct);
+    var sound:FunkinSound = pool.recycle(construct);
 
     // Load the sound.
     // Sets `exists = true` as a side effect.
@@ -297,9 +331,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     sound.persist = true;
     if (autoPlay) sound.play();
 
-    // Call OnlLoad() because the sound already loaded
+    // Call onLoad() because the sound already loaded
     if (onLoad != null && sound._sound != null) onLoad();
 
+    FlxG.sound.list.remove(FlxG.sound.music);
+
     return sound;
   }
 
@@ -307,7 +343,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
   {
     var sound:FunkinSound = new FunkinSound();
 
-    cache.add(sound);
+    pool.add(sound);
     FlxG.sound.list.add(sound);
 
     return sound;
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index dad287e82..e2edc055a 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -441,6 +441,13 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     return {fileName: entryFilePath, contents: rawJson};
   }
 
+  function hasMusicDataFile(id:String, ?variation:String):Bool
+  {
+    variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
+    var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
+    return openfl.Assets.exists(entryFilePath);
+  }
+
   function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
   {
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index a6e4b4632..7609c356b 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1,5 +1,6 @@
 package funkin.play;
 
+import funkin.audio.FunkinSound;
 import flixel.addons.display.FlxPieDial;
 import flixel.addons.display.FlxPieDial;
 import flixel.addons.transition.FlxTransitionableState;
@@ -2711,7 +2712,7 @@ class PlayState extends MusicBeatSubState
 
       if (targetSongId == null)
       {
-        FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+        FunkinSound.playMusic('freakyMenu');
 
         // transIn = FlxTransitionableState.defaultTransIn;
         // transOut = FlxTransitionableState.defaultTransOut;
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 45f9a4d27..ec873d103 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1,21 +1,19 @@
 package funkin.ui.freeplay;
 
-import flash.text.TextField;
+import openfl.text.TextField;
 import flixel.addons.display.FlxGridOverlay;
 import flixel.addons.transition.FlxTransitionableState;
 import flixel.addons.ui.FlxInputText;
 import flixel.FlxCamera;
 import flixel.FlxGame;
 import flixel.FlxSprite;
-import funkin.graphics.FunkinSprite;
 import flixel.FlxState;
 import flixel.group.FlxGroup;
 import flixel.group.FlxGroup.FlxTypedGroup;
-import flixel.group.FlxSpriteGroup;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
 import flixel.input.touch.FlxTouch;
 import flixel.math.FlxAngle;
 import flixel.math.FlxMath;
-import funkin.graphics.FunkinCamera;
 import flixel.math.FlxPoint;
 import flixel.system.debug.watch.Tracker.TrackerProfile;
 import flixel.text.FlxText;
@@ -24,9 +22,12 @@ import flixel.tweens.FlxTween;
 import flixel.util.FlxColor;
 import flixel.util.FlxSpriteUtil;
 import flixel.util.FlxTimer;
+import funkin.audio.FunkinSound;
 import funkin.data.level.LevelRegistry;
 import funkin.data.song.SongRegistry;
 import funkin.graphics.adobeanimate.FlxAtlasSprite;
+import funkin.graphics.FunkinCamera;
+import funkin.graphics.FunkinSprite;
 import funkin.graphics.shaders.AngleMask;
 import funkin.graphics.shaders.HSVShader;
 import funkin.graphics.shaders.PureColor;
@@ -187,10 +188,7 @@ class FreeplayState extends MusicBeatSubState
     isDebug = true;
     #end
 
-    if (FlxG.sound.music == null || (FlxG.sound.music != null && !FlxG.sound.music.playing))
-    {
-      FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
-    }
+    FunkinSound.playMusic('freakyMenu');
 
     // Add a null entry that represents the RANDOM option
     songs.push(null);
@@ -590,7 +588,7 @@ class FreeplayState extends MusicBeatSubState
     });
   }
 
-  public function generateSongList(?filterStuff:SongFilter, force:Bool = false)
+  public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void
   {
     curSelected = 1;
 
@@ -693,7 +691,7 @@ class FreeplayState extends MusicBeatSubState
 
   var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
 
-  override function update(elapsed:Float)
+  override function update(elapsed:Float):Void
   {
     super.update(elapsed);
 
@@ -983,7 +981,7 @@ class FreeplayState extends MusicBeatSubState
     }
   }
 
-  function changeDiff(change:Int = 0)
+  function changeDiff(change:Int = 0):Void
   {
     touchTimer = 0;
 
@@ -1173,7 +1171,7 @@ class FreeplayState extends MusicBeatSubState
     difficultyStars.difficulty = daSong?.songRating ?? 0;
   }
 
-  function changeSelection(change:Int = 0)
+  function changeSelection(change:Int = 0):Void
   {
     // NGio.logEvent('Fresh');
     FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
@@ -1228,7 +1226,7 @@ class FreeplayState extends MusicBeatSubState
         // TODO: Stream the instrumental of the selected song?
         if (prevSelected == 0)
         {
-          FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+          FunkinSound.playMusic('freakyMenu');
           FlxG.sound.music.fadeIn(2, 0, 0.8);
         }
       }
@@ -1259,7 +1257,7 @@ class DifficultySelector extends FlxSprite
     flipX = flipped;
   }
 
-  override function update(elapsed:Float)
+  override function update(elapsed:Float):Void
   {
     if (flipX && controls.UI_RIGHT_P) moveShitDown();
     if (!flipX && controls.UI_LEFT_P) moveShitDown();
@@ -1267,7 +1265,7 @@ class DifficultySelector extends FlxSprite
     super.update(elapsed);
   }
 
-  function moveShitDown()
+  function moveShitDown():Void
   {
     offset.y -= 5;
 
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index 8842c37de..1892bdec1 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -12,8 +12,10 @@ import flixel.util.typeLimit.NextState;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.input.touch.FlxTouch;
 import flixel.text.FlxText;
+import funkin.data.song.SongData.SongMusicData;
 import flixel.tweens.FlxEase;
 import funkin.graphics.FunkinCamera;
+import funkin.audio.FunkinSound;
 import flixel.tweens.FlxTween;
 import funkin.ui.MusicBeatState;
 import flixel.util.FlxTimer;
@@ -51,7 +53,7 @@ class MainMenuState extends MusicBeatState
 
     if (!(FlxG?.sound?.music?.playing ?? false))
     {
-      FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+      playMenuMusic();
     }
 
     persistentUpdate = persistentDraw = true;
@@ -151,6 +153,11 @@ class MainMenuState extends MusicBeatState
     // NG.core.calls.event.logEvent('swag').send();
   }
 
+  function playMenuMusic():Void
+  {
+    FunkinSound.playMusic('freakyMenu');
+  }
+
   function resetCamStuff()
   {
     FlxG.cameras.reset(new FunkinCamera());
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 404dfb67e..1f78eb375 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -16,6 +16,7 @@ import flixel.tweens.FlxTween;
 import flixel.util.FlxColor;
 import flixel.util.FlxTimer;
 import funkin.data.level.LevelRegistry;
+import funkin.audio.FunkinSound;
 import funkin.modding.events.ScriptEvent;
 import funkin.modding.events.ScriptEventDispatcher;
 import funkin.play.PlayState;
@@ -234,17 +235,7 @@ class StoryMenuState extends MusicBeatState
 
   function playMenuMusic():Void
   {
-    if (FlxG.sound.music == null || !FlxG.sound.music.playing)
-    {
-      var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
-      if (freakyMenuMetadata != null)
-      {
-        Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
-      }
-
-      FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
-      FlxG.sound.music.fadeIn(4, 0, 0.7);
-    }
+    FunkinSound.playMusic('freakyMenu');
   }
 
   function updateData():Void
diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx
index 5424e2255..1c194d80d 100644
--- a/source/funkin/ui/title/TitleState.hx
+++ b/source/funkin/ui/title/TitleState.hx
@@ -18,6 +18,7 @@ import funkin.graphics.FunkinSprite;
 import funkin.ui.MusicBeatState;
 import funkin.data.song.SongData.SongMusicData;
 import funkin.graphics.shaders.TitleOutline;
+import funkin.audio.FunkinSound;
 import funkin.ui.freeplay.FreeplayState;
 import funkin.ui.AtlasText;
 import openfl.Assets;
@@ -219,16 +220,11 @@ class TitleState extends MusicBeatState
 
   function playMenuMusic():Void
   {
-    if (FlxG.sound.music == null || !FlxG.sound.music.playing)
-    {
-      var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
-      if (freakyMenuMetadata != null)
-      {
-        Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
-      }
-      FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
-      FlxG.sound.music.fadeIn(4, 0, 0.7);
-    }
+    var shouldFadeIn = (FlxG.sound.music == null);
+    // Load music. Includes logic to handle BPM changes.
+    FunkinSound.playMusic('freakyMenu', false, true);
+    // Fade from 0.0 to 0.7 over 4 seconds
+    if (shouldFadeIn) FlxG.sound.music.fadeIn(4, 0, 0.7);
   }
 
   function getIntroTextShit():Array<Array<String>>
diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index 5f755872f..c53af36de 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -238,11 +238,38 @@ class LoadingState extends MusicBeatState
     FunkinSprite.cacheTexture(Paths.image('shit', 'shared'));
     FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
 
+    // List all image assets in the level's library.
+    // This is crude and I want to remove it when we have a proper asset caching system.
+    // TODO: Get rid of this junk!
+    var library = openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId);
+    var assets = library.list(lime.utils.AssetType.IMAGE);
+    trace('Got ${assets.length} assets: ${assets}');
+
+    // TODO: assets includes non-images! This is a bug with Polymod
+    for (asset in assets)
+    {
+      // Exclude items of the wrong type.
+      var path = '${PlayStatePlaylist.campaignId}:${asset}';
+      // TODO DUMB HACK DUMB HACK why doesn't filtering by AssetType.IMAGE above work
+      // I will fix this properly later I swear -eric
+      if (!path.endsWith('.png')) continue;
+
+      FunkinSprite.cacheTexture(path);
+
+      // Another dumb hack: FlxAnimate fetches from OpenFL's BitmapData cache directly and skips the FlxGraphic cache.
+      // Since FlxGraphic tells OpenFL to not cache it, we have to do it manually.
+      if (path.endsWith('spritemap1.png'))
+      {
+        openfl.Assets.getBitmapData(path, true);
+      }
+    }
+
     // FunkinSprite.cacheAllNoteStyleTextures(noteStyle) // This will replace the stuff above!
     // FunkinSprite.cacheAllCharacterTextures(player)
     // FunkinSprite.cacheAllCharacterTextures(girlfriend)
     // FunkinSprite.cacheAllCharacterTextures(opponent)
     // FunkinSprite.cacheAllStageTextures(stage)
+    // FunkinSprite.cacheAllSongTextures(stage)
 
     FunkinSprite.purgeCache();
 
diff --git a/source/funkin/util/tools/TimerTools.hx b/source/funkin/util/TimerUtil.hx
similarity index 100%
rename from source/funkin/util/tools/TimerTools.hx
rename to source/funkin/util/TimerUtil.hx
diff --git a/source/funkin/util/logging/Perf.hx b/source/funkin/util/logging/Perf.hx
new file mode 100644
index 000000000..83da7a32f
--- /dev/null
+++ b/source/funkin/util/logging/Perf.hx
@@ -0,0 +1,76 @@
+package funkin.util.logging;
+
+/**
+ * A small utility class for timing how long functions take.
+ * Specify a string as a label (or don't, by default it uses the name of the function it was called from.)
+ *
+ * Example:
+ * ```haxe
+ *
+ * var perf = new Perf();
+ * ...
+ * perf.print();
+ * ```
+ */
+class Perf
+{
+  final startTime:Float;
+  final label:Null<String>;
+  final posInfos:Null<haxe.PosInfos>;
+
+  /**
+   * Create a new performance marker.
+   * @param label Optionally specify a label to use for the performance marker. Defaults to the function name.
+   * @param posInfos The position of the calling function. Used to build the default label.
+   *   Note: `haxe.PosInfos` is magic and automatically populated by the compiler!
+   */
+  public function new(?label:String, ?posInfos:haxe.PosInfos)
+  {
+    this.label = label;
+    this.posInfos = posInfos;
+    startTime = current();
+  }
+
+  /**
+   * The current timestamp, in fractional seconds.
+   * @return The current timestamp.
+   */
+  static function current():Float
+  {
+    #if sys
+    // This one is more accurate if it's available.
+    return Sys.time();
+    #else
+    return haxe.Timer.stamp();
+    #end
+  }
+
+  /**
+   * The duration in seconds since this `Perf` was created.
+   * @return The duration in seconds
+   */
+  public function duration():Float
+  {
+    return current() - startTime;
+  }
+
+  /**
+   * A rounded millisecond duration
+   * @return The duration in milliseconds
+   */
+  public function durationClean():Float
+  {
+    var round:Float = 100;
+    return Math.floor(duration() * Constants.MS_PER_SEC * round) / round;
+  }
+
+  /**
+   * Cleanly prints the duration since this `Perf` was created.
+   */
+  public function print():Void
+  {
+    var label:String = label ?? (posInfos == null ? 'unknown' : '${posInfos.className}#${posInfos.methodName}()');
+
+    trace('[PERF] [$label] Took ${durationClean()}ms.');
+  }
+}
diff --git a/source/funkin/util/tools/StringTools.hx b/source/funkin/util/tools/StringTools.hx
index 0585ffeae..e69a13f1a 100644
--- a/source/funkin/util/tools/StringTools.hx
+++ b/source/funkin/util/tools/StringTools.hx
@@ -27,6 +27,36 @@ class StringTools
     return result;
   }
 
+  /**
+   * Strip a given prefix from a string.
+   * @param value The string to strip.
+   * @param prefix The prefix to strip. If the prefix isn't found, the original string is returned.
+   * @return The stripped string.
+   */
+  public static function stripPrefix(value:String, prefix:String):String
+  {
+    if (value.startsWith(prefix))
+    {
+      return value.substr(prefix.length);
+    }
+    return value;
+  }
+
+  /**
+   * Strip a given suffix from a string.
+   * @param value The string to strip.
+   * @param suffix The suffix to strip. If the suffix isn't found, the original string is returned.
+   * @return The stripped string.
+   */
+  public static function stripSuffix(value:String, suffix:String):String
+  {
+    if (value.endsWith(suffix))
+    {
+      return value.substr(0, value.length - suffix.length);
+    }
+    return value;
+  }
+
   /**
    * Converts a string to lower kebab case. For example, "Hello World" becomes "hello-world".
    *