Skip to content

Instantly share code, notes, and snippets.

@ArcanoxDragon
Last active January 15, 2022 21:06
Show Gist options
  • Save ArcanoxDragon/9fe0cb35d0f93dc97f4b476867658e34 to your computer and use it in GitHub Desktop.
Save ArcanoxDragon/9fe0cb35d0f93dc97f4b476867658e34 to your computer and use it in GitHub Desktop.
Switch Emulator Pro Controller Turbo

Requirements

  • FreePIE (multipurpose input scripting engine)
  • vJoy (virtual joystick device that can be scripted)
  • A Switch Pro Controller
  • A PC with Bluetooth

Instructions:

  • Install the required programs from above
  • Configure vJoy (there is a start menu entry called Configure vJoy after installing it) with the following configuration on the first vJoy controller:
    • image
  • Pair your Pro Controller with your computer and make sure it shows up in the "Set up USB game controllers" in Windows
    • Take note of where it sits in the list. If it's in the first spot, that's "0", if it's in the second it's "1", so on and so forth
  • Save the attached Python script somewhere convenient, and open it in FreePIE with File > Open
    • Under the line that says # === Configurable options ===, some settings can be changed. There's a comment above each one describing its function.
  • Press F5 in the FreePIE window to start the script. Hopefully it won't show any errors. If it does, you'll need some basic Python knowledge to troubleshoot them.

Turbo Function

Turbo can be enabled on any button by holding the "Toggle Turbo" button (defaults to the Share button on a Switch Pro Controller) and then pressing another button on the controller with it pressed. This will toggle Turbo for that button so that holding the physical button down will cause the emulated button to be pressed rapidly at the configured frequency (15 Hz by default). For example, in Metroid Dread, you can hold the "Share" button down and tap "A" at the start of the game (or while it's loading) to enable turbo-A from the start for mashing through A.D.A.M. text.

For the "Y" and "A" buttons specifically (by default), a "quick toggle" feature is provided so that if you quickly double-tap either button and then hold the button on the second tap, it will temporarily toggle the Turbo state until you release the button again. For example, in Metroid Dread, if you have Turbo enabled on "Y" for firing, but need to shoot a Charge Beam shot once, you can quickly double-tap-hold Y while Turbo is enabled, and it will temporarily stop Turbo to allow the Charge Beam shot to charge up. Once you release Y to fire the shot, Turbo will be enabled again unless you quickly double-tap the button a second time.

This works in reverse if Turbo is not enabled. If you have Turbo disabled for Y and quickly double-tap-and-hold the button, it will temporarily activate Turbo while you hold down the button, and once you release it, the button will be back to normal unless you double-tap-hold it again.

from collections import defaultdict
from math import sqrt
from time import time
if starting:
# === Configurable options ===
# debug: bool (default: False). Can be set to "True" to enable debug logging, but will cause performance issues with Turbo.
debug = False
# joyIndex: int (default: 1). Index of your physical controller (i.e. a Pro Controller) in the USB Game Controllers list (0 = first spot)
joyIndex = 1
# vJoyIndex: int (default: 0). Index of the vJoy controller to use. Probably should be "0".
vJoyIndex = 0
# numButtons: int (default: 14). Number of buttons on the physical controller. Pro Controller has 14.
numButtons = 14
# numAxes: int (default: 6). Number of axes on the physical controller. Pro Controller has 6 (for some reason...).
numAxes = 6
# numPovs: int (default: 1). Number of POV Hats/D-pads on the physical controller. Pro Controller has 1.
numPovs = 1
# turboToggleButton: int (default: 13). Index of the button to use to toggle Turbo. Defaults to "13", which is the "Share" button on a Pro Controller.
turboToggleButton = 13
# turboFrequency: int (default: 15). Frequency, in Hz, of the Turbo feature when enabled for a button.
turboFrequency = 15
# quickTurboTapWindow: float (default: 0.1). Time, in seconds, during which a "quick tap" sequence must take place to temporarily toggle the turbo state for a button.
quickTurboTapWindow = 0.1
# quickTurboPresses: int (default: 2). Number of presses that must occur in the quickTurboTapWindow in order to temporarily toggle the turbo state for a button.
quickTurboPresses = 2
# quickTurboableButtons: list[int]. The buttons for which "quick turbo" is enabled. Defaults to "1" and "2", which are A and Y respectively on a Pro Controller.
quickTurboableButtons = [
1, # A
2, # Y
]
# axisDeadzone: float (default: 0.1). The deadzone for analog axes (as a ratio of the total stick travel in one direction; axes are deadzoned in pairs)
axisDeadzone = 0.1
# axisCloneMap: dict[tuple[int, int], tuple[int, int]]. Allows making one pair of axes act as another pair, i.e. for remapping the right stick to the left.
# Defaults to the right stick acting as the left stick; comment out the line with `(0, 1): (3, 4)` to disable.
axisCloneMap = {
# Keys represent "target" axis pairs, as a tuple. Values are the respective "source" axes, also as a tuple.
# If the physical target axis pair is neutral (both inside deadzone), the axis values from the "source"
# axes will be used instead.
#
# Example: to allow right stick to act as a second left stick, use "(0, 1): (3, 4)"
(0, 1): (3, 4),
}
# buttonNames: dict[int, str]. A mapping of button indices to button names. Default is the mapping for a Pro Controller.
buttonNames = {
0: "B",
1: "A",
2: "Y",
3: "X",
4: "L",
5: "R",
6: "ZL",
7: "ZR",
8: "-",
9: "+",
10: "LS",
11: "RS",
12: "Home",
13: "Share",
}
# === Other variables (don't change stuff below here unless you know what you're doing) ===
curTime = time()
joy = joystick[joyIndex]
vj = vJoy[vJoyIndex]
axisRange = vj.axisMax
rawAxisDeadzone = axisDeadzone * axisRange
turboInterval = 1.0 / float(turboFrequency)
quickTurboConsecutiveStateChanges = quickTurboPresses * 2 - 1
# State containers
axisOverrides = dict()
lastButtonState = defaultdict(lambda: False)
turboEnabled = defaultdict(lambda: False)
turboStartTimes = dict()
lastStateChangeTimes = defaultdict(lambda: 0.0)
consecutiveStateChanges = defaultdict(lambda: 1)
# Debug values
enabledTurboButtons = ""
axisValues = ""
# Setup
joy.setRange(-axisRange, axisRange)
# Build an axis override map that's more optimized for runtime
for targets, sources in axisCloneMap.items():
for i in range(len(targets)):
targetAxis = targets[i]
sourceAxis = sources[i]
axisOverrides[targetAxis] = (targets, sourceAxis)
# Function definitions
def mapRange(value, srcMin, srcMax, dstMin, dstMax):
return (value - srcMin) / float(srcMax - srcMin) * (dstMax - dstMin) + dstMin
def mapAxisValue(raw):
if raw >= rawAxisDeadzone:
return mapRange(raw, rawAxisDeadzone, axisRange, 0, axisRange)
elif raw <= -rawAxisDeadzone:
return mapRange(raw, -rawAxisDeadzone, -axisRange, 0, -axisRange)
else:
return 0.0
def getAxisRaw(index):
if index == 0:
raw = joy.x
elif index == 1:
raw = joy.y
elif index == 2:
raw = joy.z
elif index == 3:
raw = joy.xRotation
elif index == 4:
raw = joy.yRotation
elif index == 5:
raw = joy.zRotation
else:
raw = joy.sliders[index - 6]
return raw
def isAxisPairNeutral(pair):
x, y = [getAxisRaw(i) for i in pair]
magnitude = sqrt(x ** 2 + y ** 2)
return magnitude < rawAxisDeadzone
def getAxis(index):
if index in axisOverrides:
# Must figure out if we need to use override axes
targets, sourceAxis = axisOverrides[index]
if isAxisPairNeutral(targets):
# Use the override axis instead of the physical axis
return getAxisRaw(sourceAxis)
return getAxisRaw(index)
def setAxis(index, value):
if index == 0:
vj.x = value
elif index == 1:
vj.y = value
elif index == 2:
vj.z = value
elif index == 3:
vj.rx = value
elif index == 4:
vj.ry = value
elif index == 5:
vj.rz = value
def updateAxes():
for i in range(0, numAxes):
setAxis(i, getAxis(i))
def updateButtons():
for i in range(0, numButtons):
state = joy.getDown(i)
last = lastButtonState[i]
pressed = state and not last # if the button was just pressed down since last frame
useTurbo = turboEnabled[i]
if state != last:
lastStateChangeTime = lastStateChangeTimes[i]
if curTime - lastStateChangeTime < quickTurboTapWindow:
consecutiveStateChanges[i] += 1
else:
consecutiveStateChanges[i] = 1
lastStateChangeTimes[i] = curTime
if state and consecutiveStateChanges[i] >= quickTurboConsecutiveStateChanges and i in quickTurboableButtons:
# "Quick-turbo" action enables turbo temporarily if it's not enabled, or disables it temporarily if it is
useTurbo = not useTurbo
if i != turboToggleButton:
if lastButtonState[turboToggleButton]:
# If a button is pressed down while "turboToggleButton" is held, toggle its turbo state
if pressed:
turboEnabled[i] = not turboEnabled[i]
updateTurboWatchVal()
if useTurbo:
if not i in turboStartTimes:
turboStartTimes[i] = curTime
turboTime = curTime - turboStartTimes[i]
turboCoef = (turboTime % turboInterval) / turboInterval
turboState = turboCoef >= 0.5
vj.setButton(i, turboState if state else False)
else:
vj.setButton(i, state)
if i in turboStartTimes:
turboStartTimes.pop(i)
lastButtonState[i] = state
def updatePOVs():
for i in range(0, numPovs):
vj.setAnalogPov(i, joy.pov[i])
def getButtonName(button):
return buttonNames[button] if button in buttonNames else str(button)
def updateTurboWatchVal():
global enabledTurboButtons
enabledButtons = [item[0] for item in turboEnabled.items() if item[1]]
enabledTurboButtons = "\n".join([getButtonName(button) for button in enabledButtons])
curTime = time()
updateAxes()
updateButtons()
updatePOVs()
diagnostics.watch(enabledTurboButtons)
if debug:
axisValues = "\n".join(["%d: %0.2f" % (axis, getAxis(axis)) for axis in range(0, numAxes)])
diagnostics.watch(axisValues)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment