Files
DroidDeck/tools/linuxfs/overlay/usr/local/bin/bannerlator-pad-defaults
T

534 lines
19 KiB
Python
Executable File

#!/usr/bin/python3
# Player 1 on the session's controller, for every emulator that would otherwise start on the
# keyboard. The session's pad is an Xbox 360 controller (libfakeinput's identity outside Steam) that
# SDL maps by itself; what an emulator lacks is a profile that points Player 1 at it. One is
# written only where the emulator has none yet - a profile the user made is never touched.
# Run at every desktop start and before every program from the rail.
import os
HOME = os.environ.get("HOME", "/root")
CONFIG = os.environ.get("XDG_CONFIG_HOME", os.path.join(HOME, ".config"))
def seed(path, text, what, done="Player 1 on the controller"):
if os.path.exists(path):
return False
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write(text)
os.replace(path + ".tmp", path)
print(f"bannerlator-pad-defaults: {what}: {done} ({path})")
return True
# RPCS3: its global input configuration (rpcs3::utils::get_input_config_dir() + "Default.yml").
# Without one it adds the keyboard pad handler (pad_config.cpp, cfg_input::load). SDL handler
# names from rpcs3/Input/sdl_pad_handler.cpp (button_list, init_config); the device is
# "<SDL gamepad name> <n>" (sdl_pad_handler::enumerate_devices).
RPCS3_PLAYER1 = """Player 1 Input:
Handler: SDL
Device: Xbox 360 Controller 1
Config:
Left Stick Left: LS X-
Left Stick Down: LS Y-
Left Stick Right: LS X+
Left Stick Up: LS Y+
Right Stick Left: RS X-
Right Stick Down: RS Y-
Right Stick Right: RS X+
Right Stick Up: RS Y+
Start: Start
Select: Back
PS Button: Guide
Square: West
Cross: South
Circle: East
Triangle: North
Left: Left
Down: Down
Right: Right
Up: Up
R1: RB
R2: RT
R3: RS
L1: LB
L2: LT
L3: LS
Left Stick Deadzone: 8000
Right Stick Deadzone: 8000
Left Stick Anti-Deadzone: 4259
Right Stick Anti-Deadzone: 4259
Left Trigger Threshold: 0
Right Trigger Threshold: 0
Color Value B: 20
LED battery indicator brightness: 10
Buddy Device: ""
"""
def rpcs3():
seed(os.path.join(CONFIG, "rpcs3/input_configs/global/Default.yml"), RPCS3_PLAYER1, "RPCS3")
rpcs3_shader_mode()
# RPCS3's default shader mode (Async Recompiler with Shader Interpreter) builds 6650 interpreter
# pipelines at the start of every boot, minutes on an Adreno. The async recompiler compiles only
# what a game draws, in the background, once. A new install starts with it (a config.yml that
# holds only this is merged over RPCS3's defaults); an existing one still on the default is moved
# once, and a marker keeps any later choice the user makes in RPCS3's own settings.
RPCS3_INTERPRETER = "Async Recompiler with Shader Interpreter"
RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)" # the GUI says "Async Shader Recompiler"; this is the value (system_config_types.cpp)
def rpcs3_shader_mode():
d = os.path.join(CONFIG, "rpcs3")
marker = os.path.join(d, ".droiddeck-shader-mode")
if os.path.exists(marker):
return
cfg = os.path.join(d, "config.yml")
if not os.path.isfile(cfg):
if seed(cfg, f"Video:\n Shader Mode: {RPCS3_RECOMPILER}\n", "RPCS3", f"shader mode {RPCS3_RECOMPILER}"):
open(marker, "w").close()
return
with open(cfg, encoding="utf-8", errors="replace") as f:
text = f.read()
if f"Shader Mode: {RPCS3_INTERPRETER}" in text:
text = text.replace(f"Shader Mode: {RPCS3_INTERPRETER}", f"Shader Mode: {RPCS3_RECOMPILER}", 1)
with open(cfg + ".tmp", "w", encoding="utf-8") as f:
f.write(text)
os.replace(cfg + ".tmp", cfg)
print(f"bannerlator-pad-defaults: RPCS3: shader mode {RPCS3_RECOMPILER} ({cfg})")
open(marker, "w").close()
# ---- ini helpers (DuckStation and PCSX2 share one ini dialect, keys may repeat) -------------
def ini_section(lines, name):
"""(start, end) line indices of [name]'s body, or None."""
start = None
for i, l in enumerate(lines):
t = l.strip()
if t.startswith("[") and t.endswith("]"):
if start is not None:
return start, i
if t[1:-1].strip() == name:
start = i + 1
return (start, len(lines)) if start is not None else None
def patch_ini_section(path, section, body, what, needle="SDL-", require=None, done="Player 1 on the controller"):
"""Add body's lines to [section] of an ini the emulator wrote itself, unless the section
already binds a controller (needle). Missing file: nothing - the emulator writes its own
defaults on first run and this runs again."""
if not os.path.isfile(path):
return
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
if require and require not in text:
return
lines = text.splitlines()
span = ini_section(lines, section)
if span and any(needle in l for l in lines[span[0]:span[1]]):
return
new = body.strip("\n").splitlines()
if span:
# The controller type the emulator already wrote stays; only bindings are added.
have = {l.split("=", 1)[0].strip() for l in lines[span[0]:span[1]] if "=" in l}
new = [l for l in new if not (l.split("=", 1)[0].strip() == "Type" and "Type" in have)]
end = span[1]
while end > span[0] and not lines[end - 1].strip():
end -= 1
lines[end:end] = new
else:
lines += ["", f"[{section}]"] + new
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
os.replace(path + ".tmp", path)
print(f"bannerlator-pad-defaults: {what}: {done} ({path})")
# DuckStation: SDL-<n>/<name>, axes signed (src/util/sdl_input_source.cpp); Pad1 keys from
# src/core/analog_controller.cpp. Its defaults bind the keyboard only (settings.cpp).
DUCKSTATION_PAD1 = """Type = AnalogController
Up = SDL-0/DPadUp
Down = SDL-0/DPadDown
Left = SDL-0/DPadLeft
Right = SDL-0/DPadRight
Cross = SDL-0/A
Circle = SDL-0/B
Square = SDL-0/X
Triangle = SDL-0/Y
Select = SDL-0/Back
Start = SDL-0/Start
Analog = SDL-0/Guide
L1 = SDL-0/LeftShoulder
R1 = SDL-0/RightShoulder
L2 = SDL-0/+LeftTrigger
R2 = SDL-0/+RightTrigger
L3 = SDL-0/LeftStick
R3 = SDL-0/RightStick
LLeft = SDL-0/-LeftX
LRight = SDL-0/+LeftX
LUp = SDL-0/-LeftY
LDown = SDL-0/+LeftY
RLeft = SDL-0/-RightX
RRight = SDL-0/+RightX
RUp = SDL-0/-RightY
RDown = SDL-0/+RightY
LargeMotor = SDL-0/LargeMotor
SmallMotor = SDL-0/SmallMotor
"""
def duckstation():
for d in (os.path.join(CONFIG, "duckstation"), os.path.join(HOME, ".local/share/duckstation")):
patch_ini_section(os.path.join(d, "settings.ini"), "Pad1", DUCKSTATION_PAD1, "DuckStation")
# PCSX2: FaceSouth/... names (pcsx2/Input/SDLInputSource.cpp), keys from SIO/Pad/PadDualshock2.cpp.
# An ini without [UI] SettingsVersion is thrown away and rebuilt, so only a complete one is patched.
PCSX2_PAD1 = """Type = DualShock2
Up = SDL-0/DPadUp
Right = SDL-0/DPadRight
Down = SDL-0/DPadDown
Left = SDL-0/DPadLeft
Triangle = SDL-0/FaceNorth
Circle = SDL-0/FaceEast
Cross = SDL-0/FaceSouth
Square = SDL-0/FaceWest
Select = SDL-0/Back
Start = SDL-0/Start
L1 = SDL-0/LeftShoulder
R1 = SDL-0/RightShoulder
L2 = SDL-0/+LeftTrigger
R2 = SDL-0/+RightTrigger
L3 = SDL-0/LeftStick
R3 = SDL-0/RightStick
Analog = SDL-0/Guide
LUp = SDL-0/-LeftY
LRight = SDL-0/+LeftX
LDown = SDL-0/+LeftY
LLeft = SDL-0/-LeftX
RUp = SDL-0/-RightY
RRight = SDL-0/+RightX
RDown = SDL-0/+RightY
RLeft = SDL-0/-RightX
LargeMotor = SDL-0/LargeMotor
SmallMotor = SDL-0/SmallMotor
"""
def pcsx2():
patch_ini_section(os.path.join(CONFIG, "PCSX2/inis/PCSX2.ini"), "Pad1", PCSX2_PAD1, "PCSX2",
require="SettingsVersion")
# Dolphin: SDL/<id>/<SDL gamepad name>; control names from InputCommon/.../SDL/SDLGamepad.h (Left Y+
# is up). A section that exists is taken whole - keys it lacks are unbound - so both are complete.
DOLPHIN_GC = """[GCPad1]
Device = SDL/0/Xbox 360 Controller
Buttons/A = `Button S`
Buttons/B = `Button W`
Buttons/X = `Button E`
Buttons/Y = `Button N`
Buttons/Z = `Shoulder R`
Buttons/Start = `Start`
Main Stick/Up = `Left Y+`
Main Stick/Down = `Left Y-`
Main Stick/Left = `Left X-`
Main Stick/Right = `Left X+`
C-Stick/Up = `Right Y+`
C-Stick/Down = `Right Y-`
C-Stick/Left = `Right X-`
C-Stick/Right = `Right X+`
Triggers/L = `Trigger L`
Triggers/R = `Trigger R`
Triggers/L-Analog = `Trigger L`
Triggers/R-Analog = `Trigger R`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
D-Pad/Right = `Pad E`
Rumble/Motor = `Motor`
"""
DOLPHIN_WII = """[Wiimote1]
Device = SDL/0/Xbox 360 Controller
Source = 1
Buttons/A = `Button S`
Buttons/B = `Trigger R`
Buttons/1 = `Button W`
Buttons/2 = `Button N`
Buttons/- = `Back`
Buttons/+ = `Start`
Buttons/Home = `Guide`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
D-Pad/Right = `Pad E`
IR/Up = `Right Y+`
IR/Down = `Right Y-`
IR/Left = `Right X-`
IR/Right = `Right X+`
Shake/X = `Button E`
Shake/Y = `Button E`
Shake/Z = `Button E`
Rumble/Motor = `Motor`
Extension = Nunchuk
Nunchuk/Buttons/C = `Shoulder L`
Nunchuk/Buttons/Z = `Trigger L`
Nunchuk/Stick/Up = `Left Y+`
Nunchuk/Stick/Down = `Left Y-`
Nunchuk/Stick/Left = `Left X-`
Nunchuk/Stick/Right = `Left X+`
"""
# ARMSX2 - the PS2 emulator (PCSX2 with ARM64 recompilers; upstream PCSX2 interprets on ARM64). Its
# files are PCSX2's under $XDG_CONFIG_HOME/ARMSX2 (pcsx2/Pcsx2Config.cpp). The user supplies the BIOS
# and the games; they live in the ROMs folder (/root/ROMs). The guide button opens the pause menu
# (Hotkeys OpenPauseMenu) - the way out of a game started from the front end, which runs ARMSX2
# without its window (-batch -bigpicture) - so it is not also the analog toggle here.
ARMSX2_PAD1 = "\n".join(l for l in PCSX2_PAD1.splitlines() if not l.startswith("Analog ")) + "\n"
ROMS = "/root/ROMs"
def ps2_folder():
for name in ("ps2", "PS2"):
d = os.path.join(ROMS, name)
if os.path.isdir(d):
return d
return os.path.join(ROMS, "ps2")
def ps2_bios_folder():
base = ps2_folder()
for name in ("bios", "BIOS", "firmware"):
d = os.path.join(base, name)
if os.path.isdir(d):
return d
d = os.path.join(base, "bios")
if os.path.isdir(ROMS):
try:
os.makedirs(d, exist_ok=True) # where the user is told to put their BIOS
except OSError:
pass
return d
def armsx2():
root = os.path.join(CONFIG, "ARMSX2")
ini = os.path.join(root, "inis/PCSX2.ini")
old = os.path.join(CONFIG, "PCSX2")
if not os.path.isfile(ini) and os.path.isfile(os.path.join(old, "inis/PCSX2.ini")):
# Someone who played on PCSX2 here: the same settings, BIOS choice and memory cards (the
# saves), copied once. PCSX2's own folder is left as it was.
import shutil
for sub in ("inis", "memcards", "bios", "gamesettings", "cheats", "patches", "covers",
"inputprofiles", "textures"):
src = os.path.join(old, sub)
if os.path.isdir(src):
shutil.copytree(src, os.path.join(root, sub), dirs_exist_ok=True)
print(f"bannerlator-pad-defaults: ARMSX2: settings and memory cards copied from PCSX2 ({root})")
elif not os.path.isfile(ini):
# A new player: ARMSX2's own first-time setup still runs (SetupWizardIncomplete) to pick the
# BIOS, but looking in the ROMs folder, with the ps2 folder as its game list and the
# controller already on Player 1.
bios, games = ps2_bios_folder(), ps2_folder()
seed(ini, "[UI]\nSettingsVersion = 1\nSetupWizardIncomplete = true\n\n"
f"[Folders]\nBios = {bios}\n\n"
f"[GameList]\nRecursivePaths = {games}\n\n"
# Vulkan outright rather than Auto's pick: the renderer every other path here uses.
"[EmuCore/GS]\nRenderer = 14\n\n"
"[Hotkeys]\nOpenPauseMenu = Keyboard/Escape\nOpenPauseMenu = SDL-0/Guide\n\n"
"[Pad1]\n" + ARMSX2_PAD1, "ARMSX2", "first-time setup, BIOS from " + bios)
return
patch_ini_section(ini, "Pad1", ARMSX2_PAD1, "ARMSX2", require="SettingsVersion")
# The guide button is the pause menu here; PCSX2's profile (ours, copied over) had it as the
# analog toggle as well.
with open(ini, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
kept = [l for l in lines if l.replace(" ", "") != "Analog=SDL-0/Guide"]
if len(kept) != len(lines):
with open(ini + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(kept) + "\n")
os.replace(ini + ".tmp", ini)
patch_ini_section(ini, "Hotkeys", "OpenPauseMenu = SDL-0/Guide\n", "ARMSX2", needle="SDL-0/Guide",
require="SettingsVersion", done="the guide button opens the pause menu")
patch_ini_section(ini, "GameList", f"RecursivePaths = {ps2_folder()}\n", "ARMSX2", needle=ROMS,
require="SettingsVersion", done="games listed from the ROMs folder")
def dolphin():
d = os.path.join(CONFIG, "dolphin-emu")
seed(os.path.join(d, "GCPadNew.ini"), DOLPHIN_GC, "Dolphin (GameCube)")
seed(os.path.join(d, "WiimoteNew.ini"), DOLPHIN_WII, "Dolphin (Wii)")
# Cemu: an SDLController profile for the GamePad. The uuid is "<index>_<SDL GUID>" and SDL's GUID
# carries a CRC of the device name; this is the one SDL reports for the session's pad
# ("Xbox 360 Controller (0)", 045e:028e - read from RPCS3's SDL log on the device). Mapping ids
# from VPADController.h, values from Controller.h, Cemu's own SDL default layout plus Home.
CEMU_MAP = [(1, 1), (2, 0), (3, 3), (4, 2), (5, 9), (6, 10), (7, 42), (8, 43), (9, 6), (10, 4),
(11, 11), (12, 12), (13, 13), (14, 14), (15, 7), (16, 8), (17, 45), (18, 39), (19, 44),
(20, 38), (21, 47), (22, 41), (23, 46), (24, 40), (27, 5)]
CEMU_XML = """<?xml version="1.0" encoding="UTF-8"?>
<emulated_controller>
\t<type>Wii U GamePad</type>
\t<toggle_display>0</toggle_display>
\t<controller>
\t\t<api>SDLController</api>
\t\t<uuid>0_03005e615e0400008e02000010010000</uuid>
\t\t<display_name>Xbox 360 Controller</display_name>
\t\t<rumble>0.5</rumble>
\t\t<mappings>
""" + "".join(f"\t\t\t<entry>\n\t\t\t\t<mapping>{m}</mapping>\n\t\t\t\t<button>{b}</button>\n\t\t\t</entry>\n" for m, b in CEMU_MAP) + """\t\t</mappings>
\t</controller>
</emulated_controller>
"""
def cemu():
seed(os.path.join(CONFIG, "Cemu/controllerProfiles/controller0.xml"), CEMU_XML, "Cemu")
# melonDS 1.x: raw SDL joystick indices. The session's pad has the kernel's Xbox 360 layout
# (buttons A B X Y LB RB Back Start Guide LS RS = 0..10, hat 0 for the d-pad); the d-pad values
# also take the left stick (EmuInstanceInput.cpp; encoding InputConfig/MapButton.h).
MELONDS_JOY = """[Instance0.Joystick]
A = 1
B = 0
X = 3
Y = 2
L = 4
R = 5
Select = 6
Start = 7
Up = 17891585
Down = 16843012
Left = 1114376
Right = 65794
"""
def melonds():
path = os.path.join(CONFIG, "melonDS/melonDS.toml")
if not os.path.isfile(path):
seed(path, "[Instance0]\nJoystickID = 0\n\n" + MELONDS_JOY, "melonDS")
return
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
try:
import tomllib
joy = tomllib.loads(text).get("Instance0", {}).get("Joystick", {})
except Exception:
return
if any(isinstance(v, int) and v != -1 for v in joy.values()):
return # something is bound already
# Replace melonDS's own all-unbound table with ours (a TOML table can be defined only once).
out, skipping = [], False
for line in text.splitlines():
t = line.strip()
if t.startswith("["):
skipping = t == "[Instance0.Joystick]"
if not skipping:
out.append(line)
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(out).rstrip("\n") + "\n\n" + MELONDS_JOY)
os.replace(path + ".tmp", path)
print(f"bannerlator-pad-defaults: melonDS: Player 1 on the controller ({path})")
# PPSSPP (SDL): its pad defaults leave the shoulders, the right stick and the triggers unbound on
# SDL builds (SDLJoystick.cpp codes vs KeyMapDefaults.cpp); a [ControlMapping] that exists replaces
# every default, so this one is complete, keyboard included (device 1 keyboard, 10 pad).
PPSSPP_CONTROLS = """[ControlMapping]
Up = 1-19,10-19
Down = 1-20,10-20
Left = 1-21,10-21
Right = 1-22,10-22
Cross = 1-54,10-189
Circle = 1-52,10-190
Square = 1-29,10-191
Triangle = 1-47,10-188
Start = 1-62,10-197
Select = 1-66,10-196
L = 1-45,10-193
R = 1-51,10-192
An.Up = 1-37,10-4003
An.Down = 1-39,10-4002
An.Left = 1-38,10-4001
An.Right = 1-40,10-4000
RightAn.Up = 10-4007
RightAn.Down = 10-4006
RightAn.Left = 10-4005
RightAn.Right = 10-4004
Pause = 1-111,10-4
"""
def ppsspp():
seed(os.path.join(CONFIG, "ppsspp/PSP/SYSTEM/controls.ini"), PPSSPP_CONTROLS, "PPSSPP")
# RetroArch: the udev joypad driver finds nothing here (no udev database in the runtime); sdl2
# does, and matches this profile by vendor/product (task_autodetect.c).
RETROARCH_PROFILE = """input_driver = "sdl2"
input_device = "Xbox 360 Controller"
input_vendor_id = "1118"
input_product_id = "654"
input_b_btn = "0"
input_a_btn = "1"
input_y_btn = "2"
input_x_btn = "3"
input_select_btn = "4"
input_menu_toggle_btn = "5"
input_start_btn = "6"
input_l3_btn = "7"
input_r3_btn = "8"
input_l_btn = "9"
input_r_btn = "10"
input_up_btn = "11"
input_down_btn = "12"
input_left_btn = "13"
input_right_btn = "14"
input_l2_axis = "+4"
input_r2_axis = "+5"
input_l_x_plus_axis = "+0"
input_l_x_minus_axis = "-0"
input_l_y_plus_axis = "+1"
input_l_y_minus_axis = "-1"
input_r_x_plus_axis = "+2"
input_r_x_minus_axis = "-2"
input_r_y_plus_axis = "+3"
input_r_y_minus_axis = "-3"
"""
def retroarch():
d = os.path.join(CONFIG, "retroarch")
seed(os.path.join(d, "autoconfig/sdl2/Xbox 360 Controller.cfg"), RETROARCH_PROFILE, "RetroArch (profile)")
cfg = os.path.join(d, "retroarch.cfg")
if not os.path.isfile(cfg):
seed(cfg, 'input_joypad_driver = "sdl2"\ninput_autodetect_enable = "true"\n', "RetroArch")
return
with open(cfg, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
changed = False
for i, l in enumerate(lines):
if l.replace(" ", "").startswith('input_joypad_driver="udev"'):
lines[i] = 'input_joypad_driver = "sdl2"'
changed = True
if changed:
with open(cfg + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
os.replace(cfg + ".tmp", cfg)
print(f"bannerlator-pad-defaults: RetroArch: joypad driver udev -> sdl2 ({cfg})")
for step in (rpcs3, duckstation, pcsx2, armsx2, dolphin, cemu, melonds, ppsspp, retroarch):
try:
step()
except Exception as e:
print(f"bannerlator-pad-defaults: {step.__name__}: {e}")