diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py
index 2a36690..f4f0056 100644
--- a/ports/RGSX/__main__.py
+++ b/ports/RGSX/__main__.py
@@ -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}")
diff --git a/ports/RGSX/assets/controls/nintendo_controller.json b/ports/RGSX/assets/controls/nintendo_controller.json
deleted file mode 100644
index cdcd087..0000000
--- a/ports/RGSX/assets/controls/nintendo_controller.json
+++ /dev/null
@@ -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"
- }
-}
diff --git a/ports/RGSX/assets/controls/ps5_dualsense.json b/ports/RGSX/assets/controls/ps5_dualsense.json
new file mode 100644
index 0000000..c7feaa5
--- /dev/null
+++ b/ports/RGSX/assets/controls/ps5_dualsense.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/ports/RGSX/assets/controls/retroid_pocket_flip_2.json b/ports/RGSX/assets/controls/retroid_pocket_flip_2.json
new file mode 100644
index 0000000..5227612
--- /dev/null
+++ b/ports/RGSX/assets/controls/retroid_pocket_flip_2.json
@@ -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" }
+}
diff --git a/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json b/ports/RGSX/assets/controls/rg34xx_sp.json
similarity index 97%
rename from ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json
rename to ports/RGSX/assets/controls/rg34xx_sp.json
index 412b94a..02a6acc 100644
--- a/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json
+++ b/ports/RGSX/assets/controls/rg34xx_sp.json
@@ -1,4 +1,5 @@
{
+ "device": "RG34XX-SP Controller",
"confirm": {
"type": "button",
"button": 3,
diff --git a/ports/RGSX/assets/controls/rgb10_ogs.json b/ports/RGSX/assets/controls/rgb10_ogs.json
new file mode 100644
index 0000000..173a7f1
--- /dev/null
+++ b/ports/RGSX/assets/controls/rgb10_ogs.json
@@ -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" }
+}
diff --git a/ports/RGSX/assets/controls/steam_controller.json b/ports/RGSX/assets/controls/steam_deck.json
similarity index 98%
rename from ports/RGSX/assets/controls/steam_controller.json
rename to ports/RGSX/assets/controls/steam_deck.json
index 6cf3cf0..d423e78 100644
--- a/ports/RGSX/assets/controls/steam_controller.json
+++ b/ports/RGSX/assets/controls/steam_deck.json
@@ -1,4 +1,5 @@
{
+ "device": "Steam Deck",
"confirm": {
"type": "button",
"button": 3,
diff --git a/ports/RGSX/assets/controls/trimui_controller.json b/ports/RGSX/assets/controls/trimui_smart_pro.json
similarity index 97%
rename from ports/RGSX/assets/controls/trimui_controller.json
rename to ports/RGSX/assets/controls/trimui_smart_pro.json
index 0003858..fe38054 100644
--- a/ports/RGSX/assets/controls/trimui_controller.json
+++ b/ports/RGSX/assets/controls/trimui_smart_pro.json
@@ -1,4 +1,5 @@
{
+ "device": "TRIMUI Smart Pro Controller",
"confirm": {
"type": "button",
"button": 0,
diff --git a/ports/RGSX/assets/controls/xbox_controller.json b/ports/RGSX/assets/controls/xbox_360_emulated.json
similarity index 96%
rename from ports/RGSX/assets/controls/xbox_controller.json
rename to ports/RGSX/assets/controls/xbox_360_emulated.json
index d79f9d0..d906409 100644
--- a/ports/RGSX/assets/controls/xbox_controller.json
+++ b/ports/RGSX/assets/controls/xbox_360_emulated.json
@@ -1,4 +1,5 @@
{
+ "device": "XBOX 360 For Windows (Controller)",
"confirm": {
"type": "button",
"button": 0,
diff --git a/ports/RGSX/assets/controls/eightbitdo_controller.json b/ports/RGSX/assets/controls/xbox_360_original.json
similarity index 73%
rename from ports/RGSX/assets/controls/eightbitdo_controller.json
rename to ports/RGSX/assets/controls/xbox_360_original.json
index d050482..8ca5df5 100644
--- a/ports/RGSX/assets/controls/eightbitdo_controller.json
+++ b/ports/RGSX/assets/controls/xbox_360_original.json
@@ -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"
}
-}
+}
\ No newline at end of file
diff --git a/ports/RGSX/assets/controls/xbox_elite_controller.json b/ports/RGSX/assets/controls/xbox_elite.json
similarity index 80%
rename from ports/RGSX/assets/controls/xbox_elite_controller.json
rename to ports/RGSX/assets/controls/xbox_elite.json
index 3592391..3d3caf5 100644
--- a/ports/RGSX/assets/controls/xbox_elite_controller.json
+++ b/ports/RGSX/assets/controls/xbox_elite.json
@@ -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" }
}
diff --git a/ports/RGSX/assets/controls/generic_controller.json b/ports/RGSX/assets/controls/zero_plus.json
similarity index 71%
rename from ports/RGSX/assets/controls/generic_controller.json
rename to ports/RGSX/assets/controls/zero_plus.json
index d050482..5f66c58 100644
--- a/ports/RGSX/assets/controls/generic_controller.json
+++ b/ports/RGSX/assets/controls/zero_plus.json
@@ -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"
}
-}
+}
\ No newline at end of file
diff --git a/ports/RGSX/assets/Pixel-UniCode.ttf b/ports/RGSX/assets/fonts/Pixel-UniCode.ttf
similarity index 100%
rename from ports/RGSX/assets/Pixel-UniCode.ttf
rename to ports/RGSX/assets/fonts/Pixel-UniCode.ttf
diff --git a/ports/RGSX/assets/images/button_l.svg b/ports/RGSX/assets/images/button_l.svg
new file mode 100644
index 0000000..07dc3b0
--- /dev/null
+++ b/ports/RGSX/assets/images/button_l.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/ports/RGSX/assets/images/button_lt.svg b/ports/RGSX/assets/images/button_lt.svg
new file mode 100644
index 0000000..0c4a212
--- /dev/null
+++ b/ports/RGSX/assets/images/button_lt.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/ports/RGSX/assets/images/button_r.svg b/ports/RGSX/assets/images/button_r.svg
new file mode 100644
index 0000000..8db5274
--- /dev/null
+++ b/ports/RGSX/assets/images/button_r.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/ports/RGSX/assets/images/button_rt.svg b/ports/RGSX/assets/images/button_rt.svg
new file mode 100644
index 0000000..15f0fd0
--- /dev/null
+++ b/ports/RGSX/assets/images/button_rt.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/ports/RGSX/assets/images/button_select.svg b/ports/RGSX/assets/images/button_select.svg
new file mode 100644
index 0000000..eba861d
--- /dev/null
+++ b/ports/RGSX/assets/images/button_select.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/ports/RGSX/assets/images/button_start.svg b/ports/RGSX/assets/images/button_start.svg
new file mode 100644
index 0000000..78fb563
--- /dev/null
+++ b/ports/RGSX/assets/images/button_start.svg
@@ -0,0 +1,11 @@
+
+
diff --git a/ports/RGSX/assets/images/buttons_east.svg b/ports/RGSX/assets/images/buttons_east.svg
new file mode 100644
index 0000000..9c220e6
--- /dev/null
+++ b/ports/RGSX/assets/images/buttons_east.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/ports/RGSX/assets/images/buttons_north.svg b/ports/RGSX/assets/images/buttons_north.svg
new file mode 100644
index 0000000..7813ee2
--- /dev/null
+++ b/ports/RGSX/assets/images/buttons_north.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/ports/RGSX/assets/images/buttons_south.svg b/ports/RGSX/assets/images/buttons_south.svg
new file mode 100644
index 0000000..5705926
--- /dev/null
+++ b/ports/RGSX/assets/images/buttons_south.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/ports/RGSX/assets/images/buttons_west.svg b/ports/RGSX/assets/images/buttons_west.svg
new file mode 100644
index 0000000..e1d5a4a
--- /dev/null
+++ b/ports/RGSX/assets/images/buttons_west.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/ports/RGSX/assets/images/dpad_down.svg b/ports/RGSX/assets/images/dpad_down.svg
new file mode 100644
index 0000000..84a41fc
--- /dev/null
+++ b/ports/RGSX/assets/images/dpad_down.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/ports/RGSX/assets/images/dpad_left.svg b/ports/RGSX/assets/images/dpad_left.svg
new file mode 100644
index 0000000..1b018d6
--- /dev/null
+++ b/ports/RGSX/assets/images/dpad_left.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/ports/RGSX/assets/images/dpad_right.svg b/ports/RGSX/assets/images/dpad_right.svg
new file mode 100644
index 0000000..228f5e0
--- /dev/null
+++ b/ports/RGSX/assets/images/dpad_right.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/ports/RGSX/assets/images/dpad_up.svg b/ports/RGSX/assets/images/dpad_up.svg
new file mode 100644
index 0000000..eb6dc0c
--- /dev/null
+++ b/ports/RGSX/assets/images/dpad_up.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/ports/RGSX/assets/unrar.exe b/ports/RGSX/assets/progs/unrar.exe
similarity index 100%
rename from ports/RGSX/assets/unrar.exe
rename to ports/RGSX/assets/progs/unrar.exe
diff --git a/ports/RGSX/assets/xdvdfs b/ports/RGSX/assets/progs/xdvdfs
similarity index 100%
rename from ports/RGSX/assets/xdvdfs
rename to ports/RGSX/assets/progs/xdvdfs
diff --git a/ports/RGSX/assets/xdvdfs.exe b/ports/RGSX/assets/progs/xdvdfs.exe
similarity index 100%
rename from ports/RGSX/assets/xdvdfs.exe
rename to ports/RGSX/assets/progs/xdvdfs.exe
diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py
index 1a19a76..2dab57b 100644
--- a/ports/RGSX/config.py
+++ b/ports/RGSX/config.py
@@ -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))
diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py
index 5b57689..1b5c173 100644
--- a/ports/RGSX/controls.py
+++ b/ports/RGSX/controls.py
@@ -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:
diff --git a/ports/RGSX/controls_mapper.py b/ports/RGSX/controls_mapper.py
index 2e674dd..87d9487 100644
--- a/ports/RGSX/controls_mapper.py
+++ b/ports/RGSX/controls_mapper.py
@@ -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)
\ No newline at end of file
diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py
index a9e0901..9023e47 100644
--- a/ports/RGSX/display.py
+++ b/ports/RGSX/display.py
@@ -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())
diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json
index c8e5c5c..b70e7fa 100644
--- a/ports/RGSX/languages/de.json
+++ b/ports/RGSX/languages/de.json
@@ -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}"
}
\ No newline at end of file
diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json
index 17a76e8..1e1977c 100644
--- a/ports/RGSX/languages/en.json
+++ b/ports/RGSX/languages/en.json
@@ -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}"
}
\ No newline at end of file
diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json
index 990eaa7..c78503c 100644
--- a/ports/RGSX/languages/es.json
+++ b/ports/RGSX/languages/es.json
@@ -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}"
}
\ No newline at end of file
diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json
index e93cd21..5efdec8 100644
--- a/ports/RGSX/languages/fr.json
+++ b/ports/RGSX/languages/fr.json
@@ -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}"
}
\ No newline at end of file
diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json
index 93f4675..04ab511 100644
--- a/ports/RGSX/languages/it.json
+++ b/ports/RGSX/languages/it.json
@@ -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}"
}
\ No newline at end of file
diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json
index f76fea2..8cd0158 100644
--- a/ports/RGSX/languages/pt.json
+++ b/ports/RGSX/languages/pt.json
@@ -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}"
}
\ No newline at end of file
diff --git a/pygame/controller_debug.pygame b/pygame/controller_debug.pygame
index af1d7f0..cb7615e 100644
--- a/pygame/controller_debug.pygame
+++ b/pygame/controller_debug.pygame
@@ -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.")