|
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) |