1
0
forked from Mirrors/RGSX
- updated controls mapping and reading simplified
- update translations
- move some files to reorganize folders
- add some icons to controls
This commit is contained in:
retrogamesets
2025-09-21 15:25:52 +02:00
parent 9861afb9fb
commit 9aa494a5d0
41 changed files with 1238 additions and 376 deletions

View File

@@ -16,7 +16,6 @@ import datetime
import subprocess
import sys
import config
import shutil
from display import (
init_display, draw_loading_screen, draw_error_screen, draw_platform_grid,
@@ -64,7 +63,6 @@ try: # pragma: no cover
logger.debug("API key files ensured at startup")
except Exception as _e:
logger.warning(f"Cannot prepare API key files early: {_e}")
# Mise à jour de la gamelist Windows avant toute initialisation graphique (évite les conflits avec ES)
def _run_windows_gamelist_update():
try:
@@ -198,6 +196,15 @@ for i in range(count):
joystick_names.append(j.get_name())
except Exception as e:
logger.debug(f"Impossible de lire le nom du joystick {i}: {e}")
# Enregistrer le nom du premier joystick détecté pour l'auto-préréglage
try:
if joystick_names:
config.controller_device_name = joystick_names[0]
else:
config.controller_device_name = ""
except Exception:
pass
normalized_names = [n.lower() for n in joystick_names]
if not joystick_names:
joystick_names = ["Clavier"]
@@ -205,73 +212,12 @@ if not joystick_names:
logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.")
config.joystick = False
config.keyboard = True
# Si aucune marque spécifique détectée mais un joystick est présent, marquer comme générique
if not any([config.xbox_controller, config.playstation_controller, config.nintendo_controller,
config.eightbitdo_controller, config.steam_controller, config.trimui_controller,
config.logitech_controller]):
config.generic_controller = True
logger.debug("Aucun contrôleur spécifique détecté, utilisation du profil générique")
else:
# Des joysticks sont présents, activer le mode joystick et tenter la détection spécifique
# Des joysticks sont présents: activer le mode joystick et mémoriser le nom pour l'auto-préréglage
config.joystick = True
config.keyboard = False
print(f"Joysticks détectés: YES")
logger.debug(f"Joysticks détectés: YES")
for idx, name in enumerate(joystick_names):
lname = name.lower()
# Détection Anbernic RG35XX
if ("rg35xx" in lname):
config.anbernic_rg35xx_controller = True
logger.debug(f"Anbernic Controller detected : {name}")
print(f"Controller detected : {name}")
break
# Détection spécifique Elite AVANT la détection générique Xbox
elif ("microsoft xbox controller" in lname):
config.xbox_elite_controller = True
logger.debug(f"Controller detected: {name}")
print(f"Controller detected: {name}")
break
elif ("xbox" in lname) or ("x-box" in lname) or ("xinput" in lname) or ("microsoft x-box" in lname) or ("x-box 360" in lname) or ("360" in lname):
config.xbox_controller = True
logger.debug(f"Xbox Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "playstation" in lname or "ps3" in lname or "sony" in lname:
config.playstation_controller = True
logger.debug(f"Playstation Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "nintendo" in lname:
config.nintendo_controller = True
logger.debug(f"Nintendo Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "trimui" in lname:
config.trimui_controller = True
logger.debug(f"Trimui Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "logitech" in lname:
config.logitech_controller = True
logger.debug(f"Logitech Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "8bitdo" in lname or "8-bitdo" in lname:
config.eightbitdo_controller = True
logger.debug(f"8bitdoController detected : {name}")
print(f"Controller detected : {name}")
break
elif "steam" in lname:
config.steam_controller = True
logger.debug(f"Steam Controller detected : {name}")
print(f"Controller detected : {name}")
else:
# Si aucune marque spécifique détectée mais un joystick est présent, marquer comme générique
config.generic_controller = True
logger.debug(f"Generic Controller detected : {name}")
print(f"Generic Controller detected : {name}")
# Note: virtual keyboard display now depends on controller presence (config.joystick)
logger.debug(f"Flags contrôleur: xbox={config.xbox_controller}, ps={config.playstation_controller}, nintendo={config.nintendo_controller}, eightbitdo={config.eightbitdo_controller}, steam={config.steam_controller}, trimui={config.trimui_controller}, logitech={config.logitech_controller}, generic={config.generic_controller}")
print("Joystick détecté:", ", ".join(joystick_names))
logger.debug(f"Joysticks détectés: {joystick_names}")

View File

@@ -1,74 +0,0 @@
{
"confirm": {
"type": "button",
"button": 1,
"display": "B"
},
"cancel": {
"type": "button",
"button": 0,
"display": "A"
},
"up": {
"type": "hat",
"value": [0, 1],
"display": "↑"
},
"down": {
"type": "hat",
"value": [0, -1],
"display": "↓"
},
"left": {
"type": "hat",
"value": [-1, 0],
"display": "←"
},
"right": {
"type": "hat",
"value": [1, 0],
"display": "→"
},
"start": {
"type": "button",
"button": 7,
"display": "Start"
},
"filter": {
"type": "button",
"button": 6,
"display": "Select"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": 1,
"display": "ZR"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": -1,
"display": "ZL"
},
"history": {
"type": "button",
"button": 3,
"display": "Y"
},
"clear_history": {
"type": "button",
"button": 2,
"display": "X"
},
"delete": {
"type": "button",
"button": 4,
"display": "L"
},
"space": {
"type": "button",
"button": 5,
"display": "R"
}
}

View File

@@ -0,0 +1,75 @@
{
"device": "DualSense Wireless Controller",
"up": {
"type": "button",
"button": 11,
"display": "\u2191"
},
"down": {
"type": "button",
"button": 12,
"display": "\u2193"
},
"left": {
"type": "button",
"button": 13,
"display": "\u2190"
},
"right": {
"type": "button",
"button": 14,
"display": "\u2192"
},
"confirm": {
"type": "button",
"button": 0,
"display": "A"
},
"cancel": {
"type": "button",
"button": 1,
"display": "B"
},
"history": {
"type": "button",
"button": 3,
"display": "Y"
},
"clear_history": {
"type": "button",
"button": 2,
"display": "X"
},
"start": {
"type": "button",
"button": 6,
"display": "Start"
},
"filter": {
"type": "button",
"button": 4,
"display": "Select"
},
"delete": {
"type": "button",
"button": 9,
"display": "LB"
},
"space": {
"type": "button",
"button": 10,
"display": "RB"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": 1,
"display": "LT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": 1,
"display": "RT"
}
}

View File

@@ -0,0 +1,20 @@
{
"device": "Retroid Pocket Controller",
"confirm": { "type": "button", "button": 1, "display": "A" },
"cancel": { "type": "button", "button": 2, "display": "B" },
"clear_history": { "type": "button", "button": 4, "display": "X" },
"history": { "type": "button", "button": 3, "display": "Y" },
"start": { "type": "button", "button": 8, "display": "Start" },
"filter": { "type": "button", "button": 7, "display": "Select" },
"up": { "type": "button", "button": 12, "display": "↑" },
"down": { "type": "button", "button": 13, "display": "↓" },
"left": { "type": "button", "button": 14, "display": "←" },
"right": { "type": "button", "button": 15, "display": "→" },
"delete": { "type": "button", "button": 5, "display": "LB" },
"space": { "type": "button", "button": 6, "display": "RB" },
"page_up": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" },
"page_down": { "type": "axis", "axis": 5, "direction": -1, "display": "RT" }
}

View File

@@ -1,4 +1,5 @@
{
"device": "RG34XX-SP Controller",
"confirm": {
"type": "button",
"button": 3,

View File

@@ -0,0 +1,20 @@
{
"device": "GO-Super Gamepad",
"confirm": { "type": "button", "button": 0, "display": "A" },
"cancel": { "type": "button", "button": 1, "display": "B" },
"clear_history": { "type": "button", "button": 3, "display": "X" },
"history": { "type": "button", "button": 2, "display": "Y" },
"start": { "type": "button", "button": 13, "display": "Start" },
"filter": { "type": "button", "button": 12, "display": "Select" },
"up": { "type": "button", "button": 8, "display": "↑" },
"down": { "type": "button", "button": 9, "display": "↓" },
"left": { "type": "button", "button": 10, "display": "←" },
"right": { "type": "button", "button": 11, "display": "→" },
"delete": { "type": "button", "button": 6, "display": "LB" },
"space": { "type": "button", "button": 7, "display": "RB" },
"page_up": { "type": "button", "button": 4, "display": "LT" },
"page_down": { "type": "button", "button": 5, "display": "RT" }
}

View File

@@ -1,4 +1,5 @@
{
"device": "Steam Deck",
"confirm": {
"type": "button",
"button": 3,

View File

@@ -1,4 +1,5 @@
{
"device": "TRIMUI Smart Pro Controller",
"confirm": {
"type": "button",
"button": 0,

View File

@@ -1,4 +1,5 @@
{
"device": "XBOX 360 For Windows (Controller)",
"confirm": {
"type": "button",
"button": 0,

View File

@@ -1,4 +1,37 @@
{
"device": "Xbox 360 Controller",
"up": {
"type": "hat",
"value": [
0,
1
],
"display": "\u2191"
},
"down": {
"type": "hat",
"value": [
0,
-1
],
"display": "\u2193"
},
"left": {
"type": "hat",
"value": [
-1,
0
],
"display": "\u2190"
},
"right": {
"type": "hat",
"value": [
1,
0
],
"display": "\u2192"
},
"confirm": {
"type": "button",
"button": 0,
@@ -9,48 +42,6 @@
"button": 1,
"display": "B"
},
"up": {
"type": "hat",
"value": [0, 1],
"display": "↑"
},
"down": {
"type": "hat",
"value": [0, -1],
"display": "↓"
},
"left": {
"type": "hat",
"value": [-1, 0],
"display": "←"
},
"right": {
"type": "hat",
"value": [1, 0],
"display": "→"
},
"start": {
"type": "button",
"button": 7,
"display": "Start"
},
"filter": {
"type": "button",
"button": 6,
"display": "Select"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": 1,
"display": "RT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": -1,
"display": "LT"
},
"history": {
"type": "button",
"button": 3,
@@ -61,6 +52,16 @@
"button": 2,
"display": "X"
},
"start": {
"type": "button",
"button": 7,
"display": "Start"
},
"filter": {
"type": "button",
"button": 6,
"display": "Select"
},
"delete": {
"type": "button",
"button": 4,
@@ -70,5 +71,17 @@
"type": "button",
"button": 5,
"display": "RB"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": -1,
"display": "LT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": -1,
"display": "RT"
}
}
}

View File

@@ -1,4 +1,5 @@
{
"device": "Microsoft Xbox Controller",
"confirm": { "type": "button", "button": 1, "display": "A" },
"cancel": { "type": "button", "button": 2, "display": "B" },
"clear_history": { "type": "button", "button": 3, "display": "X" },
@@ -12,8 +13,5 @@
"left": { "type": "hat", "value": [-1, 0], "display": "\u2190" },
"right": { "type": "hat", "value": [1, 0], "display": "\u2192" },
"page_up": { "type": "axis", "axis": 5, "direction": -1, "display": "RT" },
"page_down": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" },
"meta": {
"notes": "Mapping spécifique Xbox Elite basé sur log fourni. Triggers décalés: LEFT_TRIGGER=AXIS2 -, RIGHT_TRIGGER=AXIS5 -. Les boutons semblent décalés de +1 vs profil 360 standard."
}
"page_down": { "type": "axis", "axis": 2, "direction": -1, "display": "LT" }
}

View File

@@ -1,4 +1,25 @@
{
"device": "ZEROPLUS Controller",
"up": {
"type": "button",
"button": 11,
"display": "\u2191"
},
"down": {
"type": "button",
"button": 12,
"display": "\u2193"
},
"left": {
"type": "button",
"button": 13,
"display": "\u2190"
},
"right": {
"type": "button",
"button": 14,
"display": "\u2192"
},
"confirm": {
"type": "button",
"button": 0,
@@ -9,48 +30,6 @@
"button": 1,
"display": "B"
},
"up": {
"type": "hat",
"value": [0, 1],
"display": "↑"
},
"down": {
"type": "hat",
"value": [0, -1],
"display": "↓"
},
"left": {
"type": "hat",
"value": [-1, 0],
"display": "←"
},
"right": {
"type": "hat",
"value": [1, 0],
"display": "→"
},
"start": {
"type": "button",
"button": 7,
"display": "Start"
},
"filter": {
"type": "button",
"button": 6,
"display": "Select"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": 1,
"display": "RT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": -1,
"display": "LT"
},
"history": {
"type": "button",
"button": 3,
@@ -61,14 +40,36 @@
"button": 2,
"display": "X"
},
"delete": {
"start": {
"type": "button",
"button": 6,
"display": "Start"
},
"filter": {
"type": "button",
"button": 4,
"display": "Select"
},
"delete": {
"type": "button",
"button": 9,
"display": "LB"
},
"space": {
"type": "button",
"button": 5,
"button": 10,
"display": "RB"
},
"page_up": {
"type": "axis",
"axis": 4,
"direction": 1,
"display": "LT"
},
"page_down": {
"type": "axis",
"axis": 5,
"direction": 1,
"display": "RT"
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M15,19 L49,19 A13,13 90 0,1 62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 A13,13 90 0,1 15,19 Z" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<path id="button_l" d="m15 19a13 13 0 0 0-13 13 13 13 0 0 0 13 13h34a13 13 0 0 0 13-13 13 13 0 0 0-13-13h-34zm12.804688 4.433594h4.101562v13.921875h4.289062v3.210937h-8.390624v-17.132812z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M62,19 L62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 L2,19" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<line id="outline2" x1="1" y1="19" x2="63" y2="19" stroke="#fff" stroke-width="2"/>
<path id="button_lt" d="m2 19v13a13 13 0 0 0 13 13h34a13 13 0 0 0 13-13v-13h-60zm19.939453 4.433594h4.101563v13.921875h4.289062v3.210937h-8.390625v-17.132812zm9.667969 0h10.453125v3.222656h-3.1875v13.910156h-4.078125v-13.910156h-3.1875v-3.222656z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 652 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M15,19 L49,19 A13,13 90 0,1 62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 A13,13 90 0,1 15,19 Z" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<path id="button_r" d="m15 19a13 13 0 0 0-13 13 13 13 0 0 0 13 13h34a13 13 0 0 0 13-13 13 13 0 0 0-13-13h-34zm12.330078 4.433594h4.546875c1.882813 0 3.273438 0.425781 4.171875 1.277344 0.898438 0.851562 1.347656 2.15625 1.347656 3.914062 0 2.039062-0.699218 3.519531-2.097656 4.441406l3.28125 7.5h-4.324219l-2.625-6.46875h-0.210937v6.46875h-4.089844v-17.132812zm4.089844 3.175781v4.324219h0.304687c0.539063 0 0.929688-0.183594 1.171875-0.550782 0.25-0.367187 0.375-0.921874 0.375-1.664062 0-0.75-0.128906-1.289062-0.386718-1.617188-0.25-0.328124-0.644532-0.492187-1.183594-0.492187h-0.28125z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="M62,19 L62,32 A13,13 90 0,1 49,45 L15,45 A13,13 90 0,1 2,32 L2,19" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<line id="outline2" x1="1" y1="19" x2="63" y2="19" stroke="#fff" stroke-width="2"/>
<path id="button_rt" d="m2 19v13a13 13 0 0 0 13 13h34a13 13 0 0 0 13-13v-13h-60zm19.119141 4.433594h4.546875c1.882812 0 3.273437 0.425781 4.171875 1.277344 0.898437 0.851562 1.347656 2.15625 1.347656 3.914062 0 2.039062-0.699219 3.519531-2.097656 4.441406l3.28125 7.5h-4.324219l-2.625-6.46875h-0.210938v6.46875h-4.089843v-17.132812zm11.308593 0h10.453125v3.222656h-3.1875v13.910156h-4.078125v-13.910156h-3.1875v-3.222656zm-7.21875 3.175781v4.324219h0.304688c0.539062 0 0.929687-0.183594 1.171875-0.550782 0.25-0.367187 0.375-0.921874 0.375-1.664062 0-0.75-0.128906-1.289062-0.386719-1.617188-0.25-0.328124-0.644531-0.492187-1.183594-0.492187h-0.28125z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="m2 42a7 7 0 0 1 7-7h46a7 7 0 0 1 7 7 7 7 0 0 1-7 7h-46a7 7 0 0 1-7-7z" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<g id ="label_select" fill="#fff">
<path d="m7.5908203 26.665543q0.5175781 0 0.7910156-0.341797 0.2734375-0.351563 0.2734375-0.84961 0-0.507812-0.1464843-0.859375-0.1464844-0.361328-0.4296875-0.664062-0.2832032-0.302735-0.5371094-0.498047-0.2441406-0.205078-0.6152344-0.46875-1.2792969-0.898438-1.8945312-1.894531-0.6054688-0.996094-0.6054688-2.353516 0-1.875 1.1230469-2.96875 1.1328125-1.103516 2.9199219-1.103516t3.5253904 1.044922l-1.044922 2.509766q-0.05859-0.0293-0.302734-0.15625-0.244141-0.136719-0.3125-0.166016-0.06836-0.03906-0.283203-0.136718-0.2148439-0.107422-0.3027346-0.146485l-0.2636718-0.09766q-0.4296875-0.166016-0.8300782-0.166016-0.390625 0-0.625 0.361328-0.2246093 0.351563-0.2246093 0.859375 0 0.498047 0.1269531 0.830079 0.1269531 0.332031 0.390625 0.615234 0.4492187 0.46875 1.1035156 0.898437 1.2988282 0.878907 1.9531252 1.855469 0.664062 0.966797 0.664062 2.333985 0 2.041015-1.09375 3.134765-1.0937497 1.083985-3.1152341 1.083985-2.0117187 0-3.515625-0.84961v-3.203125q2.1289063 1.396485 3.2714844 1.396485z"/>
<path d="m21.125977 17.505386h-3.66211v2.88086h3.398438v2.65625h-3.398438v3.417968h3.66211v2.675782h-7.080079v-14.277344h7.080079z"/>
<path d="m26.838867 26.460464h3.574219v2.675782h-6.992188v-14.277344h3.417969z"/>
<path d="m39.602539 17.505386h-3.662109v2.88086h3.398437v2.65625h-3.398437v3.417968h3.662109v2.675782h-7.080078v-14.277344h7.080078z"/>
<path d="m46.643555 29.321793q-1.181641 0-2.109375-0.400391-0.917969-0.410156-1.503907-1.083984-0.585937-0.673829-0.966796-1.63086-0.69336-1.738281-0.69336-4.189453 0-1.933594 0.488281-3.564453 0.488282-1.650391 1.660157-2.714844 1.210937-1.074219 3.164062-1.074219 0.859375 0 1.650391 0.253907 0.800781 0.253906 1.728515 0.849609l-0.976562 2.412109q-0.07813-0.07813-0.380859-0.253906-0.302735-0.185547-0.556641-0.292969-0.673828-0.302734-1.259766-0.302734-0.576172 0-0.947265 0.341797-0.371094 0.332031-0.576172 0.820312-0.205078 0.488282-0.322266 1.171875-0.205078 1.103516-0.205078 2.373047 0 4.609375 2.099609 4.609375 0.917969 0 2.72461-1.035156v2.880859q-1.279297 0.830079-3.017578 0.830079z"/>
<path d="m59.680664 17.544449h-2.65625v11.591797h-3.398437v-11.591797h-2.65625v-2.685547h8.710937z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline" d="m2 42a7 7 0 0 1 7-7h46a7 7 0 0 1 7 7 7 7 0 0 1-7 7h-46a7 7 0 0 1-7-7z" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<g id="label_start" fill="#fff">
<path d="m11.02832 26.665543q0.517578 0 0.791016-0.341797 0.273437-0.351563 0.273437-0.84961 0-0.507812-0.146484-0.859375-0.146484-0.361328-0.429687-0.664062-0.283204-0.302735-0.53711-0.498047-0.24414-0.205078-0.615234-0.46875-1.2792971-0.898438-1.8945314-1.894531-0.6054688-0.996094-0.6054688-2.353516 0-1.875 1.1230469-2.96875 1.1328123-1.103516 2.9199223-1.103516 1.787109 0 3.52539 1.044922l-1.044922 2.509766q-0.05859-0.0293-0.302734-0.15625-0.244141-0.136719-0.3125-0.166016-0.06836-0.03906-0.283203-0.136718-0.214844-0.107422-0.302735-0.146485l-0.263671-0.09766q-0.429688-0.166016-0.830079-0.166016-0.390625 0-0.625 0.361328-0.224609 0.351563-0.224609 0.859375 0 0.498047 0.126953 0.830079 0.126953 0.332031 0.390625 0.615234 0.449219 0.46875 1.103516 0.898437 1.298828 0.878907 1.953125 1.855469 0.664062 0.966797 0.664062 2.333985 0 2.041015-1.09375 3.134765-1.09375 1.083985-3.115234 1.083985-2.0117188 0-3.5156251-0.84961v-3.203125q2.1289063 1.396485 3.2714841 1.396485z"/>
<path d="m25.149414 17.544449h-2.65625v11.591797h-3.398437v-11.591797h-2.65625v-2.685547h8.710937z"/>
<path d="m36.760742 29.136246h-3.4375l-0.791015-3.496094h-2.88086l-0.791015 3.496094h-3.427735l3.544922-14.335938h4.228516zm-4.814453-6.201172-0.849609-3.759766q-0.15625 0.751953-0.830078 3.759766z"/>
<path d="m38.108398 14.858902h3.789063q2.353516 0 3.476562 1.064453 1.123047 1.064453 1.123047 3.261719 0 2.548828-1.748047 3.701172l2.734375 6.25h-3.603515l-2.1875-5.390625h-0.175781v5.390625h-3.408204zm3.408204 2.646484v3.603516h0.253906q0.673828 0 0.976562-0.458984 0.3125-0.458985 0.3125-1.386719 0-0.9375-0.322265-1.347656-0.3125-0.410157-0.986328-0.410157z"/>
<path d="m56.243164 17.544449h-2.65625v11.591797h-3.398437v-11.591797h-2.65625v-2.685547h8.710937z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="m 50,22 c -5.522847,0 -10,4.477153 -10,10 0,5.522847 4.477153,10 10,10 5.522847,0 10,-4.477153 10,-10 0,-5.522847 -4.477153,-10 -10,-10 z"
id="button_east" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="M 32,4 C 26.477153,4 22,8.4771525 22,14 22,19.522847 26.477153,24 32,24 37.522847,24 42,19.522847 42,14 42,8.4771525 37.522847,4 32,4 Z"
id="button_north" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="m 32,40 c -5.522847,0 -10,4.477153 -10,10 0,5.522847 4.477153,10 10,10 5.522847,0 10,-4.477153 10,-10 0,-5.522847 -4.477153,-10 -10,-10 z"
id="button_south" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg7"
viewBox="0 0 64 64"
version="1.1"
height="64"
width="64">
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs11" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="50"
id="outline_east" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="50"
cx="32"
id="outline_south" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="14"
cx="32"
id="outline_north" />
<circle
stroke-width="2"
stroke="#fff"
fill="none"
r="10"
cy="32"
cx="14"
id="outline_west" />
<path
style="fill:#ffffff"
d="M 14,22 C 8.4771525,22 4,26.477153 4,32 4,37.522847 8.4771525,42 14,42 19.522847,42 24,37.522847 24,32 24,26.477153 19.522847,22 14,22 Z"
id="button_west" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path id="outline_up" d="M22,23.5 L22,6 A4,4 90 0,1 26,2 L38,2 A4,4 90 0,1 42,6 L42,23.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_right" d="M40.5,22 L58,22 A4,4 90 0,1 62,26 L62,38 A4,4 90 0,1 58,42 L40.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_down" d="M22,40.5 L22,58 A4,4 90 0,0 26,62 L38,62 A4,4 90 0,0 42,58 L42,40.5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<path id="outline_left" d="M23.5,22 L6,22 A4,4 90 0,0 2,26 L2,38 A4,4 90 0,0 6,42 L23.5,42" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="3"/>
<polygon id="dpad_up" points="27,14 37,14 32,6.2" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_right" points="50,27 50,37 57.8,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_down" points="27,50 37,50 32,57.8" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<polygon id="dpad_left" points="14,27 14,37 6.2,32" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
<circle id="dpad_thumb" cx="32" cy="32" r="5" fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.2.2.5"
app_version = "2.2.2.6"
def get_application_root():
@@ -78,9 +78,9 @@ OTA_UPDATE_ZIP = os.path.join(OTA_SERVER_URL, "RGSX.zip")
OTA_data_ZIP = os.path.join(OTA_SERVER_URL, "games.zip")
#CHEMINS DES EXECUTABLES
UNRAR_EXE = os.path.join(APP_FOLDER,"assets", "unrar.exe")
XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "xdvdfs.exe")
XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "xdvdfs")
UNRAR_EXE = os.path.join(APP_FOLDER,"assets","progs","unrar.exe")
XDVDFS_EXE = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs.exe")
XDVDFS_LINUX = os.path.join(APP_FOLDER,"assets", "progs", "xdvdfs")
if not HEADLESS:
# Print des chemins pour debug
@@ -191,6 +191,7 @@ trimui_controller = False
generic_controller = False
xbox_elite_controller = False # Flag spécifique manette Xbox Elite
anbernic_rg35xx_controller = False # Flag spécifique Anbernic RG3xxx
controller_device_name = "" # Nom exact du joystick détecté (pour auto-préréglages)
# --- Filtre plateformes (UI) ---
selected_filter_index = 0 # index dans la liste visible triée
@@ -253,7 +254,7 @@ def init_font():
search_size = 48
small_size = 28
if fam == "pixel":
path = os.path.join(APP_FOLDER, "assets", "Pixel-UniCode.ttf")
path = os.path.join(APP_FOLDER, "assets", "fonts", "Pixel-UniCode.ttf")
f = pygame.font.Font(path, int(base_size * font_scale))
t = pygame.font.Font(path, int(title_size * font_scale))
s = pygame.font.Font(path, int(search_size * font_scale))

View File

@@ -2,6 +2,7 @@ import pygame # type: ignore
import shutil
import asyncio
import json
import re
import os
import config
from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE
@@ -96,44 +97,65 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
# 2) Préréglages sans copie si aucun fichier utilisateur
try:
candidates = []
# Si aucun contrôleur détecté, privilégier le préréglage clavier
if not getattr(config, 'joystick', False) or getattr(config, 'keyboard', False):
candidates.append('keyboard.json')
# Déterminer les préréglages disponibles selon les flags détectés au démarrage
if getattr(config, 'steam_controller', False):
candidates.append('steam_controller.json')
if getattr(config, 'trimui_controller', False):
candidates.append('trimui_controller.json')
if getattr(config, 'xbox_elite_controller', False):
candidates.append('xbox_elite_controller.json')
elif getattr(config, 'xbox_controller', False):
candidates.append('xbox_controller.json')
if getattr(config, 'nintendo_controller', False):
candidates.append('nintendo_controller.json')
if getattr(config, 'eightbitdo_controller', False):
candidates.append('8bitdo_controller.json')
if getattr(config, 'anbernic_rg35xx_controller', False):
candidates.append('anbernic_rg34xx_sp_controller.json')
# Fallbacks génériques
if 'generic_controller.json' not in candidates:
candidates.append('generic_controller.json')
if 'xbox_controller.json' not in candidates:
candidates.append('xbox_controller.json')
# --- Auto-match par nom de périphérique détecté ---
def _sanitize(s: str) -> str:
s = (s or "").strip().lower()
s = re.sub(r"[^a-z0-9]+", "_", s)
s = re.sub(r"_+", "_", s).strip("_")
return s
for fname in candidates:
src = os.path.join(config.PRECONF_CONTROLS_PATH, fname)
def _extract_device_from_comment(val: str) -> str:
try:
if not isinstance(val, str):
return ""
# Expect formats like "# Device: NAME" or just NAME
if "Device:" in val:
part = val.split("Device:", 1)[1]
return part.strip().lstrip('#').strip()
return val.strip().lstrip('#').strip()
except Exception:
return ""
device_name = getattr(config, 'controller_device_name', '') or ''
if getattr(config, 'joystick', False) and device_name:
target_norm = _sanitize(device_name)
try:
for fname in os.listdir(config.PRECONF_CONTROLS_PATH):
if not fname.lower().endswith('.json'):
continue
src = os.path.join(config.PRECONF_CONTROLS_PATH, fname)
try:
with open(src, 'r', encoding='utf-8') as f:
preset = json.load(f)
except Exception:
continue
# Match by explicit device field
dev_field = preset.get('device') if isinstance(preset, dict) else None
if isinstance(dev_field, str) and _sanitize(dev_field) == target_norm:
logging.getLogger(__name__).info(f"Chargement préréglage (device) depuis le fichier: {fname}")
print(f"Chargement préréglage (device) depuis le fichier: {fname}")
return preset
except Exception as e:
logging.getLogger(__name__).warning(f"Échec scan préréglages par device: {e}")
# Fallback préréglage explicite clavier si pas de joystick
if not getattr(config, 'joystick', False) or getattr(config, 'keyboard', False):
src = os.path.join(config.PRECONF_CONTROLS_PATH, 'keyboard.json')
if os.path.exists(src):
with open(src, "r", encoding="utf-8") as f:
with open(src, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, dict) and data:
logging.getLogger(__name__).info(f"Chargement des contrôles préréglés: {fname}")
logging.getLogger(__name__).info("Chargement des contrôles préréglés: keyboard.json")
return data
except Exception as e:
logging.getLogger(__name__).warning(f"Échec du chargement des contrôles préréglés: {e}")
# 3) Fallback clavier par défaut
logging.getLogger(__name__).info("Aucun fichier utilisateur ou préréglage trouvé, utilisation des contrôles par défaut")
# 3) Fallback: si joystick présent mais aucun préréglage trouvé, retourner {} pour déclencher le remap
if getattr(config, 'joystick', False):
logging.getLogger(__name__).info("Aucun préréglage trouvé pour le joystick connecté, ouverture du remap")
return {}
# Sinon, fallback clavier par défaut
logging.getLogger(__name__).info("Aucun fichier utilisateur ou préréglage trouvé, utilisation des contrôles clavier par défaut")
return default_config.copy()
except Exception as e:

View File

@@ -1,12 +1,21 @@
import pygame # type: ignore
import json
import os
import io
import logging
import config
import language
from config import CONTROLS_CONFIG_PATH
from display import draw_gradient
import xml.etree.ElementTree as ET
from collections import OrderedDict
from typing import Optional, Tuple
# Optional: SVG to PNG conversion (if installed)
try:
import cairosvg # type: ignore
except Exception: # pragma: no cover - optional dependency
cairosvg = None # type: ignore
logger = logging.getLogger(__name__)
@@ -282,9 +291,77 @@ MOUSE_BUTTON_NAMES = {
# Durée de maintien pour valider une entrée (en millisecondes)
HOLD_DURATION = 1000
INPUT_ACCEPT_COOLDOWN = 350 # ms to ignore inputs right after accepting one (avoid axis release bounce)
JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms)
# ---- Icônes: helpers pour charger et afficher des SVG ----
_ICON_CACHE: dict[Tuple[str, int], Optional[pygame.Surface]] = {}
def _images_base_dir() -> str:
return os.path.join(os.path.dirname(__file__), "assets", "images")
def _action_icon_filename(action_name: str) -> Optional[str]:
# Map actions to icon filenames present in assets/images
mapping = {
"up": "dpad_up.svg",
"down": "dpad_down.svg",
"left": "dpad_left.svg",
"right": "dpad_right.svg",
"confirm": "buttons_south.svg", # A (south)
"cancel": "buttons_east.svg", # B (east)
"clear_history": "buttons_west.svg", # X (west)
"history": "buttons_north.svg", # Y (north)
"start": "button_start.svg",
"filter": "button_select.svg",
"delete": "button_l.svg", # LB
"space": "button_r.svg", # RB
"page_up": "button_lt.svg",
"page_down": "button_rt.svg",
}
return mapping.get(action_name)
def _load_svg_icon_surface(svg_path: str, size: int) -> Optional[pygame.Surface]:
# Try to load SVG via cairosvg; fallback: let pygame try to load (only if supported)
try:
if cairosvg is not None:
with open(svg_path, "rb") as f:
svg_bytes = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=size, output_height=size)
return pygame.image.load(io.BytesIO(png_bytes), "icon.png").convert_alpha()
else:
# Some pygame builds may support SVG; try directly.
surf = pygame.image.load(svg_path)
# Scale to requested size while keeping aspect ratio
w, h = surf.get_size()
if w != size or h != size:
# uniform scale to fit in size x size
scale = min(size / max(w, 1), size / max(h, 1))
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
surf = pygame.transform.smoothscale(surf, (new_w, new_h))
return surf.convert_alpha()
except Exception as e:
logger.debug(f"Icon load failed for {svg_path}: {e}")
return None
def get_action_icon_surface(action_name: str, size: int) -> Optional[pygame.Surface]:
key = (action_name, size)
if key in _ICON_CACHE:
return _ICON_CACHE[key]
filename = _action_icon_filename(action_name)
if not filename:
_ICON_CACHE[key] = None
return None
full_path = os.path.join(_images_base_dir(), filename)
if not os.path.exists(full_path):
logger.debug(f"Icon file not found: {full_path}")
_ICON_CACHE[key] = None
return None
surf = _load_svg_icon_surface(full_path, size)
_ICON_CACHE[key] = surf
return surf
def load_controls_config(path=CONTROLS_CONFIG_PATH):
"""Charge la configuration des contrôles depuis controls.json"""
try:
@@ -347,9 +424,64 @@ def get_readable_input_name(event):
return "Inconnu"
def get_preferred_display_for_action(action_name: str, input_type: str, input_value):
"""Retourne un libellé display standardisé pour correspondre à controller_debug.
Règles:
- Pour les actions manette, on force un libellé stable (A/B/X/Y, LB/RB, LT/RT, Start/Select, flèches).
- Pour le clavier, on conserve le nom lisible de la touche.
- Pour le D-Pad et axes directionnels, on affiche des flèches.
"""
# Clavier: garder la touche lisible
if input_type == "key":
try:
key_value = int(input_value)
except Exception:
key_value = input_value
key_value = SDL_TO_PYGAME_KEY.get(key_value, key_value)
return KEY_NAMES.get(key_value, pygame.key.name(key_value) or f"Touche {key_value}")
# Mapping stable par action (manette)
action_display = {
"confirm": "A",
"cancel": "B",
"clear_history": "X",
"history": "Y",
"start": "Start",
"filter": "Select",
"delete": "LB",
"space": "RB",
"page_up": "LT",
"page_down": "RT",
"up": "",
"down": "",
"left": "",
"right": "",
}
# Directions: flèches (peu importe hat/axis/button)
if action_name in ("up", "down", "left", "right"):
return action_display[action_name]
# Autres actions: renvoyer le libellé normalisé ci-dessus
if action_name in action_display:
return action_display[action_name]
# Fallback: si on ne sait pas, retourner chaîne vide (appelant fera un secours)
return ""
def map_controls(screen):
"""Interface de mappage des contrôles avec maintien de 3 secondes"""
controls_config = load_controls_config()
# Construire un objet ordonné pour forcer l'ordre des clés dans le JSON final
# Placer "device" en premier si disponible
controls_config = OrderedDict()
try:
device_name = getattr(config, "controller_device_name", "") or ""
if device_name:
controls_config["device"] = device_name
except Exception:
pass
current_action_index = 0
current_input = None
input_held_time = 0
@@ -357,6 +489,7 @@ def map_controls(screen):
last_frame_time = pygame.time.get_ticks()
config.needs_redraw = True
last_joyhat_time = 0
next_input_allowed_time = 0 # timestamp until which new inputs are ignored after accept
# État des entrées maintenues
held_keys = set()
@@ -424,6 +557,9 @@ def map_controls(screen):
# Détection des nouvelles entrées
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN):
# Ignorer les événements pendant un court délai après validation d'un mapping
if current_time < next_input_allowed_time:
continue
if event.type == pygame.JOYHATMOTION:
if (current_time - last_joyhat_time) < JOYHAT_DEBOUNCE:
continue
@@ -485,36 +621,41 @@ def map_controls(screen):
# Sauvegarder avec la structure attendue par controls.py
if current_input["type"] == "key":
disp = get_preferred_display_for_action(action_name, "key", current_input["value"]) or last_input_name
controls_config[action_name] = {
"type": "key",
"key": current_input["value"],
"display": last_input_name
"display": disp
}
elif current_input["type"] == "button":
disp = get_preferred_display_for_action(action_name, "button", current_input["value"]) or last_input_name
controls_config[action_name] = {
"type": "button",
"button": current_input["value"],
"display": last_input_name
"display": disp
}
elif current_input["type"] == "axis":
axis, direction = current_input["value"]
disp = get_preferred_display_for_action(action_name, "axis", (axis, direction)) or last_input_name
controls_config[action_name] = {
"type": "axis",
"axis": axis,
"direction": direction,
"display": last_input_name
"display": disp
}
elif current_input["type"] == "hat":
disp = get_preferred_display_for_action(action_name, "hat", current_input["value"]) or last_input_name
controls_config[action_name] = {
"type": "hat",
"value": current_input["value"],
"display": last_input_name
"display": disp
}
elif current_input["type"] == "mouse":
disp = get_preferred_display_for_action(action_name, "mouse", current_input["value"]) or last_input_name
controls_config[action_name] = {
"type": "mouse",
"button": current_input["value"],
"display": last_input_name
"display": disp
}
logger.debug(f"Contrôle mappé: {action_name} -> {controls_config[action_name]}")
@@ -523,6 +664,8 @@ def map_controls(screen):
input_held_time = 0
last_input_name = None
config.needs_redraw = True
# Activer un court délai pour ignorer les rebonds (ex: relâchement d'un axe)
next_input_allowed_time = pygame.time.get_ticks() + INPUT_ACCEPT_COOLDOWN
# Réinitialiser les entrées maintenues
held_keys.clear()
@@ -542,7 +685,7 @@ def map_controls(screen):
def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_progress):
#Affiche l'interface de mappage des contrôles avec une barre de progression pour le maintien
# Affiche l'interface de mappage des contrôles avec une barre de progression pour le maintien
draw_gradient(screen, (28, 37, 38), (47, 59, 61))
# Paramètres de l'interface
@@ -552,6 +695,8 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr
border_radius = 24
border_width = 4
shadow_offset = 8
bar_height = 25
min_bar_inner_width = 200 # largeur minimale utile de la barre
# Titre principal (traduction)
title_text = language.get_text("controls_mapping_title", "Configuration des contrôles")
@@ -559,6 +704,10 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 80))
screen.blit(title_surface, title_rect)
# Icône de l'action courante (facultatif si dépendances disponibles)
icon_size = 72 # px
icon_surface = get_action_icon_surface(action.get('name', ''), icon_size)
# Instructions (traduction)
instruction_text = language.get_text("controls_mapping_instruction", "Maintenez pendant 3s pour configurer :")
description_text = action.get('description', '')
@@ -574,11 +723,22 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr
input_surface = config.small_font.render(input_text, True, (0, 255, 0) if last_input else (255, 255, 255))
input_width, input_height = input_surface.get_size()
# Dimensions de la popup
text_width = max(instruction_width, description_width, input_width)
text_height = instruction_height + description_height + input_height + 2 * padding_between
popup_width = text_width + 2 * padding_horizontal
popup_height = text_height + 40 + 2 * padding_vertical # +40 pour la barre de progression
# Dimensions de la popup (s'adapte au contenu et inclut la barre)
icon_width, icon_height = (icon_surface.get_size() if icon_surface else (0, 0))
inner_text_width = max(instruction_width, description_width, input_width, min_bar_inner_width, icon_width)
inner_text_height = 0
if icon_surface:
inner_text_height += icon_height + padding_between
inner_text_height += instruction_height + description_height + input_height + 2 * padding_between
inner_width = inner_text_width
inner_height = inner_text_height + padding_between + bar_height
popup_width = inner_width + 2 * padding_horizontal
# Eviter de dépasser l'écran (marge de 20px de chaque côté)
popup_width = min(popup_width, config.screen_width - 40)
popup_height = inner_height + 2 * padding_vertical
popup_height = min(popup_height, config.screen_height - 40)
popup_x = (config.screen_width - popup_width) // 2
popup_y = (config.screen_height - popup_height) // 2
@@ -597,31 +757,40 @@ def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_pr
# Bordure blanche
pygame.draw.rect(screen, (255, 255, 255), popup_rect, border_width, border_radius=border_radius)
# Afficher les textes
# Afficher les textes (centrés dans la popup)
center_x = popup_x + popup_width // 2
start_y = popup_y + padding_vertical
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, start_y + instruction_height // 2))
# Icône en premier (si dispo)
if icon_surface:
icon_rect = icon_surface.get_rect(center=(center_x, start_y + icon_height // 2))
screen.blit(icon_surface, icon_rect)
start_y += icon_height + padding_between
instruction_rect = instruction_surface.get_rect(center=(center_x, start_y + instruction_height // 2))
screen.blit(instruction_surface, instruction_rect)
start_y += instruction_height + padding_between
description_rect = description_surface.get_rect(center=(config.screen_width // 2, start_y + description_height // 2))
description_rect = description_surface.get_rect(center=(center_x, start_y + description_height // 2))
screen.blit(description_surface, description_rect)
start_y += description_height + padding_between
input_rect = input_surface.get_rect(center=(config.screen_width // 2, start_y + input_height // 2))
input_rect = input_surface.get_rect(center=(center_x, start_y + input_height // 2))
screen.blit(input_surface, input_rect)
start_y += input_height + padding_between
# Barre de progression pour le maintien
bar_width = 300
bar_height = 25
bar_x = (config.screen_width - bar_width) // 2
bar_y = start_y + 20
pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height))
progress_width = bar_width * hold_progress
pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, progress_width, bar_height))
# Barre de progression pour le maintien (adaptée à la largeur intérieure de la popup)
bar_x = popup_x + padding_horizontal
bar_y = start_y
bar_width = popup_width - 2 * padding_horizontal
pygame.draw.rect(screen, (50, 50, 50), (bar_x, bar_y, bar_width, bar_height))
progress_width = int(bar_width * max(0.0, min(1.0, hold_progress)))
if progress_width > 0:
pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, progress_width, bar_height))
pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 2)
# Afficher le pourcentage de progression
# Pourcentage de progression (affiché au centre de la barre)
if hold_progress > 0:
progress_text = f"{int(hold_progress * 100)}%"
progress_surface = config.small_font.render(progress_text, True, (255, 255, 255))
progress_rect = progress_surface.get_rect(center=(config.screen_width // 2, bar_y + bar_height + 30))
progress_rect = progress_surface.get_rect(center=(bar_x + bar_width // 2, bar_y + bar_height // 2))
screen.blit(progress_surface, progress_rect)

View File

@@ -1,4 +1,8 @@
import pygame # type: ignore
import os
import io
import config
from utils import truncate_text_middle, wrap_text, load_system_image, truncate_text_end
import logging
@@ -10,6 +14,145 @@ logger = logging.getLogger(__name__)
OVERLAY = None # Initialisé dans init_display()
# --- Helpers: SVG icons for controls (local cache, optional cairosvg) ---
_HELP_ICON_CACHE = {}
def _images_base_dir():
try:
base_dir = os.path.join(os.path.dirname(__file__), "assets", "images")
except Exception:
base_dir = "assets/images"
return base_dir
def _action_icon_filename(action_name: str):
mapping = {
"up": "dpad_up.svg",
"down": "dpad_down.svg",
"left": "dpad_left.svg",
"right": "dpad_right.svg",
"confirm": "buttons_south.svg",
"cancel": "buttons_east.svg",
"clear_history": "buttons_west.svg",
"history": "buttons_north.svg",
"start": "button_start.svg",
"filter": "button_select.svg",
"delete": "button_l.svg",
"space": "button_r.svg",
"page_up": "button_lt.svg",
"page_down": "button_rt.svg",
}
return mapping.get(action_name)
def _load_svg_icon_surface(svg_path: str, size: int):
try:
# Prefer cairosvg if available for crisp rasterization
try:
import cairosvg # type: ignore
except Exception:
cairosvg = None # type: ignore
if cairosvg is not None:
with open(svg_path, "rb") as f:
svg_bytes = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=size, output_height=size)
return pygame.image.load(io.BytesIO(png_bytes), "icon.png").convert_alpha()
# Fallback: try direct load (works if SDL_image has SVG support)
surf = pygame.image.load(svg_path)
w, h = surf.get_size()
if w != size or h != size:
scale = min(size / max(w, 1), size / max(h, 1))
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
surf = pygame.transform.smoothscale(surf, (new_w, new_h))
return surf.convert_alpha()
except Exception as e:
try:
logger.debug(f"Help icon load failed for {svg_path}: {e}")
except Exception:
pass
return None
def get_help_icon_surface(action_name: str, size: int):
key = (action_name, size)
if key in _HELP_ICON_CACHE:
return _HELP_ICON_CACHE[key]
filename = _action_icon_filename(action_name)
if not filename:
_HELP_ICON_CACHE[key] = None
return None
full_path = os.path.join(_images_base_dir(), filename)
if not os.path.exists(full_path):
_HELP_ICON_CACHE[key] = None
return None
surf = _load_svg_icon_surface(full_path, size)
_HELP_ICON_CACHE[key] = surf
return surf
def _render_icons_line(actions, text, target_col_width, font, text_color, icon_size=28, icon_gap=8, icon_text_gap=12):
"""Compose une ligne avec une rangée d'icônes (actions) et un texte à droite.
Renvoie un pygame.Surface prêt à être blité, limité à target_col_width.
"""
# Charger icônes (ignorer celles manquantes)
icon_surfs = []
for a in actions:
surf = get_help_icon_surface(a, icon_size)
if surf is not None:
icon_surfs.append(surf)
# Si aucune icône, rendre simplement le texte (le layout appelant ajoutera les espacements)
if not icon_surfs:
try:
lines = wrap_text(text, font, target_col_width)
except Exception:
lines = [text]
line_surfs = [font.render(l, True, text_color) for l in lines]
width = max((s.get_width() for s in line_surfs), default=1)
height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4
surf = pygame.Surface((width, height), pygame.SRCALPHA)
y = 0
for s in line_surfs:
surf.blit(s, (0, y))
y += s.get_height() + 4
return surf
# Calcul largeur totale des icônes
icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap
if icons_width + icon_text_gap > target_col_width:
scale = (target_col_width - icon_text_gap) / max(1, icons_width)
scale = max(0.6, min(1.0, scale))
new_icon_surfs = []
for s in icon_surfs:
new_size = (max(1, int(s.get_width() * scale)), max(1, int(s.get_height() * scale)))
new_icon_surfs.append(pygame.transform.smoothscale(s, new_size))
icon_surfs = new_icon_surfs
icons_width = sum(s.get_width() for s in icon_surfs) + (len(icon_surfs) - 1) * icon_gap
text_area_width = max(60, target_col_width - icons_width - icon_text_gap)
try:
lines = wrap_text(text, font, text_area_width)
except Exception:
lines = [text]
line_surfs = [font.render(l, True, text_color) for l in lines]
text_block_width = max((s.get_width() for s in line_surfs), default=1)
text_block_height = sum(s.get_height() for s in line_surfs) + max(0, (len(line_surfs) - 1)) * 4
total_width = min(target_col_width, icons_width + icon_text_gap + text_block_width)
total_height = max(max((s.get_height() for s in icon_surfs), default=0), text_block_height)
surf = pygame.Surface((total_width, total_height), pygame.SRCALPHA)
x = 0
icon_y_center = total_height // 2
for idx, s in enumerate(icon_surfs):
r = s.get_rect()
y = icon_y_center - r.height // 2
surf.blit(s, (x, y))
x += r.width + (icon_gap if idx < len(icon_surfs) - 1 else 0)
text_x = x + icon_text_gap
y = (total_height - text_block_height) // 2
for ls in line_surfs:
surf.blit(ls, (text_x, y))
y += ls.get_height() + 4
return surf
# Couleurs modernes pour le thème
THEME_COLORS = {
# Fond des lignes sélectionnées
@@ -1270,6 +1413,24 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
start_button = get_control_display('start', 'START')
start_text = _("controls_action_start")
control_text = f"RGSX v{config.app_version} - {start_button} : {start_text}"
# Afficher le nom du joystick s'il est détecté
try:
device_name = getattr(config, 'controller_device_name', '') or ''
if device_name:
# Utilise la clé i18n si disponible, sinon fallback
try:
joy_label = _("footer_joystick")
except Exception:
joy_label = "Joystick: {0}"
# Formater si le placeholder {0} est présent
if isinstance(joy_label, str) and "{0}" in joy_label:
joy_text = joy_label.format(device_name)
else:
joy_text = f"{joy_label} {device_name}" if joy_label else f"Joystick: {device_name}"
control_text += f" | {joy_text}"
except Exception:
pass
# Ajouter le nom de la musique si disponible
if config.current_music_name and config.music_popup_start_time > 0:
@@ -1995,25 +2156,25 @@ def draw_filter_platforms_menu(screen):
# Menu aide contrôles
def draw_controls_help(screen, previous_state):
"""Affiche la liste des contrôles (aide) avec mise en page adaptative."""
# Contenu des catégories
# Contenu des catégories (avec icônes si disponibles)
control_categories = {
_("controls_category_navigation"): [
f"{get_control_display('up', '')} {get_control_display('down', '')} {get_control_display('left', '')} {get_control_display('right', '')} : {_('controls_navigation')}",
f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}",
("icons", ["up", "down", "left", "right"], f"{get_control_display('up', '')} {get_control_display('down', '')} {get_control_display('left', '')} {get_control_display('right', '')} : {_('controls_navigation')}"),
("icons", ["page_up", "page_down"], f"{get_control_display('page_up', 'LB')} {get_control_display('page_down', 'RB')} : {_('controls_pages')}"),
],
_("controls_category_main_actions"): [
f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}",
f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}",
f"{get_control_display('start', 'Start')} : {_('controls_action_start')}",
("icons", ["confirm"], f"{get_control_display('confirm', 'A')} : {_('controls_confirm_select')}"),
("icons", ["cancel"], f"{get_control_display('cancel', 'B')} : {_('controls_cancel_back')}"),
("icons", ["start"], f"{get_control_display('start', 'Start')} : {_('controls_action_start')}"),
],
_("controls_category_downloads"): [
f"{get_control_display('history', 'Y')} : {_('controls_action_history')}",
f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}",
("icons", ["history"], f"{get_control_display('history', 'Y')} : {_('controls_action_history')}"),
("icons", ["clear_history"], f"{get_control_display('clear_history', 'X')} : {_('controls_action_clear_history')}"),
],
_("controls_category_search"): [
f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}",
f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}",
f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}",
("icons", ["filter"], f"{get_control_display('filter', 'Select')} : {_('controls_filter_search')}"),
("icons", ["delete"], f"{get_control_display('delete', 'Suppr')} : {_('controls_action_delete')}"),
("icons", ["space"], f"{get_control_display('space', 'Espace')} : {_('controls_action_space')}"),
],
}
@@ -2062,28 +2223,40 @@ def draw_controls_help(screen, previous_state):
total_height += sec_surf.get_height() + line_spacing
for raw_line in lines:
# Wrap par mots
words = raw_line.split()
cur = ""
for word in words:
test = (cur + " " + word).strip()
if font.size(test)[0] <= target_col_width:
cur = test
else:
if cur:
line_surf = font.render(cur, True, THEME_COLORS["text"])
wrapped.append((False, line_surf))
total_height += line_surf.get_height() + line_spacing
max_width = max(max_width, line_surf.get_width())
cur = word
if cur:
line_surf = font.render(cur, True, THEME_COLORS["text"])
wrapped.append((False, line_surf))
total_height += line_surf.get_height() + line_spacing
max_width = max(max_width, line_surf.get_width())
# Deux formats possibles:
# - tuple ("icons", [actions], text)
# - chaîne texte simple
line_surface = None
if isinstance(raw_line, tuple) and len(raw_line) >= 3 and raw_line[0] == "icons":
_, actions, text = raw_line
try:
line_surface = _render_icons_line(actions, text, target_col_width, font, THEME_COLORS["text"])
except Exception:
line_surface = None
if line_surface is None:
# Fallback: traitement texte comme avant
words = str(raw_line).split()
cur = ""
for word in words:
test = (cur + " " + word).strip()
if font.size(test)[0] <= target_col_width:
cur = test
else:
if cur:
line_surf = font.render(cur, True, THEME_COLORS["text"])
wrapped.append((False, line_surf))
total_height += line_surf.get_height() + line_spacing
max_width = max(max_width, line_surf.get_width())
cur = word
if cur:
line_surf = font.render(cur, True, THEME_COLORS["text"])
wrapped.append((False, line_surf))
total_height += line_surf.get_height() + line_spacing
max_width = max(max_width, line_surf.get_width())
else:
wrapped.append((False, line_surface))
total_height += line_surface.get_height() + line_spacing
max_width = max(max_width, line_surface.get_width())
total_height += section_spacing # espace après section
max_width = max(max_width, sec_surf.get_width())

View File

@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen"
,"controls_desc_confirm": "Bestätigen (z.B. A/Kreuz)"
,"controls_desc_cancel": "Abbrechen/Zurück (z.B. B/Kreis)"
,"controls_desc_up": "Nach oben navigieren"
,"controls_desc_down": "Nach unten navigieren"
,"controls_desc_left": "Nach links navigieren"
,"controls_desc_right": "Nach rechts navigieren"
,"controls_desc_page_up": "Schnell nach oben (z.B. LB/L1)"
,"controls_desc_page_down": "Schnell nach unten (z.B. RB/R1)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Schnell nach oben (z.B. LT/L2)"
,"controls_desc_page_down": "Schnell nach unten (z.B. RT/R2)"
,"controls_desc_history": "Verlauf öffnen (z.B. Y/Dreieck)"
,"controls_desc_clear_history": "Downloads: Mehrfachauswahl / Verlauf: Leeren (z.B. X/Quadrat)"
,"controls_desc_filter": "Filtermodus: Öffnen/Bestätigen (z.B. Select)"
,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LT/L2)"
,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RT/R2)"
,"controls_desc_delete": "Filtermodus: Zeichen löschen (z.B. LB/L1)"
,"controls_desc_space": "Filtermodus: Leerzeichen hinzufügen (z.B. RB/R1)"
,"controls_desc_start": "Pausenmenü öffnen (z.B. Start)"
,"controls_mapping_title": "Steuerungszuordnung"
,"controls_mapping_instruction": "Zum Bestätigen gedrückt halten:"
,"controls_mapping_waiting": "Warte auf eine Taste oder einen Button..."
,"controls_mapping_press": "Drücke eine Taste oder einen Button"
,"footer_joystick": "Joystick: {0}"
}

View File

@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "See detected premium provider API keys"
,"controls_desc_confirm": "Confirm (e.g. A/Cross)"
,"controls_desc_cancel": "Cancel/Back (e.g. B/Circle)"
,"controls_desc_up": "Navigate up"
,"controls_desc_down": "Navigate down"
,"controls_desc_left": "Navigate left"
,"controls_desc_right": "Navigate right"
,"controls_desc_page_up": "Fast scroll up (e.g. LB/L1)"
,"controls_desc_page_down": "Fast scroll down (e.g. RB/R1)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Fast scroll up (e.g. LT/L2)"
,"controls_desc_page_down": "Fast scroll down (e.g. RT/R2)"
,"controls_desc_history": "Open history (e.g. Y/Triangle)"
,"controls_desc_clear_history": "Downloads: Multi-select / History: Clear (e.g. X/Square)"
,"controls_desc_filter": "Filter mode: Open/Confirm (e.g. Select)"
,"controls_desc_delete": "Filter mode: Delete character (e.g. LT/L2)"
,"controls_desc_space": "Filter mode: Add space (e.g. RT/R2)"
,"controls_desc_delete": "Filter mode: Delete character (e.g. LB/L1)"
,"controls_desc_space": "Filter mode: Add space (e.g. RB/R1)"
,"controls_desc_start": "Open pause menu (e.g. Start)"
,"controls_mapping_title": "Controls mapping"
,"controls_mapping_instruction": "Hold to confirm the mapping:"
,"controls_mapping_waiting": "Waiting for a key or button..."
,"controls_mapping_press": "Press a key or a button"
,"footer_joystick": "Joystick: {0}"
}

View File

@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Ver claves API premium detectadas"
,"controls_desc_confirm": "Confirmar (ej. A/Cruz)"
,"controls_desc_cancel": "Cancelar/Volver (ej. B/Círculo)"
,"controls_desc_up": "Navegar hacia arriba"
,"controls_desc_down": "Navegar hacia abajo"
,"controls_desc_left": "Navegar a la izquierda"
,"controls_desc_right": "Navegar a la derecha"
,"controls_desc_page_up": "Desplazamiento rápido - (ej. LB/L1)"
,"controls_desc_page_down": "Desplazamiento rápido + (ej. RB/R1)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Desplazamiento rápido - (ej. LT/L2)"
,"controls_desc_page_down": "Desplazamiento rápido + (ej. RT/R2)"
,"controls_desc_history": "Abrir historial (ej. Y/Triángulo)"
,"controls_desc_clear_history": "Descargas: Selección múltiple / Historial: Limpiar (ej. X/Cuadrado)"
,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ej. Select)"
,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LT/L2)"
,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RT/R2)"
,"controls_desc_delete": "Modo filtro: Eliminar carácter (ej. LB/L1)"
,"controls_desc_space": "Modo filtro: Añadir espacio (ej. RB/R1)"
,"controls_desc_start": "Abrir menú pausa (ej. Start)"
,"controls_mapping_title": "Asignación de controles"
,"controls_mapping_instruction": "Mantén para confirmar la asignación:"
,"controls_mapping_waiting": "Esperando una tecla o botón..."
,"controls_mapping_press": "Pulsa una tecla o un botón"
,"footer_joystick": "Joystick: {0}"
}

View File

@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Voir les clés API détectées des services premium"
,"controls_desc_confirm": "Valider (ex: A/Croix)"
,"controls_desc_cancel": "Annuler/Retour (ex: B/Rond)"
,"controls_desc_up": "Naviguer vers le haut"
,"controls_desc_down": "Naviguer vers le bas"
,"controls_desc_left": "Naviguer à gauche"
,"controls_desc_right": "Naviguer à droite"
,"controls_desc_page_up": "Défilement Rapide - (ex: LB/L1)"
,"controls_desc_page_down": "Défilement Rapide + (ex: RB/R1)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Défilement Rapide - (ex: LT/L2)"
,"controls_desc_page_down": "Défilement Rapide + (ex: RT/R2)"
,"controls_desc_history": "Ouvrir l'historique (ex: Y/Triangle)"
,"controls_desc_clear_history": "Téléchargements : Sélection multiple / Historique : Vider (ex: X/Carré)"
,"controls_desc_filter": "Mode Filtre : Ouvrir/Valider (ex: Select)"
,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LT/L2)"
,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RT/R2)"
,"controls_desc_delete": "Mode Filtre : Supprimer caractère (ex: LB/L1)"
,"controls_desc_space": "Mode Filtre : Ajouter espace (ex: RB/R1)"
,"controls_desc_start": "Ouvrir le menu pause (ex: Start)"
,"controls_mapping_title": "Configuration des contrôles"
,"controls_mapping_instruction": "Maintenez pour confirmer l'association :"
,"controls_mapping_waiting": "En attente d'une touche ou d'un bouton..."
,"controls_mapping_press": "Appuyez sur une touche ou un bouton"
,"footer_joystick": "Joystick : {0}"
}

View File

@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate"
,"controls_desc_confirm": "Confermare (es. A/Croce)"
,"controls_desc_cancel": "Annullare/Indietro (es. B/Cerchio)"
,"controls_desc_up": "Navigare verso l'alto"
,"controls_desc_down": "Navigare verso il basso"
,"controls_desc_left": "Navigare a sinistra"
,"controls_desc_right": "Navigare a destra"
,"controls_desc_page_up": "Scorrimento rapido su (es. LB/L1)"
,"controls_desc_page_down": "Scorrimento rapido giù (es. RB/R1)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Scorrimento rapido su (es. LT/L2)"
,"controls_desc_page_down": "Scorrimento rapido giù (es. RT/R2)"
,"controls_desc_history": "Aprire cronologia (es. Y/Triangolo)"
,"controls_desc_clear_history": "Download: Selezione multipla / Cronologia: Svuotare (es. X/Quadrato)"
,"controls_desc_filter": "Modalità filtro: Aprire/Confermare (es. Select)"
,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LT/L2)"
,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RT/R2)"
,"controls_desc_delete": "Modalità filtro: Eliminare carattere (es. LB/L1)"
,"controls_desc_space": "Modalità filtro: Aggiungere spazio (es. RB/R1)"
,"controls_desc_start": "Aprire menu pausa (es. Start)"
,"controls_mapping_title": "Mappatura controlli"
,"controls_mapping_instruction": "Tieni premuto per confermare l'associazione:"
,"controls_mapping_waiting": "In attesa di un tasto o pulsante..."
,"controls_mapping_press": "Premi un tasto o un pulsante"
,"footer_joystick": "Joystick: {0}"
}

View File

@@ -180,16 +180,21 @@
,"instruction_settings_api_keys": "Ver chaves API premium detectadas"
,"controls_desc_confirm": "Confirmar (ex. A/Cruz)"
,"controls_desc_cancel": "Cancelar/Voltar (ex. B/Círculo)"
,"controls_desc_up": "Navegar para cima"
,"controls_desc_down": "Navegar para baixo"
,"controls_desc_left": "Navegar para esquerda"
,"controls_desc_right": "Navegar para direita"
,"controls_desc_page_up": "Rolagem rápida para cima (ex. LB/L1)"
,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RB/R1)"
,"controls_desc_up": "UP ↑"
,"controls_desc_down": "DOWN ↓"
,"controls_desc_left": "LEFT ←"
,"controls_desc_right": "RIGHT →"
,"controls_desc_page_up": "Rolagem rápida para cima (ex. LT/L2)"
,"controls_desc_page_down": "Rolagem rápida para baixo (ex. RT/R2)"
,"controls_desc_history": "Abrir histórico (ex. Y/Triângulo)"
,"controls_desc_clear_history": "Downloads: Seleção múltipla / Histórico: Limpar (ex. X/Quadrado)"
,"controls_desc_filter": "Modo filtro: Abrir/Confirmar (ex. Select)"
,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LT/L2)"
,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RT/R2)"
,"controls_desc_delete": "Modo filtro: Deletar caractere (ex. LB/L1)"
,"controls_desc_space": "Modo filtro: Adicionar espaço (ex. RB/R1)"
,"controls_desc_start": "Abrir menu pausa (ex. Start)"
,"controls_mapping_title": "Mapeamento de controles"
,"controls_mapping_instruction": "Mantenha para confirmar o mapeamento:"
,"controls_mapping_waiting": "Aguardando uma tecla ou botão..."
,"controls_mapping_press": "Pressione uma tecla ou um botão"
,"footer_joystick": "Joystick: {0}"
}

View File

@@ -2,8 +2,10 @@
import os
import sys
import time
import json
import re
import traceback
from typing import Any, Dict, Tuple, List
from typing import Any, Dict, Tuple, List, Optional
try:
import pygame # type: ignore
@@ -234,6 +236,116 @@ def write_log(path: str, mapping: Dict[str, Tuple[str, Any]], device_name: str)
log(f"Saved mapping to: {path}")
# --- JSON preset generation ---
def sanitize_device_name(name: str) -> str:
s = name.strip().lower()
# Replace non-alphanumeric with underscore
s = re.sub(r"[^a-z0-9]+", "_", s)
s = re.sub(r"_+", "_", s).strip("_")
return s or "controller"
def to_json_binding(kind: str, data: Any, display: Optional[str] = None) -> Optional[Dict[str, Any]]:
if kind == "button" and isinstance(data, int):
return {"type": "button", "button": data, **({"display": display} if display else {})}
if kind == "hat" and isinstance(data, (tuple, list)) and len(data) == 2:
val = list(data)
return {"type": "hat", "value": val, **({"display": display} if display else {})}
if kind == "axis" and isinstance(data, dict):
axis = data.get("axis")
direction = data.get("direction")
if isinstance(axis, int) and direction in (-1, 1):
return {"type": "axis", "axis": axis, "direction": int(direction), **({"display": display} if display else {})}
return None
def build_controls_json(mapping: Dict[str, Tuple[str, Any]]) -> Dict[str, Any]:
# Map logical prompts to action keys and preferred display labels
prompt_map = {
"SOUTH_BUTTON - CONFIRM": ("confirm", "A"),
"EAST_BUTTON - CANCEL": ("cancel", "B"),
"WEST_BUTTON - CLEAR HISTORY / SELECT GAMES": ("clear_history", "X"),
"NORTH_BUTTON - HISTORY": ("history", "Y"),
"START - PAUSE": ("start", "Start"),
"SELECT - FILTER": ("filter", "Select"),
"DPAD_UP - MOVE UP": ("up", "↑"),
"DPAD_DOWN - MOVE DOWN": ("down", "↓"),
"DPAD_LEFT - MOVE LEFT": ("left", "←"),
"DPAD_RIGHT - MOVE RIGHT": ("right", "→"),
"LEFT_BUMPER - LB/L1 - Delete last char": ("delete", "LB"),
"RIGHT_BUMPER - RB/R1 - Add space": ("space", "RB"),
# Triggers per prompts: LEFT=page_up, RIGHT=page_down
"LEFT_TRIGGER - LT/L2 - Page +": ("page_up", "LT"),
"RIGHT_TRIGGER - RT/R2 - Page -": ("page_down", "RT"),
# Left stick directions (fallbacks for arrows)
"JOYSTICK_LEFT_UP - MOVE UP": ("up", "J↑"),
"JOYSTICK_LEFT_DOWN - MOVE DOWN": ("down", "J↓"),
"JOYSTICK_LEFT_LEFT - MOVE LEFT": ("left", "J←"),
"JOYSTICK_LEFT_RIGHT - MOVE RIGHT": ("right", "J→"),
}
result: Dict[str, Any] = {}
# First pass: take direct DPAD/face/meta/bumper/trigger bindings
for prompt, (action, disp) in prompt_map.items():
if prompt not in mapping:
continue
kind, data = mapping[prompt]
if kind in ("ignored", "skipped"):
continue
# Prefer DPAD over JOYSTICK for directions: handle fallback later
if action in ("up", "down", "left", "right"):
if prompt.startswith("DPAD_"):
b = to_json_binding(kind, data, disp)
if b:
result[action] = b
# Joystick handled as fallback if DPAD missing
else:
b = to_json_binding(kind, data, disp)
if b:
result[action] = b
# Second pass: fallback to joystick directions if arrows missing
fallbacks = [
("JOYSTICK_LEFT_UP - MOVE UP", "up", "J↑"),
("JOYSTICK_LEFT_DOWN - MOVE DOWN", "down", "J↓"),
("JOYSTICK_LEFT_LEFT - MOVE LEFT", "left", "J←"),
("JOYSTICK_LEFT_RIGHT - MOVE RIGHT", "right", "J→"),
]
for prompt, action, disp in fallbacks:
if action in result:
continue
if prompt in mapping:
kind, data = mapping[prompt]
if kind in ("ignored", "skipped"):
continue
b = to_json_binding(kind, data, disp)
if b:
result[action] = b
return result
def write_controls_json(device_name: str, controls: Dict[str, Any]) -> str:
"""Write the generated controls preset JSON in the same folder as this script.
Also embeds a JSON-safe comment with the device name under the _comment key.
"""
# Same folder as the launched script
base_dir = os.path.dirname(os.path.abspath(__file__))
fname = f"{sanitize_device_name(device_name)}_controller.json"
out_path = os.path.join(base_dir, fname)
# Include the detected device name for auto-preset matching
payload = {"device": device_name}
payload.update(controls)
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
return out_path
except Exception:
return out_path
def main() -> None:
init_screen()
js = init_joystick()
@@ -253,6 +365,13 @@ def main() -> None:
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "controller_mapping.log")
write_log(log_path, mapping, js.get_name())
# Build and write ready-to-use JSON controls preset
controls = build_controls_json(mapping)
if controls:
out_json = write_controls_json(js.get_name(), controls)
log(f"Saved JSON preset to: {out_json}")
else:
log("No usable inputs captured to build a JSON preset.")
log("Done. Press Q or close the window to exit.")