From 5931345c71a7e3ddd0d71a9ac17cb64b76af3480 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 5 Oct 2023 02:21:01 -0400
Subject: [PATCH] Rewrite precise inputs to work on gamepad

---
 hmm.json                                   |   6 +-
 source/funkin/Controls.hx                  |  10 +
 source/funkin/PlayerSettings.hx            |   7 +-
 source/funkin/input/PreciseInputManager.hx | 211 +++++++++++++++++++--
 source/funkin/play/PlayState.hx            |  98 ----------
 source/funkin/ui/ControlsMenu.hx           |  51 ++++-
 source/funkin/ui/StickerSubState.hx        |   2 +-
 source/funkin/util/FlxGamepadUtil.hx       |  44 +++++
 8 files changed, 309 insertions(+), 120 deletions(-)
 create mode 100644 source/funkin/util/FlxGamepadUtil.hx

diff --git a/hmm.json b/hmm.json
index 47460facf..34e9efb02 100644
--- a/hmm.json
+++ b/hmm.json
@@ -104,7 +104,7 @@
       "name": "lime",
       "type": "git",
       "dir": null,
-      "ref": "f195121ebec688b417e38ab115185c8d93c349d3",
+      "ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
       "url": "https://github.com/EliteMasterEric/lime"
     },
     {
@@ -139,7 +139,7 @@
       "name": "openfl",
       "type": "git",
       "dir": null,
-      "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08",
+      "ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
       "url": "https://github.com/EliteMasterEric/openfl"
     },
     {
@@ -160,4 +160,4 @@
       "version": "0.11.0"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx
index 81055fb34..9372c4dc6 100644
--- a/source/funkin/Controls.hx
+++ b/source/funkin/Controls.hx
@@ -1,5 +1,7 @@
+
 package funkin;
 
+import flixel.input.gamepad.FlxGamepad;
 import flixel.util.FlxDirectionFlags;
 import flixel.FlxObject;
 import flixel.input.FlxInput;
@@ -832,6 +834,14 @@ class Controls extends FlxActionSet
     fromSaveData(padData, Gamepad(id));
   }
 
+  public function getGamepadIds():Array<Int> {
+    return gamepadsAdded;
+  }
+
+  public function getGamepads():Array<FlxGamepad> {
+    return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
+  }
+
   inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
   {
     gamepadsAdded.push(id);
diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx
index a4d8a3b5c..e97cfe384 100644
--- a/source/funkin/PlayerSettings.hx
+++ b/source/funkin/PlayerSettings.hx
@@ -77,6 +77,11 @@ class PlayerSettings
     this.id = id;
     this.controls = new Controls('player$id', None);
 
+    addKeyboard();
+  }
+
+  function addKeyboard():Void
+  {
     var useDefault = true;
     if (Save.get().hasControls(id, Keys))
     {
@@ -96,7 +101,6 @@ class PlayerSettings
       controls.setKeyboardScheme(Solo);
     }
 
-    // Apply loaded settings.
     PreciseInputManager.instance.initializeKeys(controls);
   }
 
@@ -124,6 +128,7 @@ class PlayerSettings
       trace("Loading gamepad control scheme");
       controls.addDefaultGamepad(gamepad.id);
     }
+    PreciseInputManager.instance.initializeButtons(controls, gamepad);
   }
 
   /**
diff --git a/source/funkin/input/PreciseInputManager.hx b/source/funkin/input/PreciseInputManager.hx
index 6217b2fe7..897f738fb 100644
--- a/source/funkin/input/PreciseInputManager.hx
+++ b/source/funkin/input/PreciseInputManager.hx
@@ -1,18 +1,25 @@
 package funkin.input;
 
-import openfl.ui.Keyboard;
-import funkin.play.notes.NoteDirection;
-import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
-import openfl.events.KeyboardEvent;
 import flixel.FlxG;
+import flixel.input.FlxInput;
 import flixel.input.FlxInput.FlxInputState;
 import flixel.input.FlxKeyManager;
+import flixel.input.gamepad.FlxGamepad;
+import flixel.input.gamepad.FlxGamepadInputID;
 import flixel.input.keyboard.FlxKey;
+import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
 import flixel.input.keyboard.FlxKeyList;
 import flixel.util.FlxSignal.FlxTypedSignal;
+import funkin.play.notes.NoteDirection;
+import funkin.util.FlxGamepadUtil;
 import haxe.Int64;
+import lime.ui.Gamepad as LimeGamepad;
+import lime.ui.GamepadAxis as LimeGamepadAxis;
+import lime.ui.GamepadButton as LimeGamepadButton;
 import lime.ui.KeyCode;
 import lime.ui.KeyModifier;
+import openfl.events.KeyboardEvent;
+import openfl.ui.Keyboard;
 
 /**
  * A precise input manager that:
@@ -43,6 +50,20 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
    */
   var _keyListDir:Map<FlxKey, NoteDirection>;
 
+  /**
+   * A FlxGamepadID->Array<FlxGamepadInputID>, with FlxGamepadInputID being the counterpart to FlxKey.
+   */
+  var _buttonList:Map<Int, Array<FlxGamepadInputID>>;
+
+  var _buttonListArray:Array<FlxInput<FlxGamepadInputID>>;
+
+  var _buttonListMap:Map<Int, Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>>;
+
+  /**
+   * A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey.
+   */
+  var _buttonListDir:Map<Int, Map<FlxGamepadInputID, NoteDirection>>;
+
   /**
    * The timestamp at which a given note direction was last pressed.
    */
@@ -53,17 +74,32 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
    */
   var _dirReleaseTimestamps:Map<NoteDirection, Int64>;
 
+  var _deviceBinds:Map<FlxGamepad,
+    {
+      onButtonDown:LimeGamepadButton->Int64->Void,
+      onButtonUp:LimeGamepadButton->Int64->Void
+    }>;
+
   public function new()
   {
     super(PreciseInputList.new);
 
+    _deviceBinds = [];
+
     _keyList = [];
-    _dirPressTimestamps = new Map<NoteDirection, Int64>();
-    _dirReleaseTimestamps = new Map<NoteDirection, Int64>();
+    // _keyListMap
+    // _keyListArray
     _keyListDir = new Map<FlxKey, NoteDirection>();
 
-    FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
-    FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
+    _buttonList = [];
+    _buttonListMap = [];
+    _buttonListArray = [];
+    _buttonListDir = new Map<Int, Map<FlxGamepadInputID, NoteDirection>>();
+
+    _dirPressTimestamps = new Map<NoteDirection, Int64>();
+    _dirReleaseTimestamps = new Map<NoteDirection, Int64>();
+
+    // Keyboard
     FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
     FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp);
 
@@ -84,6 +120,17 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
     };
   }
 
+  public static function getButtonsForDirection(controls:Controls, noteDirection:NoteDirection)
+  {
+    return switch (noteDirection)
+    {
+      case NoteDirection.LEFT: controls.getButtonsForAction(NOTE_LEFT);
+      case NoteDirection.DOWN: controls.getButtonsForAction(NOTE_DOWN);
+      case NoteDirection.UP: controls.getButtonsForAction(NOTE_UP);
+      case NoteDirection.RIGHT: controls.getButtonsForAction(NOTE_RIGHT);
+    };
+  }
+
   /**
    * Convert from int to Int64.
    */
@@ -138,6 +185,43 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
     }
   }
 
+  public function initializeButtons(controls:Controls, gamepad:FlxGamepad):Void
+  {
+    clearButtons();
+
+    var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
+    var callbacks =
+      {
+        onButtonDown: handleButtonDown.bind(gamepad),
+        onButtonUp: handleButtonUp.bind(gamepad)
+      };
+    limeGamepad.onButtonDownPrecise.add(callbacks.onButtonDown);
+    limeGamepad.onButtonUpPrecise.add(callbacks.onButtonUp);
+
+    for (noteDirection in DIRECTIONS)
+    {
+      var buttons = getButtonsForDirection(controls, noteDirection);
+      for (button in buttons)
+      {
+        var input = new FlxInput<FlxGamepadInputID>(button);
+
+        var buttonListEntry = _buttonList.get(gamepad.id);
+        if (buttonListEntry == null) _buttonList.set(gamepad.id, buttonListEntry = []);
+        buttonListEntry.push(button);
+
+        _buttonListArray.push(input);
+
+        var buttonListMapEntry = _buttonListMap.get(gamepad.id);
+        if (buttonListMapEntry == null) _buttonListMap.set(gamepad.id, buttonListMapEntry = new Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>());
+        buttonListMapEntry.set(button, input);
+
+        var buttonListDirEntry = _buttonListDir.get(gamepad.id);
+        if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map<FlxGamepadInputID, NoteDirection>());
+        buttonListDirEntry.set(button, noteDirection);
+      }
+    }
+  }
+
   /**
    * Get the time, in nanoseconds, since the given note direction was last pressed.
    * @param noteDirection The note direction to check.
@@ -165,11 +249,41 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
     return _keyListMap.get(key);
   }
 
+  public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
+  {
+    return _buttonListMap.get(gamepad.id).get(button);
+  }
+
   public function getDirectionForKey(key:FlxKey):NoteDirection
   {
     return _keyListDir.get(key);
   }
 
+  public function getDirectionForButton(gamepad:FlxGamepad, button:FlxGamepadInputID):NoteDirection
+  {
+    return _buttonListDir.get(gamepad.id).get(button);
+  }
+
+  function getButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
+  {
+    return _buttonListMap.get(gamepad.id).get(button);
+  }
+
+  function updateButtonStates(gamepad:FlxGamepad, button:FlxGamepadInputID, down:Bool):Void
+  {
+    var input = getButton(gamepad, button);
+    if (input == null) return;
+
+    if (down)
+    {
+      input.press();
+    }
+    else
+    {
+      input.release();
+    }
+  }
+
   function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
   {
     var key:FlxKey = convertKeyCode(keyCode);
@@ -181,7 +295,7 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
 
     updateKeyStates(key, true);
 
-    if (getInputByKey(key) ?.justPressed ?? false)
+    if (getInputByKey(key)?.justPressed ?? false)
     {
       onInputPressed.dispatch(
         {
@@ -198,12 +312,12 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
     if (_keyList.indexOf(key) == -1) return;
 
     // TODO: Remove this line with SDL3 when timestamps change meaning.
-    // This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
+    // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
     timestamp *= Constants.NS_PER_MS;
 
     updateKeyStates(key, false);
 
-    if (getInputByKey(key) ?.justReleased ?? false)
+    if (getInputByKey(key)?.justReleased ?? false)
     {
       onInputReleased.dispatch(
         {
@@ -214,6 +328,54 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
     }
   }
 
+  function handleButtonDown(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
+  {
+    var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
+
+    var buttonListEntry = _buttonList.get(gamepad.id);
+    if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
+
+    // TODO: Remove this line with SDL3 when timestamps change meaning.
+    // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
+    timestamp *= Constants.NS_PER_MS;
+
+    updateButtonStates(gamepad, buttonId, true);
+
+    if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
+    {
+      onInputPressed.dispatch(
+        {
+          noteDirection: getDirectionForButton(gamepad, buttonId),
+          timestamp: timestamp
+        });
+      _dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
+    }
+  }
+
+  function handleButtonUp(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
+  {
+    var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
+
+    var buttonListEntry = _buttonList.get(gamepad.id);
+    if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
+
+    // TODO: Remove this line with SDL3 when timestamps change meaning.
+    // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
+    timestamp *= Constants.NS_PER_MS;
+
+    updateButtonStates(gamepad, buttonId, false);
+
+    if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
+    {
+      onInputReleased.dispatch(
+        {
+          noteDirection: getDirectionForButton(gamepad, buttonId),
+          timestamp: timestamp
+        });
+      _dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
+    }
+  }
+
   static function convertKeyCode(input:KeyCode):FlxKey
   {
     @:privateAccess
@@ -228,6 +390,31 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
     _keyListMap.clear();
     _keyListDir.clear();
   }
+
+  function clearButtons():Void
+  {
+    _buttonListArray = [];
+    _buttonListDir.clear();
+
+    for (gamepad in _deviceBinds.keys())
+    {
+      var callbacks = _deviceBinds.get(gamepad);
+      var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
+      limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown);
+      limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp);
+    }
+    _deviceBinds.clear();
+  }
+
+  public override function destroy():Void
+  {
+    // Keyboard
+    FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown);
+    FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp);
+
+    clearKeys();
+    clearButtons();
+  }
 }
 
 class PreciseInputList extends FlxKeyList
@@ -264,7 +451,7 @@ class PreciseInputList extends FlxKeyList
   {
     for (key in getKeysForDir(noteDir))
     {
-      if (check(_preciseInputManager.getInputByKey(key) ?.ID)) return true;
+      if (check(_preciseInputManager.getInputByKey(key)?.ID)) return true;
     }
     return false;
   }
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 26aef9b3d..ce3000645 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -909,7 +909,6 @@ class PlayState extends MusicBeatSubState
     }
 
     // Handle keybinds.
-    // if (!isInCutscene && !disableKeys) keyShit(true);
     processInputQueue();
     if (!isInCutscene && !disableKeys) debugKeyShit();
     if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
@@ -2014,103 +2013,6 @@ class PlayState extends MusicBeatSubState
     }
   }
 
-  /**
-   * Handle player inputs.
-   */
-  function keyShit(test:Bool):Void
-  {
-    // control arrays, order L D R U
-    var holdArray:Array<Bool> = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT];
-    var pressArray:Array<Bool> = [
-      controls.NOTE_LEFT_P,
-      controls.NOTE_DOWN_P,
-      controls.NOTE_UP_P,
-      controls.NOTE_RIGHT_P
-    ];
-    var releaseArray:Array<Bool> = [
-      controls.NOTE_LEFT_R,
-      controls.NOTE_DOWN_R,
-      controls.NOTE_UP_R,
-      controls.NOTE_RIGHT_R
-    ];
-
-    // if (pressArray.contains(true))
-    // {
-    //   var lol:Array<Int> = cast pressArray;
-    //   inputSpitter.push(Std.int(Conductor.songPosition) + ' ' + lol.join(' '));
-    // }
-
-    // HOLDS, check for sustain notes
-    if (holdArray.contains(true) && generatedMusic)
-    {
-      /*
-        activeNotes.forEachAlive(function(daNote:Note) {
-          if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) goodNoteHit(daNote);
-        });
-       */
-    }
-
-    // PRESSES, check for note hits
-    if (pressArray.contains(true) && generatedMusic)
-    {
-      Haptic.vibrate(100, 100);
-
-      if (currentStage != null && currentStage.getBoyfriend() != null)
-      {
-        currentStage.getBoyfriend().holdTimer = 0;
-      }
-
-      var possibleNotes:Array<NoteSprite> = []; // notes that can be hit
-      var directionList:Array<Int> = []; // directions that can be hit
-      var dumbNotes:Array<NoteSprite> = []; // notes to kill later
-
-      for (note in dumbNotes)
-      {
-        FlxG.log.add('killing dumb ass note at ' + note.noteData.time);
-        note.kill();
-        // activeNotes.remove(note, true);
-        note.destroy();
-      }
-
-      possibleNotes.sort((a, b) -> Std.int(a.noteData.time - b.noteData.time));
-
-      if (perfectMode)
-      {
-        goodNoteHit(possibleNotes[0], null);
-      }
-      else if (possibleNotes.length > 0)
-      {
-        for (shit in 0...pressArray.length)
-        { // if a direction is hit that shouldn't be
-          if (pressArray[shit] && !directionList.contains(shit)) ghostNoteMiss(shit);
-        }
-        for (coolNote in possibleNotes)
-        {
-          if (pressArray[coolNote.noteData.getDirection()]) goodNoteHit(coolNote, null);
-        }
-      }
-      else
-      {
-        // HNGGG I really want to add an option for ghost tapping
-        // L + ratio
-        for (shit in 0...pressArray.length)
-          if (pressArray[shit]) ghostNoteMiss(shit, false);
-      }
-    }
-
-    if (currentStage == null) return;
-
-    for (keyId => isPressed in pressArray)
-    {
-      if (playerStrumline == null) continue;
-
-      var dir:NoteDirection = Strumline.DIRECTIONS[keyId];
-
-      if (isPressed && !playerStrumline.isConfirm(dir)) playerStrumline.playPress(dir);
-      if (!holdArray[keyId]) playerStrumline.playStatic(dir);
-    }
-  }
-
   function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void
   {
     var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true);
diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx
index 0d9db5b34..8197424ee 100644
--- a/source/funkin/ui/ControlsMenu.hx
+++ b/source/funkin/ui/ControlsMenu.hx
@@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
 
   function onSelect():Void
   {
-    keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+    switch (currentDevice)
+    {
+      case Keys:
+        {
+          keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+        }
+      case Gamepad(id):
+        {
+          buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID();
+        }
+    }
 
     controlGrid.enabled = false;
     canExit = false;
@@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
   }
 
   var keyUsedToEnterPrompt:Null<Int> = null;
+  var buttonUsedToEnterPrompt:Null<Int> = null;
 
   override function update(elapsed:Float):Void
   {
@@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
         case Gamepad(id):
           {
             var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
-            if (button != NONE && button != keyUsedToEnterPrompt)
+            if (button != NONE && button != buttonUsedToEnterPrompt)
             {
               if (button != BACK) onInputSelect(button);
               closePrompt();
             }
+
+            var key = FlxG.keys.firstJustReleased();
+            if (key != NONE && key != keyUsedToEnterPrompt)
+            {
+              if (key == ESCAPE)
+              {
+                closePrompt();
+              }
+              else if (key == BACKSPACE)
+              {
+                onInputSelect(NONE);
+                closePrompt();
+              }
+            }
           }
       }
     }
 
-    var keyJustReleased:Int = FlxG.keys.firstJustReleased();
-    if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+    switch (currentDevice)
     {
-      keyUsedToEnterPrompt = null;
+      case Keys:
+        {
+          var keyJustReleased:Int = FlxG.keys.firstJustReleased();
+          if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+          {
+            keyUsedToEnterPrompt = null;
+          }
+          buttonUsedToEnterPrompt = null;
+        }
+      case Gamepad(id):
+        {
+          var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID();
+          if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt)
+          {
+            buttonUsedToEnterPrompt = null;
+          }
+          keyUsedToEnterPrompt = null;
+        }
     }
   }
 
diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx
index 067f50c31..1d9edab0e 100644
--- a/source/funkin/ui/StickerSubState.hx
+++ b/source/funkin/ui/StickerSubState.hx
@@ -67,7 +67,7 @@ class StickerSubState extends MusicBeatSubState
       new FlxTimer().start(sticker.timing, _ -> {
         sticker.visible = false;
 
-        if (ind == grpStickers.members.length - 1)
+        if (grpStickers == null || ind == grpStickers.members.length - 1)
         {
           switchingState = false;
           close();
diff --git a/source/funkin/util/FlxGamepadUtil.hx b/source/funkin/util/FlxGamepadUtil.hx
new file mode 100644
index 000000000..d768b42c4
--- /dev/null
+++ b/source/funkin/util/FlxGamepadUtil.hx
@@ -0,0 +1,44 @@
+package funkin.util;
+
+import flixel.input.gamepad.FlxGamepad;
+import flixel.input.gamepad.FlxGamepadInputID;
+import lime.ui.Gamepad as LimeGamepad;
+import lime.ui.GamepadAxis as LimeGamepadAxis;
+import lime.ui.GamepadButton as LimeGamepadButton;
+
+class FlxGamepadUtil
+{
+  public static function getInputID(gamepad:FlxGamepad, button:LimeGamepadButton):FlxGamepadInputID
+  {
+    #if FLX_GAMEINPUT_API
+    // FLX_GAMEINPUT_API internally assigns 6 axes to IDs 0-5, which LimeGamepadButton doesn't account for, so we need to offset the ID by 6.
+    final OFFSET:Int = 6;
+    #else
+    final OFFSET:Int = 0;
+    #end
+
+    var result:FlxGamepadInputID = gamepad.mapping.getID(button + OFFSET);
+    if (result == NONE) return NONE;
+    return result;
+  }
+
+  public static function getLimeGamepad(input:FlxGamepad):Null<LimeGamepad>
+  {
+    #if FLX_GAMEINPUT_API @:privateAccess
+    return input._device.getLimeGamepad();
+    #else
+    return null;
+    #end
+  }
+
+  @:privateAccess
+  public static function getFlxGamepadByLimeGamepad(gamepad:LimeGamepad):FlxGamepad
+  {
+    // Why is this so elaborate?
+    @:privateAccess
+    var gameInputDevice:openfl.ui.GameInputDevice = openfl.ui.GameInput.__getDevice(gamepad);
+    @:privateAccess
+    var gamepadIndex:Int = FlxG.gamepads.findGamepadIndex(gameInputDevice);
+    return FlxG.gamepads.getByID(gamepadIndex);
+  }
+}