1
0
forked from Mirrors/RGSX

Add support for Anbernic RG35XX controller and local custom sources ZIP handling

This commit is contained in:
retrogamesets
2025-09-16 00:23:48 +02:00
parent d5bc56be64
commit 158be30667
6 changed files with 203 additions and 46 deletions

View File

@@ -219,6 +219,12 @@ else:
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}")
# ne pas break ici pour permettre une détection plus spécifique (xbox elite) si nécessaire
# Détection spécifique Elite AVANT la détection générique Xbox
if ("microsoft xbox controller" in lname):
config.xbox_elite_controller = True
@@ -958,53 +964,83 @@ async def main():
try:
zip_path = os.path.join(config.SAVE_FOLDER, "data_download.zip")
headers = {'User-Agent': 'Mozilla/5.0'}
# Déterminer l'URL à utiliser selon le mode (RGSX ou custom)
sources_zip_url = get_sources_zip_url(OTA_data_ZIP)
if sources_zip_url is None:
# Mode custom sans URL valide -> pas de téléchargement, jeux vides
logger.warning("Mode custom actif mais aucune URL valide fournie. Liste de jeux vide.")
config.popup_message = _("sources_mode_custom_missing_url").format(config.RGSX_SETTINGS_PATH)
config.popup_timer = 5000
else:
try:
with requests.get(sources_zip_url, stream=True, headers=headers, timeout=30) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale du ZIP : {total_size} octets")
downloaded = 0
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
with open(zip_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
config.download_progress[sources_zip_url] = {
"downloaded_size": downloaded,
"total_size": total_size,
"status": "Téléchargement",
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
}
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
config.needs_redraw = True
await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {zip_path}")
# Support des sources custom locales: prioriser un ZIP présent dans SAVE_FOLDER
try:
from rgsx_settings import get_sources_mode
from rgsx_settings import find_local_custom_sources_zip
mode = get_sources_mode()
except Exception:
mode = "rgsx"
find_local_custom_sources_zip = lambda: None # type: ignore
config.current_loading_system = _("loading_extracting_data")
config.loading_progress = 60.0
config.needs_redraw = True
dest_dir = config.SAVE_FOLDER
success, message = extract_zip_data(zip_path, dest_dir, sources_zip_url)
local_zip = find_local_custom_sources_zip() if mode == "custom" else None
if local_zip and os.path.isfile(local_zip):
# Extraire directement depuis le ZIP local
config.current_loading_system = _("loading_extracting_data")
config.loading_progress = 60.0
config.needs_redraw = True
dest_dir = config.SAVE_FOLDER
try:
success, message = extract_zip_data(local_zip, dest_dir, local_zip)
if success:
logger.debug(f"Extraction réussie : {message}")
logger.debug(f"Extraction locale réussie : {message}")
config.loading_progress = 70.0
config.needs_redraw = True
else:
raise Exception(f"Échec de l'extraction : {message}")
raise Exception(f"Échec de l'extraction locale : {message}")
except Exception as de:
logger.error(f"Erreur téléchargement custom source: {de}")
logger.error(f"Erreur extraction ZIP local custom: {de}")
config.popup_message = _("sources_mode_custom_download_error")
config.popup_timer = 5000
# Pas d'arrêt : continuer avec jeux vides
# Continuer avec jeux vides
else:
# Déterminer l'URL à utiliser selon le mode (RGSX ou custom)
sources_zip_url = get_sources_zip_url(OTA_data_ZIP)
if sources_zip_url is None:
# Mode custom sans fichier local ni URL valide -> pas de téléchargement, jeux vides
logger.warning("Mode custom actif mais aucun ZIP local et aucune URL valide fournie. Liste de jeux vide.")
config.popup_message = _("sources_mode_custom_missing_url").format(config.RGSX_SETTINGS_PATH)
config.popup_timer = 5000
else:
try:
with requests.get(sources_zip_url, stream=True, headers=headers, timeout=30) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
logger.debug(f"Taille totale du ZIP : {total_size} octets")
downloaded = 0
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
with open(zip_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
config.download_progress[sources_zip_url] = {
"downloaded_size": downloaded,
"total_size": total_size,
"status": "Téléchargement",
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
}
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
config.needs_redraw = True
await asyncio.sleep(0)
logger.debug(f"ZIP téléchargé : {zip_path}")
config.current_loading_system = _("loading_extracting_data")
config.loading_progress = 60.0
config.needs_redraw = True
dest_dir = config.SAVE_FOLDER
success, message = extract_zip_data(zip_path, dest_dir, sources_zip_url)
if success:
logger.debug(f"Extraction réussie : {message}")
config.loading_progress = 70.0
config.needs_redraw = True
else:
raise Exception(f"Échec de l'extraction : {message}")
except Exception as de:
logger.error(f"Erreur téléchargement custom source: {de}")
config.popup_message = _("sources_mode_custom_download_error")
config.popup_timer = 5000
# Pas d'arrêt : continuer avec jeux vides
except Exception as e:
logger.error(f"Erreur lors du téléchargement/extraction du Dossier Data : {str(e)}")
# En mode custom on ne bloque pas le chargement ; en mode RGSX (sources_zip_url non None et OTA) on affiche une erreur

View File

@@ -0,0 +1,72 @@
{
"confirm": {
"type": "button",
"button": 3,
"display": "A"
},
"cancel": {
"type": "button",
"button": 4,
"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": 10,
"display": "Start"
},
"filter": {
"type": "button",
"button": 9,
"display": "Select"
},
"page_up": {
"type": "button",
"button": 7,
"display": "LT"
},
"page_down": {
"type": "button",
"button": 8,
"display": "RT"
},
"history": {
"type": "button",
"button": 6,
"display": "Y"
},
"clear_history": {
"type": "button",
"button": 5,
"display": "X"
},
"delete": {
"type": "button",
"button": 13,
"display": "LB"
},
"space": {
"type": "button",
"button": 14,
"display": "RB"
}
}

View File

@@ -190,6 +190,7 @@ steam_controller = False
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
# --- Filtre plateformes (UI) ---
selected_filter_index = 0 # index dans la liste visible triée

View File

@@ -113,6 +113,8 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
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')

View File

@@ -197,6 +197,40 @@ def get_sources_zip_url(fallback_url):
return None
return fallback_url
def find_local_custom_sources_zip():
"""Recherche un fichier ZIP local à la racine de SAVE_FOLDER pour le mode custom.
Priorité sur quelques noms courants afin d'éviter toute ambiguïté.
Retourne le chemin absolu du ZIP si trouvé, sinon None.
"""
try:
from config import SAVE_FOLDER
candidates = [
"games.zip",
"custom_sources.zip",
"rgsx_custom_sources.zip",
"data.zip",
]
if not os.path.isdir(SAVE_FOLDER):
return None
for name in candidates:
p = os.path.join(SAVE_FOLDER, name)
if os.path.isfile(p):
return p
# Option avancée: prendre le plus récent *.zip si aucun nom connu trouvé
try:
zips = [os.path.join(SAVE_FOLDER, f) for f in os.listdir(SAVE_FOLDER) if f.lower().endswith('.zip')]
zips = [z for z in zips if os.path.isfile(z)]
if zips:
newest = max(zips, key=lambda z: os.path.getmtime(z))
return newest
except Exception:
pass
return None
except Exception as e:
logger.debug(f"find_local_custom_sources_zip error: {e}")
return None
# ----------------------- Unsupported platforms toggle ----------------------- #
def get_show_unsupported_platforms(settings=None):

View File

@@ -37,13 +37,9 @@ PROMPTS = [
"JOYSTICK_LEFT_DOWN - MOVE DOWN",
"JOYSTICK_LEFT_LEFT - MOVE LEFT",
"JOYSTICK_LEFT_RIGHT - MOVE RIGHT",
# Right stick directions
"JOYSTICK_RIGHT_UP - MOVE U P",
"JOYSTICK_RIGHT_DOWN - MOVE DOWN",
"JOYSTICK_RIGHT_LEFT - MOVE LEFT",
"JOYSTICK_RIGHT_RIGHT - MOVE RIGHT",
]
INPUT_TIMEOUT_SECONDS = 10 # Temps max par entrée avant "ignored"
# --- Minimal on-screen console (Pygame window) ---
SURFACE = None # type: ignore
@@ -108,6 +104,8 @@ def init_joystick() -> pygame.joystick.Joystick:
js.init()
name = js.get_name()
log(f"Using joystick 0: {name}")
log("")
log(f"Note: each input will auto-ignore after {INPUT_TIMEOUT_SECONDS}s if not present (e.g. missing L2/R2)")
return js
@@ -147,7 +145,7 @@ def wait_for_stable(js: pygame.joystick.Joystick, settle_ms: int = 250, deadband
pygame.time.wait(10)
def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_threshold: float = 0.6) -> Tuple[str, Any]:
def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_threshold: float = 0.6, timeout_sec: int = INPUT_TIMEOUT_SECONDS) -> Tuple[str, Any]:
"""Wait for a joystick event for the given logical control.
Returns a tuple of (kind, data):
@@ -158,10 +156,18 @@ def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_thresho
# Ensure prior motion has settled to avoid capturing a release
wait_for_stable(js)
log("")
log(f"Press {logical_name} (ESC to skip, close window to quit)…")
deadline = time.time() + max(1, int(timeout_sec))
log(f"Press {logical_name} (Wait {timeout_sec}s to skip/ignore) if not present")
# Flush old events
pygame.event.clear()
while True:
# Update window title with countdown if we have a surface
try:
remaining = int(max(0, deadline - time.time()))
if SURFACE is not None:
pygame.display.set_caption(f"Controller Tester — {logical_name} — {remaining}s left")
except Exception:
pass
for event in pygame.event.get():
# Keyboard helpers
if event.type == pygame.KEYDOWN:
@@ -195,6 +201,10 @@ def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_thresho
return ("axis", {"axis": axis, "direction": direction, "raw": value})
draw_log()
# Timeout?
if time.time() >= deadline:
log(f"Ignored {logical_name} (timeout {timeout_sec}s)")
return ("ignored", None)
time.sleep(0.005)
@@ -213,6 +223,8 @@ def write_log(path: str, mapping: Dict[str, Tuple[str, Any]], device_name: str)
lines.append(f"{name} = AXIS {ax} dir {direction}\n")
elif kind == "skipped":
lines.append(f"{name} = SKIPPED\n")
elif kind == "ignored":
lines.append(f"{name} = IGNORED\n")
else:
lines.append(f"{name} = UNKNOWN {data}\n")