From 82383018f66147df8374212abd8b54d9cc8145cc Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 14 Mar 2024 19:27:07 -0700
Subject: [PATCH 1/9] funkin soundtray

---
 assets                                      |   2 +-
 source/Main.hx                              |   9 +-
 source/funkin/ui/options/FunkinSoundTray.hx | 138 ++++++++++++++++++++
 3 files changed, 147 insertions(+), 2 deletions(-)
 create mode 100644 source/funkin/ui/options/FunkinSoundTray.hx

diff --git a/assets b/assets
index 0e2c5bf21..223722892 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 0e2c5bf2134c7e517b70cf74afd58abe5c7b5e50
+Subproject commit 2237228923c6bd35df1e68e3b2a13dfffd1c243d
diff --git a/source/Main.hx b/source/Main.hx
index a40fda29d..758edcc65 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -100,8 +100,15 @@ class Main extends Sprite
 
     // George recommends binding the save before FlxGame is created.
     Save.load();
+    var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen);
 
-    addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));
+    // FlxG.game._customSoundTray wants just the class, it calls new from
+    // create() in there, which gets called when it's added to stage
+    // which is why it needs to be added before addChild(game) here
+    @:privateAccess
+    game._customSoundTray = funkin.ui.options.FunkinSoundTray;
+
+    addChild(game);
 
     #if hxcpp_debug_server
     trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
diff --git a/source/funkin/ui/options/FunkinSoundTray.hx b/source/funkin/ui/options/FunkinSoundTray.hx
new file mode 100644
index 000000000..31c38286d
--- /dev/null
+++ b/source/funkin/ui/options/FunkinSoundTray.hx
@@ -0,0 +1,138 @@
+package funkin.ui.options;
+
+import flixel.system.ui.FlxSoundTray;
+import flixel.tweens.FlxTween;
+import flixel.system.FlxAssets;
+import flixel.tweens.FlxEase;
+import openfl.display.Bitmap;
+import openfl.display.BitmapData;
+import openfl.utils.Assets;
+import funkin.util.MathUtil;
+
+/**
+ *  Extends the default flixel soundtray, but with some art
+ *  and lil polish!
+ *
+ *  Gets added to the game in Main.hx, right after FlxGame is new'd
+ *  since it's a Sprite rather than Flixel related object
+ */
+class FunkinSoundTray extends FlxSoundTray
+{
+  var graphicScale:Float = 0.30;
+  var lerpYPos:Float = 0;
+
+  var volumeMaxSound:String;
+
+  public function new()
+  {
+    // calls super, then removes all children to add our own
+    // graphics
+    super();
+    removeChildren();
+
+    var bg:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/volumebox")));
+    bg.scaleX = graphicScale;
+    bg.scaleY = graphicScale;
+    addChild(bg);
+
+    y = -height;
+    visible = false;
+
+    // clear the bars array entirely, it was initialized
+    // in the super class
+    _bars = [];
+
+    // 1...11 due to how block named the assets,
+    // we are trying to get assets bars_1-10
+    for (i in 1...11)
+    {
+      var bar:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/bars_" + i)));
+      bar.x = 10;
+      bar.y = 5;
+      bar.scaleX = graphicScale;
+      bar.scaleY = graphicScale;
+      addChild(bar);
+      _bars.push(bar);
+    }
+
+    y = -height;
+    screenCenter();
+
+    volumeUpSound = Paths.sound("soundtray/Volup");
+    volumeDownSound = Paths.sound("soundtray/Voldown");
+    volumeMaxSound = Paths.sound("soundtray/VolMAX");
+
+    trace("Custom tray added!");
+  }
+
+  override public function update(MS:Float):Void
+  {
+    y = MathUtil.coolLerp(y, lerpYPos, 0.1);
+
+    // Animate sound tray thing
+    if (_timer > 0)
+    {
+      _timer -= (MS / 1000);
+    }
+    else if (y > -height)
+    {
+      lerpYPos = -height;
+
+      if (y <= -height)
+      {
+        visible = false;
+        active = false;
+
+        #if FLX_SAVE
+        // Save sound preferences
+        if (FlxG.save.isBound)
+        {
+          FlxG.save.data.mute = FlxG.sound.muted;
+          FlxG.save.data.volume = FlxG.sound.volume;
+          FlxG.save.flush();
+        }
+        #end
+      }
+    }
+  }
+
+  /**
+   * Makes the little volume tray slide out.
+   *
+   * @param	up Whether the volume is increasing.
+   */
+  override public function show(up:Bool = false):Void
+  {
+    _timer = 1;
+    lerpYPos = 0;
+    visible = true;
+    active = true;
+    var globalVolume:Int = Math.round(FlxG.sound.volume * 10);
+
+    if (FlxG.sound.muted)
+    {
+      globalVolume = 0;
+    }
+
+    if (!silent)
+    {
+      var sound = up ? volumeUpSound : volumeDownSound;
+
+      if (globalVolume == 10) sound = volumeMaxSound;
+
+      if (sound != null) FlxG.sound.load(sound).play();
+    }
+
+    for (i in 0..._bars.length)
+    {
+      if (i < globalVolume)
+      {
+        _bars[i].visible = true;
+      }
+      else
+      {
+        _bars[i].visible = false;
+      }
+    }
+  }
+}

From d68ea0eb465c0405f20cde6e75b8a2667d756bb3 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 14 Mar 2024 19:40:07 -0700
Subject: [PATCH 2/9] lil polish

---
 source/funkin/ui/options/FunkinSoundTray.hx | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/options/FunkinSoundTray.hx b/source/funkin/ui/options/FunkinSoundTray.hx
index 31c38286d..4af94569b 100644
--- a/source/funkin/ui/options/FunkinSoundTray.hx
+++ b/source/funkin/ui/options/FunkinSoundTray.hx
@@ -38,6 +38,15 @@ class FunkinSoundTray extends FlxSoundTray
     y = -height;
     visible = false;
 
+    // makes an alpha'd version of all the bars (bar_10.png)
+    var backingBar:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/bars_10")));
+    backingBar.x = 10;
+    backingBar.y = 5;
+    backingBar.scaleX = graphicScale;
+    backingBar.scaleY = graphicScale;
+    addChild(backingBar);
+    backingBar.alpha = 0.4;
+
     // clear the bars array entirely, it was initialized
     // in the super class
     _bars = [];
@@ -76,7 +85,7 @@ class FunkinSoundTray extends FlxSoundTray
     }
     else if (y > -height)
     {
-      lerpYPos = -height;
+      lerpYPos = -height - 10;
 
       if (y <= -height)
       {
@@ -104,7 +113,7 @@ class FunkinSoundTray extends FlxSoundTray
   override public function show(up:Bool = false):Void
   {
     _timer = 1;
-    lerpYPos = 0;
+    lerpYPos = 10;
     visible = true;
     active = true;
     var globalVolume:Int = Math.round(FlxG.sound.volume * 10);

From a8ebdc5ee82e3cf0b04900756b104053e612c58e Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 18 Mar 2024 22:27:19 -0400
Subject: [PATCH 3/9] Update Polymod to fix several bugs on web

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

diff --git a/hmm.json b/hmm.json
index 42d17743f..b16576e66 100644
--- a/hmm.json
+++ b/hmm.json
@@ -146,7 +146,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "be712450e5d3ba446008884921bb56873b299a64",
+      "ref": "6d1bdf79b463ca0baa8471dfc6873ab7701c46ee",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {

From a7780474feccce6e58c41f7339dc1837b3f53bf0 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Mar 2024 00:28:25 -0400
Subject: [PATCH 4/9] Improve error handling for bad script superclasses

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

diff --git a/hmm.json b/hmm.json
index b16576e66..a75dee432 100644
--- a/hmm.json
+++ b/hmm.json
@@ -146,7 +146,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "6d1bdf79b463ca0baa8471dfc6873ab7701c46ee",
+      "ref": "5547763a22858a1f10939e082de421d587c862bf",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {

From 4d1bcbc193b316715a650b38f54cfaf8de12a488 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Mar 2024 00:29:01 -0400
Subject: [PATCH 5/9] Update assets submodule

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

diff --git a/assets b/assets
index 0e2c5bf21..6846ed8d2 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 0e2c5bf2134c7e517b70cf74afd58abe5c7b5e50
+Subproject commit 6846ed8d26b0ab531b758bb009d0ebcd515acb21

From 34cb3ceb24e9418b43a391b040801cee4f28a012 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Mar 2024 00:53:01 -0400
Subject: [PATCH 6/9] Update assets submodule

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

diff --git a/assets b/assets
index 6846ed8d2..b272232aa 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 6846ed8d26b0ab531b758bb009d0ebcd515acb21
+Subproject commit b272232aaf5a748db4b0dd5ac0cc98b72d4a636c

From 42eec667532c330ed4e87ce7d3826a62f1ab5301 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Mar 2024 15:56:00 -0400
Subject: [PATCH 7/9] Rework HTML5 video cutscenes to properly support the
 pause menu and volume controls.

---
 source/funkin/audio/FunkinSound.hx            |  24 +++-
 source/funkin/graphics/FunkinSprite.hx        |  44 +++++++-
 .../graphics/framebuffer/FixedBitmapData.hx   |   6 +-
 source/funkin/graphics/video/FlxVideo.hx      | 105 +++++++++++++++---
 4 files changed, 153 insertions(+), 26 deletions(-)

diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index 9efa6ed50..c64240909 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -1,11 +1,8 @@
 package funkin.audio;
 
-#if flash11
-import flash.media.Sound;
-import flash.utils.ByteArray;
-#end
 import flixel.sound.FlxSound;
 import flixel.group.FlxGroup.FlxTypedGroup;
+import flixel.util.FlxSignal.FlxTypedSignal;
 import flixel.system.FlxAssets.FlxSoundAsset;
 import funkin.util.tools.ICloneable;
 import funkin.data.song.SongData.SongMusicData;
@@ -27,6 +24,25 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
 {
   static final MAX_VOLUME:Float = 1.0;
 
+  /**
+   * An FlxSignal which is dispatched when the volume changes.
+   */
+  public static var onVolumeChanged(get, never):FlxTypedSignal<Float->Void>;
+
+  static var _onVolumeChanged:Null<FlxTypedSignal<Float->Void>> = null;
+
+  static function get_onVolumeChanged():FlxTypedSignal<Float->Void>
+  {
+    if (_onVolumeChanged == null)
+    {
+      _onVolumeChanged = new FlxTypedSignal<Float->Void>();
+      FlxG.sound.volumeHandler = function(volume:Float) {
+        _onVolumeChanged.dispatch(volume);
+      }
+    }
+    return _onVolumeChanged;
+  }
+
   /**
    * Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
    */
diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx
index 03382f757..7ead7f1fb 100644
--- a/source/funkin/graphics/FunkinSprite.hx
+++ b/source/funkin/graphics/FunkinSprite.hx
@@ -3,6 +3,9 @@ package funkin.graphics;
 import flixel.FlxSprite;
 import flixel.util.FlxColor;
 import flixel.graphics.FlxGraphic;
+import openfl.display3D.textures.TextureBase;
+import funkin.graphics.framebuffer.FixedBitmapData;
+import openfl.display.BitmapData;
 
 /**
  * An FlxSprite with additional functionality.
@@ -41,7 +44,7 @@ class FunkinSprite extends FlxSprite
    */
   public static function create(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
   {
-    var sprite = new FunkinSprite(x, y);
+    var sprite:FunkinSprite = new FunkinSprite(x, y);
     sprite.loadTexture(key);
     return sprite;
   }
@@ -55,7 +58,7 @@ class FunkinSprite extends FlxSprite
    */
   public static function createSparrow(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
   {
-    var sprite = new FunkinSprite(x, y);
+    var sprite:FunkinSprite = new FunkinSprite(x, y);
     sprite.loadSparrow(key);
     return sprite;
   }
@@ -69,7 +72,7 @@ class FunkinSprite extends FlxSprite
    */
   public static function createPacker(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
   {
-    var sprite = new FunkinSprite(x, y);
+    var sprite:FunkinSprite = new FunkinSprite(x, y);
     sprite.loadPacker(key);
     return sprite;
   }
@@ -89,6 +92,30 @@ class FunkinSprite extends FlxSprite
     return this;
   }
 
+  /**
+   * Apply an OpenFL `BitmapData` to this sprite.
+   * @param input The OpenFL `BitmapData` to apply
+   * @return This sprite, for chaining
+   */
+  public function loadBitmapData(input:BitmapData):FunkinSprite
+  {
+    loadGraphic(input);
+
+    return this;
+  }
+
+  /**
+   * Apply an OpenFL `TextureBase` to this sprite.
+   * @param input The OpenFL `TextureBase` to apply
+   * @return This sprite, for chaining
+   */
+  public function loadTextureBase(input:TextureBase):FunkinSprite
+  {
+    var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
+
+    return loadBitmapData(inputBitmap);
+  }
+
   /**
    * Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
    * @param key The key of the texture to load.
@@ -119,11 +146,20 @@ class FunkinSprite extends FlxSprite
     return this;
   }
 
+  /**
+   * Determine whether the texture with the given key is cached.
+   * @param key The key of the texture to check.
+   * @return Whether the texture is cached.
+   */
   public static function isTextureCached(key:String):Bool
   {
     return FlxG.bitmap.get(key) != null;
   }
 
+  /**
+   * Ensure the texture with the given key is cached.
+   * @param key The key of the texture to cache.
+   */
   public static function cacheTexture(key:String):Void
   {
     // We don't want to cache the same texture twice.
@@ -139,7 +175,7 @@ class FunkinSprite extends FlxSprite
     }
 
     // Else, texture is currently uncached.
-    var graphic = flixel.graphics.FlxGraphic.fromAssetKey(key, false, null, true);
+    var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
     if (graphic == null)
     {
       FlxG.log.warn('Failed to cache graphic: $key');
diff --git a/source/funkin/graphics/framebuffer/FixedBitmapData.hx b/source/funkin/graphics/framebuffer/FixedBitmapData.hx
index 00b39ce1c..4ffcbb867 100644
--- a/source/funkin/graphics/framebuffer/FixedBitmapData.hx
+++ b/source/funkin/graphics/framebuffer/FixedBitmapData.hx
@@ -32,11 +32,11 @@ class FixedBitmapData extends BitmapData
   public static function fromTexture(texture:TextureBase):FixedBitmapData
   {
     if (texture == null) return null;
-    final bitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
-    bitmapData.readable = false;
+    final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
+    // bitmapData.readable = false;
     bitmapData.__texture = texture;
     bitmapData.__textureContext = texture.__textureContext;
-    bitmapData.image = null;
+    // bitmapData.image = null;
     return bitmapData;
   }
 }
diff --git a/source/funkin/graphics/video/FlxVideo.hx b/source/funkin/graphics/video/FlxVideo.hx
index 5e178efb3..a0fab9c09 100644
--- a/source/funkin/graphics/video/FlxVideo.hx
+++ b/source/funkin/graphics/video/FlxVideo.hx
@@ -1,45 +1,58 @@
 package funkin.graphics.video;
 
-import flixel.FlxBasic;
-import flixel.FlxSprite;
+import flixel.util.FlxColor;
+import flixel.util.FlxSignal.FlxTypedSignal;
+import funkin.audio.FunkinSound;
+import openfl.display3D.textures.TextureBase;
 import openfl.events.NetStatusEvent;
+import openfl.media.SoundTransform;
 import openfl.media.Video;
 import openfl.net.NetConnection;
 import openfl.net.NetStream;
 
 /**
  * Plays a video via a NetStream. Only works on HTML5.
- * This does NOT replace hxCodec, nor does hxCodec replace this. hxCodec only works on desktop and does not work on HTML5!
+ * This does NOT replace hxCodec, nor does hxCodec replace this.
+ * hxCodec only works on desktop and does not work on HTML5!
  */
-class FlxVideo extends FlxBasic
+class FlxVideo extends FunkinSprite
 {
   var video:Video;
   var netStream:NetStream;
-
-  public var finishCallback:Void->Void;
+  var videoPath:String;
 
   /**
-   * Doesn't actually interact with Flixel shit, only just a pleasant to use class
+   * A callback to execute when the video finishes.
    */
+  public var finishCallback:Void->Void;
+
   public function new(videoPath:String)
   {
     super();
 
+    this.videoPath = videoPath;
+
+    makeGraphic(2, 2, FlxColor.TRANSPARENT);
+
     video = new Video();
     video.x = 0;
     video.y = 0;
+    video.alpha = 0;
 
-    FlxG.addChildBelowMouse(video);
+    FlxG.game.addChild(video);
 
-    var netConnection = new NetConnection();
+    var netConnection:NetConnection = new NetConnection();
     netConnection.connect(null);
 
     netStream = new NetStream(netConnection);
-    netStream.client = {onMetaData: client_onMetaData};
-    netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
+    netStream.client = {onMetaData: onClientMetaData};
+    netConnection.addEventListener(NetStatusEvent.NET_STATUS, onNetConnectionNetStatus);
     netStream.play(videoPath);
   }
 
+  /**
+   * Tell the FlxVideo to pause playback.
+   */
   public function pauseVideo():Void
   {
     if (netStream != null)
@@ -48,6 +61,9 @@ class FlxVideo extends FlxBasic
     }
   }
 
+  /**
+   * Tell the FlxVideo to resume if it is paused.
+   */
   public function resumeVideo():Void
   {
     // Resume playing the video.
@@ -57,6 +73,29 @@ class FlxVideo extends FlxBasic
     }
   }
 
+  var videoAvailable:Bool = false;
+  var frameTimer:Float;
+
+  static final FRAME_RATE:Float = 60;
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (frameTimer >= (1 / FRAME_RATE))
+    {
+      frameTimer = 0;
+      // TODO: We just draw the video buffer to the sprite 60 times a second.
+      // Can we copy the video buffer instead somehow?
+      pixels.draw(video);
+    }
+
+    if (videoAvailable) frameTimer += elapsed;
+  }
+
+  /**
+   * Tell the FlxVideo to seek to the beginning.
+   */
   public function restartVideo():Void
   {
     // Seek to the beginning of the video.
@@ -66,6 +105,9 @@ class FlxVideo extends FlxBasic
     }
   }
 
+  /**
+   * Tell the FlxVideo to end.
+   */
   public function finishVideo():Void
   {
     netStream.dispose();
@@ -74,15 +116,48 @@ class FlxVideo extends FlxBasic
     if (finishCallback != null) finishCallback();
   }
 
-  public function client_onMetaData(metaData:Dynamic)
+  public override function destroy():Void
+  {
+    if (netStream != null)
+    {
+      netStream.dispose();
+
+      if (FlxG.game.contains(video)) FlxG.game.removeChild(video);
+    }
+
+    super.destroy();
+  }
+
+  /**
+   * Callback executed when the video stream loads.
+   * @param metaData The metadata of the video
+   */
+  public function onClientMetaData(metaData:Dynamic):Void
   {
     video.attachNetStream(netStream);
 
-    video.width = FlxG.width;
-    video.height = FlxG.height;
+    onVideoReady();
   }
 
-  function netConnection_onNetStatus(event:NetStatusEvent):Void
+  function onVideoReady():Void
+  {
+    video.width = FlxG.width;
+    video.height = FlxG.height;
+
+    videoAvailable = true;
+
+    FunkinSound.onVolumeChanged.add(onVolumeChanged);
+    onVolumeChanged(FlxG.sound.muted ? 0 : FlxG.sound.volume);
+
+    makeGraphic(Std.int(video.width), Std.int(video.height), FlxColor.TRANSPARENT);
+  }
+
+  function onVolumeChanged(volume:Float):Void
+  {
+    netStream.soundTransform = new SoundTransform(volume);
+  }
+
+  function onNetConnectionNetStatus(event:NetStatusEvent):Void
   {
     if (event.info.code == 'NetStream.Play.Complete') finishVideo();
   }

From a93aa6c68d5ee30c41fce642a9e4f3ad1a38acbf Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 20 Mar 2024 15:40:33 -0700
Subject: [PATCH 8/9] submod update

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

diff --git a/assets b/assets
index b272232aa..86927859c 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit b272232aaf5a748db4b0dd5ac0cc98b72d4a636c
+Subproject commit 86927859c8e73a9a0b44738a4b50ec97f38444e0

From 105aca4707b30f19f45cab09f732f5cf522e3b0d Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 21 Mar 2024 19:44:02 -0400
Subject: [PATCH 9/9] Fix an issue where hidden difficulties could end up in
 the difficulty list.

---
 source/funkin/play/song/Song.hx | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 1b7740408..42266a6ae 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -367,11 +367,14 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
 
   /**
    * List all the difficulties in this song.
+   *
    * @param variationId Optionally filter by a single variation.
    * @param variationIds Optionally filter by multiple variations.
+   * @param showHidden Include charts which are not accessible to the player.
+   *
    * @return The list of difficulties.
    */
-  public function listDifficulties(?variationId:String, ?variationIds:Array<String>):Array<String>
+  public function listDifficulties(?variationId:String, ?variationIds:Array<String>, showHidden:Bool = false):Array<String>
   {
     if (variationIds == null) variationIds = [];
     if (variationId != null) variationIds.push(variationId);
@@ -387,6 +390,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
       return difficulty.difficulty;
     }).nonNull().unique();
 
+    diffFiltered = diffFiltered.filter(function(diffId:String):Bool {
+      if (showHidden) return true;
+      for (targetVariation in variationIds)
+      {
+        if (isDifficultyVisible(diffId, targetVariation)) return true;
+      }
+      return false;
+    });
+
     diffFiltered.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
 
     return diffFiltered;
@@ -405,6 +417,13 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return false;
   }
 
+  public function isDifficultyVisible(diffId:String, variationId:String):Bool
+  {
+    var variation = _metadata.get(variationId);
+    if (variation == null) return false;
+    return variation.playData.difficulties.contains(diffId);
+  }
+
   /**
    * Purge the cached chart data for each difficulty of this song.
    */