From 17f56bc9e96193503a136976757c7e2f286f6bb8 Mon Sep 17 00:00:00 2001 From: skymike03 Date: Tue, 9 Sep 2025 04:20:10 +0200 Subject: [PATCH] v2.1.0.0 - Retrobat: automatic `gamelist.xml` update on launch to immediately show scraped images/videos in ES. - System images loading prioritizes explicit `platform_image` from systems JSON. - Auto-detect supported extensions by parsing `es_systems.cfg`; generate and cache automatically `/saves/ports/rgsx/rom_extensions.json`. - Auto-hide unsupported platforms at start if roms folders not exist / not match `es_systems.cfg`) with a toggle to re enable in the Display menu. - Automatic restart after update configuration (beta) - New Display option to change systems grid layout (3x3, 3x4, 4x3, 4x4). - Pause menu reorganized - Translations updated. - Minor display fixes and spacing polish. --- README.md | 34 +- README_FR.md | 34 +- ports/RGSX/__main__.py | 114 +- ports/RGSX/config.py | 34 +- ports/RGSX/controls.py | 220 ++- ports/RGSX/display.py | 103 +- ports/RGSX/languages/de.json | 10 +- ports/RGSX/languages/en.json | 10 +- ports/RGSX/languages/es.json | 10 +- ports/RGSX/languages/fr.json | 10 +- ports/RGSX/languages/pt.json | 10 +- ports/RGSX/network.py | 28 +- ports/RGSX/rgsx_settings.py | 47 +- ports/RGSX/rom_extensions.json | 2317 ------------------------- ports/RGSX/update_gamelist_windows.py | 138 ++ ports/RGSX/utils.py | 329 +++- windows/RGSX Retrobat.bat | 50 +- 17 files changed, 1037 insertions(+), 2461 deletions(-) delete mode 100644 ports/RGSX/rom_extensions.json create mode 100644 ports/RGSX/update_gamelist_windows.py diff --git a/README.md b/README.md index b22caaf..1b17f92 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,23 @@ The application supports multiple sources like myrient and 1fichier. These sourc ## ✨ Features -- **Game downloads** : Support for ZIP files and handling of unsupported extensions thanks to the `info.txt` file in each folder (batocera), which automatically extracts if the system doesn't support archives. +- **Game downloads** : Support for ZIP files and handling of unsupported extensions based on EmulationStation's `es_systems.cfg` (and custom `es_systems_*.cfg` on Batocera). RGSX reads allowed extensions per system from these configs and will automatically extract archives when a system doesn't support them. - Downloads require no authentication or account for most sources. - Systems marked `(1fichier)` in the name will only be accessible if you provide your 1fichier API key (see below). - **Download history** : View and re-download previous files. - **Multi-select downloads** : Mark multiple games in the game list with the key mapped to Clear History (default X) to enqueue several downloads in one batch. Press Confirm to start batch. - **Control customization** : Remap keyboard or controller keys to your preference with automatic button name detection from EmulationStation (beta). +- **Systems grid layout**: Change the platforms grid (3x3, 3x4, 4x3, 4x4) from the Display menu. +- **Show/hide unsupported systems**: Auto-hide platforms whose ROM folder is missing according to `es_systems.cfg`, with a toggle in the Display menu. +- **Smarter system images**: Image loading prioritizes explicit `platform_image` from your systems list JSON before falling back to `.png` or folder images. - **Font size adjustment** : If you find the text too small/too large, you can change it in the menu. - **Search mode** : Filter games by name for quick navigation with virtual keyboard on controller. - **Multilingual support** : Interface available in multiple languages. You can choose the language in the menu. - **Error handling** with informative messages and LOG file. - **Adaptive interface** : The interface adapts to all resolutions from 800x600 to 4K (not tested beyond 1920x1080). - **Automatic updates** : the application must be restarted after an update. +- **Automatic supported extensions cache**: On first use, RGSX reads `es_systems.cfg` (RetroBat/Batocera) and generates `/saves/ports/rgsx/rom_extensions.json` with allowed extensions per system. +- **Retrobat gamelist auto-update**: On Retrobat, the Windows `gamelist.xml` is updated automatically at launch so your images/videos appear in EmulationStation. --- @@ -91,6 +96,13 @@ INFO: for retrobat on first launch, the application will download Python in the - From the pause menu, access history, control help (control display changes depending on which menu you're in) or reconfiguration of keys, languages, font size. - You can also, from the menu, regenerate the cache of the systems/games/images list to be sure to have the latest updates. +#### Display menu + +- Layout: switch platforms grid between 3x3, 3x4, 4x3, 4x4. +- Font size: adjust text scale (accessibility). +- Show unsupported systems: toggle visibility for platforms whose ROM folder is missing. +- Filter systems: quickly show/hide platforms by name (persistent). + --- ### Download @@ -148,19 +160,23 @@ RGSX/ ├── language.py # Multilingual support management. ├── accessibility.py # Accessibility settings management. ├── utils.py # Utility functions (text wrap, truncation etc.). -├── update_gamelist.py # Game list update. +├── update_gamelist.py # Game list update (Batocera/Knulli). +├── update_gamelist_windows.py # Retrobat-only: auto-update ES gamelist.xml on launch. ├── assets/ # Application resources (fonts, executables, music). -├── games/ # Game system configuration files. -├── images/ # System images. + ├── languages/ # Translation files. └── logs/ └── RGSX.log # Log file. /saves/ports/RGSX/ │ +├── systems_list.json # Available Systems names / folders / images +├── games/ # Links for games. +├── images/ # System images. ├── rgsx_settings.json # Unified configuration file (settings, accessibility, language, music, symlinks). ├── controls.json # Control mapping file (generated after first startup). ├── history.json # Download history database (generated after first download). +├── rom_extensions.json # Generated from es_systems.cfg: per-system allowed ROM extensions cache. └── 1FichierAPI.txt # 1fichier API key (premium account and + only) (empty by default). ``` @@ -207,6 +223,16 @@ Developed with ❤️ for retro gaming enthusiasts. ## 🔄 Changelog +### 2.1.0.0 (2025-09-09) +- Retrobat: automatic `gamelist.xml` update on launch to immediately show scraped images/videos in ES. +- System image loading prioritizes explicit `platform_image` from systems JSON. +- Auto-detect supported extensions by parsing `es_systems.cfg`; generate and cache `/saves/ports/rgsx/rom_extensions.json`. +- Auto-hide unsupported platforms (missing ROM folder per `es_systems.cfg`) with a toggle in the Display menu. +- New Display option to change systems grid layout (3x3, 3x4, 4x3, 4x4). +- Pause menu reorganized to surface the most used items. +- Translations updated. +- Minor display fixes and spacing polish. + ### 2.0.0.0 (2025-09-05) - Complete sources system overhaul: centralized management through `/saves/ports/rgsx/systems_list.json` (order preserved), automatic platform addition by dropping its JSON file into `/saves/ports/rgsx/games/` (auto-created if missing) — after first creation edit the generated "dossier" field so it matches your downloads folder structure. - Systems visibility filter menu (show/hide platforms with persistent hidden list in settings) diff --git a/README_FR.md b/README_FR.md index 40948e5..a0e2252 100644 --- a/README_FR.md +++ b/README_FR.md @@ -10,18 +10,23 @@ L'application prend en charge plusieurs sources comme myrient, 1fichier. Ces sou ## ✨ Fonctionnalités -- **Téléchargement de jeux** : Prise en charge des fichiers ZIP et gestion des extensions non supportées grâce au fichier `info.txt` dans chaque dossier (batocera), qui extrait automatiquement si le système ne supporte pas les archives. +- **Téléchargement de jeux** : Prise en charge des fichiers ZIP et gestion des extensions non supportées à partir du fichier `es_systems.cfg` d'EmulationStation (et des `es_systems_*.cfg` personnalisés sur Batocera). RGSX lit les extensions autorisées par système depuis ces configurations et extrait automatiquement les archives si le système ne les supporte pas. - Les téléchargements ne nécessitent aucune authentification ni compte pour la plupart. - Les systèmes notés `(1fichier)` dans le nom ne seront accessibles que si vous renseignez votre clé API 1fichier (voir plus bas). - **Historique des téléchargements** : Consultez et retéléchargez les anciens fichiers. - **Téléchargements multi-sélection** : Marquez plusieurs jeux dans la liste avec la touche associée à Vider Historique (par défaut X) pour préparer un lot. Appuyez ensuite sur Confirmer pour lancer les téléchargements en séquence. - **Personnalisation des contrôles** : Remappez les touches du clavier ou de la manette à votre convenance avec détection automatique des noms de boutons depuis EmulationStation(beta). +- **Grille des plateformes** : changez la disposition de la grille (3x3, 3x4, 4x3, 4x4) depuis le menu Affichage. +- **Afficher/Masquer plateformes non supportées** : masquage automatique des systèmes dont le dossier ROM est absent selon `es_systems.cfg`, avec un interrupteur dans le menu Affichage. +- **Images système plus intelligentes** : priorité à l’image explicite `platform_image` issue du JSON des systèmes avant les fallback `.png` ou dossier. - **Changement de taille de police** : Si vous trouvez les écritures trop petites/trop grosses, vous pouvez le changer dans le menu. - **Mode recherche** : Filtrez les jeux par nom pour une navigation rapide avec clavier virtuel sur manette. - **Support multilingue** : Interface disponible en plusieurs langues. Vous pourrez choisir la langue dans le menu. - **Gestion des erreurs** avec messages informatifs et fichier de LOG. - **Interface adaptative** : L'interface s'adapte à toutes résolutions de 800x600 à 4K (non testé au-delà de 1920x1080). - **Mise à jour automatique** : l'application doit être relancée après une mise à jour. +- **Cache des extensions supportées** : à la première utilisation, RGSX lit `es_systems.cfg` (RetroBat/Batocera) et génère `/saves/ports/rgsx/rom_extensions.json` avec les extensions autorisées par système. +- **Mise à jour automatique de la gamelist (Retrobat)** : sur Retrobat, le `gamelist.xml` Windows est mis à jour automatiquement au lancement pour afficher les images/vidéos dans EmulationStation. --- @@ -89,6 +94,13 @@ INFO : pour retrobat au premier lancement, l'application téléchargera Python d - Depuis le menu pause, accédez à l'historique, à l'aide des contrôles (l'affichage des contrôles change suivant le menu où vous êtes) ou à la reconfiguration des touches, des langues, de la taille de la police. - Vous pouvez aussi, depuis le menu, régénérer le cache de la liste des systèmes/jeux/images pour être sûr d'avoir les dernières mises à jour. +#### Menu Affichage + +- Disposition: basculez la grille des plateformes entre 3x3, 3x4, 4x3, 4x4. +- Taille de police: ajustez l’échelle du texte (accessibilité). +- Afficher plateformes non supportées: afficher/masquer les systèmes dont le dossier ROM est absent. +- Filtrer les systèmes: afficher/masquer rapidement des plateformes par nom (persistant). + --- ### Téléchargement @@ -127,6 +139,16 @@ Les logs sont enregistrés dans `roms/ports/RGSX/logs/RGSX.log` sur batocera et ## 🔄 Journal des modifications +### 2.1.0.0 (2025-09-09) +- Retrobat : mise à jour automatique de `gamelist.xml` au lancement pour afficher immédiatement les images/vidéos dans ES. +- Chargement des images systèmes : priorité à `platform_image` défini dans le JSON des systèmes. +- Détection automatique des extensions supportées via `es_systems.cfg`; génération et cache dans `/saves/ports/rgsx/rom_extensions.json`. +- Masquage automatique des plateformes non supportées (dossier ROM manquant selon `es_systems.cfg`) avec interrupteur dans le menu Affichage. +- Nouveau réglage dans Affichage pour changer la grille des plateformes (3x3, 3x4, 4x3, 4x4). +- Réorganisation du menu pause pour mettre en avant les options courantes. +- Traductions mises à jour. +- Corrections visuelles mineures et ajustements d’espacements. + ### 2.0.0.0 (2025-09-05) - Refonte complète du système de sources : gestion centralisée via `/saves/ports/rgsx/systems_list.json` (ordre conservé), ajout automatique d’une plateforme en déposant son fichier JSON dans `/saves/ports/rgsx/games/` (création si absente) — pensez ensuite à éditer le champ "dossier" généré pour qu’il corresponde à votre organisation de téléchargements. - Nouveau menu de filtrage des systèmes (afficher/masquer plateformes avec persistance dans les paramètres) @@ -205,19 +227,23 @@ RGSX/ ├── language.py # Gestion du support multilingue. ├── accessibility.py # Gestion des paramètres d'accessibilité. ├── utils.py # Fonctions utilitaires (wrap du texte, troncage etc.). -├── update_gamelist.py # Mise à jour de la liste des jeux. +├── update_gamelist.py # Mise à jour de la liste des jeux (Batocera/Knulli). +├── update_gamelist_windows.py # Spécifique Retrobat : mise à jour auto de gamelist.xml au lancement. ├── assets/ # Ressources de l'application (polices, exécutables, musique). -├── games/ # Fichiers de configuration des systèmes de jeux. -├── images/ # Images des systèmes. + ├── languages/ # Fichiers de traduction. └── logs/ └── RGSX.log # Fichier de logs. /saves/ports/RGSX/ │ +├── systems_list.json # Liste des systèmes +├── games/ # Liens des systèmes +├── images/ # Images des systèmes. ├── rgsx_settings.json # Fichier de configuration unifié (paramètres, accessibilité, langue, musique, symlinks). ├── controls.json # Fichier de mappage des contrôles (généré après le premier démarrage). ├── history.json # Base de données de l'historique de téléchargements (généré après le premier téléchargement). +├── rom_extensions.json # Généré depuis es_systems.cfg : cache des extensions autorisées par système. └── 1FichierAPI.txt # Clé API 1fichier (compte premium et + uniquement) (vide par défaut). ``` diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index cb71c62..e4d0708 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -7,12 +7,15 @@ import logging import requests import queue import datetime +import subprocess +import sys import config from display import ( init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_controls, draw_virtual_keyboard, draw_extension_warning, draw_pause_menu, draw_controls_help, draw_game_list, + draw_display_menu, draw_history_list, draw_clear_history_dialog, draw_cancel_download_dialog, draw_confirm_dialog, draw_redownload_game_cache_dialog, draw_popup, draw_gradient, THEME_COLORS @@ -47,6 +50,33 @@ except Exception as e: logger = logging.getLogger(__name__) +# Mise à jour de la gamelist Windows avant toute initialisation graphique (évite les conflits avec ES) +def _run_windows_gamelist_update(): + try: + if platform.system() != "Windows": + return + script_path = os.path.join(config.APP_FOLDER, "update_gamelist_windows.py") + if not os.path.exists(script_path): + return + logger.info("Lancement de update_gamelist_windows.py depuis __main__ (pré-init)") + exe = sys.executable or "python" + # Exécuter rapidement avec capture sortie pour journaliser tout message utile + result = subprocess.run( + [exe, script_path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=config.APP_FOLDER, + text=True, + timeout=30, + ) + logger.info(f"update_gamelist_windows.py terminé avec code {result.returncode}") + if result.stdout: + logger.debug(result.stdout.strip()) + except Exception as e: + logger.exception(f"Échec lors de l'exécution de update_gamelist_windows.py: {e}") + +_run_windows_gamelist_update() + # Initialisation de Pygame pygame.init() pygame.joystick.init() @@ -54,9 +84,56 @@ logger.debug("------------------------------------------------------------------ logger.debug("---------------------------DEBUT LOG--------------------------------") logger.debug("--------------------------------------------------------------------") - +#Récupération des noms des joysticks si pas de joystick connecté, verifier si clavier connecté +joystick_names = [pygame.joystick.Joystick(i).get_name() for i in range(pygame.joystick.get_count())] +if not joystick_names: + joystick_names = ["Clavier"] + print("Aucun joystick détecté, utilisation du clavier par défaut") + logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.") + config.joystick = False + config.keyboard = True +else: + config.joystick = True + config.keyboard = False + print(f"Joysticks détectés: {joystick_names}") + logger.debug(f"Joysticks détectés: {joystick_names}, utilisation du joystick par défaut.") + # Test des boutons du joystick + for name in joystick_names: + if "Xbox" in name or "PlayStation" in name or "Logitech" in name: + config.xbox_controller = True + logger.debug(f"Manette Xbox/PlayStation/Logitech détectée: {name}") + print(f"Manette Xbox/PlayStation/Logitech détectée: {name}") + break + elif "Nintendo" in name: + config.nintendo_controller = True + logger.debug(f"Manette Nintendo détectée: {name}") + print(f"Manette Nintendo détectée: {name}") + elif "8Bitdo" in name: + config.eightbitdo_controller = True + logger.debug(f"Manette 8Bitdo détectée: {name}") + print(f"Manette 8Bitdo détectée: {name}") + elif "Steam" in name: + config.steam_controller = True + logger.debug(f"Manette Steam détectée: {name}") + print(f"Manette Steam détectée: {name}") + elif "TRIMUI Smart Pro" in name: + config.trimui_controller = True + logger.debug(f"TRIMUI Smart Pro détectée: {name}") + print(f"TRIMUI Smart Pro détectée: {name}") + else: + config.generic_controller = True + logger.debug(f"Manette générique détectée: {name}") + print(f"Manette générique détectée: {name}") # Chargement des paramètres d'accessibilité config.accessibility_settings = load_accessibility_settings() +# Appliquer la grille d'affichage depuis les paramètres +try: + from rgsx_settings import get_display_grid + gcols, grows = get_display_grid() + config.GRID_COLS, config.GRID_ROWS = gcols, grows + logger.debug(f"Grille d'affichage initiale: {gcols}x{grows}") +except Exception as e: + logger.error(f"Erreur chargement grille d'affichage initiale: {e}") for i, scale in enumerate(config.font_scale_options): if scale == config.accessibility_settings.get("font_scale", 1.0): config.current_font_scale_index = i @@ -189,6 +266,18 @@ async def main(): current_time = pygame.time.get_ticks() + # Déclenchement d'un redémarrage planifié (permet d'afficher une popup avant) + try: + pending = getattr(config, 'pending_restart_at', 0) + if pending and pygame.time.get_ticks() >= pending: + logger.info("Redémarrage planifié déclenché") + # Clear the flag to avoid repeated triggers in case restart fails + config.pending_restart_at = 0 + from utils import restart_application + restart_application(0) + except Exception as e: + logger.error(f"Erreur lors du déclenchement du redémarrage planifié: {e}") + # Forcer redraw toutes les 100 ms dans download_progress if config.menu_state == "download_progress" and current_time - last_redraw_time >= 100: config.needs_redraw = True @@ -276,6 +365,11 @@ async def main(): if handle_accessibility_events(event): config.needs_redraw = True continue + if config.menu_state == "display_menu": + # Les événements sont gérés dans controls.handle_controls + action = handle_controls(event, sources, joystick, screen) + config.needs_redraw = True + continue if config.menu_state == "controls_help": action = handle_controls(event, sources, joystick, screen) @@ -583,7 +677,6 @@ async def main(): config.needs_redraw = True del config.download_tasks[task_id] - # Popup download_result supprimé : plus de temporisation de 3s # Affichage if config.needs_redraw: @@ -637,6 +730,8 @@ async def main(): elif config.menu_state == "accessibility_menu": from accessibility import draw_accessibility_menu draw_accessibility_menu(screen) + elif config.menu_state == "display_menu": + draw_display_menu(screen) elif config.menu_state == "language_select": from display import draw_language_menu draw_language_menu(screen) @@ -854,17 +949,22 @@ async def main(): await asyncio.sleep(0.01) pygame.mixer.music.stop() - - process_name = "emulatorLauncher.exe" - result = os.system(f"taskkill /f /im {process_name}") + result = subprocess.run(["taskkill", "/f", "/im", "emulatorLauncher.exe"]) if result == 0: - logger.debug(f"Quitté avec succès: {process_name}") + logger.debug(f"Quitté avec succès: emulatorLauncher.exe") else: logger.debug("Error en essayant de quitter emulatorlauncher.") - pygame.quit() logger.debug("Application terminée") + result2 = subprocess.run(["batocera-es-swissknife", "--emukill"]) + if result2 == 0: + logger.debug(f"Quitté avec succès") + else: + logger.debug("Error en essayant de quitter batocera-es-swissknife.") + + + if platform.system() == "Emscripten": asyncio.ensure_future(main()) else: diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index c44c525..cb5ee02 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -5,7 +5,7 @@ import platform from rgsx_settings import load_rgsx_settings, save_rgsx_settings # Version actuelle de l'application -app_version = "2.0.0.1" +app_version = "2.1.0.0" def get_operating_system(): """Renvoie le nom du système d'exploitation.""" @@ -60,17 +60,14 @@ def get_system_root(): return "/" if not OPERATING_SYSTEM == "Windows" else os.path.splitdrive(os.getcwd())[0] + os.sep + # Chemins de base SYSTEM_FOLDER = get_system_root() APP_FOLDER = os.path.join(get_application_root(), "RGSX") ROMS_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "roms") SAVE_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "saves", "ports", "rgsx") -BIOS_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))) +RETROBAT_DATA_FOLDER = os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))) -print(f"BIOS_FOLDER: {BIOS_FOLDER}") -print(f"ROMS_FOLDER: {ROMS_FOLDER}") -print(f"SAVE_FOLDER: {SAVE_FOLDER}") -print(f"RGSX APP_FOLDER: {APP_FOLDER}") # Configuration du logging @@ -80,17 +77,17 @@ log_file = os.path.join(log_dir, "RGSX.log") # Chemins de base GAMELISTXML = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "roms", "ports", "gamelist.xml") - +GAMELISTXML_WINDOWS = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(APP_FOLDER))), "roms", "windows", "gamelist.xml") #Dossier /roms/ports/rgsx UPDATE_FOLDER = os.path.join(APP_FOLDER, "update") LANGUAGES_FOLDER = os.path.join(APP_FOLDER, "languages") -JSON_EXTENSIONS = os.path.join(APP_FOLDER, "rom_extensions.json") MUSIC_FOLDER = os.path.join(APP_FOLDER, "assets", "music") #Dossier /saves/ports/rgsx IMAGES_FOLDER = os.path.join(SAVE_FOLDER, "images") GAMES_FOLDER = os.path.join(SAVE_FOLDER, "games") SOURCES_FILE = os.path.join(SAVE_FOLDER, "systems_list.json") +JSON_EXTENSIONS = os.path.join(SAVE_FOLDER, "rom_extensions.json") CONTROLS_CONFIG_PATH = os.path.join(SAVE_FOLDER, "controls.json") HISTORY_PATH = os.path.join(SAVE_FOLDER, "history.json") API_KEY_1FICHIER = os.path.join(SAVE_FOLDER, "1fichierAPI.txt") @@ -112,6 +109,27 @@ xdvdfs_download_exe = os.path.join(OTA_SERVER_URL, "xdvdfs.exe") xdvdfs_download_linux = os.path.join(OTA_SERVER_URL, "xdvdfs") +# Print des chemins pour debug +print(f"RETROBAT_DATA_FOLDER: {RETROBAT_DATA_FOLDER}") +print(f"ROMS_FOLDER: {ROMS_FOLDER}") +print(f"SAVE_FOLDER: {SAVE_FOLDER}") +print(f"RGSX APP_FOLDER: {APP_FOLDER}") +print(f"RGSX LOGS_FOLDER: {log_dir}") +print(f"RGSX SETTINGS PATH: {RGSX_SETTINGS_PATH}") +print(f"GAMELISTXML: {GAMELISTXML}") +print(f"GAMELISTXML_WINDOWS: {GAMELISTXML_WINDOWS}") +print(f"UPDATE_FOLDER: {UPDATE_FOLDER}") +print(f"LANGUAGES_FOLDER: {LANGUAGES_FOLDER}") +print(f"JSON_EXTENSIONS: {JSON_EXTENSIONS}") +print(f"MUSIC_FOLDER: {MUSIC_FOLDER}") +print(f"IMAGES_FOLDER: {IMAGES_FOLDER}") +print(f"GAMES_FOLDER: {GAMES_FOLDER}") +print(f"SOURCES_FILE: {SOURCES_FILE}") +print(f"CONTROLS_CONFIG_PATH: {CONTROLS_CONFIG_PATH}") +print(f"HISTORY_PATH: {HISTORY_PATH}") + + + # Constantes pour la répétition automatique dans pause_menu REPEAT_DELAY = 350 # Délai initial avant répétition (ms) - augmenté pour éviter les doubles actions REPEAT_INTERVAL = 120 # Intervalle entre répétitions (ms) - ajusté pour une navigation plus contrôlée diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 2a2be5a..2e48594 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -3,10 +3,11 @@ import pygame # type: ignore import config # Constantes pour la répétition automatique - importées de config.py from config import REPEAT_DELAY, REPEAT_INTERVAL, REPEAT_ACTION_DEBOUNCE -from config import CONTROLS_CONFIG_PATH , GRID_COLS, GRID_ROWS +from config import CONTROLS_CONFIG_PATH import asyncio import json import os +import sys from display import draw_validation_transition from network import download_rom, download_from_1fichier, is_1fichier_url from utils import ( @@ -32,7 +33,7 @@ VALID_STATES = [ "platform", "game", "confirm_exit", "extension_warning", "pause_menu", "controls_help", "history", "controls_mapping", "redownload_game_cache", "restart_popup", "error", "loading", "confirm_clear_history", - "language_select", "filter_platforms" + "language_select", "filter_platforms", "display_menu" ] def validate_menu_state(state): @@ -157,19 +158,19 @@ def handle_controls(event, sources, joystick, screen): #Plateformes elif config.menu_state == "platform": - systems_per_page = GRID_COLS * GRID_ROWS + systems_per_page = config.GRID_COLS * config.GRID_ROWS max_index = min(systems_per_page, len(config.platforms) - config.current_page * systems_per_page) - 1 current_grid_index = config.selected_platform - config.current_page * systems_per_page - row = current_grid_index // GRID_COLS - col = current_grid_index % GRID_COLS + row = current_grid_index // config.GRID_COLS + col = current_grid_index % config.GRID_COLS # Espace réservé pour des fonctions helper si nécessaire if is_input_matched(event, "down"): # Navigation vers le bas avec gestion des limites de page - if current_grid_index + GRID_COLS <= max_index: + if current_grid_index + config.GRID_COLS <= max_index: # Déplacement normal vers le bas - config.selected_platform += GRID_COLS + config.selected_platform += config.GRID_COLS update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else @@ -178,7 +179,7 @@ def handle_controls(event, sources, joystick, screen): # Passage à la page suivante si on est en bas de la grille config.current_page += 1 new_row = 0 # Première ligne de la nouvelle page - config.selected_platform = config.current_page * systems_per_page + new_row * GRID_COLS + col + config.selected_platform = config.current_page * systems_per_page + new_row * config.GRID_COLS + col if config.selected_platform >= len(config.platforms): config.selected_platform = len(config.platforms) - 1 update_key_state("down", True, event.type, event.key if event.type == pygame.KEYDOWN else @@ -187,9 +188,9 @@ def handle_controls(event, sources, joystick, screen): event.value) elif is_input_matched(event, "up"): # Navigation vers le haut avec gestion des limites de page - if current_grid_index - GRID_COLS >= 0: + if current_grid_index - config.GRID_COLS >= 0: # Déplacement normal vers le haut - config.selected_platform -= GRID_COLS + config.selected_platform -= config.GRID_COLS update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else @@ -197,8 +198,8 @@ def handle_controls(event, sources, joystick, screen): elif config.current_page > 0: # Passage à la page précédente si on est en haut de la grille config.current_page -= 1 - new_row = GRID_ROWS - 1 # Dernière ligne de la page précédente - config.selected_platform = config.current_page * systems_per_page + new_row * GRID_COLS + col + new_row = config.GRID_ROWS - 1 # Dernière ligne de la page précédente + config.selected_platform = config.current_page * systems_per_page + new_row * config.GRID_COLS + col if config.selected_platform >= len(config.platforms): config.selected_platform = len(config.platforms) - 1 update_key_state("up", True, event.type, event.key if event.type == pygame.KEYDOWN else @@ -216,7 +217,7 @@ def handle_controls(event, sources, joystick, screen): elif config.current_page > 0: # Passage à la page précédente si on est à la première colonne config.current_page -= 1 - config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + (GRID_COLS - 1) + config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS + (config.GRID_COLS - 1) if config.selected_platform >= len(config.platforms): config.selected_platform = len(config.platforms) - 1 update_key_state("left", True, event.type, event.key if event.type == pygame.KEYDOWN else @@ -224,7 +225,7 @@ def handle_controls(event, sources, joystick, screen): (event.axis, event.value) if event.type == pygame.JOYAXISMOTION else event.value) elif is_input_matched(event, "right"): - if col < GRID_COLS - 1 and current_grid_index < max_index: + if col < config.GRID_COLS - 1 and current_grid_index < max_index: # Déplacement normal vers la droite config.selected_platform += 1 update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else @@ -234,7 +235,7 @@ def handle_controls(event, sources, joystick, screen): elif (config.current_page + 1) * systems_per_page < len(config.platforms): # Passage à la page suivante si on est à la dernière colonne config.current_page += 1 - config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS if config.selected_platform >= len(config.platforms): config.selected_platform = len(config.platforms) - 1 update_key_state("right", True, event.type, event.key if event.type == pygame.KEYDOWN else @@ -245,7 +246,7 @@ def handle_controls(event, sources, joystick, screen): # Navigation rapide vers la page suivante if (config.current_page + 1) * systems_per_page < len(config.platforms): config.current_page += 1 - config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + col + config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS + col if config.selected_platform >= len(config.platforms): config.selected_platform = len(config.platforms) - 1 # Réinitialiser la répétition pour éviter des comportements inattendus @@ -258,7 +259,7 @@ def handle_controls(event, sources, joystick, screen): # Navigation rapide vers la page précédente if config.current_page > 0: config.current_page -= 1 - config.selected_platform = config.current_page * systems_per_page + row * GRID_COLS + col + config.selected_platform = config.current_page * systems_per_page + row * config.GRID_COLS + col if config.selected_platform >= len(config.platforms): config.selected_platform = len(config.platforms) - 1 # Réinitialiser la répétition pour éviter des comportements inattendus @@ -514,8 +515,8 @@ def handle_controls(event, sources, joystick, screen): platform, load_extensions_json() ) - ext = os.path.splitext(url)[1].lower() - if not is_supported and ext not in ARCHIVE_EXTENSIONS: + zip_ok = bool(config.pending_download[3]) # True only if archive and system known + if not is_supported and not zip_ok: # Stocker comme pending sans dupliquer l'entrée config.batch_pending_game = (url, platform, game_name, config.pending_download[3]) config.previous_menu_state = config.menu_state @@ -588,8 +589,8 @@ def handle_controls(event, sources, joystick, screen): platform, load_extensions_json() ) - ext = os.path.splitext(url)[1].lower() - if not is_supported and ext not in ARCHIVE_EXTENSIONS: + zip_ok = bool(config.pending_download[3]) + if not is_supported and not zip_ok: config.previous_menu_state = config.menu_state config.menu_state = "extension_warning" config.extension_confirm_selection = 0 @@ -621,8 +622,8 @@ def handle_controls(event, sources, joystick, screen): platform, load_extensions_json() ) - ext = os.path.splitext(url)[1].lower() - if not is_supported and ext not in ARCHIVE_EXTENSIONS: + zip_ok = bool(config.pending_download[3]) + if not is_supported and not zip_ok: config.previous_menu_state = config.menu_state config.menu_state = "extension_warning" config.extension_confirm_selection = 0 @@ -669,7 +670,7 @@ def handle_controls(event, sources, joystick, screen): config.menu_state = "error" config.error_message = _( "error_api_key" - ).format(os.join(config.SAVE_FOLDER,"1fichierAPI.txt")) + ).format(os.path.join(config.SAVE_FOLDER,"1fichierAPI.txt")) config.history[-1]["status"] = "Erreur" config.history[-1]["progress"] = 0 config.history[-1]["message"] = "Erreur API : Clé API 1fichier absente" @@ -711,8 +712,8 @@ def handle_controls(event, sources, joystick, screen): if not config.pending_download: continue is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json()) - ext = os.path.splitext(url)[1].lower() - if not is_supported and ext not in ARCHIVE_EXTENSIONS: + zip_ok = bool(config.pending_download[3]) + if not is_supported and not zip_ok: config.batch_pending_game = (url, platform, game_name, config.pending_download[3]) config.previous_menu_state = config.menu_state config.menu_state = "extension_warning" @@ -770,8 +771,8 @@ def handle_controls(event, sources, joystick, screen): if not config.pending_download: continue is_supported = is_extension_supported(sanitize_filename(game_name), platform, load_extensions_json()) - ext = os.path.splitext(url)[1].lower() - if not is_supported and ext not in ARCHIVE_EXTENSIONS: + zip_ok = bool(config.pending_download[3]) + if not is_supported and not zip_ok: config.batch_pending_game = (url, platform, game_name, config.pending_download[3]) config.previous_menu_state = config.menu_state config.menu_state = "extension_warning" @@ -863,7 +864,13 @@ def handle_controls(event, sources, joystick, screen): config.pending_download = check_extension_before_download(game[1], platform, game_name) if config.pending_download: url, platform, game_name, is_zip_non_supported = config.pending_download - if is_zip_non_supported and os.path.splitext(url)[1].lower() not in ARCHIVE_EXTENSIONS: + # Recalculer le support exact et décider via le flag is_zip_non_supported + is_supported = is_extension_supported( + sanitize_filename(game_name), + platform, + load_extensions_json() + ) + if not is_supported and not is_zip_non_supported: config.previous_menu_state = config.menu_state config.menu_state = "extension_warning" config.extension_confirm_selection = 0 @@ -990,12 +997,17 @@ def handle_controls(event, sources, joystick, screen): # Menu pause elif config.menu_state == "pause_menu": #logger.debug(f"État pause_menu, selected_option={config.selected_option}, événement={event.type}, valeur={getattr(event, 'value', None)}") - if is_input_matched(event, "up"): + # Start toggles back to previous state when already in pause + if is_input_matched(event, "start"): + config.menu_state = validate_menu_state(config.previous_menu_state) + config.needs_redraw = True + logger.debug(f"Start: retour à {config.menu_state} depuis pause_menu") + elif is_input_matched(event, "up"): config.selected_option = max(0, config.selected_option - 1) config.needs_redraw = True elif is_input_matched(event, "down"): # Nombre d'options dynamique (inclut éventuellement l'option source des jeux) - total = getattr(config, 'pause_menu_total_options', 9) # fallback 9 + total = getattr(config, 'pause_menu_total_options', 11) # fallback 11 (Restart added) config.selected_option = min(total - 1, config.selected_option + 1) config.needs_redraw = True elif is_input_matched(event, "confirm"): @@ -1031,18 +1043,14 @@ def handle_controls(event, sources, joystick, screen): config.selected_language_index = 0 config.needs_redraw = True logger.debug(f"Passage à language_select depuis pause_menu") - elif config.selected_option == 4: # Accessibility + elif config.selected_option == 4: # Display config.previous_menu_state = validate_menu_state(config.previous_menu_state) - config.menu_state = "accessibility_menu" + config.menu_state = "display_menu" + if not hasattr(config, 'display_menu_selection'): + config.display_menu_selection = 0 config.needs_redraw = True - logger.debug("Passage au menu accessibilité") - elif config.selected_option == 5: # Filter platforms - # Ne pas écraser previous_menu_state; il référence l'état avant l'ouverture du pause menu - config.menu_state = "filter_platforms" - config.selected_filter_index = 0 - config.filter_platforms_scroll_offset = 0 - config.needs_redraw = True - elif config.selected_option == 6: # Source toggle (index shifted by new option) + logger.debug("Passage au menu affichage") + elif config.selected_option == 5: # Source toggle (index shifted by removal of filter) try: from rgsx_settings import get_sources_mode, set_sources_mode current_mode = get_sources_mode() @@ -1058,13 +1066,13 @@ def handle_controls(event, sources, joystick, screen): logger.info(f"Changement du mode des sources vers {new_mode}") except Exception as e: logger.error(f"Erreur changement mode sources: {e}") - elif config.selected_option == 7: # Redownload game cache + elif config.selected_option == 6: # Redownload game cache config.previous_menu_state = validate_menu_state(config.previous_menu_state) config.menu_state = "redownload_game_cache" config.redownload_confirm_selection = 0 config.needs_redraw = True logger.debug(f"Passage à redownload_game_cache depuis pause_menu") - elif config.selected_option == 8: # Music toggle + elif config.selected_option == 7: # Music toggle config.music_enabled = not config.music_enabled save_music_config() if config.music_enabled: @@ -1076,7 +1084,7 @@ def handle_controls(event, sources, joystick, screen): pygame.mixer.music.stop() config.needs_redraw = True logger.info(f"Musique {'activée' if config.music_enabled else 'désactivée'} via menu pause") - elif config.selected_option == 9: # Symlink option + elif config.selected_option == 8: # Symlink option from rgsx_settings import set_symlink_option, get_symlink_option current_status = get_symlink_option() success, message = set_symlink_option(not current_status) @@ -1084,6 +1092,9 @@ def handle_controls(event, sources, joystick, screen): config.popup_timer = 3000 if success else 5000 config.needs_redraw = True logger.info(f"Symlink option {'activée' if not current_status else 'désactivée'} via menu pause") + elif config.selected_option == 9: # Restart + from utils import restart_application + restart_application(2000) elif config.selected_option == 10: # Quit config.previous_menu_state = validate_menu_state(config.previous_menu_state) config.menu_state = "confirm_exit" @@ -1102,6 +1113,88 @@ def handle_controls(event, sources, joystick, screen): config.needs_redraw = True logger.debug("Retour au menu pause depuis controls_help") + # Menu Affichage (layout, police, unsupported) + elif config.menu_state == "display_menu": + sel = getattr(config, 'display_menu_selection', 0) + if is_input_matched(event, "up"): + config.display_menu_selection = (sel - 1) % 4 + config.needs_redraw = True + elif is_input_matched(event, "down"): + config.display_menu_selection = (sel + 1) % 4 + config.needs_redraw = True + elif is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm"): + sel = getattr(config, 'display_menu_selection', 0) + # 0: layout change + if sel == 0 and (is_input_matched(event, "left") or is_input_matched(event, "right")): + layouts = [(3,3),(3,4),(4,3),(4,4)] + try: + idx = layouts.index((config.GRID_COLS, config.GRID_ROWS)) + except ValueError: + idx = 1 + idx = (idx - 1) % len(layouts) if is_input_matched(event, "left") else (idx + 1) % len(layouts) + new_cols, new_rows = layouts[idx] + try: + from rgsx_settings import set_display_grid + set_display_grid(new_cols, new_rows) + except Exception as e: + logger.error(f"Erreur set_display_grid: {e}") + config.GRID_COLS = new_cols + config.GRID_ROWS = new_rows + config.needs_redraw = True + # Redémarrage automatique pour appliquer proprement la modification de layout + try: + from utils import restart_application + # Montrer brièvement l'info puis redémarrer + config.menu_state = "restart_popup" + config.popup_message = _("popup_restarting") + config.popup_timer = 2000 + restart_application(2000) + except Exception as e: + logger.error(f"Erreur lors du redémarrage après changement de layout: {e}") + # 1: font size adjust + elif sel == 1 and (is_input_matched(event, "left") or is_input_matched(event, "right")): + from accessibility import save_accessibility_settings + opts = getattr(config, 'font_scale_options', [0.75, 1.0, 1.25, 1.5, 1.75]) + idx = getattr(config, 'current_font_scale_index', 1) + idx = max(0, idx - 1) if is_input_matched(event, "left") else min(len(opts)-1, idx + 1) + if idx != getattr(config, 'current_font_scale_index', 1): + config.current_font_scale_index = idx + scale = opts[idx] + config.accessibility_settings["font_scale"] = scale + try: + save_accessibility_settings(config.accessibility_settings) + except Exception as e: + logger.error(f"Erreur sauvegarde accessibilité: {e}") + try: + config.init_font() + except Exception as e: + logger.error(f"Erreur init polices: {e}") + config.needs_redraw = True + # 2: toggle unsupported + elif sel == 2 and (is_input_matched(event, "left") or is_input_matched(event, "right") or is_input_matched(event, "confirm")): + try: + from rgsx_settings import get_show_unsupported_platforms, set_show_unsupported_platforms + current = get_show_unsupported_platforms() + new_val = set_show_unsupported_platforms(not current) + from utils import load_sources + load_sources() + config.popup_message = _("menu_show_unsupported_enabled") if new_val else _("menu_show_unsupported_disabled") + config.popup_timer = 3000 + config.needs_redraw = True + except Exception as e: + logger.error(f"Erreur toggle unsupported: {e}") + # 3: open filter platforms menu + elif sel == 3 and (is_input_matched(event, "confirm") or is_input_matched(event, "right")): + # Remember return target so the filter menu can go back to display + config.filter_return_to = "display_menu" + config.menu_state = "filter_platforms" + config.selected_filter_index = 0 + config.filter_platforms_scroll_offset = 0 + config.needs_redraw = True + elif is_input_matched(event, "cancel"): + config.menu_state = "pause_menu" + config.needs_redraw = True + # Remap controls elif config.menu_state == "controls_mapping": if is_input_matched(event, "cancel"): @@ -1137,9 +1230,12 @@ def handle_controls(event, sources, joystick, screen): logger.debug("Dossier images supprimé avec succès") config.menu_state = "restart_popup" config.popup_message = _("popup_redownload_success") - config.popup_timer = 5000 # 5 secondes + config.popup_timer = 2000 # bref message config.needs_redraw = True logger.debug("Passage à restart_popup") + # Redémarrage automatique + from utils import restart_application + restart_application(2000) except Exception as e: logger.error(f"Erreur lors de la suppression du fichier sources.json ou dossiers: {e}") config.menu_state = "error" @@ -1150,9 +1246,11 @@ def handle_controls(event, sources, joystick, screen): logger.debug("Fichier sources.json non trouvé, passage à restart_popup") config.menu_state = "restart_popup" config.popup_message = _("popup_no_cache") - config.popup_timer = 5000 # 5 secondes + config.popup_timer = 2000 config.needs_redraw = True logger.debug("Passage à restart_popup") + from utils import restart_application + restart_application(2000) else: # Non config.menu_state = validate_menu_state(config.previous_menu_state) config.needs_redraw = True @@ -1213,7 +1311,7 @@ def handle_controls(event, sources, joystick, screen): config.needs_redraw = True logger.debug("Annulation de la sélection de langue, retour au menu pause") - # Menu filtre plateformes + # Menu filtre plateformes elif config.menu_state == "filter_platforms": total_items = len(config.filter_platforms_selection) action_buttons = 4 @@ -1271,7 +1369,7 @@ def handle_controls(event, sources, joystick, screen): load_sources() # Recalibrer la sélection et la page courante si elles dépassent la nouvelle liste visible try: - systems_per_page = GRID_COLS * GRID_ROWS + systems_per_page = config.GRID_COLS * config.GRID_ROWS if config.current_page * systems_per_page >= len(config.platforms): config.current_page = 0 if config.selected_platform >= len(config.platforms): @@ -1281,12 +1379,32 @@ def handle_controls(event, sources, joystick, screen): config.current_page = 0 config.selected_platform = 0 config.filter_platforms_dirty = False - config.menu_state = "pause_menu" + # Return either to display menu or pause menu depending on origin + target = getattr(config, 'filter_return_to', 'pause_menu') + config.menu_state = target + if target == 'display_menu': + # reset display selection to the Filter item for convenience + config.display_menu_selection = 3 + else: + config.selected_option = 5 # keep pointer on Filter in pause menu + config.filter_return_to = None elif btn_idx == 3: # back - config.menu_state = "pause_menu" + target = getattr(config, 'filter_return_to', 'pause_menu') + config.menu_state = target + if target == 'display_menu': + config.display_menu_selection = 3 + else: + config.selected_option = 5 + config.filter_return_to = None config.needs_redraw = True elif is_input_matched(event, "cancel"): - config.menu_state = "pause_menu" + target = getattr(config, 'filter_return_to', 'pause_menu') + config.menu_state = target + if target == 'display_menu': + config.display_menu_selection = 3 + else: + config.selected_option = 5 + config.filter_return_to = None config.needs_redraw = True diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index 83c86d5..aa8e71b 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -27,6 +27,8 @@ THEME_COLORS = { "button_hover": (255, 0, 255, 220), # Rose # Générique "text": (255, 255, 255), # blanc + # Texte sélectionné (alias pour compatibilité) + "text_selected": (0, 255, 0), # utilise le même vert que fond_lignes # Erreur "error_text": (255, 0, 0), # rouge # Avertissement @@ -439,8 +441,8 @@ def draw_platform_grid(screen): margin_right = int(config.screen_width * 0.026) margin_top = int(config.screen_height * 0.140) margin_bottom = int(config.screen_height * 0.0648) - num_cols = 3 - num_rows = 4 + num_cols = getattr(config, 'GRID_COLS', 3) + num_rows = getattr(config, 'GRID_ROWS', 4) systems_per_page = num_cols * num_rows available_width = config.screen_width - margin_left - margin_right @@ -820,12 +822,22 @@ def draw_history_list(screen): screen.blit(text_surface, text_rect) return - available_height = config.screen_height - title_height - extra_margin_top - extra_margin_bottom - 2 * margin_top_bottom - items_per_page = available_height // line_height + # Espace visible garanti entre le titre et la liste, et au-dessus du footer + top_gap = 20 + bottom_reserved = 70 # réserve pour le footer (barre des contrôles) + marge visuelle (réduit) + + # Positionner la liste juste après le titre, avec un espace dédié + # Utiliser le rectangle du titre déjà dessiné pour une meilleure précision + title_bottom = title_rect_inflated.bottom + rect_y = title_bottom + top_gap + + # Calculer l'espace disponible en bas en réservant une zone pour le footer + available_height = max(0, config.screen_height - rect_y - bottom_reserved) + # Déterminer le nombre d'éléments par page en tenant compte de l'en-tête et des marges internes + items_per_page = max(1, (available_height - header_height - 2 * margin_top_bottom) // line_height) rect_height = header_height + items_per_page * line_height + 2 * margin_top_bottom rect_x = (config.screen_width - rect_width) // 2 - rect_y = title_height + extra_margin_top + (config.screen_height - title_height - extra_margin_top - extra_margin_bottom - rect_height) // 2 config.history_scroll_offset = max(0, min(config.history_scroll_offset, max(0, len(history) - items_per_page))) if config.current_history_item < config.history_scroll_offset: @@ -1090,9 +1102,6 @@ def draw_progress_screen(screen): # Limiter le pourcentage entre 0 et 100 pour l'affichage de la barre progress_width = int(bar_width * (min(100, max(0, progress_percent)) / 100)) - -## Ancienne fonction draw_popup_result_download supprimée (popup de fin de téléchargement retiré) - # Écran avertissement extension non supportée téléchargement def draw_extension_warning(screen): """Affiche un avertissement pour une extension non reconnue ou un fichier ZIP.""" @@ -1103,7 +1112,7 @@ def draw_extension_warning(screen): game_name = "Inconnu" else: url, platform, game_name, is_zip_non_supported = config.pending_download - logger.debug(f"config.pending_download: url={url}, platform={platform}, game_name={game_name}, is_zip_non_supported={is_zip_non_supported}") + # Log réduit: pas de détail verbeux ici is_zip = is_zip_non_supported if not game_name: game_name = "Inconnu" @@ -1116,7 +1125,6 @@ def draw_extension_warning(screen): max_width = config.screen_width - 80 lines = wrap_text(message, config.font, max_width) - logger.debug(f"Lignes générées : {lines}") try: line_height = config.font.get_height() + 5 @@ -1217,9 +1225,9 @@ def draw_language_menu(screen): button_height = 60 button_width = 300 button_spacing = 20 - - total_height = len(available_languages) * (button_height + button_spacing) - button_spacing - start_y = (config.screen_height - total_height) // 2 + + # Démarrer la liste juste sous le titre avec le même écart que les boutons + start_y = title_bg_rect.bottom + button_spacing for i, lang_code in enumerate(available_languages): # Obtenir le nom de la langue @@ -1245,10 +1253,69 @@ def draw_language_menu(screen): instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 50)) screen.blit(instruction_surface, instruction_rect) +def draw_display_menu(screen): + """Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés).""" + screen.blit(OVERLAY, (0, 0)) + + # États actuels + layout_str = f"{getattr(config, 'GRID_COLS', 3)}x{getattr(config, 'GRID_ROWS', 4)}" + font_scale = config.accessibility_settings.get("font_scale", 1.0) + from rgsx_settings import get_show_unsupported_platforms + show_unsupported = get_show_unsupported_platforms() + + # Libellés + options = [ + f"{_('display_layout')}: {layout_str}", + _("accessibility_font_size").format(f"{font_scale:.1f}"), + _("menu_show_unsupported_on") if show_unsupported else _("menu_show_unsupported_off"), + _("menu_filter_platforms"), + ] + + selected = getattr(config, 'display_menu_selection', 0) + + # Dimensions du cadre (cohérent avec le menu pause) + title_text = _("menu_display") + title_surface = config.title_font.render(title_text, True, THEME_COLORS["text"]) + title_height = title_surface.get_height() + 10 + menu_width = int(config.screen_width * 0.7) + button_height = int(config.screen_height * 0.0463) + margin_top_bottom = 20 + vertical_spacing = 10 + menu_height = title_height + len(options) * (button_height + vertical_spacing) + 2 * margin_top_bottom + menu_x = (config.screen_width - menu_width) // 2 + menu_y = (config.screen_height - menu_height) // 2 + + # Cadre + pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=12) + pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=12) + + # Titre centré dans le cadre + title_rect = title_surface.get_rect(center=(config.screen_width // 2, menu_y + margin_top_bottom + title_surface.get_height() // 2)) + screen.blit(title_surface, title_rect) + + # Boutons des options + for i, option_text in enumerate(options): + y = menu_y + margin_top_bottom + title_height + i * (button_height + vertical_spacing) + draw_stylized_button( + screen, + option_text, + menu_x + 20, + y, + menu_width - 40, + button_height, + selected=(i == selected) + ) + + # Aide en bas de l'écran + instruction_text = _("language_select_instruction") + instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"]) + instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, config.screen_height - 50)) + screen.blit(instruction_surface, instruction_rect) + def draw_pause_menu(screen, selected_option): """Dessine le menu pause avec un style moderne.""" screen.blit(OVERLAY, (0, 0)) - from rgsx_settings import get_symlink_option, get_sources_mode + from rgsx_settings import get_symlink_option, get_sources_mode, get_show_unsupported_platforms mode = get_sources_mode() source_label = _("games_source_rgsx") if mode == "rgsx" else _("games_source_custom") if config.music_enabled: @@ -1262,13 +1329,13 @@ def draw_pause_menu(screen, selected_option): _("menu_remap_controls"), # 1 _("menu_history"), # 2 _("menu_language"), # 3 - _("menu_accessibility"), # 4 - _("menu_filter_platforms"), # 5 new filter option - f"{_('menu_games_source_prefix')}: {source_label}", # 5 + _("menu_display"), # 4 new merged display menu + f"{_('menu_games_source_prefix')}: {source_label}", # 5 (shifted left) _("menu_redownload_cache"), # 6 music_option, # 7 symlink_option, # 8 - _("menu_quit") # 9 + _("menu_restart"), # 9 (new) + _("menu_quit") # 10 ] menu_width = int(config.screen_width * 0.8) line_height = config.font.get_height() + 10 diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index 068479e..0e125be 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -57,7 +57,7 @@ "download_canceled": "Download vom Benutzer abgebrochen.", "extension_warning_zip": "Die Datei '{0}' ist ein Archiv und Batocera unterstützt keine Archive für dieses System. Die automatische Extraktion der Datei erfolgt nach dem Download, fortfahren?", - "extension_warning_unsupported": "Die Erweiterung der Datei '{0}' wird laut der Datei info.txt von Batocera nicht unterstützt. Möchtest du fortfahren?", + "extension_warning_unsupported": "Die Dateierweiterung für '{0}' wird laut der Konfiguration es_systems.cfg von Batocera nicht unterstützt. Möchtest du fortfahren?", "confirm_exit": "Anwendung beenden?", "confirm_clear_history": "Verlauf löschen?", @@ -76,10 +76,13 @@ "menu_history": "Verlauf", "menu_language": "Sprache", "menu_accessibility": "Barrierefreiheit", + "menu_display": "Anzeige", + "display_layout": "Anzeigelayout", "menu_redownload_cache": "Spieleliste aktualisieren", "menu_music_toggle": "Musik ein/aus", "menu_music_enabled": "Musik aktiviert: {0}", "menu_music_disabled": "Musik deaktiviert", + "menu_restart": "Neustart", "menu_filter_platforms": "Systeme filtern", "filter_platforms_title": "Systemsichtbarkeit", "filter_all": "Alle anzeigen", @@ -88,11 +91,16 @@ "filter_back": "Zurück", "filter_platforms_info": "Sichtbar: {0} | Versteckt: {1} / Gesamt: {2}", "filter_unsaved_warning": "Ungespeicherte Änderungen", + "menu_show_unsupported_on": "Nicht unterstützte Systeme anzeigen: Ja", + "menu_show_unsupported_off": "Nicht unterstützte Systeme anzeigen: Nein", + "menu_show_unsupported_enabled": "Sichtbarkeit nicht unterstützter Systeme aktiviert", + "menu_show_unsupported_disabled": "Sichtbarkeit nicht unterstützter Systeme deaktiviert", "menu_quit": "Beenden", "button_yes": "Ja", "button_no": "Nein", "button_OK": "OK", + "popup_restarting": "Neustart...", "controls_hold_message": "3 Sekunden halten für: '{0}'", "controls_skip_message": "Drücke Esc, um zu überspringen (nur PC)", diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index 368b907..e0b0a59 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -57,7 +57,7 @@ "download_canceled": "Download canceled by user.", "extension_warning_zip": "The file '{0}' is an archive and Batocera does not support archives for this system. Automatic extraction will occur after download, continue?", - "extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the info.txt file. Do you want to continue?", + "extension_warning_unsupported": "The file extension for '{0}' is not supported by Batocera according to the es_systems.cfg configuration. Do you want to continue?", "confirm_exit": "Exit application?", "confirm_clear_history": "Clear history?", @@ -76,10 +76,13 @@ "menu_history": "History", "menu_language": "Language", "menu_accessibility": "Accessibility", + "menu_display": "Display", + "display_layout": "Display layout", "menu_redownload_cache": "Update games list", "menu_music_toggle": "Toggle music", "menu_music_enabled": "Music enabled: {0}", "menu_music_disabled": "Music disabled", + "menu_restart": "Restart", "menu_filter_platforms": "Filter systems", "filter_platforms_title": "Systems visibility", "filter_all": "Show all", @@ -88,11 +91,16 @@ "filter_back": "Back", "filter_platforms_info": "Visible: {0} | Hidden: {1} / Total: {2}", "filter_unsaved_warning": "Unsaved changes", + "menu_show_unsupported_on": "Show unsupported systems: Yes", + "menu_show_unsupported_off": "Show unsupported systems: No", + "menu_show_unsupported_enabled": "Unsupported systems visibility enabled", + "menu_show_unsupported_disabled": "Unsupported systems visibility disabled", "menu_quit": "Quit", "button_yes": "Yes", "button_no": "No", "button_OK": "OK", + "popup_restarting": "Restarting...", "controls_hold_message": "Hold for 3s for: '{0}'", "controls_skip_message": "Press Esc to skip (PC only)", diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index 7650a8d..0d02865 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -58,7 +58,7 @@ "download_canceled": "Descarga cancelada por el usuario.", "extension_warning_zip": "El archivo '{0}' es un archivo comprimido y Batocera no soporta archivos comprimidos para este sistema. La extracción automática del archivo se realizará después de la descarga, ¿continuar?", - "extension_warning_unsupported": "La extensión del archivo '{0}' no es soportada por Batocera según el archivo info.txt. ¿Deseas continuar?", + "extension_warning_unsupported": "La extensión del archivo '{0}' no está soportada por Batocera según la configuración es_systems.cfg. ¿Deseas continuar?", "confirm_exit": "¿Salir de la aplicación?", "confirm_clear_history": "¿Vaciar el historial?", @@ -77,10 +77,13 @@ "menu_history": "Historial", "menu_language": "Idioma", "menu_accessibility": "Accesibilidad", + "menu_display": "Pantalla", + "display_layout": "Distribución", "menu_redownload_cache": "Actualizar lista de juegos", "menu_music_toggle": "Activar/Desactivar música", "menu_music_enabled": "Música activada: {0}", "menu_music_disabled": "Música desactivada", + "menu_restart": "Reiniciar", "menu_filter_platforms": "Filtrar sistemas", "filter_platforms_title": "Visibilidad de sistemas", "filter_all": "Mostrar todo", @@ -89,11 +92,16 @@ "filter_back": "Volver", "filter_platforms_info": "Visibles: {0} | Ocultos: {1} / Total: {2}", "filter_unsaved_warning": "Cambios no guardados", + "menu_show_unsupported_on": "Mostrar sistemas no soportados: Sí", + "menu_show_unsupported_off": "Mostrar sistemas no soportados: No", + "menu_show_unsupported_enabled": "Visibilidad de sistemas no soportados activada", + "menu_show_unsupported_disabled": "Visibilidad de sistemas no soportados desactivada", "menu_quit": "Salir", "button_yes": "Sí", "button_no": "No", "button_OK": "OK", + "popup_restarting": "Reiniciando...", "controls_hold_message": "Mantén presionado durante 3s para: '{0}'", "controls_skip_message": "Presiona Esc para omitir (solo PC)", diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index 5261cd2..f0fe2df 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -54,7 +54,7 @@ "download_canceled": "Téléchargement annulé par l'utilisateur.", "extension_warning_zip": "Le fichier '{0}' est une archive et Batocera ne prend pas en charge les archives pour ce système. L'extraction automatique du fichier aura lieu après le téléchargement, continuer ?", - "extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après le fichier info.txt. Voulez-vous continuer ?", + "extension_warning_unsupported": "L'extension du fichier '{0}' n'est pas supportée par Batocera d'après la configuration es_systems.cfg. Voulez-vous continuer ?", "confirm_exit": "Quitter l'application ?", "confirm_clear_history": "Vider l'historique ?", @@ -73,11 +73,14 @@ "menu_history": "Historique", "menu_language": "Langue", "menu_accessibility": "Accessibilité", + "menu_display": "Affichage", + "display_layout": "Disposition", "menu_redownload_cache": "Mettre à jour la liste des jeux", "menu_quit": "Quitter", "menu_music_toggle": "Activer/Désactiver la musique", "menu_music_enabled": "Musique activée : {0}", "menu_music_disabled": "Musique désactivée", + "menu_restart": "Redémarrer", "menu_filter_platforms": "Filtrer les systèmes", "filter_platforms_title": "Affichage des systèmes", "filter_all": "Tout afficher", @@ -86,10 +89,15 @@ "filter_back": "Retour", "filter_platforms_info": "Visibles: {0} | Masqués: {1} / Total: {2}", "filter_unsaved_warning": "Modifications non sauvegardées", + "menu_show_unsupported_on": "Afficher systèmes non supportés : Oui", + "menu_show_unsupported_off": "Afficher systèmes non supportés : Non", + "menu_show_unsupported_enabled": "Affichage systèmes non supportés activé", + "menu_show_unsupported_disabled": "Affichage systèmes non supportés désactivé", "button_yes": "Oui", "button_no": "Non", "button_OK": "OK", + "popup_restarting": "Redémarrage...", "controls_hold_message": "Maintenez pendant 3s pour : '{0}'", "controls_skip_message": "Appuyez sur Échap pour passer(Pc only)", diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index dee2e74..db45a0f 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -57,7 +57,7 @@ "download_canceled": "Download cancelado pelo usuário.", "extension_warning_zip": "O arquivo '{0}' é um arquivo compactado e o Batocera não suporta arquivos compactados para este sistema. A extração automática ocorrerá após o download, continuar?", - "extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo o arquivo info.txt. Deseja continuar?", + "extension_warning_unsupported": "A extensão do arquivo '{0}' não é suportada pelo Batocera segundo a configuração es_systems.cfg. Deseja continuar?", "confirm_exit": "Sair da aplicação?", "confirm_clear_history": "Limpar histórico?", @@ -76,10 +76,13 @@ "menu_history": "Histórico", "menu_language": "Idioma", "menu_accessibility": "Acessibilidade", + "menu_display": "Exibição", + "display_layout": "Layout de exibição", "menu_redownload_cache": "Atualizar lista de jogos", "menu_music_toggle": "Ativar/Desativar música", "menu_music_enabled": "Música ativada: {0}", "menu_music_disabled": "Música desativada", + "menu_restart": "Reiniciar", "menu_filter_platforms": "Filtrar sistemas", "filter_platforms_title": "Visibilidade dos sistemas", "filter_all": "Mostrar todos", @@ -88,11 +91,16 @@ "filter_back": "Voltar", "filter_platforms_info": "Visíveis: {0} | Ocultos: {1} / Total: {2}", "filter_unsaved_warning": "Alterações não salvas", + "menu_show_unsupported_on": "Mostrar sistemas não suportados: Sim", + "menu_show_unsupported_off": "Mostrar sistemas não suportados: Não", + "menu_show_unsupported_enabled": "Visibilidade de sistemas não suportados ativada", + "menu_show_unsupported_disabled": "Visibilidade de sistemas não suportados desativada", "menu_quit": "Sair", "button_yes": "Sim", "button_no": "Não", "button_OK": "OK", + "popup_restarting": "Reiniciando...", "controls_hold_message": "Mantenha pressionado por 3s para: '{0}'", "controls_skip_message": "Pressione Esc para ignorar (somente PC)", diff --git a/ports/RGSX/network.py b/ports/RGSX/network.py index baf6392..c2dcf0c 100644 --- a/ports/RGSX/network.py +++ b/ports/RGSX/network.py @@ -183,17 +183,21 @@ async def check_for_updates(): config.needs_redraw = True logger.debug("Mise à jour terminée avec succès") - # Configurer la popup pour afficher le message de succès - config.menu_state = "update_result" - # Message succès de mise à jour + # Configurer la popup puis redémarrer automatiquement + config.menu_state = "restart_popup" config.update_result_message = _("network_update_success").format(latest_version) - # Utiliser aussi le système générique de popup pour affichage config.popup_message = config.update_result_message - config.popup_timer = 5000 # 5 secondes + config.popup_timer = 2000 config.update_result_error = False config.update_result_start_time = pygame.time.get_ticks() config.needs_redraw = True - logger.debug(f"Affichage de la popup de mise à jour réussie") + logger.debug(f"Affichage de la popup de mise à jour réussie, redémarrage imminent") + + try: + from utils import restart_application + restart_application(2000) + except Exception as e: + logger.error(f"Erreur lors du redémarrage après mise à jour: {e}") return True, _("network_update_success_message") else: @@ -270,10 +274,10 @@ async def download_rom(url, platform, game_name, is_zip_non_supported=False, tas platform_folder = normalize_platform_name(platform) dest_dir = apply_symlink_path(config.ROMS_FOLDER, platform_folder) - # Spécifique: si le système est "00 BIOS" on force le dossier BIOS - if platform == "00 BIOS": - dest_dir = config.BIOS_FOLDER - logger.debug(f"Plateforme '00 BIOS' détectée, destination forcée vers BIOS_FOLDER: {dest_dir}") + # Spécifique: si le système est "BIOS" on force le dossier BIOS + if platform == "- BIOS by TMCTV": + dest_dir = config.RETROBAT_DATA_FOLDER + logger.debug(f"Plateforme 'BIOS' détectée, destination forcée vers RETROBAT_DATA_FOLDER: {dest_dir}") os.makedirs(dest_dir, exist_ok=True) if not os.access(dest_dir, os.W_OK): @@ -557,8 +561,8 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= # Spécifique: si le système est "00 BIOS" on force le dossier BIOS if platform == "00 BIOS": - dest_dir = config.BIOS_FOLDER - logger.debug(f"Plateforme '00 BIOS' détectée, destination forcée vers BIOS_FOLDER: {dest_dir}") + dest_dir = config.RETROBAT_DATA_FOLDER + logger.debug(f"Plateforme '00 BIOS' détectée, destination forcée vers RETROBAT_DATA_FOLDER: {dest_dir}") logger.debug(f"Vérification répertoire destination: {dest_dir}") os.makedirs(dest_dir, exist_ok=True) diff --git a/ports/RGSX/rgsx_settings.py b/ports/RGSX/rgsx_settings.py index e03c647..d3342fa 100644 --- a/ports/RGSX/rgsx_settings.py +++ b/ports/RGSX/rgsx_settings.py @@ -25,6 +25,9 @@ def load_rgsx_settings(): "accessibility": { "font_scale": 1.0 }, + "display": { + "grid": "3x4" + }, "symlink": { "enabled": False, "target_directory": "" @@ -32,7 +35,8 @@ def load_rgsx_settings(): "sources": { "mode": "rgsx", "custom_url": "" - } + }, + "show_unsupported_platforms": False } try: @@ -160,3 +164,44 @@ def get_sources_zip_url(fallback_url): # Pas de fallback : retourner None pour signaler une source vide return None return fallback_url + +# ----------------------- Unsupported platforms toggle ----------------------- # + +def get_show_unsupported_platforms(settings=None): + """Retourne True si l'affichage des systèmes non supportés est activé.""" + if settings is None: + settings = load_rgsx_settings() + return bool(settings.get("show_unsupported_platforms", False)) + + +def set_show_unsupported_platforms(enabled: bool): + """Active/désactive l'affichage des systèmes non supportés et sauvegarde.""" + settings = load_rgsx_settings() + settings["show_unsupported_platforms"] = bool(enabled) + save_rgsx_settings(settings) + return settings["show_unsupported_platforms"] + +# ----------------------- Display layout (grid) ----------------------- # + +def get_display_grid(settings=None): + """Retourne (cols, rows) pour la grille d'affichage, par défaut (3,4).""" + if settings is None: + settings = load_rgsx_settings() + disp = settings.get("display", {}) + grid = disp.get("grid", "3x4") + try: + cols, rows = map(int, grid.lower().split("x")) + return cols, rows + except Exception: + return 3, 4 + +def set_display_grid(cols: int, rows: int): + """Définit et sauvegarde la grille d'affichage (cols x rows) parmi options autorisées.""" + allowed = {(3,3), (3,4), (4,3), (4,4)} + if (cols, rows) not in allowed: + cols, rows = 3, 4 + settings = load_rgsx_settings() + disp = settings.setdefault("display", {}) + disp["grid"] = f"{cols}x{rows}" + save_rgsx_settings(settings) + return cols, rows diff --git a/ports/RGSX/rom_extensions.json b/ports/RGSX/rom_extensions.json deleted file mode 100644 index 84a77d5..0000000 --- a/ports/RGSX/rom_extensions.json +++ /dev/null @@ -1,2317 +0,0 @@ -[ - { - "system": "3DO INTERACTIVE MULTIPLAYER", - "folder": "3do", - "extensions": [ - ".iso", - ".chd", - ".cue" - ] - }, - { - "system": "3DS", - "folder": "3ds", - "extensions": [ - ".3ds", - ".3dsx", - ".cxi", - ".axf", - ".elf", - ".app", - ".squashfs" - ] - }, - { - "system": "ABUSE", - "folder": "abuse", - "extensions": [ - ".game" - ] - }, - { - "system": "ADAM", - "folder": "adam", - "extensions": [ - ".wav", - ".ddp", - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".rom", - ".col", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "ADVENTURE VISION", - "folder": "advision", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "AMIGA AGA", - "folder": "amiga1200", - "extensions": [ - ".adf", - ".uae", - ".ipf", - ".dms", - ".dmz", - ".adz", - ".lha", - ".hdf", - ".exe", - ".m3u", - ".zip", - ".raw", - ".scp" - ] - }, - { - "system": "AMIGA OCS/ECS", - "folder": "amiga500", - "extensions": [ - ".adf", - ".uae", - ".ipf", - ".dms", - ".dmz", - ".adz", - ".lha", - ".hdf", - ".exe", - ".m3u", - ".zip", - ".raw", - ".scp" - ] - }, - { - "system": "AMIGA CD32", - "folder": "amigacd32", - "extensions": [ - ".bin", - ".cue", - ".iso", - ".chd" - ] - }, - { - "system": "AMIGA CDTV", - "folder": "amigacdtv", - "extensions": [ - ".bin", - ".cue", - ".iso", - ".chd", - ".m3u" - ] - }, - { - "system": "AMSTRAD CPC", - "folder": "amstradcpc", - "extensions": [ - ".dsk", - ".sna", - ".tap", - ".cdt", - ".voc", - ".m3u", - ".zip", - ".7z" - ] - }, - { - "system": "M-1000", - "folder": "apfm1000", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "APPLE II", - "folder": "apple2", - "extensions": [ - ".nib", - ".do", - ".po", - ".dsk", - ".mfi", - ".dfi", - ".rti", - ".edd", - ".woz", - ".wav", - ".zip", - ".7z", - ".chd", - ".hdv", - ".2mg" - ] - }, - { - "system": "APPLE IIGS", - "folder": "apple2gs", - "extensions": [ - ".2mg", - ".do", - ".nib", - ".po", - ".dsk", - ".mfi", - ".dfi", - ".rti", - ".edd", - ".woz", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqui", - ".ima", - ".img", - ".ufi", - ".360", - ".ipf", - ".dc42", - ".zip", - ".7z" - ] - }, - { - "system": "ARCADIA 2001", - "folder": "arcadia", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "ARCHIMEDES", - "folder": "archimedes", - "extensions": [ - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".ima", - ".img", - ".ufi", - ".360", - ".ipf", - ".adf", - ".apd", - ".jfd", - ".ads", - ".adm", - ".adl", - ".ssd", - ".bbc", - ".dsd", - ".st", - ".msa", - ".chd", - ".zip", - ".7z" - ] - }, - { - "system": "ARDUBOY", - "folder": "arduboy", - "extensions": [ - ".hex", - ".zip", - ".7z" - ] - }, - { - "system": "ASTROCADE", - "folder": "astrocde", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "ATARI 2600", - "folder": "atari2600", - "extensions": [ - ".a26", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "ATARI 5200", - "folder": "atari5200", - "extensions": [ - ".rom", - ".xfd", - ".atr", - ".atx", - ".cdm", - ".cas", - ".car", - ".bin", - ".a52", - ".xex", - ".zip", - ".7z" - ] - }, - { - "system": "ATARI 7800", - "folder": "atari7800", - "extensions": [ - ".a78", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "ATARI 800", - "folder": "atari800", - "extensions": [ - ".rom", - ".xfd", - ".atr", - ".atx", - ".cdm", - ".cas", - ".car", - ".bin", - ".a52", - ".xex", - ".zip", - ".7z", - ".m3u" - ] - }, - { - "system": "ATARI ST", - "folder": "atarist", - "extensions": [ - ".st", - ".msa", - ".stx", - ".dim", - ".ipf", - ".m3u", - ".zip", - ".7z", - ".hd", - ".gemdos" - ] - }, - { - "system": "ATOM", - "folder": "atom", - "extensions": [ - ".wav", - ".tap", - ".csw", - ".uef", - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".40t", - ".atm", - ".bin", - ".rom", - ".zip", - ".7z" - ] - }, - { - "system": "ATOMISWAVE", - "folder": "atomiswave", - "extensions": [ - ".lst", - ".bin", - ".dat", - ".zip", - ".7z" - ] - }, - { - "system": "BBC MICRO", - "folder": "bbc", - "extensions": [ - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".ima", - ".img", - ".ufi", - ".360", - ".ipf", - ".ssd", - ".bbc", - ".dsd", - ".adf", - ".ads", - ".adm", - ".adl", - ".fsd", - ".wav", - ".tap", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "COMMODORE 128", - "folder": "c128", - "extensions": [ - ".d64", - ".d81", - ".prg", - ".lnx", - ".m3u", - ".zip", - ".7z" - ] - }, - { - "system": "COMMODORE VIC-20", - "folder": "c20", - "extensions": [ - ".20", - ".40", - ".60", - ".rom", - ".a0", - ".b0", - ".crt", - ".d64", - ".d81", - ".prg", - ".tap", - ".t64", - ".m3u", - ".zip", - ".7z" - ] - }, - { - "system": "COMMODORE 64", - "folder": "c64", - "extensions": [ - ".d64", - ".d71", - ".d81", - ".crt", - ".prg", - ".tap", - ".t64", - ".m3u", - ".zip", - ".7z", - ".nib", - ".g64" - ] - }, - { - "system": "CAMPUTERS LYNX", - "folder": "camplynx", - "extensions": [ - ".wav", - ".tap", - ".ldf", - ".zip", - ".7z" - ] - }, - { - "system": "CANNONBALL", - "folder": "cannonball", - "extensions": [ - ".cannonball" - ] - }, - { - "system": "CAVE STORY", - "folder": "cavestory", - "extensions": [ - ".exe" - ] - }, - { - "system": "CD-I", - "folder": "cdi", - "extensions": [ - ".chd", - ".cue", - ".toc", - ".nrg", - ".gdi", - ".iso", - ".cdr" - ] - }, - { - "system": "C-DOGS SDL", - "folder": "cdogs", - "extensions": [ - ".game" - ] - }, - { - "system": "COMMANDER GENIUS", - "folder": "cgenius", - "extensions": [ - ".cgenius" - ] - }, - { - "system": "CHANNEL-F", - "folder": "channelf", - "extensions": [ - ".zip", - ".rom", - ".bin", - ".chf" - ] - }, - { - "system": "SEGA CHIHIRO", - "folder": "chihiro", - "extensions": [ - ".iso" - ] - }, - { - "system": "COLOR COMPUTER", - "folder": "coco", - "extensions": [ - ".wav", - ".cas", - ".dsk", - ".ccc", - ".rom", - ".zip", - ".7z" - ] - }, - { - "system": "COLECOVISION", - "folder": "colecovision", - "extensions": [ - ".bin", - ".col", - ".rom", - ".zip", - ".7z" - ] - }, - { - "system": "COMMANDER X16", - "folder": "commanderx16", - "extensions": [ - ".bas", - ".img", - ".prg" - ] - }, - { - "system": "CORSIXTH", - "folder": "corsixth", - "extensions": [ - ".game" - ] - }, - { - "system": "COMMODORE PLUS4", - "folder": "cplus4", - "extensions": [ - ".d64", - ".prg", - ".tap", - ".m3u", - ".zip", - ".7z" - ] - }, - { - "system": "CREATIVISION", - "folder": "crvision", - "extensions": [ - ".bin", - ".rom", - ".zip", - ".7z" - ] - }, - { - "system": "DAPHNE", - "folder": "daphne", - "extensions": [ - ".daphne", - ".squashfs" - ] - }, - { - "system": "DIABLO", - "folder": "devilutionx", - "extensions": [ - ".mpq" - ] - }, - { - "system": "DOOM 3", - "folder": "doom3", - "extensions": [ - ".d3" - ] - }, - { - "system": "DOS (X86)", - "folder": "dos", - "extensions": [ - ".pc", - ".dos", - ".zip", - ".squashfs", - ".dosz", - ".m3u", - ".iso", - ".cue" - ] - }, - { - "system": "DREAMCAST", - "folder": "dreamcast", - "extensions": [ - ".cdi", - ".cue", - ".gdi", - ".chd", - ".m3u" - ] - }, - { - "system": "DXX REBIRTH", - "folder": "dxx-rebirth", - "extensions": [ - ".d1x", - ".d2x" - ] - }, - { - "system": "EASYRPG", - "folder": "easyrpg", - "extensions": [ - ".easyrpg", - ".squashfs", - ".zip" - ] - }, - { - "system": "ECWOLF", - "folder": "ecwolf", - "extensions": [ - ".ecwolf", - ".pk3", - ".squashfs" - ] - }, - { - "system": "EDUKE32", - "folder": "eduke32", - "extensions": [ - ".eduke32" - ] - }, - { - "system": "ELECTRON", - "folder": "electron", - "extensions": [ - ".wav", - ".csw", - ".uef", - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".ssd", - ".bbc", - ".img", - ".dsd", - ".adf", - ".ads", - ".adm", - ".adl", - ".rom", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "WOLFENSTEIN - ENEMY TERRITORY", - "folder": "etlegacy", - "extensions": [ - ".etl" - ] - }, - { - "system": "FALLOUT COMMUNITY EDITION", - "folder": "fallout1-ce", - "extensions": [ - ".f1ce" - ] - }, - { - "system": "FALLOUT 2 COMMUNITY EDITION", - "folder": "fallout2-ce", - "extensions": [ - ".f2ce" - ] - }, - { - "system": "FINAL BURN NEO", - "folder": "fbneo", - "extensions": [ - ".zip", - ".7z" - ] - }, - { - "system": "FAMILY COMPUTER DISK SYSTEM", - "folder": "fds", - "extensions": [ - ".fds", - ".zip", - ".7z" - ] - }, - { - "system": "FLASH PLAYER", - "folder": "flash", - "extensions": [ - ".swf" - ] - }, - { - "system": "APPLICATIONS", - "folder": "flatpak", - "extensions": [ - ".flatpak" - ] - }, - { - "system": "FM-7", - "folder": "fm7", - "extensions": [ - ".wav", - ".t77", - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".zip", - ".7z" - ] - }, - { - "system": "FM-TOWNS", - "folder": "fmtowns", - "extensions": [ - ".bin", - ".m3u", - ".cue", - ".d88", - ".d77", - ".xdf", - ".iso", - ".chd", - ".toc", - ".nrg", - ".gdi", - ".cdr", - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".zip", - ".7z" - ] - }, - { - "system": "FUTURE PINBALL", - "folder": "fpinball", - "extensions": [ - ".fpt" - ] - }, - { - "system": "ION FURY", - "folder": "fury", - "extensions": [ - ".grp" - ] - }, - { - "system": "GAMATE", - "folder": "gamate", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "GAME AND WATCH", - "folder": "gameandwatch", - "extensions": [ - ".mgw", - ".zip", - ".7z" - ] - }, - { - "system": "GAME.COM", - "folder": "gamecom", - "extensions": [ - ".bin", - ".tgc", - ".zip", - ".7z" - ] - }, - { - "system": "GAMECUBE", - "folder": "gamecube", - "extensions": [ - ".gcm", - ".iso", - ".gcz", - ".ciso", - ".wbfs", - ".rvz", - ".elf", - ".dol", - ".m3u", - ".json" - ] - }, - { - "system": "GAME GEAR", - "folder": "gamegear", - "extensions": [ - ".bin", - ".gg", - ".zip", - ".7z" - ] - }, - { - "system": "GAME POCKET COMPUTER", - "folder": "gamepock", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "GAME BOY", - "folder": "gb", - "extensions": [ - ".gb", - ".zip", - ".7z" - ] - }, - { - "system": "GAME BOY (2 PLAYERS)", - "folder": "gb2players", - "extensions": [ - ".gb", - ".gb2", - ".gbc2", - ".zip", - ".7z" - ] - }, - { - "system": "GAME BOY ADVANCE", - "folder": "gba", - "extensions": [ - ".gba", - ".zip", - ".7z" - ] - }, - { - "system": "GAME BOY COLOR", - "folder": "gbc", - "extensions": [ - ".gbc", - ".zip", - ".7z" - ] - }, - { - "system": "GAME BOY COLOR (2 PLAYERS)", - "folder": "gbc2players", - "extensions": [ - ".gbc", - ".gb2", - ".gbc2", - ".zip", - ".7z" - ] - }, - { - "system": "GAME MASTER", - "folder": "gmaster", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "GP32", - "folder": "gp32", - "extensions": [ - ".smc", - ".zip", - ".7z" - ] - }, - { - "system": "GX4000", - "folder": "gx4000", - "extensions": [ - ".dsk", - ".m3u", - ".cpr", - ".zip", - ".7z" - ] - }, - { - "system": "GZDOOM", - "folder": "gzdoom", - "extensions": [ - ".wad", - ".iwad", - ".pwad", - ".gzdoom" - ] - }, - { - "system": "HYDRA CASTLE LABYRINTH", - "folder": "hcl", - "extensions": [ - ".game" - ] - }, - { - "system": "HURRICAN", - "folder": "hurrican", - "extensions": [ - ".game" - ] - }, - { - "system": "IKEMEN", - "folder": "ikemen", - "extensions": [ - ".ikemen", - ".pc" - ] - }, - { - "system": "MATTEL INTELLIVISION", - "folder": "intellivision", - "extensions": [ - ".int", - ".bin", - ".rom", - ".zip", - ".7z" - ] - }, - { - "system": "RETURN TO CASTLE WOLFENSTEIN", - "folder": "iortcw", - "extensions": [ - ".rtcw" - ] - }, - { - "system": "JAGUAR", - "folder": "jaguar", - "extensions": [ - ".j64", - ".jag", - ".cof", - ".abs", - ".rom", - ".zip", - ".7z" - ] - }, - { - "system": "JAGUAR CD", - "folder": "jaguarcd", - "extensions": [ - ".cue", - ".cdi" - ] - }, - { - "system": "JAZZ JACKRABBIT 2", - "folder": "jazz2", - "extensions": [ - ".game" - ] - }, - { - "system": "LASER 310", - "folder": "laser310", - "extensions": [ - ".vz", - ".wav", - ".cas", - ".zip", - ".7z" - ] - }, - { - "system": "LCD GAMES", - "folder": "lcdgames", - "extensions": [ - ".mgw", - ".zip", - ".7z" - ] - }, - { - "system": "LOWRES NX", - "folder": "lowresnx", - "extensions": [ - ".nx", - ".zip", - ".7z" - ] - }, - { - "system": "LUTRO", - "folder": "lutro", - "extensions": [ - ".lutro", - ".zip", - ".7z" - ] - }, - { - "system": "ATARI LYNX", - "folder": "lynx", - "extensions": [ - ".bll", - ".lnx", - ".lyx", - ".o", - ".zip", - ".7z" - ] - }, - { - "system": "MACINTOSH", - "folder": "macintosh", - "extensions": [ - ".dsk", - ".zip", - ".7z", - ".mfi", - ".dfi", - ".hfe", - ".mfm", - ".td0", - ".imd", - ".d77", - ".d88", - ".1dd", - ".cqm", - ".cqi", - ".dsk", - ".ima", - ".img", - ".ufi", - ".ipf", - ".dc42", - ".woz", - ".2mg", - ".360", - ".chd", - ".cue", - ".toc", - ".nrg", - ".gdi", - ".iso", - ".cdr", - ".hd", - ".hdv", - ".2mg", - ".hdi" - ] - }, - { - "system": "MAME", - "folder": "mame", - "extensions": [ - ".zip", - ".7z" - ] - }, - { - "system": "MASTER SYSTEM", - "folder": "mastersystem", - "extensions": [ - ".bin", - ".sms", - ".zip", - ".7z" - ] - }, - { - "system": "MEGA DRIVE", - "folder": "megadrive", - "extensions": [ - ".bin", - ".gen", - ".md", - ".sg", - ".smd", - ".zip", - ".7z" - ] - }, - { - "system": "MEGA DUCK / COUGAR BOY", - "folder": "megaduck", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "MODEL 2", - "folder": "model2", - "extensions": [ - ".zip" - ] - }, - { - "system": "MODEL 3", - "folder": "model3", - "extensions": [ - ".zip" - ] - }, - { - "system": "MOONLIGHT EMBEDDED", - "folder": "moonlight", - "extensions": [ - ".moonlight" - ] - }, - { - "system": "MRBOOM", - "folder": "mrboom", - "extensions": [ - ".libretro" - ] - }, - { - "system": "MSU-MD", - "folder": "msu-md", - "extensions": [ - ".md", - ".zip", - ".7z", - ".squashfs" - ] - }, - { - "system": "MSX1", - "folder": "msx1", - "extensions": [ - ".dsk", - ".mx1", - ".rom", - ".zip", - ".7z", - ".cas", - ".m3u", - ".ogv", - ".openmsx" - ] - }, - { - "system": "MSX2", - "folder": "msx2", - "extensions": [ - ".dsk", - ".mx2", - ".rom", - ".zip", - ".7z", - ".cas", - ".m3u", - ".ogv", - ".openmsx" - ] - }, - { - "system": "MSX2+", - "folder": "msx2+", - "extensions": [ - ".dsk", - ".mx2", - ".rom", - ".zip", - ".7z", - ".cas", - ".m3u", - ".openmsx" - ] - }, - { - "system": "MSX TURBO-R", - "folder": "msxturbor", - "extensions": [ - ".dsk", - ".mx2", - ".rom", - ".zip", - ".7z", - ".openmsx", - ".m3u" - ] - }, - { - "system": "MUGEN", - "folder": "mugen", - "extensions": [ - ".pc" - ] - }, - { - "system": "OTHELLO MULTIVISION", - "folder": "multivision", - "extensions": [ - ".bin", - ".sg", - ".zip", - ".7z" - ] - }, - { - "system": "NINTENDO 64", - "folder": "n64", - "extensions": [ - ".z64", - ".n64", - ".v64", - ".zip", - ".7z" - ] - }, - { - "system": "NINTENDO 64 DISK DRIVE", - "folder": "n64dd", - "extensions": [ - ".z64", - ".n64", - ".ndd", - ".zip", - ".7z" - ] - }, - { - "system": "NAMCO 246/256", - "folder": "namco2x6", - "extensions": [ - ".zip" - ] - }, - { - "system": "NAOMI", - "folder": "naomi", - "extensions": [ - ".lst", - ".bin", - ".dat", - ".zip", - ".7z" - ] - }, - { - "system": "NAOMI 2", - "folder": "naomi2", - "extensions": [ - ".zip", - ".7z" - ] - }, - { - "system": "NINTENDO DS", - "folder": "nds", - "extensions": [ - ".nds", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "NEO-GEO", - "folder": "neogeo", - "extensions": [ - ".7z", - ".zip" - ] - }, - { - "system": "NEO-GEO CD", - "folder": "neogeocd", - "extensions": [ - ".cue", - ".iso", - ".chd" - ] - }, - { - "system": "NINTENDO ENTERTAINMENT SYSTEM", - "folder": "nes", - "extensions": [ - ".nes", - ".unif", - ".unf", - ".zip", - ".7z" - ] - }, - { - "system": "NEO-GEO POCKET", - "folder": "ngp", - "extensions": [ - ".ngp", - ".zip", - ".7z" - ] - }, - { - "system": "NEO-GEO POCKET COLOR", - "folder": "ngpc", - "extensions": [ - ".ngc", - ".zip", - ".7z" - ] - }, - { - "system": "ODYSSEY2", - "folder": "o2em", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "OD-COMMANDER", - "folder": "odcommander", - "extensions": [ - ".odc" - ] - }, - { - "system": "OPENBOR", - "folder": "openbor", - "extensions": [ - ".pak" - ] - }, - { - "system": "JAZZ JACKRABBIT", - "folder": "openjazz", - "extensions": [ - ".game" - ] - }, - { - "system": "OPENLARA", - "folder": "openlara", - "extensions": [ - ".croft" - ] - }, - { - "system": "PC-8800", - "folder": "pc88", - "extensions": [ - ".cmt", - ".d88", - ".u88", - ".m3u" - ] - }, - { - "system": "PC-9800", - "folder": "pc98", - "extensions": [ - ".d98", - ".zip", - ".98d", - ".fdi", - ".fdd", - ".2hd", - ".tfd", - ".d88", - ".88d", - ".hdm", - ".xdf", - ".dup", - ".cmd", - ".hdi", - ".thd", - ".nhd", - ".hdd", - ".hdn", - ".m3u" - ] - }, - { - "system": "PC ENGINE", - "folder": "pcengine", - "extensions": [ - ".pce", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "PC ENGINE CD", - "folder": "pcenginecd", - "extensions": [ - ".pce", - ".cue", - ".ccd", - ".iso", - ".img", - ".chd" - ] - }, - { - "system": "PC-FX", - "folder": "pcfx", - "extensions": [ - ".cue", - ".ccd", - ".toc", - ".chd", - ".zip", - ".7z", - ".m3u" - ] - }, - { - "system": "PDP-1", - "folder": "pdp1", - "extensions": [ - ".zip", - ".7z", - ".tap", - ".rim", - ".drm" - ] - }, - { - "system": "COMMODORE PET", - "folder": "pet", - "extensions": [ - ".a0", - ".b0", - ".crt", - ".d64", - ".d81", - ".prg", - ".tap", - ".t64", - ".m3u", - ".zip", - ".7z" - ] - }, - { - "system": "SEGA PICO", - "folder": "pico", - "extensions": [ - ".bin", - ".md", - ".zip", - ".7z" - ] - }, - { - "system": "PICO-8", - "folder": "pico8", - "extensions": [ - ".p8", - ".png", - ".m3u" - ] - }, - { - "system": "PLUG AND PLAY TV GAMES", - "folder": "plugnplay", - "extensions": [ - ".zip", - ".7z" - ] - }, - { - "system": "POKEMON MINI", - "folder": "pokemini", - "extensions": [ - ".min", - ".zip", - ".7z" - ] - }, - { - "system": "PRBOOM", - "folder": "prboom", - "extensions": [ - ".wad", - ".iwad", - ".pwad" - ] - }, - { - "system": "PLAYSTATION 2", - "folder": "ps2", - "extensions": [ - ".iso", - ".mdf", - ".nrg", - ".bin", - ".img", - ".dump", - ".gz", - ".cso", - ".chd", - ".m3u" - ] - }, - { - "system": "PLAYSTATION 3", - "folder": "ps3", - "extensions": [ - ".ps3", - ".psn", - ".squashfs" - ] - }, - { - "system": "PLAYSTATION PORTABLE", - "folder": "psp", - "extensions": [ - ".iso", - ".cso", - ".pbp", - ".chd" - ] - }, - { - "system": "PLAYSTATION VITA", - "folder": "psvita", - "extensions": [ - ".zip", - ".psvita" - ] - }, - { - "system": "PLAYSTATION", - "folder": "psx", - "extensions": [ - ".cue", - ".img", - ".mdf", - ".pbp", - ".toc", - ".cbn", - ".m3u", - ".ccd", - ".chd", - ".iso" - ] - }, - { - "system": "PV-1000", - "folder": "pv1000", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "PYGAME", - "folder": "pygame", - "extensions": [ - ".pygame" - ] - }, - { - "system": "PYXEL", - "folder": "pyxel", - "extensions": [ - ".py", - ".pyxapp" - ] - }, - { - "system": "QUAKE III", - "folder": "quake3", - "extensions": [ - ".quake3" - ] - }, - { - "system": "RAZE", - "folder": "raze", - "extensions": [ - ".raze" - ] - }, - { - "system": "REMINISCENCE", - "folder": "reminiscence", - "extensions": [ - ".rem" - ] - }, - { - "system": "RISE OF THE TRIAD", - "folder": "rott", - "extensions": [ - ".rott" - ] - }, - { - "system": "SAM COUPÉ", - "folder": "samcoupe", - "extensions": [ - ".cpm", - ".dsk", - ".sad", - ".mgt", - ".sdf", - ".td0", - ".sbt", - ".zip" - ] - }, - { - "system": "SATELLAVIEW", - "folder": "satellaview", - "extensions": [ - ".bs", - ".smc", - ".sfc", - ".zip", - ".7z", - ".squashfs" - ] - }, - { - "system": "SATURN", - "folder": "saturn", - "extensions": [ - ".cue", - ".ccd", - ".m3u", - ".chd", - ".iso", - ".zip", - ".mds" - ] - }, - { - "system": "SCUMMVM", - "folder": "scummvm", - "extensions": [ - ".scummvm", - ".squashfs" - ] - }, - { - "system": "SUPER CASSETTE VISION", - "folder": "scv", - "extensions": [ - ".bin", - ".zip", - ".0" - ] - }, - { - "system": "SDLPOP", - "folder": "sdlpop", - "extensions": [ - ".sdlpop" - ] - }, - { - "system": "32X", - "folder": "sega32x", - "extensions": [ - ".32x", - ".chd", - ".smd", - ".bin", - ".md", - ".zip", - ".7z" - ] - }, - { - "system": "SEGA CD", - "folder": "segacd", - "extensions": [ - ".cue", - ".iso", - ".chd", - ".m3u" - ] - }, - { - "system": "SG-1000", - "folder": "sg1000", - "extensions": [ - ".bin", - ".sg", - ".zip", - ".7z" - ] - }, - { - "system": "SUPER GAME BOY", - "folder": "sgb", - "extensions": [ - ".gb", - ".gbc", - ".zip", - ".7z" - ] - }, - { - "system": "SINGE", - "folder": "singe", - "extensions": [ - ".daphne", - ".squashfs" - ] - }, - { - "system": "SUPER NINTENDO ENTERTAINMENT SYSTEM", - "folder": "snes", - "extensions": [ - ".smc", - ".fig", - ".sfc", - ".gd3", - ".gd7", - ".dx2", - ".bsx", - ".swc", - ".zip", - ".7z" - ] - }, - { - "system": "SUPER DISC (MSU1)", - "folder": "snes-msu1", - "extensions": [ - ".smc", - ".sfc", - ".squashfs" - ] - }, - { - "system": "SOCRATES", - "folder": "socrates", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "SOLARUS", - "folder": "solarus", - "extensions": [ - ".zip", - ".solarus" - ] - }, - { - "system": "SONIC MANIA", - "folder": "sonic-mania", - "extensions": [ - ".sman" - ] - }, - { - "system": "SONIC 3 A.I.R.", - "folder": "sonic3-air", - "extensions": [ - ".s3air" - ] - }, - { - "system": "SONIC RETRO ENGINE", - "folder": "sonicretro", - "extensions": [ - ".son", - ".scd" - ] - }, - { - "system": "SPECTRAVIDEO SV-328", - "folder": "spectravideo", - "extensions": [ - ".zip", - ".7z", - ".cas" - ] - }, - { - "system": "STEAM", - "folder": "steam", - "extensions": [ - ".steam" - ] - }, - { - "system": "SUFAMI TURBO", - "folder": "sufami", - "extensions": [ - ".st", - ".fig", - ".bs", - ".smc", - ".sfc", - ".zip", - ".7z" - ] - }, - { - "system": "SUPER MARIO WAR", - "folder": "superbroswar", - "extensions": [ - ".game" - ] - }, - { - "system": "SUPERGRAFX", - "folder": "supergrafx", - "extensions": [ - ".pce", - ".sgx", - ".cue", - ".ccd", - ".chd", - ".zip", - ".7z" - ] - }, - { - "system": "SUPERVISION", - "folder": "supervision", - "extensions": [ - ".sv", - ".zip", - ".7z" - ] - }, - { - "system": "SUPER A'CAN", - "folder": "supracan", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "SEGA SP", - "folder": "systemsp", - "extensions": [ - ".lst", - ".bin", - ".dat", - ".zip", - ".7z" - ] - }, - { - "system": "THE FORCE ENGINE", - "folder": "theforceengine", - "extensions": [ - ".tfe" - ] - }, - { - "system": "THEXTECH", - "folder": "thextech", - "extensions": [ - ".smbx", - ".squashfs" - ] - }, - { - "system": "THOMSON - MO/TO (THEODORE)", - "folder": "thomson", - "extensions": [ - ".fd", - ".sap", - ".k7", - ".m7", - ".m5", - ".rom", - ".zip" - ] - }, - { - "system": "TI-99", - "folder": "ti99", - "extensions": [ - ".rpk", - ".wav", - ".zip", - ".7z" - ] - }, - { - "system": "TIC-80", - "folder": "tic80", - "extensions": [ - ".tic" - ] - }, - { - "system": "TRIFORCE", - "folder": "triforce", - "extensions": [ - ".gcm", - ".iso", - ".gcz", - ".ciso", - ".wbfs", - ".elf", - ".dol", - ".m3u" - ] - }, - { - "system": "TUTOR", - "folder": "tutor", - "extensions": [ - ".bin", - ".wav", - ".zip", - ".7z" - ] - }, - { - "system": "TYRIAN", - "folder": "tyrian", - "extensions": [ - ".game" - ] - }, - { - "system": "TYRQUAKE", - "folder": "tyrquake", - "extensions": [ - ".pak" - ] - }, - { - "system": "UZEBOX", - "folder": "uzebox", - "extensions": [ - ".uze" - ] - }, - { - "system": "VC 4000", - "folder": "vc4000", - "extensions": [ - ".bin", - ".rom", - ".pgm", - ".tvc", - ".zip", - ".7z" - ] - }, - { - "system": "VECTREX", - "folder": "vectrex", - "extensions": [ - ".bin", - ".gam", - ".vec", - ".zip", - ".7z" - ] - }, - { - "system": "VIDEO GAME MUSIC PLAYER", - "folder": "vgmplay", - "extensions": [ - ".vgm", - ".vgz", - ".zip", - ".7z" - ] - }, - { - "system": "VIDEOPAC+ G7400", - "folder": "videopacplus", - "extensions": [ - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "VIRCON32", - "folder": "vircon32", - "extensions": [ - ".v32", - ".zip" - ] - }, - { - "system": "VIRTUAL BOY", - "folder": "virtualboy", - "extensions": [ - ".vb", - ".zip", - ".7z" - ] - }, - { - "system": "TANDY VIDEO INFORMATION SYSTEM", - "folder": "vis", - "extensions": [ - ".chd", - ".cue", - ".toc", - ".nrg", - ".gdi", - ".iso", - ".cdr" - ] - }, - { - "system": "QUAKE II", - "folder": "vitaquake2", - "extensions": [ - ".pak", - ".zip", - ".7zip" - ] - }, - { - "system": "VISUAL PINBALL X", - "folder": "vpinball", - "extensions": [ - ".vpx" - ] - }, - { - "system": "V.SMILE", - "folder": "vsmile", - "extensions": [ - ".u1", - ".u3", - ".bin", - ".zip", - ".7z" - ] - }, - { - "system": "WASM4", - "folder": "wasm4", - "extensions": [ - ".wasm" - ] - }, - { - "system": "WII", - "folder": "wii", - "extensions": [ - ".gcm", - ".iso", - ".gcz", - ".ciso", - ".wbfs", - ".wad", - ".rvz", - ".elf", - ".dol", - ".m3u", - ".json" - ] - }, - { - "system": "WII U", - "folder": "wiiu", - "extensions": [ - ".wua", - ".wup", - ".wud", - ".wux", - ".rpx", - ".squashfs", - ".wuhb" - ] - }, - { - "system": "WINDOWS", - "folder": "windows", - "extensions": [ - ".pc", - ".exe", - ".wine", - ".wsquashfs", - ".wtgz" - ] - }, - { - "system": "INSTALL A NEW WINDOWS GAME", - "folder": "windows_installers", - "extensions": [ - ".exe", - ".iso" - ] - }, - { - "system": "WONDERSWAN", - "folder": "wswan", - "extensions": [ - ".ws", - ".zip", - ".7z" - ] - }, - { - "system": "WONDERSWAN COLOR", - "folder": "wswanc", - "extensions": [ - ".wsc", - ".zip", - ".7z" - ] - }, - { - "system": "SHARP X1", - "folder": "x1", - "extensions": [ - ".dx1", - ".zip", - ".2d", - ".2hd", - ".tfd", - ".d88", - ".88d", - ".hdm", - ".xdf", - ".dup", - ".cmd", - ".7z" - ] - }, - { - "system": "SHARP X68000", - "folder": "x68000", - "extensions": [ - ".dim", - ".img", - ".d88", - ".88d", - ".hdm", - ".dup", - ".2hd", - ".xdf", - ".hdf", - ".cmd", - ".m3u", - ".zip", - ".7z" - ] - }, - { - "system": "HALF-LIFE 1", - "folder": "xash3d_fwgs", - "extensions": [ - ".game" - ] - }, - { - "system": "XBOX", - "folder": "xbox", - "extensions": [ - ".iso", - ".squashfs" - ] - }, - { - "system": "XBOX 360", - "folder": "xbox360", - "extensions": [ - ".iso", - ".xex", - ".xbox360", - ".zar" - ] - }, - { - "system": "ATARI XE GAME SYSTEM", - "folder": "xegs", - "extensions": [ - ".atr", - ".dsk", - ".xfd", - ".bin", - ".rom", - ".car", - ".zip", - ".7z" - ] - }, - { - "system": "XRICK", - "folder": "xrick", - "extensions": [ - ".zip" - ] - }, - { - "system": "ZELDA CLASSIC", - "folder": "zc210", - "extensions": [ - ".qst" - ] - }, - { - "system": "ZX81", - "folder": "zx81", - "extensions": [ - ".tzx", - ".p", - ".zip", - ".7z" - ] - }, - { - "system": "ZX SPECTRUM", - "folder": "zxspectrum", - "extensions": [ - ".tzx", - ".tap", - ".z80", - ".rzx", - ".scl", - ".trd", - ".dsk", - ".zip", - ".7z" - ] - } -] \ No newline at end of file diff --git a/ports/RGSX/update_gamelist_windows.py b/ports/RGSX/update_gamelist_windows.py new file mode 100644 index 0000000..15c636d --- /dev/null +++ b/ports/RGSX/update_gamelist_windows.py @@ -0,0 +1,138 @@ +import os +import xml.dom.minidom +import xml.etree.ElementTree as ET +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +RGSX_ENTRY = { + "path": "./RGSX Retrobat.bat", + # 'name' left optional to preserve ES-chosen display name if already present + "name": "RGSX", + "desc": "Retro Games Sets X - Games Downloader", + "image": "./images/RGSX.png", + "video": "./videos/RGSX.mp4", + "marquee": "./images/RGSX.png", + "thumbnail": "./images/RGSX.png", + "fanart": "./images/RGSX.png", + # Avoid forcing rating to not conflict with ES metadata; set only if absent + # "rating": "1", + "releasedate": "20250620T165718", + "developer": "RetroGameSets.fr", + "genre": "Various / Utilities" +} + +def _get_root_dir(): + """Détecte le dossier racine RetroBat sans importer config.""" + # Ce script est dans .../roms/ports/RGSX/ + here = os.path.abspath(os.path.dirname(__file__)) + # Remonter à .../roms/ports/ + ports_dir = os.path.dirname(here) + # Remonter à .../roms/ + roms_dir = os.path.dirname(ports_dir) + # Remonter à la racine RetroBat + root_dir = os.path.dirname(roms_dir) + return root_dir + + +def update_gamelist(): + try: + root_dir = _get_root_dir() + gamelist_xml = os.path.join(root_dir, "roms", "windows", "gamelist.xml") + # Si le fichier n'existe pas, est vide ou non valide, créer une nouvelle structure + if not os.path.exists(gamelist_xml) or os.path.getsize(gamelist_xml) == 0: + logger.info(f"Création de {gamelist_xml}") + root = ET.Element("gameList") + else: + try: + logger.info(f"Lecture de {gamelist_xml}") + tree = ET.parse(gamelist_xml) + root = tree.getroot() + if root.tag != "gameList": + logger.info(f"{gamelist_xml} n'a pas de balise , création d'une nouvelle structure") + root = ET.Element("gameList") + except ET.ParseError: + logger.info(f"{gamelist_xml} est invalide, création d'une nouvelle structure") + root = ET.Element("gameList") + + # Rechercher une entrée existante pour ce chemin + game_elem = None + for game in root.findall("game"): + path = game.find("path") + if path is not None and path.text == RGSX_ENTRY["path"]: + game_elem = game + break + + if game_elem is None: + # Créer une nouvelle entrée si absente + game_elem = ET.SubElement(root, "game") + for key, value in RGSX_ENTRY.items(): + elem = ET.SubElement(game_elem, key) + elem.text = value + logger.info("Nouvelle entrée RGSX ajoutée") + else: + # Fusionner: préserver les champs gérés par ES, compléter/mettre à jour nos champs + def ensure(tag, value): + elem = game_elem.find(tag) + if elem is None: + elem = ET.SubElement(game_elem, tag) + if elem.text is None or elem.text.strip() == "": + elem.text = value + + # S'assurer du chemin + ensure("path", RGSX_ENTRY["path"]) + # Ne pas écraser le nom s'il existe déjà (ES peut le définir selon le fichier) + name_elem = game_elem.find("name") + existing_name = "" + if name_elem is not None and name_elem.text: + existing_name = name_elem.text.strip() + if not existing_name: + ensure("name", RGSX_ENTRY.get("name", "RGSX")) + + # Champs d'habillage que nous voulons imposer/mettre à jour + for tag in ("desc", "image", "video", "marquee", "thumbnail", "fanart", "developer", "genre", "releasedate"): + val = RGSX_ENTRY.get(tag) + if val: + elem = game_elem.find(tag) + if elem is None: + elem = ET.SubElement(game_elem, tag) + # Toujours aligner ces champs sur nos valeurs pour garder l'expérience RGSX + elem.text = val + + # Ne pas toucher aux champs: playcount, lastplayed, gametime, lang, favorite, kidgame, hidden, rating + logger.info("Entrée RGSX mise à jour (fusion)") + + # Générer le XML avec minidom pour une indentation correcte + rough_string = '\n' + ET.tostring(root, encoding='unicode') + parsed = xml.dom.minidom.parseString(rough_string) + pretty_xml = parsed.toprettyxml(indent="\t", encoding='utf-8').decode('utf-8') + # Supprimer les lignes vides inutiles générées par minidom + pretty_xml = '\n'.join(line for line in pretty_xml.split('\n') if line.strip()) + with open(gamelist_xml, 'w', encoding='utf-8') as f: + f.write(pretty_xml) + logger.info(f"{gamelist_xml} mis à jour avec succès") + + # Définir les permissions + try: + os.chmod(gamelist_xml, 0o644) + except Exception: + # Sur Windows, chmod peut être partiel; ignorer silencieusement + pass + + except Exception as e: + logger.error(f"Erreur lors de la mise à jour de la gamelist Windows: {e}") + raise + +def load_gamelist(path): + """Charge le fichier gamelist.xml.""" + try: + tree = ET.parse(path) + return tree.getroot() + except (FileNotFoundError, ET.ParseError) as e: + logging.error(f"Erreur lors de la lecture de {path} : {e}") + return None + +if __name__ == "__main__": + update_gamelist() \ No newline at end of file diff --git a/ports/RGSX/utils.py b/ports/RGSX/utils.py index 16e6277..cc5d6c3 100644 --- a/ports/RGSX/utils.py +++ b/ports/RGSX/utils.py @@ -7,6 +7,7 @@ import logging import platform import subprocess import config +import glob import threading from rgsx_settings import load_rgsx_settings, save_rgsx_settings import zipfile @@ -16,6 +17,7 @@ import config from history import save_history from language import _ from datetime import datetime +import sys logger = logging.getLogger(__name__) @@ -26,6 +28,50 @@ logging.getLogger("requests").setLevel(logging.WARNING) # Liste globale pour stocker les systèmes avec une erreur 404 unavailable_systems = [] +# Cache/process flags for extensions generation/loading + + +def restart_application(delay_ms: int = 2000): + """Schedule a restart with a visible popup; actual restart happens in the main loop. + + - Sets popup_restarting and schedules config.pending_restart_at = now + delay_ms. + - Main loop (__main__) detects pending_restart_at and calls restart_application(0) to perform the execl. + """ + try: + # Show popup and schedule + if hasattr(config, 'popup_message'): + try: + config.popup_message = _("popup_restarting") + except Exception: + config.popup_message = "Restarting..." + config.popup_timer = max(config.popup_timer, int(delay_ms)) if hasattr(config, 'popup_timer') else int(delay_ms) + config.menu_state = getattr(config, 'menu_state', 'restart_popup') or 'restart_popup' + config.needs_redraw = True + # Schedule actual restart in main loop + now = pygame.time.get_ticks() if hasattr(pygame, 'time') else 0 + config.pending_restart_at = now + max(0, int(delay_ms)) + logger.debug(f"Redémarrage planifié dans {delay_ms} ms (pending_restart_at={getattr(config, 'pending_restart_at', 0)})") + + # If delay_ms is 0, perform immediately here + if int(delay_ms) <= 0: + try: + try: + pygame.mixer.music.stop() + except Exception: + pass + try: + pygame.quit() + except Exception: + pass + exe = sys.executable or "python" + os.execl(exe, exe, *sys.argv) + except Exception as e: + logger.exception(f"Failed to restart immediately: {e}") + except Exception as e: + logger.exception(f"Failed to schedule restart: {e}") +_extensions_cache = None # type: ignore +_extensions_json_regenerated = False + # Détection système non-PC def detect_non_pc(): @@ -45,13 +91,165 @@ def detect_non_pc(): # Fonction pour charger le fichier JSON des extensions supportées def load_extensions_json(): - """Charge le fichier JSON contenant les extensions supportées.""" + """Charge le JSON des extensions supportées. + - Régénère une seule fois par exécution (au premier appel ou si le fichier est absent). + - Met en cache le résultat pour éviter les relectures et logs répétés. + """ + global _extensions_cache, _extensions_json_regenerated try: - with open(config.JSON_EXTENSIONS, 'r', encoding='utf-8') as f: - return json.load(f) + # Retour immédiat si déjà en cache + if _extensions_cache is not None: + return _extensions_cache + + os.makedirs(os.path.dirname(config.JSON_EXTENSIONS), exist_ok=True) + + # Régénération unique au premier appel (ou si le fichier est manquant) + if not _extensions_json_regenerated or not os.path.exists(config.JSON_EXTENSIONS): + try: + generated = generate_extensions_json_from_es_systems() + if generated: + with open(config.JSON_EXTENSIONS, 'w', encoding='utf-8') as wf: + json.dump(generated, wf, ensure_ascii=False, indent=2) + logger.info(f"rom_extensions régénéré ({len(generated)} systèmes): {config.JSON_EXTENSIONS}") + else: + logger.warning("Aucune donnée générée depuis es_systems.cfg; on conserve l'existant si présent") + _extensions_json_regenerated = True + except Exception as ge: + logger.error(f"Échec lors de la régénération de {config.JSON_EXTENSIONS} depuis es_systems.cfg: {ge}") + + # Lecture du fichier (nouveau ou existant) + if os.path.exists(config.JSON_EXTENSIONS): + with open(config.JSON_EXTENSIONS, 'r', encoding='utf-8') as f: + _extensions_cache = json.load(f) + return _extensions_cache + _extensions_cache = [] + return _extensions_cache except Exception as e: logger.error(f"Erreur lors de la lecture de {config.JSON_EXTENSIONS}: {e}") + _extensions_cache = [] + return _extensions_cache + +def _detect_es_systems_cfg_paths(): + """Retourne une liste de chemins possibles pour es_systems.cfg selon l'OS. + - RetroBat (Windows): {config.RETROBAT_DATA_FOLDER}\\system\\templates\\emulationstation\\es_systems.cfg + - Batocera (Linux): /usr/share/emulationstation/es_systems.cfg + Ajoute aussi les fichiers customs: /userdata/system/configs/emulationstation/es_systems_*.cfg + """ + candidates = [] + try: + if platform.system() == 'Windows': + base = getattr(config, 'RETROBAT_DATA_FOLDER', None) + if base: + candidates.append(os.path.join(base, 'system', 'templates', 'emulationstation', 'es_systems.cfg')) + else: + # Batocera / Linux classiques + candidates.append('/usr/share/emulationstation/es_systems.cfg') + candidates.append('/etc/emulationstation/es_systems.cfg') + # Batocera customs + custom_dir = '/userdata/system/configs/emulationstation' + try: + for p in glob.glob(os.path.join(custom_dir, 'es_systems_*.cfg')): + candidates.append(p) + direct_cfg = os.path.join(custom_dir, 'es_systems.cfg') + if os.path.exists(direct_cfg): + candidates.append(direct_cfg) + except Exception: + pass + except Exception: + pass + existing = [p for p in candidates if p and os.path.exists(p)] + # Logs réduits: on ne conserve que les résumés plus loin + return existing + +def _parse_es_systems_cfg(cfg_path): + """Parse un es_systems.cfg minimalement pour extraire (folder, extensions). + Retourne une liste de dicts: { 'folder': , 'extensions': [..] } + - folder: dérivé de la balise en prenant la partie après 'roms/' (ou '\\roms\\' sous Windows) + - extensions: liste normalisée de .ext (point + minuscule) + """ + try: + # Lire tel quel (pas besoin d'un parseur XML strict, mais ElementTree suffit) + import xml.etree.ElementTree as ET + # Log détaillé supprimé pour alléger les traces + tree = ET.parse(cfg_path) + root = tree.getroot() + out = [] + for sys_elem in root.findall('system'): + path_text = (sys_elem.findtext('path') or '').strip() + ext_text = (sys_elem.findtext('extension') or '').strip() + if not path_text: + continue + # Extraire le dossier après 'roms' + folder = None + norm = path_text.replace('\\', '/').lower() + marker = '/roms/' + if marker in norm: + after = norm.split(marker, 1)[1] + folder = after.strip().strip('/\\') + if not folder: + # fallback: si le chemin finit par .../roms/ + parts = norm.strip('/').split('/') + if len(parts) >= 2 and parts[-2] == 'roms': + folder = parts[-1] + if not folder: + continue + + # Extensions: split par espaces, normaliser en .ext + exts = [] + for tok in ext_text.split(): + tok = tok.strip().lower() + if not tok: + continue + if not tok.startswith('.'): + # Certaines entrées peuvent omettre le point + tok = '.' + tok + exts.append(tok) + # Dédupliquer tout en conservant l'ordre + seen = set() + norm_exts = [] + for e in exts: + if e not in seen: + seen.add(e) + norm_exts.append(e) + out.append({'folder': folder, 'extensions': norm_exts}) + # Résumé final affiché ailleurs + return out + except Exception as e: + logger.error(f"Erreur parsing es_systems.cfg ({cfg_path}): {e}") return [] + +def generate_extensions_json_from_es_systems(): + """Essaie de construire la liste des extensions à partir des es_systems.cfg disponibles. + Priorité: RetroBat si présent, sinon Batocera. Fusionne si plusieurs trouvés, en préférant RetroBat. + """ + combined = {} + paths = _detect_es_systems_cfg_paths() + if not paths: + logger.warning("Aucun chemin es_systems.cfg détecté (RetroBat/Batocera)") + return [] + # Prioriser RetroBat en tête si présent + def score(p): + return 0 if 'templates' in p.replace('\\', '/').lower() else 1 + for cfg in sorted(paths, key=score): + if not os.path.exists(cfg): + continue + items = _parse_es_systems_cfg(cfg) + for itm in items: + folder = itm['folder'] + exts = itm['extensions'] + if folder in combined: + # Fusionner: ajouter extensions manquantes + present = set(combined[folder]) + for e in exts: + if e not in present: + combined[folder].append(e) + present.add(e) + else: + combined[folder] = list(exts) + # Convertir en liste triée par dossier + result = [{'folder': k, 'extensions': v} for k, v in sorted(combined.items(), key=lambda x: x[0])] + logger.info(f"Extensions combinées totales: {len(result)} systèmes") + return result def check_extension_before_download(url, platform, game_name): """Vérifie l'extension avant de lancer le téléchargement et retourne un tuple de 4 éléments.""" @@ -66,10 +264,14 @@ def check_extension_before_download(url, platform, game_name): extension = os.path.splitext(sanitized_name)[1].lower() is_archive = extension in (".zip", ".rar") + # Déterminer si le système (dossier) est connu dans extensions_data + dest_folder_name = _get_dest_folder_name(platform) + system_known = any(s.get("folder") == dest_folder_name for s in extensions_data) + if is_supported: logger.debug(f"L'extension de {sanitized_name} est supportée pour {platform}") return (url, platform, game_name, False) - elif is_archive: + elif is_archive and system_known: logger.debug(f"Archive {extension.upper()} détectée pour {sanitized_name}, extraction automatique prévue") return (url, platform, game_name, True) else: @@ -91,10 +293,10 @@ def is_extension_supported(filename, platform_key, extensions_data): if platform_dict.get("platform_name") == platform_key: dest_dir = os.path.join(config.ROMS_FOLDER, platform_dict.get("folder")) break - + if not dest_dir: - logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform}") - dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform) + logger.warning(f"Aucun dossier 'folder' trouvé pour la plateforme {platform_key}") + dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform_key) dest_folder_name = os.path.basename(dest_dir) for i, system in enumerate(extensions_data): @@ -106,6 +308,20 @@ def is_extension_supported(filename, platform_key, extensions_data): return False +def _get_dest_folder_name(platform_key: str) -> str: + """Retourne le nom du dossier de destination pour une plateforme (basename du dossier).""" + dest_dir = None + for platform_dict in config.platform_dicts: + if platform_dict.get("platform_name") == platform_key: + folder = platform_dict.get("folder") + if folder: + dest_dir = os.path.join(config.ROMS_FOLDER, folder) + break + if not dest_dir: + dest_dir = os.path.join(os.path.dirname(os.path.dirname(config.APP_FOLDER)), platform_key) + return os.path.basename(dest_dir) + + # Fonction pour charger sources.json @@ -218,6 +434,39 @@ def load_sources(): hidden = set(settings.get("hidden_platforms", [])) if isinstance(settings, dict) else set() all_sorted_names = [s.get("platform_name", "") for s in sorted_for_display] visible_names = [n for n in all_sorted_names if n and n not in hidden] + + # Masquer automatiquement les systèmes dont le dossier ROM n'existe pas (selon le toggle) + unsupported = [] + try: + from rgsx_settings import get_show_unsupported_platforms + show_unsupported = get_show_unsupported_platforms(settings) + sources_by_name = {s.get("platform_name", ""): s for s in sources if isinstance(s, dict)} + for name in list(visible_names): + entry = sources_by_name.get(name) or {} + folder = entry.get("folder") + # Conserver BIOS même sans dossier, et ignorer entrées sans folder + bios_name = name.strip() + if not folder or bios_name == "- BIOS by TMCTV -" or bios_name == "- BIOS": + continue + expected_dir = os.path.join(config.ROMS_FOLDER, folder) + if not os.path.isdir(expected_dir): + unsupported.append(name) + if show_unsupported: + config.unsupported_platforms = unsupported + else: + if unsupported: + # Filtrer la liste visible + visible_names = [n for n in visible_names if n not in set(unsupported)] + config.unsupported_platforms = unsupported + # Log concis + détaillé en DEBUG uniquement + logger.info(f"Plateformes masquées (dossier rom absent): {len(unsupported)}") + logger.debug("Détails plateformes masquées: " + ", ".join(unsupported)) + else: + config.unsupported_platforms = [] + except Exception as e: + logger.error(f"Erreur détection plateformes non supportées (dossiers manquants): {e}") + config.unsupported_platforms = [] + config.platforms = visible_names config.platform_names = {p: p for p in config.platforms} # Nouveau mapping par nom pour éviter décalages index après tri d'affichage @@ -303,7 +552,7 @@ def load_games(platform_id): else: logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}") - logger.debug(f"Jeux chargés pour {platform_id} depuis {os.path.basename(game_file)}: {len(normalized)} entrées") + logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux") return normalized except Exception as e: logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}") @@ -451,27 +700,57 @@ def wrap_text(text, font, max_width): return lines def load_system_image(platform_dict): - """Charge une image système avec priorité: - 1. Fichier nommé exactement .png - 2. Champ platform_image si non vide - 3. Fallback default.png""" + """Charge une image système avec la priorité suivante: + 1. platform_image explicite s'il est défini + 2. .png + 3. .png si disponible + 4. Recherche fallback dans le dossier images de l'app (APP_FOLDER/images) avec le même ordre + 5. default.png (dans SAVE_FOLDER/images), sinon default.png de l'app + + Cela évite d'échouer lorsque le nom affiché ne correspond pas au fichier image + et respecte un mapping explicite fourni par systems_list.json.""" platform_name = platform_dict.get("platform_name", "unknown") - preferred_filename = f"{platform_name}.png" - preferred_path = os.path.join(config.IMAGES_FOLDER, preferred_filename) + folder_name = platform_dict.get("folder") or "" - # Normaliser platform_image pouvant être vide - platform_image_field = platform_dict.get("platform_image") or "" - explicit_image_path = os.path.join(config.IMAGES_FOLDER, platform_image_field) if platform_image_field else None - default_path = os.path.join(config.IMAGES_FOLDER, "default.png") + # Dossiers d'images + save_images = config.IMAGES_FOLDER + app_images = os.path.join(config.APP_FOLDER, "images") + # Candidats, par ordre de priorité + candidates = [] + platform_image_field = (platform_dict.get("platform_image") or "").strip() + if platform_image_field: + candidates.append(os.path.join(save_images, platform_image_field)) + candidates.append(os.path.join(save_images, f"{platform_name}.png")) + if folder_name: + candidates.append(os.path.join(save_images, f"{folder_name}.png")) + + # Fallback: images packagées avec l'app + if platform_image_field: + candidates.append(os.path.join(app_images, platform_image_field)) + candidates.append(os.path.join(app_images, f"{platform_name}.png")) + if folder_name: + candidates.append(os.path.join(app_images, f"{folder_name}.png")) + + # Charger le premier fichier existant try: - if os.path.exists(preferred_path): - return pygame.image.load(preferred_path).convert_alpha() - if explicit_image_path and os.path.exists(explicit_image_path): - return pygame.image.load(explicit_image_path).convert_alpha() - if os.path.exists(default_path): - return pygame.image.load(default_path).convert_alpha() - logger.error(f"Aucune image trouvée pour {platform_name} (cherché: {preferred_path}, {explicit_image_path}, default.png)") + for path in candidates: + if path and os.path.exists(path): + return pygame.image.load(path).convert_alpha() + + # default.png (save d'abord, sinon app) + default_save = os.path.join(save_images, "default.png") + if os.path.exists(default_save): + return pygame.image.load(default_save).convert_alpha() + default_app = os.path.join(app_images, "default.png") + if os.path.exists(default_app): + return pygame.image.load(default_app).convert_alpha() + + logger.error( + f"Aucune image trouvée pour {platform_name}. Candidats: " + + ", ".join(candidates) + + f"; default cherchés: {default_save}, {default_app}" + ) return None except Exception as e: logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}") diff --git a/windows/RGSX Retrobat.bat b/windows/RGSX Retrobat.bat index 37e0db2..131a112 100644 --- a/windows/RGSX Retrobat.bat +++ b/windows/RGSX Retrobat.bat @@ -2,8 +2,16 @@ setlocal EnableDelayedExpansion :: Fichier de log -if not exist %CD%\logs MD %CD%\logs -set LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt +if not exist "%CD%\logs" MD "%CD%\logs" +set "LOG_FILE=%CD%\logs\Retrobat_RGSX_log.txt" +:: Fichier de log (chemin absolu pour fiabilité) +:: Détecter la racine (ROOT_DIR) d'abord pour construire un chemin stable +set CURRENT_DIR=%CD% +pushd "%CURRENT_DIR%\..\.." +set "ROOT_DIR=%CD%" +popd +if not exist "%ROOT_DIR%\roms\windows\logs" MD "%ROOT_DIR%\roms\windows\logs" +set "LOG_FILE=%ROOT_DIR%\roms\windows\logs\Retrobat_RGSX_log.txt" :: Ajouter un horodatage au début du log echo [%DATE% %TIME%] Script start >> "%LOG_FILE%" @@ -25,9 +33,13 @@ popd :: Définir le chemin du script principal selon les spécifications set "MAIN_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\__main__.py" +:: Definir le chemin du script de mise à jour de la gamelist Windows +set "UPDATE_GAMELIST_SCRIPT=%ROOT_DIR%\roms\ports\RGSX\update_gamelist_windows.py" + :: Convertir les chemins relatifs en absolus avec pushd/popd pushd "%ROOT_DIR%\system\tools\Python" set "PYTHON_EXE_FULL=%ROOT_DIR%\system\tools\Python\!PYTHON_EXE!" +set "PYTHONW_EXE_FULL=%ROOT_DIR%\system\tools\Python\pythonw.exe" popd :: Afficher et logger les variables @@ -37,6 +49,7 @@ echo CURRENT_DIR : !CURRENT_DIR! >> "%LOG_FILE%" echo ROOT_DIR : !ROOT_DIR! >> "%LOG_FILE%" echo PYTHON_EXE_FULL : !PYTHON_EXE_FULL! >> "%LOG_FILE%" echo MAIN_SCRIPT : !MAIN_SCRIPT! >> "%LOG_FILE%" +echo UPDATE_GAMELIST_SCRIPT : !UPDATE_GAMELIST_SCRIPT! >> "%LOG_FILE%" :: Vérifier si l'exécutable Python existe echo Checking python.exe... @@ -101,16 +114,35 @@ if not exist "!MAIN_SCRIPT!" ( echo __main__.py found. echo [%DATE% %TIME%] __main__.py found. >> "%LOG_FILE%" -:: Exécuter le script Python -echo Executing __main__.py... -echo [%DATE% %TIME%] Executing "!MAIN_SCRIPT!" with !PYTHON_EXE_FULL! >> "%LOG_FILE%" -"!PYTHON_EXE_FULL!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1 -if %ERRORLEVEL% equ 0 ( +:: L'étape de mise à jour de la gamelist est désormais appelée depuis __main__.py +echo [%DATE% %TIME%] Skipping external gamelist update (handled in app). >> "%LOG_FILE%" + +echo Launching __main__.py (attached)... +echo [%DATE% %TIME%] Preparing to launch main. >> "%LOG_FILE%" + +:: Assurer le bon dossier de travail pour l'application +cd /d "%ROOT_DIR%\roms\ports\RGSX" + +:: Forcer les drivers SDL côté Windows et réduire le bruit console +set PYGAME_HIDE_SUPPORT_PROMPT=1 +set SDL_VIDEODRIVER=windows +set SDL_AUDIODRIVER=directsound +echo [%DATE% %TIME%] CWD before launch: %CD% >> "%LOG_FILE%" + +:: Lancer l'application dans la même console et attendre sa fin +:: Forcer python.exe pour capturer la sortie +set "PY_MAIN_EXE=!PYTHON_EXE_FULL!" +echo [%DATE% %TIME%] Using interpreter: !PY_MAIN_EXE! >> "%LOG_FILE%" +echo [%DATE% %TIME%] Launching "!MAIN_SCRIPT!" now... >> "%LOG_FILE%" +"!PY_MAIN_EXE!" "!MAIN_SCRIPT!" >> "%LOG_FILE%" 2>&1 +set EXITCODE=!ERRORLEVEL! +echo [%DATE% %TIME%] __main__.py exit code: !EXITCODE! >> "%LOG_FILE%" +if "!EXITCODE!"=="0" ( echo Execution finished successfully. echo [%DATE% %TIME%] Execution of __main__.py finished successfully. >> "%LOG_FILE%" ) else ( - echo Error: Failed to execute __main__.py (code %ERRORLEVEL%). - echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code %ERRORLEVEL%. >> "%LOG_FILE%" + echo Error: Failed to execute __main__.py (code !EXITCODE!). + echo [%DATE% %TIME%] Error: Failed to execute __main__.py with error code !EXITCODE!. >> "%LOG_FILE%" goto :error )