diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index a5d04c4..fa59a21 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -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 diff --git a/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json b/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json new file mode 100644 index 0000000..412b94a --- /dev/null +++ b/ports/RGSX/assets/controls/anbernic_rg34xx_sp_controller.json @@ -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" + } +} diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index fb9b546..69dffa3 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -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 diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 98157c1..5b57689 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -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') diff --git a/ports/RGSX/rgsx_settings.py b/ports/RGSX/rgsx_settings.py index b41411d..86cb260 100644 --- a/ports/RGSX/rgsx_settings.py +++ b/ports/RGSX/rgsx_settings.py @@ -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): diff --git a/pygame/controller_debug.pygame b/pygame/controller_debug.pygame index 38c2ad4..af1d7f0 100644 --- a/pygame/controller_debug.pygame +++ b/pygame/controller_debug.pygame @@ -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")