Files
Steerify-main/controller_profiles.py
2026-05-12 19:16:58 -05:00

248 lines
7.0 KiB
Python

import copy
import json
import uuid
from pathlib import Path
CONFIG_PATH = Path(__file__).with_name("wheel_bindings.json")
ACTION_ORDER = [
"spotify pause play",
"spotify next track",
"spotify previous track",
"spotify vol up",
"spotify vol down",
"spotify mute toggle",
]
ACTION_LABELS = {
"": "None",
"spotify pause play": "Pause / Play",
"spotify next track": "Next Track",
"spotify previous track": "Previous Track",
"spotify vol up": "Volume Up",
"spotify vol down": "Volume Down",
"spotify mute toggle": "Mute Toggle",
}
DEFAULT_WHEEL_PROFILE_ID = "friend_directinput_wheel"
DEFAULT_WHEEL_BINDINGS = {
"button:33": "spotify vol up",
"button:31": "spotify vol down",
"button:22": "spotify next track",
"button:16": "spotify previous track",
"button:21": "spotify pause play",
"button:19": "spotify mute toggle",
}
XBOX_360_BINDINGS = {
"button:0": "spotify pause play",
"button:1": "spotify next track",
"button:2": "spotify previous track",
"button:3": "spotify vol up",
"button:4": "spotify vol down",
"button:5": "spotify mute toggle",
}
def makeDefaultConfig():
return {
"version": 1,
"default_profile_id": DEFAULT_WHEEL_PROFILE_ID,
"button_presses_logged": True,
"global_settings": {
"debounce_seconds": 0.08,
"volume_increment": 5,
"polling_interval_ms": 120,
},
"profiles": [
{
"id": DEFAULT_WHEEL_PROFILE_ID,
"name": "Friend DirectInput Wheel",
"controller": None,
"bindings": copy.deepcopy(DEFAULT_WHEEL_BINDINGS),
"button_settings": {},
},
{
"id": "xbox_360_default",
"name": "Xbox 360 Default",
"controller": None,
"bindings": copy.deepcopy(XBOX_360_BINDINGS),
"button_settings": {},
},
],
}
def loadConfig():
if not CONFIG_PATH.exists():
config = makeDefaultConfig()
saveConfig(config)
return config
with CONFIG_PATH.open("r", encoding="utf-8") as file:
data = json.load(file)
if isFlatBindingFile(data):
config = makeDefaultConfig()
config["profiles"][0]["bindings"] = data
saveConfig(config)
return config
config = normalizeConfig(data)
saveConfig(config)
return config
def saveConfig(config):
with CONFIG_PATH.open("w", encoding="utf-8") as file:
json.dump(config, file, indent=4)
file.write("\n")
def isFlatBindingFile(data):
if not isinstance(data, dict):
return False
if "profiles" in data:
return False
return all(key.startswith("button:") for key in data.keys())
def normalizeConfig(config):
defaultConfig = makeDefaultConfig()
if not isinstance(config, dict):
return defaultConfig
config.setdefault("version", 1)
config.setdefault("default_profile_id", DEFAULT_WHEEL_PROFILE_ID)
config.setdefault("button_presses_logged", True)
config.setdefault("global_settings", {})
config["global_settings"].setdefault("debounce_seconds", 0.08)
config["global_settings"].setdefault("volume_increment", 5)
config["global_settings"].setdefault("polling_interval_ms", 120)
config.setdefault("profiles", [])
existingProfileIds = {profile.get("id") for profile in config["profiles"] if isinstance(profile, dict)}
for defaultProfile in defaultConfig["profiles"]:
if defaultProfile["id"] not in existingProfileIds:
config["profiles"].append(defaultProfile)
for profile in config["profiles"]:
profile.setdefault("id", makeProfileId(profile.get("name", "Controller")))
profile.setdefault("name", "Controller")
profile.setdefault("controller", None)
profile.setdefault("bindings", {})
profile.setdefault("button_settings", {})
return config
def getProfile(config, profileId):
for profile in config.get("profiles", []):
if profile.get("id") == profileId:
return profile
return None
def getDefaultProfile(config):
profile = getProfile(config, config.get("default_profile_id"))
if profile is not None:
return profile
return config["profiles"][0]
def makeProfileId(profileName):
cleaned = "".join(character.lower() if character.isalnum() else "_" for character in profileName)
cleaned = "_".join(part for part in cleaned.split("_") if part)
if not cleaned:
cleaned = "controller"
return f"{cleaned}_{uuid.uuid4().hex[:8]}"
def controllerFromJoystick(joystick):
joystick.init()
return {
"index": joystick.get_id(),
"instance_id": joystick.get_instance_id(),
"name": joystick.get_name(),
"guid": joystick.get_guid(),
"numaxes": joystick.get_numaxes(),
"numbuttons": joystick.get_numbuttons(),
"numhats": joystick.get_numhats(),
}
def stableControllerIdentity(controller):
return {
"name": controller.get("name"),
"guid": controller.get("guid"),
"numaxes": controller.get("numaxes"),
"numbuttons": controller.get("numbuttons"),
"numhats": controller.get("numhats"),
}
def profileHasController(profile):
controller = profile.get("controller")
return isinstance(controller, dict) and bool(controller.get("name") or controller.get("guid"))
def controllerMatches(profile, controller):
storedController = profile.get("controller")
if not isinstance(storedController, dict):
return False
if storedController.get("guid") and controller.get("guid"):
return storedController.get("guid") == controller.get("guid")
return (
storedController.get("name") == controller.get("name")
and storedController.get("numaxes") == controller.get("numaxes")
and storedController.get("numbuttons") == controller.get("numbuttons")
and storedController.get("numhats") == controller.get("numhats")
)
def findMatchingProfile(config, controller):
for profile in config.get("profiles", []):
if controllerMatches(profile, controller):
return profile
return None
def saveControllerProfile(config, controller, profileName, bindings, buttonSettings=None, existingProfileId=None):
profile = getProfile(config, existingProfileId) if existingProfileId else None
if profile is None or not profileHasController(profile):
profile = {
"id": makeProfileId(profileName),
"name": profileName,
"controller": stableControllerIdentity(controller),
"bindings": {},
"button_settings": {},
}
config["profiles"].append(profile)
profile["name"] = profileName
profile["controller"] = stableControllerIdentity(controller)
profile["bindings"] = copy.deepcopy(bindings)
profile["button_settings"] = copy.deepcopy(buttonSettings or {})
saveConfig(config)
return profile