From 1191fff913c143e8171a8d35fd17a543f44f5900 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 15 Jun 2023 00:28:40 -0400
Subject: [PATCH] Additional controls (volume, cutscene skip)

---
 source/funkin/Controls.hx        | 339 ++++++++++++++++++++++++-------
 source/funkin/ui/ControlsMenu.hx | 125 ++++++++++--
 2 files changed, 373 insertions(+), 91 deletions(-)

diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx
index 243570c27..46681adbd 100644
--- a/source/funkin/Controls.hx
+++ b/source/funkin/Controls.hx
@@ -41,12 +41,18 @@ enum Control
   ACCEPT;
   BACK;
   PAUSE;
+  CUTSCENE_ADVANCE;
+  CUTSCENE_SKIP;
+  VOLUME_UP;
+  VOLUME_DOWN;
+  VOLUME_MUTE;
   #if CAN_CHEAT
   CHEAT;
   #end
 }
 
-enum abstract Action(String) to String from String
+@:enum
+abstract Action(String) to String from String
 {
   var UI_UP = "ui_up";
   var UI_LEFT = "ui_left";
@@ -75,6 +81,11 @@ enum abstract Action(String) to String from String
   var ACCEPT = "accept";
   var BACK = "back";
   var PAUSE = "pause";
+  var CUTSCENE_ADVANCE = "cutscene_advance";
+  var CUTSCENE_SKIP = "cutscene_skip";
+  var VOLUME_UP = "volume_up";
+  var VOLUME_DOWN = "volume_down";
+  var VOLUME_MUTE = "volume_mute";
   var RESET = "reset";
   #if CAN_CHEAT
   var CHEAT = "cheat";
@@ -129,6 +140,11 @@ class Controls extends FlxActionSet
   var _back = new FlxActionDigital(Action.BACK);
   var _pause = new FlxActionDigital(Action.PAUSE);
   var _reset = new FlxActionDigital(Action.RESET);
+  var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE);
+  var _cutscene_skip = new FlxActionDigital(Action.CUTSCENE_SKIP);
+  var _volume_up = new FlxActionDigital(Action.VOLUME_UP);
+  var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN);
+  var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE);
   #if CAN_CHEAT
   var _cheat = new FlxActionDigital(Action.CHEAT);
   #end
@@ -273,6 +289,31 @@ class Controls extends FlxActionSet
   inline function get_PAUSE()
     return _pause.check();
 
+  public var CUTSCENE_ADVANCE(get, never):Bool;
+
+  inline function get_CUTSCENE_ADVANCE()
+    return _cutscene_advance.check();
+
+  public var CUTSCENE_SKIP(get, never):Bool;
+
+  inline function get_CUTSCENE_SKIP()
+    return _cutscene_skip.check();
+
+  public var VOLUME_UP(get, never):Bool;
+
+  inline function get_VOLUME_UP()
+    return _volume_up.check();
+
+  public var VOLUME_DOWN(get, never):Bool;
+
+  inline function get_VOLUME_DOWN()
+    return _volume_down.check();
+
+  public var VOLUME_MUTE(get, never):Bool;
+
+  inline function get_VOLUME_MUTE()
+    return _volume_mute.check();
+
   public var RESET(get, never):Bool;
 
   inline function get_RESET()
@@ -316,6 +357,11 @@ class Controls extends FlxActionSet
     add(_accept);
     add(_back);
     add(_pause);
+    add(_cutscene_advance);
+    add(_cutscene_skip);
+    add(_volume_up);
+    add(_volume_down);
+    add(_volume_mute);
     add(_reset);
     #if CAN_CHEAT
     add(_cheat);
@@ -377,6 +423,11 @@ class Controls extends FlxActionSet
       case BACK: _back;
       case PAUSE: _pause;
       case RESET: _reset;
+      case CUTSCENE_ADVANCE: _cutscene_advance;
+      case CUTSCENE_SKIP: _cutscene_skip;
+      case VOLUME_UP: _volume_up;
+      case VOLUME_DOWN: _volume_down;
+      case VOLUME_MUTE: _volume_mute;
       #if CAN_CHEAT
       case CHEAT: _cheat;
       #end
@@ -437,6 +488,16 @@ class Controls extends FlxActionSet
         func(_back, JUST_PRESSED);
       case PAUSE:
         func(_pause, JUST_PRESSED);
+      case CUTSCENE_ADVANCE:
+        func(_cutscene_advance, JUST_PRESSED);
+      case CUTSCENE_SKIP:
+        func(_cutscene_skip, PRESSED);
+      case VOLUME_UP:
+        func(_volume_up, JUST_PRESSED);
+      case VOLUME_DOWN:
+        func(_volume_down, JUST_PRESSED);
+      case VOLUME_MUTE:
+        func(_volume_mute, JUST_PRESSED);
       case RESET:
         func(_reset, JUST_PRESSED);
       #if CAN_CHEAT
@@ -454,37 +515,70 @@ class Controls extends FlxActionSet
     switch(device)
     {
       case Keys:
-        forEachBound(control, function(action, _) replaceKey(action, toAdd, toRemove));
+        forEachBound(control, function(action, state) replaceKey(action, toAdd, toRemove, state));
 
       case Gamepad(id):
-        forEachBound(control, function(action, _) replaceButton(action, id, toAdd, toRemove));
+        forEachBound(control, function(action, state) replaceButton(action, id, toAdd, toRemove, state));
     }
   }
 
-  function replaceKey(action:FlxActionDigital, toAdd:Int, toRemove:Int)
+  function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState)
   {
+    if (action.inputs.length == 0) {
+      // Add the keybind, don't replace.
+      addKeys(action, [toAdd], state);
+      return;
+    }
+
+    var hasReplaced:Bool = false;
     for (i in 0...action.inputs.length)
     {
       var input = action.inputs[i];
+      if (input == null) continue;
+
       if (input.device == KEYBOARD && input.inputID == toRemove)
       {
-        @:privateAccess
-        action.inputs[i].inputID = toAdd;
+        if (toAdd == FlxKey.NONE) {
+          // Remove the keybind, don't replace.
+          action.inputs.remove(input);
+        } else {
+          // Replace the keybind.
+          @:privateAccess
+          action.inputs[i].inputID = toAdd;
+        }
+        hasReplaced = true;
       }
     }
+
+    if (!hasReplaced) {
+      addKeys(action, [toAdd], state);
+    }
   }
 
-  function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:Int, toRemove:Int)
+  function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState)
   {
+    if (action.inputs.length == 0) {
+      addButtons(action, [toAdd], state, deviceID);
+      return;
+    }
+
+    var hasReplaced:Bool = false;
     for (i in 0...action.inputs.length)
     {
       var input = action.inputs[i];
+      if (input == null) continue;
+
       if (isGamepad(input, deviceID) && input.inputID == toRemove)
       {
         @:privateAccess
         action.inputs[i].inputID = toAdd;
+        hasReplaced = true;
       }
     }
+
+    if (!hasReplaced) {
+      addButtons(action, [toAdd], state, deviceID);
+    }
   }
 
   public function copyFrom(controls:Controls, ?device:Device)
@@ -558,10 +652,12 @@ class Controls extends FlxActionSet
     forEachBound(control, function(action, _) removeKeys(action, keys));
   }
 
-  inline static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
+  static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
   {
-    for (key in keys)
+    for (key in keys) {
+      if (key == FlxKey.NONE) continue; // Ignore unbound keys.
       action.addKey(key, state);
+    }
   }
 
   static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>)
@@ -582,54 +678,95 @@ class Controls extends FlxActionSet
 
     keyboardScheme = scheme;
 
-    switch(scheme)
-    {
-      case Solo:
-        bindKeys(Control.UI_UP, [W, FlxKey.UP]);
-        bindKeys(Control.UI_DOWN, [S, FlxKey.DOWN]);
-        bindKeys(Control.UI_LEFT, [A, FlxKey.LEFT]);
-        bindKeys(Control.UI_RIGHT, [D, FlxKey.RIGHT]);
-        bindKeys(Control.NOTE_UP, [W, FlxKey.UP]);
-        bindKeys(Control.NOTE_DOWN, [S, FlxKey.DOWN]);
-        bindKeys(Control.NOTE_LEFT, [A, FlxKey.LEFT]);
-        bindKeys(Control.NOTE_RIGHT, [D, FlxKey.RIGHT]);
-        bindKeys(Control.ACCEPT, [Z, SPACE, ENTER]);
-        bindKeys(Control.BACK, [X, BACKSPACE, ESCAPE]);
-        bindKeys(Control.PAUSE, [P, ENTER, ESCAPE]);
-        bindKeys(Control.RESET, [R]);
-      case Duo(true):
-        bindKeys(Control.UI_UP, [W]);
-        bindKeys(Control.UI_DOWN, [S]);
-        bindKeys(Control.UI_LEFT, [A]);
-        bindKeys(Control.UI_RIGHT, [D]);
-        bindKeys(Control.NOTE_UP, [W]);
-        bindKeys(Control.NOTE_DOWN, [S]);
-        bindKeys(Control.NOTE_LEFT, [A]);
-        bindKeys(Control.NOTE_RIGHT, [D]);
-        bindKeys(Control.ACCEPT, [G, Z]);
-        bindKeys(Control.BACK, [H, X]);
-        bindKeys(Control.PAUSE, [ONE]);
-        bindKeys(Control.RESET, [R]);
-      case Duo(false):
-        bindKeys(Control.UI_UP, [FlxKey.UP]);
-        bindKeys(Control.UI_DOWN, [FlxKey.DOWN]);
-        bindKeys(Control.UI_LEFT, [FlxKey.LEFT]);
-        bindKeys(Control.UI_RIGHT, [FlxKey.RIGHT]);
-        bindKeys(Control.NOTE_UP, [FlxKey.UP]);
-        bindKeys(Control.NOTE_DOWN, [FlxKey.DOWN]);
-        bindKeys(Control.NOTE_LEFT, [FlxKey.LEFT]);
-        bindKeys(Control.NOTE_RIGHT, [FlxKey.RIGHT]);
-        bindKeys(Control.ACCEPT, [O]);
-        bindKeys(Control.BACK, [P]);
-        bindKeys(Control.PAUSE, [ENTER]);
-        bindKeys(Control.RESET, [BACKSPACE]);
-      case None: // nothing
-      case Custom: // nothing
-    }
+    bindKeys(Control.UI_UP, getDefaultKeybinds(scheme, Control.UI_UP));
+    bindKeys(Control.UI_DOWN, getDefaultKeybinds(scheme, Control.UI_DOWN));
+    bindKeys(Control.UI_LEFT, getDefaultKeybinds(scheme, Control.UI_LEFT));
+    bindKeys(Control.UI_RIGHT, getDefaultKeybinds(scheme, Control.UI_RIGHT));
+    bindKeys(Control.NOTE_UP, getDefaultKeybinds(scheme, Control.NOTE_UP));
+    bindKeys(Control.NOTE_DOWN, getDefaultKeybinds(scheme, Control.NOTE_DOWN));
+    bindKeys(Control.NOTE_LEFT, getDefaultKeybinds(scheme, Control.NOTE_LEFT));
+    bindKeys(Control.NOTE_RIGHT, getDefaultKeybinds(scheme, Control.NOTE_RIGHT));
+    bindKeys(Control.ACCEPT, getDefaultKeybinds(scheme, Control.ACCEPT));
+    bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK));
+    bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE));
+    bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
+    bindKeys(Control.CUTSCENE_SKIP, getDefaultKeybinds(scheme, Control.CUTSCENE_SKIP));
+    bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
+    bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
+    bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
 
     bindMobileLol();
   }
 
+  function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array<FlxKey> {
+    switch (scheme) {
+      case Solo:
+        switch (control) {
+          case Control.UI_UP: return [W, FlxKey.UP];
+          case Control.UI_DOWN: return [S, FlxKey.DOWN];
+          case Control.UI_LEFT: return [A, FlxKey.LEFT];
+          case Control.UI_RIGHT: return [D, FlxKey.RIGHT];
+          case Control.NOTE_UP: return [W, FlxKey.UP];
+          case Control.NOTE_DOWN: return [S, FlxKey.DOWN];
+          case Control.NOTE_LEFT: return [A, FlxKey.LEFT];
+          case Control.NOTE_RIGHT: return [D, FlxKey.RIGHT];
+          case Control.ACCEPT: return [Z, SPACE, ENTER];
+          case Control.BACK: return [X, BACKSPACE, ESCAPE];
+          case Control.PAUSE: return [P, ENTER, ESCAPE];
+          case Control.CUTSCENE_ADVANCE: return [Z, ENTER];
+          case Control.CUTSCENE_SKIP: return [P, ESCAPE];
+          case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
+          case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
+          case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
+          case Control.RESET: return [R];
+        }
+      case Duo(true):
+        switch (control) {
+          case Control.UI_UP: return [W];
+          case Control.UI_DOWN: return [S];
+          case Control.UI_LEFT: return [A];
+          case Control.UI_RIGHT: return [D];
+          case Control.NOTE_UP: return [W];
+          case Control.NOTE_DOWN: return [S];
+          case Control.NOTE_LEFT: return [A];
+          case Control.NOTE_RIGHT: return [D];
+          case Control.ACCEPT: return [G, Z];
+          case Control.BACK: return [H, X];
+          case Control.PAUSE: return [ONE];
+          case Control.CUTSCENE_ADVANCE: return [G, Z];
+          case Control.CUTSCENE_SKIP: return [ONE];
+          case Control.VOLUME_UP: return [PLUS];
+          case Control.VOLUME_DOWN: return [MINUS];
+          case Control.VOLUME_MUTE: return [ZERO];
+          case Control.RESET: return [R];
+        }
+      case Duo(false):
+        switch (control) {
+          case Control.UI_UP: return [FlxKey.UP];
+          case Control.UI_DOWN: return [FlxKey.DOWN];
+          case Control.UI_LEFT: return [FlxKey.LEFT];
+          case Control.UI_RIGHT: return [FlxKey.RIGHT];
+          case Control.NOTE_UP: return [FlxKey.UP];
+          case Control.NOTE_DOWN: return [FlxKey.DOWN];
+          case Control.NOTE_LEFT: return [FlxKey.LEFT];
+          case Control.NOTE_RIGHT: return [FlxKey.RIGHT];
+          case Control.ACCEPT: return [ENTER];
+          case Control.BACK: return [ESCAPE];
+          case Control.PAUSE: return [ONE];
+          case Control.CUTSCENE_ADVANCE: return [ENTER];
+          case Control.CUTSCENE_SKIP: return [ONE];
+          case Control.VOLUME_UP: return [NUMPADPLUS];
+          case Control.VOLUME_DOWN: return [NUMPADMINUS];
+          case Control.VOLUME_MUTE: return [NUMPADZERO];
+          case Control.RESET: return [R];
+        }
+      default:
+        // Fallthrough.
+    }
+
+    return [];
+  }
+
   function bindMobileLol()
   {
     #if FLX_TOUCH
@@ -704,23 +841,51 @@ class Controls extends FlxActionSet
   {
     addGamepadLiteral(id, [
 
-      Control.ACCEPT => [#if switch B #else A #end],
-      Control.BACK => [#if switch A #else B #end, FlxGamepadInputID.BACK],
-      Control.UI_UP => [DPAD_UP, LEFT_STICK_DIGITAL_UP],
-      Control.UI_DOWN => [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN],
-      Control.UI_LEFT => [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT],
-      Control.UI_RIGHT => [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT],
+      Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT),
+      Control.BACK => getDefaultGamepadBinds(Control.BACK),
+      Control.UI_UP => getDefaultGamepadBinds(Control.UI_UP),
+      Control.UI_DOWN => getDefaultGamepadBinds(Control.UI_DOWN),
+      Control.UI_LEFT => getDefaultGamepadBinds(Control.UI_LEFT),
+      Control.UI_RIGHT => getDefaultGamepadBinds(Control.UI_RIGHT),
       // don't swap A/B or X/Y for switch on these. A is always the bottom face button
-      Control.NOTE_UP => [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP],
-      Control.NOTE_DOWN => [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN],
-      Control.NOTE_LEFT => [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT],
-      Control.NOTE_RIGHT => [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT],
-      Control.PAUSE => [START],
-      Control.RESET => [RIGHT_SHOULDER]
-      #if CAN_CHEAT, Control.CHEAT => [X] #end
+      Control.NOTE_UP => getDefaultGamepadBinds(Control.NOTE_UP),
+      Control.NOTE_DOWN => getDefaultGamepadBinds(Control.NOTE_DOWN),
+      Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT),
+      Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT),
+      Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE),
+      // Control.VOLUME_UP => [RIGHT_SHOULDER],
+      // Control.VOLUME_DOWN => [LEFT_SHOULDER],
+      // Control.VOLUME_MUTE => [RIGHT_TRIGGER],
+      Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE),
+      Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP),
+      Control.RESET => getDefaultGamepadBinds(Control.RESET),
+      #if CAN_CHEAT, Control.CHEAT => getDefaultGamepadBinds(Control.CHEAT) #end
     ]);
   }
 
+  function getDefaultGamepadBinds(control:Control):Array<FlxGamepadInputID> {
+    switch(control) {
+      case Control.ACCEPT: return [#if switch B #else A #end];
+      case Control.BACK: return [#if switch A #else B #end, FlxGamepadInputID.BACK];
+      case Control.UI_UP: return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
+      case Control.UI_DOWN: return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
+      case Control.UI_LEFT: return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
+      case Control.UI_RIGHT: return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
+      case Control.NOTE_UP: return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
+      case Control.NOTE_DOWN: return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
+      case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
+      case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
+      case Control.PAUSE: return [START];
+      case Control.CUTSCENE_ADVANCE: return [A];
+      case Control.CUTSCENE_SKIP: return [START];
+      case Control.RESET: return [RIGHT_SHOULDER];
+      #if CAN_CHEAT, Control.CHEAT: return [X]; #end
+      default:
+        // Fallthrough.
+    }
+    return [];
+  }
+
   /**
    * Sets all actions that pertain to the binder to trigger when the supplied keys are used.
    * If binder is a literal you can inline this
@@ -749,8 +914,10 @@ class Controls extends FlxActionSet
 
   inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
   {
-    for (button in buttons)
+    for (button in buttons) {
+      if (button == FlxGamepadInputID.NONE) continue; // Ignore unbound keys.
       action.addGamepad(button, state, id);
+    }
   }
 
   static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>)
@@ -798,6 +965,11 @@ class Controls extends FlxActionSet
     }
   }
 
+  /**
+   * NOTE: When loading controls:
+   * An EMPTY array means the control is uninitialized and needs to be reset to default.
+   * An array with a single FlxKey.NONE means the control was intentionally unbound by the user.
+   */
   public function fromSaveData(data:Dynamic, device:Device)
   {
     for (control in Control.createAll())
@@ -805,17 +977,44 @@ class Controls extends FlxActionSet
       var inputs:Array<Int> = Reflect.field(data, control.getName());
       if (inputs != null)
       {
+        if (inputs.length == 0) {
+          trace('Control ${control} is missing bindings, resetting to default.');
+          switch(device)
+          {
+            case Keys:
+              bindKeys(control, getDefaultKeybinds(Solo, control));
+            case Gamepad(id):
+              bindButtons(control, id, getDefaultGamepadBinds(control));
+          }
+        } else if (inputs == [FlxKey.NONE]) {
+          trace('Control ${control} is unbound, leaving it be.');
+        } else {
+          switch(device)
+          {
+            case Keys:
+              bindKeys(control, inputs.copy());
+            case Gamepad(id):
+              bindButtons(control, id, inputs.copy());
+          }
+        }
+      } else {
+        trace('Control ${control} is missing bindings, resetting to default.');
         switch(device)
         {
           case Keys:
-            bindKeys(control, inputs.copy());
+            bindKeys(control, getDefaultKeybinds(Solo, control));
           case Gamepad(id):
-            bindButtons(control, id, inputs.copy());
+            bindButtons(control, id, getDefaultGamepadBinds(control));
         }
       }
     }
   }
 
+  /**
+   * NOTE: When saving controls:
+   * An EMPTY array means the control is uninitialized and needs to be reset to default.
+   * An array with a single FlxKey.NONE means the control was intentionally unbound by the user.
+   */
   public function createSaveData(device:Device):Dynamic
   {
     var isEmpty = true;
@@ -825,6 +1024,8 @@ class Controls extends FlxActionSet
       var inputs = getInputsFor(control, device);
       isEmpty = isEmpty && inputs.length == 0;
 
+      if (inputs.length == 0) inputs = [FlxKey.NONE];
+
       Reflect.setField(data, control.getName(), inputs);
     }
 
diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx
index bd81cae5c..0d9db5b34 100644
--- a/source/funkin/ui/ControlsMenu.hx
+++ b/source/funkin/ui/ControlsMenu.hx
@@ -14,7 +14,7 @@ import funkin.ui.TextMenuList;
 
 class ControlsMenu extends funkin.ui.OptionsState.Page
 {
-  inline static public var COLUMNS = 2;
+  public static inline final COLUMNS = 2;
   static var controlList = Control.createAll();
   /*
    * Defines groups of controls that cannot share inputs, like left and right. Say, if ACCEPT is Z, Back is X,
@@ -23,7 +23,9 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
    */
   static var controlGroups:Array<Array<Control>> = [
     [NOTE_UP, NOTE_DOWN, NOTE_LEFT, NOTE_RIGHT],
-    [UI_UP, UI_DOWN, UI_LEFT, UI_RIGHT, ACCEPT, BACK]
+    [UI_UP, UI_DOWN, UI_LEFT, UI_RIGHT, ACCEPT, BACK],
+    [CUTSCENE_ADVANCE, CUTSCENE_SKIP],
+    [VOLUME_UP, VOLUME_DOWN, VOLUME_MUTE]
   ];
 
   var itemGroups:Array<Array<InputItem>> = [for (i in 0...controlGroups.length) []];
@@ -36,7 +38,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
   var labels:FlxTypedGroup<AtlasText>;
 
   var currentDevice:Device = Keys;
-  var deviceListSelected = false;
+  var deviceListSelected:Bool = false;
 
   public function new()
   {
@@ -48,7 +50,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
     camera = menuCamera;
 
     labels = new FlxTypedGroup<AtlasText>();
-    var headers = new FlxTypedGroup<AtlasText>();
+    var headers:FlxTypedGroup<AtlasText> = new FlxTypedGroup<AtlasText>();
     controlGrid = new MenuTypedList(Columns(COLUMNS), Vertical);
 
     add(labels);
@@ -57,20 +59,20 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
 
     if (FlxG.gamepads.numActiveGamepads > 0)
     {
-      var devicesBg = new FlxSprite();
-      devicesBg.makeGraphic(FlxG.width, 100, 0xFFfafd6d);
+      var devicesBg:FlxSprite = new FlxSprite();
+      devicesBg.makeGraphic(FlxG.width, 100, 0xFFFAFD6D);
       add(devicesBg);
       deviceList = new TextMenuList(Horizontal, None);
       add(deviceList);
       deviceListSelected = true;
 
-      var item;
+      var item:TextMenuItem;
 
-      item = deviceList.createItem("Keyboard", AtlasFont.BOLD, selectDevice.bind(Keys));
+      item = deviceList.createItem('Keyboard', AtlasFont.BOLD, selectDevice.bind(Keys));
       item.x = FlxG.width / 2 - item.width - 30;
       item.y = (devicesBg.height - item.height) / 2;
 
-      item = deviceList.createItem("Gamepad", AtlasFont.BOLD, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id)));
+      item = deviceList.createItem('Gamepad', AtlasFont.BOLD, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id)));
       item.x = FlxG.width / 2 + 30;
       item.y = (devicesBg.height - item.height) / 2;
     }
@@ -96,6 +98,18 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
         headers.add(new AtlasText(0, y, "NOTES", AtlasFont.BOLD)).screenCenter(X);
         y += spacer;
       }
+      else if (currentHeader != "CUTSCENE_" && name.indexOf("CUTSCENE_") == 0)
+      {
+        currentHeader = "CUTSCENE_";
+        headers.add(new AtlasText(0, y, "CUTSCENE", AtlasFont.BOLD)).screenCenter(X);
+        y += spacer;
+      }
+      else if (currentHeader != "VOLUME_" && name.indexOf("VOLUME_") == 0)
+      {
+        currentHeader = "VOLUME_";
+        headers.add(new AtlasText(0, y, "VOLUME", AtlasFont.BOLD)).screenCenter(X);
+        y += spacer;
+      }
 
       if (currentHeader != null && name.indexOf(currentHeader) == 0) name = name.substr(currentHeader.length);
 
@@ -128,7 +142,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
       labels.members[Std.int(controlGrid.selectedIndex / COLUMNS)].alpha = 1.0;
     });
 
-    prompt = new Prompt("\nPress any key to rebind\n\n\n\n    Escape to cancel", None);
+    prompt = new Prompt("\nPress any key to rebind\n\n\nBackspace to unbind\n    Escape to cancel", None);
     prompt.create();
     prompt.createBgFromMargin(100, 0xFFfafd6d);
     prompt.back.scrollFactor.set(0, 0);
@@ -149,6 +163,8 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
 
   function onSelect():Void
   {
+    keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+
     controlGrid.enabled = false;
     canExit = false;
     prompt.exists = true;
@@ -187,7 +203,9 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
     canExit = false;
   }
 
-  override function update(elapsed:Float)
+  var keyUsedToEnterPrompt:Null<Int> = null;
+
+  override function update(elapsed:Float):Void
   {
     super.update(elapsed);
 
@@ -200,18 +218,35 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
       {
         case Keys:
           {
-            // check released otherwise bugs can happen when you change the BACK key
+            // Um?
+            // Checking pressed causes problems when you change the BACK key,
+            // but checking released causes problems when the prompt is instant.
+
+            // keyUsedToEnterPrompt is my weird workaround.
+
             var key = FlxG.keys.firstJustReleased();
-            if (key != NONE)
+            if (key != NONE && key != keyUsedToEnterPrompt)
             {
-              if (key != ESCAPE) onInputSelect(key);
-              closePrompt();
+              if (key == ESCAPE)
+              {
+                closePrompt();
+              }
+              else if (key == BACKSPACE)
+              {
+                onInputSelect(NONE);
+                closePrompt();
+              }
+              else
+              {
+                onInputSelect(key);
+                closePrompt();
+              }
             }
           }
         case Gamepad(id):
           {
             var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
-            if (button != NONE)
+            if (button != NONE && button != keyUsedToEnterPrompt)
             {
               if (button != BACK) onInputSelect(button);
               closePrompt();
@@ -219,23 +254,32 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
           }
       }
     }
+
+    var keyJustReleased:Int = FlxG.keys.firstJustReleased();
+    if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+    {
+      keyUsedToEnterPrompt = null;
+    }
   }
 
-  function onInputSelect(input:Int)
+  function onInputSelect(input:Int):Void
   {
     var item = controlGrid.selectedItem;
 
     // check if that key is already set for this
-    var column0 = Math.floor(controlGrid.selectedIndex / 2) * 2;
-    for (i in 0...COLUMNS)
+    if (input != FlxKey.NONE)
     {
-      if (controlGrid.members[column0 + i].input == input) return;
+      var column0 = Math.floor(controlGrid.selectedIndex / 2) * 2;
+      for (i in 0...COLUMNS)
+      {
+        if (controlGrid.members[column0 + i].input == input) return;
+      }
     }
 
     // Check if items in the same group already have the new input
     for (group in itemGroups)
     {
-      if (group.contains(item))
+      if (input != FlxKey.NONE && group.contains(item))
       {
         for (otherItem in group)
         {
@@ -252,10 +296,45 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
     }
 
     PlayerSettings.player1.controls.replaceBinding(item.control, currentDevice, input, item.input);
+
     // Don't use resetItem() since items share names/labels
     item.input = input;
     item.label.text = item.getLabel(input);
 
+    // Shift left on the grid if the item on the right is bound and the item on the left is unbound.
+    if (controlGrid.selectedIndex % 2 == 1)
+    {
+      trace('Modified item on right side of grid');
+      var leftItem = controlGrid.members[controlGrid.selectedIndex - 1];
+      if (leftItem != null && input != FlxKey.NONE && leftItem.input == FlxKey.NONE)
+      {
+        trace('Left item is unbound and right item is not!');
+        // Swap them.
+        var temp = leftItem.input;
+        leftItem.input = item.input;
+        item.input = temp;
+
+        leftItem.label.text = leftItem.getLabel(leftItem.input);
+        item.label.text = item.getLabel(item.input);
+      }
+    }
+    else
+    {
+      trace('Modified item on left side of grid');
+      var rightItem = controlGrid.members[controlGrid.selectedIndex + 1];
+      if (rightItem != null && input == FlxKey.NONE && rightItem.input != FlxKey.NONE)
+      {
+        trace('Left item is unbound and right item is not!');
+        // Swap them.
+        var temp = item.input;
+        item.input = rightItem.input;
+        rightItem.input = temp;
+
+        item.label.text = item.getLabel(item.input);
+        rightItem.label.text = rightItem.getLabel(rightItem.input);
+      }
+    }
+
     PlayerSettings.player1.saveControls();
   }
 
@@ -306,6 +385,8 @@ class InputItem extends TextMenuItem
     this.input = getInput();
 
     super(x, y, getLabel(input), DEFAULT, callback);
+
+    this.fireInstantly = true;
   }
 
   public function updateDevice(device:Device)
@@ -334,6 +415,6 @@ class InputItem extends TextMenuItem
 
   public function getLabel(input:Int)
   {
-    return input == -1 ? "---" : InputFormatter.format(input, device);
+    return input == FlxKey.NONE ? "---" : InputFormatter.format(input, device);
   }
 }