From 45f5d8bf7b35944531870b4bbb153e1eb9535900 Mon Sep 17 00:00:00 2001 From: skymike03 Date: Fri, 12 Sep 2025 17:00:51 +0200 Subject: [PATCH] v2.2.2.2 - add new instructions on menus to describe each function - upgrade controller_debug.pygame file to create a controller support - update command-line interface to be more effiscient and readable --- README_CLI.md | 214 +++++++++++++++------ README_CLI_EN.md | 174 +++++++++++++---- ports/RGSX/__main__.py | 20 +- ports/RGSX/config.py | 7 +- ports/RGSX/display.py | 151 +++++++++++++++ ports/RGSX/languages/de.json | 23 +++ ports/RGSX/languages/en.json | 25 ++- ports/RGSX/languages/es.json | 23 +++ ports/RGSX/languages/fr.json | 23 +++ ports/RGSX/languages/it.json | 23 +++ ports/RGSX/languages/pt.json | 23 +++ ports/RGSX/network.py | 1 + ports/RGSX/rgsx_cli.py | 336 ++++++++++++++++++++++++++++++--- pygame/controller_debug.pygame | 44 ++--- 14 files changed, 925 insertions(+), 162 deletions(-) diff --git a/README_CLI.md b/README_CLI.md index 6d3ddc1..9bad705 100644 --- a/README_CLI.md +++ b/README_CLI.md @@ -2,11 +2,114 @@ Ce guide couvre toutes les commandes disponibles du CLI et fournit des exemples prêts à copier (Windows PowerShell). +## Nouveau: mode interactif +Vous pouvez maintenant lancer une session interactive et enchaîner les commandes sans retaper `python rgsx_cli.py` à chaque fois : + +```powershell +python rgsx_cli.py +``` +Vous verrez : +``` +RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit. +rgsx> +``` +Dans cette session tapez directement les sous-commandes : +``` +rgsx> platforms +rgsx> games --platform snes --search mario +rgsx> download --platform snes --game "Super Mario World (USA).zip" +rgsx> history --tail 10 +rgsx> exit +``` +Extras : +- `help` ou `?` affiche l’aide globale. +- `exit` ou `quit` quitte la session. +- `--verbose` une fois active les logs détaillés pour toute la session. + +## Tableau formaté (platforms) +La commande `platforms` affiche maintenant un tableau ASCII à largeur fixe (sauf avec `--json`) : +``` ++--------------------------------+-----------------+ +| Nom de plateforme | Dossier | ++--------------------------------+-----------------+ +| Nintendo Entertainment System | nes | +| Super Nintendo Entertainment.. | snes | +| Sega Mega Drive | megadrive | ++--------------------------------+-----------------+ +``` +Colonnes : 30 caractères pour le nom, 15 pour le dossier (troncature par `...`). + +## Aliases & synonymes d’options (mis à jour) +Aliases des sous-commandes : +- `platforms` → `p` +- `games` → `g` +- `download` → `dl` +- `clear-history` → `clear` + +Options équivalentes (toutes les formes listées sont acceptées) : +- Plateforme : `--platform`, `--p`, `-p` +- Jeu : `--game`, `--g`, `-g` +- Recherche : `--search`, `--s`, `-s` +- Forcer (download) : `--force`, `-f` +- Mode interactif (download) : `--interactive`, `-i` + +Exemples avec alias : +```powershell +python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip" +python rgsx_cli.py g --p snes --s mario +python rgsx_cli.py p --json +python rgsx_cli.py clear +``` + +## Sélection ambiguë lors d’un download (nouveau tableau) +Quand vous tentez un téléchargement avec un titre non exact et que le mode interactif est actif (TTY ou `--interactive`), les correspondances s’affichent en tableau : +``` +No exact result found for this game: mario super yoshi +Select a match to download: ++------+--------------------------------------------------------------+------------+ +| # | Title | Size | ++------+--------------------------------------------------------------+------------+ +| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M | +| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M | +| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M | +| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M | +| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M | ++------+--------------------------------------------------------------+------------+ +Enter number (or press Enter to cancel): +``` +Si vous annulez ou que le mode interactif n’est pas actif, un tableau similaire est affiché (sans le prompt) suivi d’un conseil. + +## Recherche améliorée (multi‑tokens) pour `games` +L’option `--search` / `--s` / `-s` utilise maintenant la même logique de classement que les suggestions du download : +1. Correspondance sous-chaîne (position la plus tôt) — priorité 0 +2. Séquence de tokens dans l’ordre (non contiguë) — priorité 1 (écart le plus faible) +3. Tous les tokens présents dans n’importe quel ordre — priorité 2 (ensemble de tokens plus petit privilégié) + +Les doublons sont dédupliqués en gardant le meilleur score. Ainsi une requête : +```powershell +python rgsx_cli.py games --p snes --s "super mario yoshi" +``` +affiche toutes les variantes pertinentes de "Super Mario World 2 - Yoshi's Island" même si l’ordre des mots diffère. + +Exemple de sortie : +``` ++--------------------------------------------------------------+------------+ +| Game Title | Size | ++--------------------------------------------------------------+------------+ +| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M | +| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M | +| Super Mario - Yoshi Island (Japan).zip | 3.2M | +| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M | +| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M | ++--------------------------------------------------------------+------------+ +``` +Si aucun résultat n’est trouvé, seul l’en-tête est affiché puis un message. + ## Prérequis - Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne s’ouvrira). - Exécuter depuis le dossier contenant `rgsx_cli.py`. -## Syntaxe générale +## Syntaxe générale (mode classique) Les options globales peuvent être placées avant ou après la sous-commande. - Forme 1: @@ -18,73 +121,69 @@ Les options globales peuvent être placées avant ou après la sous-commande. python rgsx_cli.py [options] [--verbose] [--force-update|-force-update] ``` -- `--verbose` active les logs détaillés (DEBUG) sur la sortie standard d’erreur. +- `--verbose` active les logs détaillés (DEBUG) sur stderr. - `--force-update` (ou `-force-update`) purge les données locales et force le re-téléchargement du pack de données (systems_list, games/*.json, images). -Lorsque les données sources sont manquantes, le CLI télécharge et extrait automatiquement le pack (avec une barre de progression). +Quand les données sources sont manquantes, le CLI télécharge et extrait automatiquement le pack (avec progression). ## Commandes -### 1) platforms — lister les plateformes +### 1) platforms (`platforms` / `p`) — lister les plateformes - Options: - `--json`: sortie JSON (objets `{ name, folder }`). Exemples: ```powershell python rgsx_cli.py platforms -python rgsx_cli.py platforms --json -python rgsx_cli.py --verbose platforms -python rgsx_cli.py platforms --verbose +python rgsx_cli.py p --json +python rgsx_cli.py --verbose p +python rgsx_cli.py p --verbose ``` Sortie texte: une ligne par plateforme, au format `NomDossier`. -### 2) games — lister les jeux d’une plateforme +### 2) games (`games` / `g`) — lister les jeux d’une plateforme - Options: - - `--platform ` (ex: `n64` ou "Nintendo 64"). - - `--search `: filtre par sous-chaîne dans le nom du jeu. + - `--platform | --p | -p ` (ex: `n64` ou "Nintendo 64"). + - `--search | --s | -s `: filtre par sous-chaîne. Exemples: ```powershell python rgsx_cli.py games --platform n64 -python rgsx_cli.py games --platform "Nintendo 64" --search zelda -python rgsx_cli.py games --platform n64 --verbose +python rgsx_cli.py g --p "Nintendo 64" --s zelda +python rgsx_cli.py g -p n64 --verbose ``` Remarques: -- La plateforme est résolue par nom affiché (platform_name) ou par dossier (folder), sans tenir compte de la casse. +- La plateforme est résolue par nom affiché (platform_name) ou dossier, insensible à la casse. -### 3) download — télécharger un jeu +### 3) download (`download` / `dl`) — télécharger un jeu - Options: - - `--platform ` - - `--game ""` - - `--force`: ignorer l’avertissement si l’extension du fichier n’est pas répertoriée comme supportée pour la plateforme. + - `--platform | --p | -p ` + - `--game | --g | -g ""` + - `--force | -f`: ignorer l’avertissement d’extension non supportée. + - `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact n’est trouvé. Exemples: ```powershell # Titre exact -python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" +python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" -# Correspondance partielle -# Si aucun titre exact n’est trouvé, le CLI n’autosélectionne plus. Il affiche des correspondances possibles. -python rgsx_cli.py download --platform n64 --game "Ocarina of Time (Beta)" -# ➜ Le CLI proposera une liste de titres potentiels (à relancer ensuite avec le titre exact). +# Titre partiel (sélection numérotée si aucun exact) +python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)" -Mode interactif par défaut: -- Si aucun titre exact n’est trouvé et que vous êtes dans un terminal interactif (TTY), une liste numérotée s’affiche automatiquement pour choisir un match et lancer le téléchargement. +# Forcer malgré extension +python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f -# Forcer si l’extension semble non supportée (ex: .rar) -python rgsx_cli.py download --platform snes --game "pack_roms.rar" --force - -# Verbose positionné après la sous-commande -python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose +# Verbose après sous-commande +python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose ``` -Pendant le téléchargement, une progression en pourcentage, taille (MB) et vitesse (MB/s) s’affiche. Le résultat final est également écrit dans l’historique. +Pendant le téléchargement: progression %, taille (MB), vitesse (MB/s). Résultat final aussi dans l’historique. Notes: -- Les ROMs sont enregistrées dans le dossier de la plateforme correspondante (ex: `R:\roms\n64`). -- Si le fichier est une archive (zip/rar) et que la plateforme ne supporte pas l’extension, un avertissement est affiché (vous pouvez utiliser `--force`). +- Les ROMs sont enregistrées dans le dossier plateforme correspondant (ex: `R:\roms\n64`). +- Si le fichier est une archive (zip/rar) et que la plateforme ne supporte pas l’extension, un avertissement apparaît (utiliser `--force`). ### 4) history — afficher l’historique - Options: @@ -98,62 +197,55 @@ python rgsx_cli.py history --tail 20 python rgsx_cli.py history --json ``` -### 5) clear-history — vider l’historique +### 5) clear-history (`clear-history` / `clear`) — vider l’historique Exemple: ```powershell -python rgsx_cli.py clear-history +python rgsx_cli.py clear ``` ### Option globale: --force-update — purge + re-téléchargement des données -- Supprime `systems_list.json`, le dossier `games/` et `images/`, puis télécharge/extrait à nouveau le pack de données. +- Supprime `systems_list.json`, `games/`, `images/` puis retélécharge/extrait le pack. Exemples: ```powershell -# Sans sous-commande: purge + re-téléchargement puis sortie python rgsx_cli.py --force-update - -# Placé après une sous-commande (accepté aussi) -python rgsx_cli.py platforms --force-update +python rgsx_cli.py p --force-update ``` ## Comportements et conseils -- Résolution de plateforme: par nom affiché ou dossier, insensible à la casse. Pour la commande `games` et `download`, une recherche par sous-chaîne est utilisée si la correspondance exacte n’est pas trouvée. -- Logs `--verbose`: principalement utiles lors des téléchargements/extractions; émis en DEBUG. -- Téléchargement de données manquantes: automatique avec progression harmonisée (téléchargement puis extraction). +- Résolution plateforme: par nom affiché ou dossier, insensible à la casse. +- `--verbose`: utile surtout pour téléchargements/extractions. +- Données manquantes: téléchargement + extraction automatiques. - Codes de sortie (indicatif): - `0`: succès - - `1`: échec du téléchargement/erreur générique + - `1`: échec téléchargement/erreur générique - `2`: plateforme introuvable - `3`: jeu introuvable - `4`: extension non supportée (sans `--force`) ## Exemples rapides (copier-coller) ```powershell -# Lister plateformes (texte) -python rgsx_cli.py platforms +# Démarrer le shell interactif +python rgsx_cli.py + +# Lister plateformes (alias) +python rgsx_cli.py p # Lister plateformes (JSON) -python rgsx_cli.py platforms --json +python rgsx_cli.py p --json -# Lister jeux N64 avec filtre -python rgsx_cli.py games --platform n64 --search zelda +# Lister jeux N64 avec filtre (synonymes) +python rgsx_cli.py g --p n64 --s zelda -# Télécharger un jeu N64 (titre exact) -python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" +# Télécharger un jeu N64 (titre exact) avec alias +python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" -# Télécharger un jeu N64 (titre aproximatif) -python rgsx_cli.py download --platform n64 --game "Ocarina of Time" -Resultat (exemple) : -No exact result found for this game: Ocarina of Time -Select a match to download: - 1. Legend of Zelda, The - Ocarina of Time (Europe) (Beta) (2003-02-13) (GameCube).zip - 2. Legend of Zelda, The - Ocarina of Time (Europe) (Beta) (2003-02-21) (GameCube) (Debug).zip - ... - 15. F-Zero X (USA) (Beta) (The Legend of Zelda - Ocarina of Time leftover data).zip +# Télécharger (titre partiel) + sélection +python rgsx_cli.py dl -p n64 -g "Ocarina of Time" -# Voir l’historique (20 dernières entrées) +# Historique (20 dernières entrées) python rgsx_cli.py history --tail 20 -# Purger et recharger les données de listes des systèmes et des jeux +# Purger et recharger le pack python rgsx_cli.py --force-update ``` diff --git a/README_CLI_EN.md b/README_CLI_EN.md index b2f5426..edb0cf7 100644 --- a/README_CLI_EN.md +++ b/README_CLI_EN.md @@ -6,7 +6,110 @@ This guide covers all available CLI commands with copy-ready Windows PowerShell - Python installed and on PATH (the app runs in headless mode; no window will open). - Run commands from the folder that contains `rgsx_cli.py`. -## General syntax +## Quick interactive mode (new) +You can now start an interactive shell once and issue multiple commands without retyping `python rgsx_cli.py` each time: + +```powershell +python rgsx_cli.py +``` +You will see a prompt like: +``` +RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit. +rgsx> +``` +Inside this shell type subcommands exactly as you would after `python rgsx_cli.py`: +``` +rgsx> platforms +rgsx> games --platform snes --search mario +rgsx> download --platform snes --game "Super Mario World (USA).zip" +rgsx> history --tail 10 +rgsx> exit +``` +Extras: +- `help` or `?` prints the global help. +- `exit` or `quit` leaves the shell. +- `--verbose` once sets persistent verbose logging for the rest of the session. + +## Formatted table output (platforms) +The `platforms` command now renders a fixed-width ASCII table (unless `--json` is used): +``` ++--------------------------------+-----------------+ +| Platform Name | Folder | ++--------------------------------+-----------------+ +| Nintendo Entertainment System | nes | +| Super Nintendo Entertainment.. | snes | +| Sega Mega Drive | megadrive | ++--------------------------------+-----------------+ +``` +Columns: 30 chars for name, 15 for folder (values longer are truncated with `...`). + +## Aliases & option synonyms (updated) +Subcommand aliases: +- `platforms` → `p` +- `games` → `g` +- `download` → `dl` +- `clear-history` → `clear` + +Option aliases (all shown forms are accepted; they are equivalent): +- Platform: `--platform`, `--p`, `-p` +- Game: `--game`, `--g`, `-g` +- Search: `--search`, `--s`, `-s` +- Force (download): `--force`, `-f` +- Interactive (download): `--interactive`, `-i` + +Examples with aliases: +```powershell +python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip" +python rgsx_cli.py g --p snes --s mario +python rgsx_cli.py p --json +python rgsx_cli.py clear +``` + +## Ambiguous download selection (new table) +When you attempt a download with a non-exact title and interactive mode is active (TTY or `--interactive`), matches are displayed in a table: +``` +No exact result found for this game: mario super yoshi +Select a match to download: ++------+--------------------------------------------------------------+------------+ +| # | Title | Size | ++------+--------------------------------------------------------------+------------+ +| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M | +| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M | +| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M | +| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M | +| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M | ++------+--------------------------------------------------------------+------------+ +Enter number (or press Enter to cancel): +``` +If you cancel or are not in interactive mode, a similar table is still shown (without the prompt) followed by a tip. + +## Improved fuzzy search for games (multi-token) +The `--search` / `--s` / `-s` option now uses the same multi-strategy ranking as the download suggestion logic: +1. Substring match (position-based) — highest priority +2. Ordered non-contiguous token sequence (smallest gap wins) +3. All tokens present in any order (smaller token set size wins) + +Duplicate titles are deduplicated by keeping the best scoring strategy. This means queries like: +```powershell +python rgsx_cli.py games --p snes --s "super mario yoshi" +``` +will surface all relevant "Super Mario World 2 - Yoshi's Island" variants even if the word order differs. + +Example output: +``` ++--------------------------------------------------------------+------------+ +| Game Title | Size | ++--------------------------------------------------------------+------------+ +| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M | +| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M | +| Super Mario - Yoshi Island (Japan).zip | 3.2M | +| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M | +| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M | ++--------------------------------------------------------------+------------+ +``` +If no results are found the table displays only headers followed by a message. + +## General syntax (non-interactive) Global options can be placed before or after the subcommand. - Form 1: @@ -25,59 +128,55 @@ When source data is missing, the CLI will automatically download and extract the ## Commands -### 1) platforms — list platforms +### 1) platforms (`platforms` / `p`) — list platforms - Options: - `--json`: JSON output (objects `{ name, folder }`). Examples: ```powershell python rgsx_cli.py platforms -python rgsx_cli.py platforms --json -python rgsx_cli.py --verbose platforms -python rgsx_cli.py platforms --verbose +python rgsx_cli.py p --json +python rgsx_cli.py --verbose p +python rgsx_cli.py p --verbose ``` Text output: one line per platform, formatted as `NameFolder`. -### 2) games — list games for a platform +### 2) games (`games` / `g`) — list games for a platform - Options: - - `--platform ` (e.g., `n64` or "Nintendo 64"). - - `--search `: filter by substring in game title. + - `--platform | --p | -p ` (e.g., `n64` or "Nintendo 64"). + - `--search | --s | -s `: filter by substring in game title. Examples: ```powershell python rgsx_cli.py games --platform n64 -python rgsx_cli.py games --platform "Nintendo 64" --search zelda -python rgsx_cli.py games --platform n64 --verbose +python rgsx_cli.py g --p "Nintendo 64" --s zelda +python rgsx_cli.py g -p n64 --verbose ``` Notes: - The platform is resolved by display name (platform_name) or folder, case-insensitively. -### 3) download — download a game +### 3) download (`download` / `dl`) — download a game - Options: - - `--platform ` - - `--game ""` - - `--force`: ignore unsupported-extension warning for the platform. + - `--platform | --p | -p ` + - `--game | --g | -g ""` + - `--force | -f`: ignore unsupported-extension warning for the platform. + - `--interactive | -i`: prompt to choose from matches when no exact title is found. Examples: ```powershell # Exact title -python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" +python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" -# Partial match -# If no exact title is found, the CLI no longer auto-selects; it displays suggestions. -python rgsx_cli.py download --platform n64 --game "Ocarina of Time (Beta)" -# ➜ The CLI shows a list of candidates (then run again with the exact title). +# Partial match (interactive numbered selection if no exact match) +python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)" -Interactive mode by default: -- If no exact title is found and you are in an interactive terminal (TTY), a numbered list is shown automatically so you can pick and start the download. +# Forced despite extension +python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f -# Force if the file extension seems unsupported (e.g., .rar) -python rgsx_cli.py download --platform snes --game "pack_roms.rar" --force - -# Verbose placed after the subcommand -python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose +# Verbose after subcommand +python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose ``` During download, progress %, size (MB) and speed (MB/s) are shown. The final result is also written to history. @@ -98,10 +197,10 @@ python rgsx_cli.py history --tail 20 python rgsx_cli.py history --json ``` -### 5) clear-history — clear history +### 5) clear-history (`clear-history` / `clear`) — clear history Example: ```powershell -python rgsx_cli.py clear-history +python rgsx_cli.py clear ``` ### Global option: --force-update — purge + re-download data @@ -113,7 +212,7 @@ Examples: python rgsx_cli.py --force-update # Placed after a subcommand (also accepted) -python rgsx_cli.py platforms --force-update +python rgsx_cli.py p --force-update ``` ## Behavior and tips @@ -129,20 +228,23 @@ python rgsx_cli.py platforms --force-update ## Quick examples (copy/paste) ```powershell +# Start interactive shell +python rgsx_cli.py + # List platforms (text) -python rgsx_cli.py platforms +python rgsx_cli.py p # List platforms (JSON) -python rgsx_cli.py platforms --json +python rgsx_cli.py p --json -# List N64 games with filter -python rgsx_cli.py games --platform n64 --search zelda +# List N64 games with filter (using alias synonyms) +python rgsx_cli.py g --p n64 --s zelda -# Download an N64 game (exact title) -python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" +# Download an N64 game (exact title) using aliases +python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" # Download with approximate title (suggestions + interactive pick) -python rgsx_cli.py download --platform n64 --game "Ocarina of Time" +python rgsx_cli.py dl -p n64 -g "Ocarina of Time" # View last 20 history entries python rgsx_cli.py history --tail 20 diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index 0abfd8d..0680d6a 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -215,38 +215,38 @@ else: # Détection spécifique Elite AVANT la détection générique Xbox if ("microsoft xbox controller" in lname): config.xbox_elite_controller = True - logger.debug(f"Controller detected (Xbox Elite): {name}") - print(f"Controller detected (Xbox Elite): {name}") + logger.debug(f"Controller detected: {name}") + print(f"Controller detected: {name}") break if ("xbox" in lname) or ("x-box" in lname) or ("xinput" in lname) or ("microsoft x-box" in lname) or ("x-box 360" in lname) or ("360" in lname): config.xbox_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"Xbox Controller detected : {name}") print(f"Controller detected : {name}") break - elif "playstation" in lname: + elif "playstation" in lname or "ps3" in lname or "sony" in lname: config.playstation_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"Playstation Controller detected : {name}") print(f"Controller detected : {name}") break elif "nintendo" in lname: config.nintendo_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"Nintendo Controller detected : {name}") print(f"Controller detected : {name}") elif "trimui" in lname: config.trimui_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"Trimui Controller detected : {name}") print(f"Controller detected : {name}") elif "logitech" in lname: config.logitech_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"Logitech Controller detected : {name}") print(f"Controller detected : {name}") elif "8bitdo" in lname or "8-bitdo" in lname: config.eightbitdo_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"8bitdoController detected : {name}") print(f"Controller detected : {name}") elif "steam" in lname: config.steam_controller = True - logger.debug(f"Controller detected : {name}") + logger.debug(f"Steam Controller detected : {name}") print(f"Controller detected : {name}") # Note: virtual keyboard display now depends on controller presence (config.joystick) logger.debug(f"Flags contrôleur: xbox={config.xbox_controller}, ps={config.playstation_controller}, nintendo={config.nintendo_controller}, eightbitdo={config.eightbitdo_controller}, steam={config.steam_controller}, trimui={config.trimui_controller}, logitech={config.logitech_controller}, generic={config.generic_controller}") diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 5cca260..48a1a63 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -13,7 +13,7 @@ except Exception: pygame = None # type: ignore # Version actuelle de l'application -app_version = "2.2.2.1" +app_version = "2.2.2.2" def get_application_root(): @@ -173,14 +173,9 @@ batch_download_indices = [] # File d'attente des indices de jeux à traiter en batch_in_progress = False # Indique qu'un lot est en cours batch_pending_game = None # Données du jeu en attente de confirmation d'extension -# --- Premium systems filtering --- -# Liste des marqueurs (substrings) indiquant qu'un système/plateforme requiert un compte premium ou une clé API. -# On teste la présence (case-insensitive) de ces marqueurs dans le nom du système (ex: "Microsoft Windows (1Fichier)"). -# Ajoutez librement d'autres valeurs (ex: 'RealDebrid', 'AllDebrid') si de futurs systèmes nécessitent un compte. PREMIUM_HOST_MARKERS = [ "1Fichier", ] -# Flag runtime contrôlant le masquage des systèmes premium dans le menu pause > games. hide_premium_systems = False # Indicateurs d'entrée (détectés au démarrage) diff --git a/ports/RGSX/display.py b/ports/RGSX/display.py index 9908da6..b134946 100644 --- a/ports/RGSX/display.py +++ b/ports/RGSX/display.py @@ -1389,6 +1389,28 @@ def draw_language_menu(screen): instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y)) screen.blit(instruction_surface, instruction_rect) +def draw_menu_instruction(screen, instruction_text, last_button_bottom=None): + """Dessine une ligne d'instruction centrée au-dessus du footer. + + - Réserve une zone footer (72px) + marge bas. + - Si last_button_bottom est fourni, s'assure d'un écart minimal (16px). + - Utilise la petite police et couleurs du thème. + """ + if not instruction_text: + return + try: + instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"]) + footer_reserved = 72 + bottom_margin = 12 + instruction_y = config.screen_height - footer_reserved - bottom_margin + min_gap = 16 + if last_button_bottom is not None and instruction_y - last_button_bottom < min_gap: + instruction_y = last_button_bottom + min_gap + instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y)) + screen.blit(instruction_surface, instruction_rect) + except Exception as e: + logger.error(f"Erreur draw_menu_instruction: {e}") + def draw_display_menu(screen): """Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés).""" screen.blit(OVERLAY, (0, 0)) @@ -1494,6 +1516,28 @@ def draw_pause_menu(screen, selected_option): ) config.pause_menu_total_options = len(options) + # Instruction contextuelle pour l'option sélectionnée + # Mapping des clés i18n parallèles à la liste options (même ordre) + instruction_keys = [ + "instruction_pause_language", + "instruction_pause_controls", + "instruction_pause_display", + "instruction_pause_games", + "instruction_pause_settings", + "instruction_pause_restart", + "instruction_pause_quit", + ] + try: + key = instruction_keys[selected_option] + instruction_text = _(key) + except Exception: + instruction_text = "" # Sécurité si index hors borne + + if instruction_text: + # Calcul de la position du dernier bouton pour éviter chevauchement + last_button_bottom = menu_y + margin_top_bottom + (len(options) - 1) * (button_height + 12) + button_height + draw_menu_instruction(screen, instruction_text, last_button_bottom) + def _draw_submenu_generic(screen, title, options, selected_index): """Helper générique pour dessiner un sous-menu hiérarchique.""" screen.blit(OVERLAY, (0, 0)) @@ -1529,6 +1573,57 @@ def draw_pause_controls_menu(screen, selected_index): _("menu_back") if _ else "Back" ] _draw_submenu_generic(screen, _("menu_controls") if _ else "Controls", options, selected_index) + # Instructions contextuelles + instruction_keys = [ + "instruction_controls_help", # pour menu_controls (afficher l'aide) + "instruction_controls_remap", # remap + "instruction_generic_back", # retour + ] + key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None + if key: + last_button_bottom = None # recalculer via géométrie si nécessaire; ici on réutilise calcul simple + # Reconstituer la position du dernier bouton comme dans _draw_submenu_generic + menu_width = int(config.screen_width * 0.72) + button_height = int(config.screen_height * 0.045) + margin_top_bottom = 26 + menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom + menu_y = (config.screen_height - menu_height) // 2 + # Title height approximatif + title_surface = config.font.render("X", True, THEME_COLORS["text"]) # hauteur représentative + title_rect_height = title_surface.get_height() + start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 # approx: title center adjust + bottom spacing + last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height + text = _(key) + if key == "instruction_display_hide_premium": + # Inject dynamic list of premium providers from config.PREMIUM_HOST_MARKERS + try: + from config import PREMIUM_HOST_MARKERS + # Clean, preserve order, remove duplicates (case-insensitive) + seen = set() + providers_clean = [] + for p in PREMIUM_HOST_MARKERS: + if not p: continue + norm = p.strip() + if not norm: continue + low = norm.lower() + if low in seen: continue + seen.add(low) + providers_clean.append(norm) + providers_str = ", ".join(providers_clean) + if not providers_str: + providers_str = "-" + if "{providers}" in text: + try: + text = text.format(providers=providers_str) + except Exception: + # Fallback if formatting fails + text = f"{text.replace('{providers}','').strip()} {providers_str}".strip() + else: + # Append providers if placeholder missing (backward compatibility) + text = f"{text} : {providers_str}" if providers_str else text + except Exception: + pass + draw_menu_instruction(screen, text, last_button_bottom) def draw_pause_display_menu(screen, selected_index): from rgsx_settings import ( @@ -1583,6 +1678,28 @@ def draw_pause_display_menu(screen, selected_index): back_txt = _("menu_back") if _ else "Back" options = [layout_txt, font_txt, font_family_txt, unsupported_txt, unknown_txt, hide_premium_txt, filter_txt, back_txt] _draw_submenu_generic(screen, _("menu_display"), options, selected_index) + instruction_keys = [ + "instruction_display_layout", + "instruction_display_font_size", + "instruction_display_font_family", + "instruction_display_show_unsupported", + "instruction_display_unknown_ext", + "instruction_display_hide_premium", + "instruction_display_filter_platforms", + "instruction_generic_back", + ] + key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None + if key: + button_height = int(config.screen_height * 0.045) + menu_width = int(config.screen_width * 0.72) + margin_top_bottom = 26 + menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom + menu_y = (config.screen_height - menu_height) // 2 + title_surface = config.font.render("X", True, THEME_COLORS["text"]) + title_rect_height = title_surface.get_height() + start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 + last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height + draw_menu_instruction(screen, _(key), last_button_bottom) def draw_pause_games_menu(screen, selected_index): from rgsx_settings import get_sources_mode @@ -1594,6 +1711,23 @@ def draw_pause_games_menu(screen, selected_index): back_txt = _("menu_back") if _ else "Back" options = [history_txt, source_txt, update_txt, back_txt] _draw_submenu_generic(screen, _("menu_games") if _ else "Games", options, selected_index) + instruction_keys = [ + "instruction_games_history", + "instruction_games_source_mode", + "instruction_games_update_cache", + "instruction_generic_back", + ] + key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None + if key: + button_height = int(config.screen_height * 0.045) + margin_top_bottom = 26 + menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom + menu_y = (config.screen_height - menu_height) // 2 + title_surface = config.font.render("X", True, THEME_COLORS["text"]) + title_rect_height = title_surface.get_height() + start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 + last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height + draw_menu_instruction(screen, _(key), last_button_bottom) def draw_pause_settings_menu(screen, selected_index): from rgsx_settings import get_symlink_option @@ -1618,6 +1752,23 @@ def draw_pause_settings_menu(screen, selected_index): back_txt = _("menu_back") if _ else "Back" options = [music_option, symlink_option, api_keys_txt, back_txt] _draw_submenu_generic(screen, _("menu_settings_category") if _ else "Settings", options, selected_index) + instruction_keys = [ + "instruction_settings_music", + "instruction_settings_symlink", + "instruction_settings_api_keys", + "instruction_generic_back", + ] + key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None + if key: + button_height = int(config.screen_height * 0.045) + margin_top_bottom = 26 + menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom + menu_y = (config.screen_height - menu_height) // 2 + title_surface = config.font.render("X", True, THEME_COLORS["text"]) + title_rect_height = title_surface.get_height() + start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 + last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height + draw_menu_instruction(screen, _(key), last_button_bottom) def draw_pause_api_keys_status(screen): screen.blit(OVERLAY, (0,0)) diff --git a/ports/RGSX/languages/de.json b/ports/RGSX/languages/de.json index 97d378b..dfa80f3 100644 --- a/ports/RGSX/languages/de.json +++ b/ports/RGSX/languages/de.json @@ -155,4 +155,27 @@ "popup_hide_premium_off": "Premium-Systeme sichtbar" ,"submenu_display_font_family": "Schrift" ,"popup_font_family_changed": "Schrift geändert: {0}" + ,"instruction_pause_language": "Sprache der Oberfläche ändern" + ,"instruction_pause_controls": "Steuerungsübersicht ansehen oder neu zuordnen" + ,"instruction_pause_display": "Layout, Schriften und Systemsichtbarkeit konfigurieren" + ,"instruction_pause_games": "Verlauf öffnen, Quelle wechseln oder Liste aktualisieren" + ,"instruction_pause_settings": "Musik, Symlink-Option & API-Schlüsselstatus" + ,"instruction_pause_restart": "RGSX neu starten um Konfiguration neu zu laden" + ,"instruction_pause_quit": "RGSX Anwendung beenden" + ,"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen" + ,"instruction_controls_remap": "Tasten / Buttons neu zuordnen" + ,"instruction_generic_back": "Zum vorherigen Menü zurückkehren" + ,"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten" + ,"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen" + ,"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln" + ,"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden" + ,"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten" + ,"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}" + ,"instruction_display_filter_platforms": "Manuell wählen welche Systeme sichtbar sind" + ,"instruction_games_history": "Vergangene Downloads und Status anzeigen" + ,"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln" + ,"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren" + ,"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren" + ,"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten" + ,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen" } \ No newline at end of file diff --git a/ports/RGSX/languages/en.json b/ports/RGSX/languages/en.json index eab9ef0..0dac695 100644 --- a/ports/RGSX/languages/en.json +++ b/ports/RGSX/languages/en.json @@ -154,5 +154,28 @@ ,"popup_hide_premium_on": "Premium systems hidden" ,"popup_hide_premium_off": "Premium systems visible" ,"submenu_display_font_family": "Font" - ,"popup_font_family_changed": "Font changed: {0}" + ,"popup_font_family_changed": "Font changed: {0}", + "instruction_pause_language": "Change the interface language", + "instruction_pause_controls": "View control layout or start remapping", + "instruction_pause_display": "Configure layout, fonts and system visibility", + "instruction_pause_games": "Open history, switch source or refresh list", + "instruction_pause_settings": "Music, symlink option & API keys status", + "instruction_pause_restart": "Restart RGSX to reload configuration" + ,"instruction_pause_quit": "Exit the RGSX application" + ,"instruction_controls_help": "Show full controller & keyboard reference" + ,"instruction_controls_remap": "Change button / key bindings" + ,"instruction_generic_back": "Return to the previous menu" + ,"instruction_display_layout": "Cycle grid dimensions (columns × rows)" + ,"instruction_display_font_size": "Adjust text scale for readability" + ,"instruction_display_font_family": "Switch between available font families" + ,"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg" + ,"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg" + ,"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}" + ,"instruction_display_filter_platforms": "Manually choose which systems are visible" + ,"instruction_games_history": "List past downloads and statuses" + ,"instruction_games_source_mode": "Switch between RGSX or your own custom list source" + ,"instruction_games_update_cache": "Redownload & refresh current games list" + ,"instruction_settings_music": "Enable or disable background music playback" + ,"instruction_settings_symlink": "Toggle using filesystem symlinks for installs" + ,"instruction_settings_api_keys": "See detected premium provider API keys" } \ No newline at end of file diff --git a/ports/RGSX/languages/es.json b/ports/RGSX/languages/es.json index 641b3bd..0c1297c 100644 --- a/ports/RGSX/languages/es.json +++ b/ports/RGSX/languages/es.json @@ -155,4 +155,27 @@ "popup_hide_premium_off": "Sistemas Premium visibles" ,"submenu_display_font_family": "Fuente" ,"popup_font_family_changed": "Fuente cambiada: {0}" + ,"instruction_pause_language": "Cambiar el idioma de la interfaz" + ,"instruction_pause_controls": "Ver esquema de controles o remapear" + ,"instruction_pause_display": "Configurar distribución, fuentes y visibilidad de sistemas" + ,"instruction_pause_games": "Abrir historial, cambiar fuente o refrescar lista" + ,"instruction_pause_settings": "Música, opción symlink y estado de claves API" + ,"instruction_pause_restart": "Reiniciar RGSX para recargar configuración" + ,"instruction_pause_quit": "Salir de la aplicación RGSX" + ,"instruction_controls_help": "Mostrar referencia completa de mando y teclado" + ,"instruction_controls_remap": "Cambiar asignación de botones / teclas" + ,"instruction_generic_back": "Volver al menú anterior" + ,"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)" + ,"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad" + ,"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles" + ,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg" + ,"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg" + ,"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}" + ,"instruction_display_filter_platforms": "Elegir manualmente qué sistemas son visibles" + ,"instruction_games_history": "Ver descargas pasadas y su estado" + ,"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada" + ,"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos" + ,"instruction_settings_music": "Activar o desactivar música de fondo" + ,"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones" + ,"instruction_settings_api_keys": "Ver claves API premium detectadas" } \ No newline at end of file diff --git a/ports/RGSX/languages/fr.json b/ports/RGSX/languages/fr.json index 94b072d..dc7192b 100644 --- a/ports/RGSX/languages/fr.json +++ b/ports/RGSX/languages/fr.json @@ -155,4 +155,27 @@ "popup_hide_premium_off": "Systèmes Premium visibles" ,"submenu_display_font_family": "Police" ,"popup_font_family_changed": "Police changée : {0}" + ,"instruction_pause_language": "Changer la langue de l'interface" + ,"instruction_pause_controls": "Afficher la configuration ou remapper" + ,"instruction_pause_display": "Agencer l'affichage, polices et systèmes visibles" + ,"instruction_pause_games": "Historique, source de liste ou rafraîchissement" + ,"instruction_pause_settings": "Musique, option symlink & statut des clés API" + ,"instruction_pause_restart": "Redémarrer RGSX pour recharger la configuration" + ,"instruction_pause_quit": "Quitter l'application RGSX" + ,"instruction_controls_help": "Afficher la référence complète manette & clavier" + ,"instruction_controls_remap": "Modifier l'association boutons / touches" + ,"instruction_generic_back": "Revenir au menu précédent" + ,"instruction_display_layout": "Changer les dimensions de la grille" + ,"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité" + ,"instruction_display_font_family": "Basculer entre les polices disponibles" + ,"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg" + ,"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg" + ,"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}" + ,"instruction_display_filter_platforms": "Choisir manuellement les systèmes visibles" + ,"instruction_games_history": "Lister les téléchargements passés et leur statut" + ,"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée" + ,"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux" + ,"instruction_settings_music": "Activer ou désactiver la lecture musicale" + ,"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation" + ,"instruction_settings_api_keys": "Voir les clés API détectées des services premium" } \ No newline at end of file diff --git a/ports/RGSX/languages/it.json b/ports/RGSX/languages/it.json index c1835ab..ac7670f 100644 --- a/ports/RGSX/languages/it.json +++ b/ports/RGSX/languages/it.json @@ -155,4 +155,27 @@ "popup_hide_premium_off": "Sistemi Premium visibili" ,"submenu_display_font_family": "Font" ,"popup_font_family_changed": "Font cambiato: {0}" + ,"instruction_pause_language": "Cambiare la lingua dell'interfaccia" + ,"instruction_pause_controls": "Vedere schema controlli o avviare rimappatura" + ,"instruction_pause_display": "Configurare layout, font e visibilità sistemi" + ,"instruction_pause_games": "Aprire cronologia, cambiare sorgente o aggiornare elenco" + ,"instruction_pause_settings": "Musica, opzione symlink e stato chiavi API" + ,"instruction_pause_restart": "Riavvia RGSX per ricaricare la configurazione" + ,"instruction_pause_quit": "Uscire dall'applicazione RGSX" + ,"instruction_controls_help": "Mostrare riferimento completo controller & tastiera" + ,"instruction_controls_remap": "Modificare associazione pulsanti / tasti" + ,"instruction_generic_back": "Tornare al menu precedente" + ,"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)" + ,"instruction_display_font_size": "Regolare dimensione testo per leggibilità" + ,"instruction_display_font_family": "Cambiare famiglia di font disponibile" + ,"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg" + ,"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg" + ,"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}" + ,"instruction_display_filter_platforms": "Scegliere manualmente quali sistemi sono visibili" + ,"instruction_games_history": "Elencare download passati e stato" + ,"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata" + ,"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi" + ,"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo" + ,"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni" + ,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate" } \ No newline at end of file diff --git a/ports/RGSX/languages/pt.json b/ports/RGSX/languages/pt.json index 9a6c692..e07a2ca 100644 --- a/ports/RGSX/languages/pt.json +++ b/ports/RGSX/languages/pt.json @@ -155,4 +155,27 @@ "popup_hide_premium_off": "Sistemas Premium visíveis" ,"submenu_display_font_family": "Fonte" ,"popup_font_family_changed": "Fonte alterada: {0}" + ,"instruction_pause_language": "Alterar o idioma da interface" + ,"instruction_pause_controls": "Ver esquema de controles ou iniciar remapeamento" + ,"instruction_pause_display": "Configurar layout, fontes e visibilidade de sistemas" + ,"instruction_pause_games": "Abrir histórico, mudar fonte ou atualizar lista" + ,"instruction_pause_settings": "Música, opção symlink e status das chaves API" + ,"instruction_pause_restart": "Reiniciar RGSX para recarregar configuração" + ,"instruction_pause_quit": "Sair da aplicação RGSX" + ,"instruction_controls_help": "Mostrar referência completa de controle e teclado" + ,"instruction_controls_remap": "Modificar associação de botões / teclas" + ,"instruction_generic_back": "Voltar ao menu anterior" + ,"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)" + ,"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade" + ,"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis" + ,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg" + ,"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg" + ,"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}" + ,"instruction_display_filter_platforms": "Escolher manualmente quais sistemas são visíveis" + ,"instruction_games_history": "Listar downloads anteriores e status" + ,"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada" + ,"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos" + ,"instruction_settings_music": "Ativar ou desativar música de fundo" + ,"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações" + ,"instruction_settings_api_keys": "Ver chaves API premium detectadas" } \ No newline at end of file diff --git a/ports/RGSX/network.py b/ports/RGSX/network.py index c324ca5..9bb40f1 100644 --- a/ports/RGSX/network.py +++ b/ports/RGSX/network.py @@ -1233,6 +1233,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported= cancel_events.pop(task_id, None) logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}") return result[0], result[1] + def is_1fichier_url(url): """Détecte si l'URL est un lien 1fichier.""" return "1fichier.com" in url \ No newline at end of file diff --git a/ports/RGSX/rgsx_cli.py b/ports/RGSX/rgsx_cli.py index ec84979..44101b8 100644 --- a/ports/RGSX/rgsx_cli.py +++ b/ports/RGSX/rgsx_cli.py @@ -24,6 +24,53 @@ from rgsx_settings import get_sources_zip_url logger = logging.getLogger("rgsx.cli") +# Unified size display helper: preserve pre-formatted locale strings (MiB, GiB, Go, Mo, Ko, MB, KB, bytes). +# If numeric (int/float or pure digit string), convert to binary units with suffix B, KiB, MiB, GiB. +# Otherwise return original string. +def display_size(val): + try: + if val is None: + return '' + if isinstance(val, (list, tuple)): + return '' + s = str(val).strip() + if not s: + return '' + lower = s.lower() + # Already human formatted (English or French common units or contains a space + unit token) + known_tokens = ("mib", "gib", "kib", "kb", "mb", "gb", "bytes", " b", " mo", " go", " ko", "mb ", "gb ") + if any(tok in lower for tok in known_tokens): + return s + # Pure numeric => treat as bytes + import re as _re + if _re.fullmatch(r"\d+", s): + b = float(s) + else: + # Leading numeric? if not, return original + m = _re.match(r"^([0-9]+(?:\.[0-9]+)?)", s) + if not m: + return s + # If trailing unit unknown, assume already human string + if len(s) > len(m.group(0)): + return s + b = float(m.group(1)) + if b < 1024: + return f"{int(b)} B" + kib = b / 1024 + if kib < 1024: + return f"{kib:.2f} KiB" + mib = kib / 1024 + if mib < 1024: + return f"{mib:.2f} MiB" + gib = mib / 1024 + return f"{gib:.2f} GiB" + except Exception: + try: + return str(val) + except Exception: + return '' + + def setup_logging(verbose: bool): level = logging.DEBUG if verbose else logging.WARNING logging.basicConfig(level=level, format='%(levelname)s: %(message)s') @@ -153,9 +200,26 @@ def cmd_platforms(args): if getattr(args, 'json', False): print(json.dumps(items, ensure_ascii=False, indent=2)) else: + # Hint before table + print("hint: you can use either the exact platform name or folder in --platform (e.g. 'SNK Neo Geo' or 'neogeo')") + # ASCII table with fixed widths: name=35, folder=15 + NAME_W = 35 + FOLDER_W = 15 + def fmt_cell(text, width): + if len(text) <= width: + return text + ' ' * (width - len(text)) + if width <= 3: + return text[:width] + return text[:width-3] + '...' + border = "+" + "-" * (NAME_W + 2) + "+" + "-" * (FOLDER_W + 2) + "+" + header = f"| {'Platform Name'.ljust(NAME_W)} | {'Folder'.ljust(FOLDER_W)} |" + print(border) + print(header) + print(border) for it in items: - # name TAB folder (folder may be empty for BIOS/virtual) - print(f"{it['name']}\t{it['folder']}") + row = f"| {fmt_cell(it['name'], NAME_W)} | {fmt_cell(it['folder'], FOLDER_W)} |" + print(row) + print(border) def _resolve_platform(sources, platform_name: str): @@ -188,13 +252,100 @@ def cmd_games(args): or args.platform ) games = load_games(platform_id) + + # Fuzzy ranking similar to download suggestions when --search provided if args.search: - q = args.search.lower() - games = [g for g in games if q in (g[0] or '').lower()] + query_raw = args.search.strip() + def _strip_ext(name: str) -> str: + try: + base, _ = os.path.splitext(name) + return base + except Exception: + return name + def _tokens(s: str) -> list[str]: + return re.findall(r"[a-z0-9]+", s.lower()) + q_lower = query_raw.lower() + q_no_ext = _strip_ext(query_raw).lower() + q_tokens = _tokens(query_raw) + suggestions = [] # (priority, score, game_obj) + # 1) Substring match (full or sans extension) priority 0, score = position + for g in games: + title = g[0] if isinstance(g, (list, tuple)) and g else None + if not title: + continue + t_lower = title.lower() + t_no_ext = _strip_ext(t_lower) + pos_full = t_lower.find(q_lower) if q_lower else -1 + pos_noext = t_no_ext.find(q_no_ext) if q_no_ext else -1 + if pos_full != -1 or pos_noext != -1: + pos = pos_full if pos_full != -1 else pos_noext + suggestions.append((0, max(0, pos), g)) + # Helper for ordered gap score + def ordered_gap_score(qt: list[str], tt: list[str]): + pos = [] + start = 0 + for tok in qt: + try: + i = next(i for i in range(start, len(tt)) if tt[i] == tok) + except StopIteration: + return None + pos.append(i) + start = i + 1 + gap = (pos[-1] - pos[0]) - (len(qt) - 1) + return max(0, gap) + # 2) Ordered non-contiguous tokens (priority 1) + if q_tokens: + for g in games: + title = g[0] if isinstance(g, (list, tuple)) and g else None + if not title: + continue + tt = _tokens(title) + score = ordered_gap_score(q_tokens, tt) + if score is not None: + suggestions.append((1, score, g)) + # 3) All tokens present, any order (priority 2), score = token set size + if q_tokens: + for g in games: + title = g[0] if isinstance(g, (list, tuple)) and g else None + if not title: + continue + t_tokens = set(_tokens(title)) + if all(tok in t_tokens for tok in q_tokens): + suggestions.append((2, len(t_tokens), g)) + # Deduplicate by title keeping best (lowest priority, then score) + best = {} + for prio, score, g in suggestions: + title = g[0] if isinstance(g, (list, tuple)) and g else str(g) + key = title.lower() + cur = best.get(key) + if cur is None or (prio, score) < (cur[0], cur[1]): + best[key] = (prio, score, g) + ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower())) + games = [g for _, _, g in ranked] + # Table: Name (60) | Size (12) to allow "xxxx.xx MiB" + NAME_W = 60 + SIZE_W = 12 + def trunc(text, width): + if len(text) <= width: + return text + ' ' * (width - len(text)) + if width <= 3: + return text[:width] + return text[:width-3] + '...' + border = "+" + "-" * (NAME_W + 2) + "+" + "-" * (SIZE_W + 2) + "+" + header = f"| {'Game Title'.ljust(NAME_W)} | {'Size'.ljust(SIZE_W)} |" + print(border) + print(header) + print(border) for g in games: - # games items can be (name, url) or (name, url, size) title = g[0] if isinstance(g, (list, tuple)) and g else str(g) - print(title) + size_val = '' + if isinstance(g, (list, tuple)) and len(g) >= 3: + size_val = display_size(g[2]) + row = f"| {trunc(title, NAME_W)} | {trunc(size_val, SIZE_W)} |" + print(row) + print(border) + if args.search and not games: + print("No results for search.") def cmd_history(args): @@ -382,10 +533,37 @@ def cmd_download(args): interactive = bool(getattr(args, 'interactive', False)) if interactive: print("Select a match to download:") + # Tableau formaté: # (4) | Title (60) | Size (12) + NUM_W = 4 + TITLE_W = 60 + SIZE_W = 12 + def trunc(text, width): + if len(text) <= width: + return text + ' ' * (width - len(text)) + if width <= 3: + return text[:width] + return text[:width-3] + '...' + # Use shared display_size + border = "+" + "-" * (NUM_W + 2) + "+" + "-" * (TITLE_W + 2) + "+" + "-" * (SIZE_W + 2) + "+" + header = f"| {'#'.ljust(NUM_W)} | {'Title'.ljust(TITLE_W)} | {'Size'.ljust(SIZE_W)} |" + print(border) + print(header) + print(border) for i, s in enumerate(shown, start=1): - print(f" {i}. {s[2]}") + title = s[2] + size_val = '' + size_raw = None + for g in games: + if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3: + size_raw = g[2] + break + if size_raw is not None: + size_val = display_size(size_raw) + row = f"| {str(i).ljust(NUM_W)} | {trunc(title, TITLE_W)} | {trunc(size_val, SIZE_W)} |" + print(row) + print(border) if len(suggestions) > limit: - print(f" ... and {len(suggestions) - limit} more not shown") + print(f"... {len(suggestions) - limit} more not shown") try: choice = input("Enter number (or press Enter to cancel): ").strip() except EOFError: @@ -400,15 +578,41 @@ def cmd_download(args): pass if not match: print("Here are potential matches (use the exact title with --game):") + NUM_W = 4 + TITLE_W = 60 + SIZE_W = 12 + def trunc(text, width): + if len(text) <= width: + return text + ' ' * (width - len(text)) + if width <= 3: + return text[:width] + return text[:width-3] + '...' + # Use shared display_size + border = "+" + "-" * (NUM_W + 2) + "+" + "-" * (TITLE_W + 2) + "+" + "-" * (SIZE_W + 2) + "+" + header = f"| {'#'.ljust(NUM_W)} | {'Title'.ljust(TITLE_W)} | {'Size'.ljust(SIZE_W)} |" + print(border) + print(header) + print(border) for i, s in enumerate(shown, start=1): - print(f" {i}. {s[2]}") + title = s[2] + size_val = '' + size_raw = None + for g in games: + if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3: + size_raw = g[2] + break + if size_raw is not None: + size_val = display_size(size_raw) + row = f"| {str(i).ljust(NUM_W)} | {trunc(title, TITLE_W)} | {trunc(size_val, SIZE_W)} |" + print(row) + print(border) if len(suggestions) > limit: - print(f" ... and {len(suggestions) - limit} more") - print("Tip: list games with: python rgsx_cli.py games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw)) + print(f"... {len(suggestions) - limit} more") + print("Tip: list games with: games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw)) sys.exit(3) else: print("No similar titles found.") - print("Tip: list games with: python rgsx_cli.py games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw)) + print("Tip: list games with: games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw)) sys.exit(3) title, url = match @@ -454,33 +658,109 @@ def cmd_download(args): sys.exit(exit_code) +def interactive_loop(parser): + """Simple REPL so user can run multiple subcommands without retyping python rgsx_cli.py. + + Rules: + - Empty line: ignore + - help / ?: show help + - exit / quit: leave loop + - Global flags like --verbose can be set per command; verbose persists for session once set. + """ + persistent_verbose = False + print("RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.") + while True: + try: + line = input("rgsx> ").strip() + except EOFError: + print() + break + except KeyboardInterrupt: + print() + break + if not line: + continue + if line in {"exit", "quit"}: + break + if line in {"help", "?"}: + parser.print_help() + continue + # Tokenize respecting simple quotes + try: + import shlex + argv = shlex.split(line) + except Exception: + argv = line.split() + # Inject persistent verbose if previously enabled and not explicitly disabled + if persistent_verbose and "--verbose" not in argv: + argv.insert(0, "--verbose") + try: + args = parser.parse_args(argv) + except SystemExit as se: + # argparse already printed error; continue loop + continue + # Update persistent verbose state + if getattr(args, 'verbose', False): + persistent_verbose = True + # Dispatch + if not getattr(args, 'cmd', None): + # If user typed e.g. just global flags + print("No command provided. Type 'help' to list commands.") + continue + setup_logging(getattr(args, 'verbose', False)) + # Global force-update handling (duplicate minimal logic to avoid leaving loop early) + if getattr(args, 'force_update', False): + try: + if os.path.exists(config.SOURCES_FILE): + os.remove(config.SOURCES_FILE) + except Exception: + pass + try: + shutil.rmtree(config.GAMES_FOLDER, ignore_errors=True) + except Exception: + pass + try: + shutil.rmtree(config.IMAGES_FOLDER, ignore_errors=True) + except Exception: + pass + ok = ensure_data_present(verbose=True) + if not ok: + print("force-update failed; aborting command.") + continue + try: + args.func(args) + except SystemExit: + # Subcommand may sys.exit on errors; swallow in REPL + continue + except Exception as e: + print(f"Error: {e}") + continue + + def build_parser(): p = argparse.ArgumentParser(prog="rgsx-cli", description="RGSX headless CLI") p.add_argument("--verbose", action="store_true", help="Verbose logging") p.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload") sub = p.add_subparsers(dest="cmd") - sp = sub.add_parser("platforms", help="List available platforms") + sp = sub.add_parser("platforms", aliases=["p"], help="List available platforms") sp.add_argument("--json", action="store_true", help="Output JSON with name and folder") - # Also accept global flags after the subcommand sp.add_argument("--verbose", action="store_true", help="Verbose logging") sp.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload") sp.set_defaults(func=cmd_platforms) - sg = sub.add_parser("games", help="List games for a platform") - sg.add_argument("--platform", required=True, help="Platform name or key") - sg.add_argument("--search", help="Filter by name contains") - # Also accept global flags after the subcommand + sg = sub.add_parser("games", aliases=["g"], help="List games for a platform") + sg.add_argument("--platform", "--p", "-p", required=True, help="Platform name or key") + sg.add_argument("--search", "--s", "-s", help="Filter by name contains") sg.add_argument("--verbose", action="store_true", help="Verbose logging") sg.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload") sg.set_defaults(func=cmd_games) - sd = sub.add_parser("download", help="Download a game by title") - sd.add_argument("--platform", required=True) - sd.add_argument("--game", required=True) - sd.add_argument("--force", action="store_true", help="Override unsupported extension warning") + sd = sub.add_parser("download", aliases=["dl"], help="Download a game by title") + sd.add_argument("--platform", "--p", "-p", required=True) + sd.add_argument("--game", "--g", "-g", required=True) + sd.add_argument("--force", "-f", action="store_true", help="Override unsupported extension warning") sd.add_argument("--interactive", "-i", action="store_true", help="Prompt to choose from matches when no exact title is found") - # Also accept global flags after the subcommand sd.add_argument("--verbose", action="store_true", help="Verbose logging") sd.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload") sd.set_defaults(func=cmd_download) @@ -488,13 +768,11 @@ def build_parser(): sh = sub.add_parser("history", help="Show recent history") sh.add_argument("--tail", type=int, default=50, help="Last N entries") sh.add_argument("--json", action="store_true") - # Also accept global flags after the subcommand sh.add_argument("--verbose", action="store_true", help="Verbose logging") sh.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload") sh.set_defaults(func=cmd_history) - sc = sub.add_parser("clear-history", help="Clear history") - # Also accept global flags after the subcommand + sc = sub.add_parser("clear-history", aliases=["clear"], help="Clear history") sc.add_argument("--verbose", action="store_true", help="Verbose logging") sc.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload") sc.set_defaults(func=cmd_clear_history) @@ -507,6 +785,12 @@ def main(argv=None): # Force headless mode for CLI os.environ.setdefault("RGSX_HEADLESS", "1") parser = build_parser() + if not argv: + # Start interactive mode + try: + interactive_loop(parser) + finally: + return args = parser.parse_args(argv) setup_logging(args.verbose) diff --git a/pygame/controller_debug.pygame b/pygame/controller_debug.pygame index fa78905..38c2ad4 100644 --- a/pygame/controller_debug.pygame +++ b/pygame/controller_debug.pygame @@ -14,34 +14,34 @@ except Exception as e: PROMPTS = [ # Face buttons - "SOUTH_BUTTON", # A on Xbox - "EAST_BUTTON", # B on Xbox - "WEST_BUTTON", # X on Xbox - "NORTH_BUTTON", # Y on Xbox + "SOUTH_BUTTON - CONFIRM", # A on Xbox + "EAST_BUTTON - CANCEL", # B on Xbox + "WEST_BUTTON - CLEAR HISTORY / SELECT GAMES", # X on Xbox + "NORTH_BUTTON - HISTORY", # Y on Xbox # Meta - "START", - "SELECT", + "START - PAUSE", + "SELECT - FILTER", # D-Pad - "DPAD_UP", - "DPAD_DOWN", - "DPAD_LEFT", - "DPAD_RIGHT", + "DPAD_UP - MOVE UP", + "DPAD_DOWN - MOVE DOWN", + "DPAD_LEFT - MOVE LEFT", + "DPAD_RIGHT - MOVE RIGHT", # Bumpers - "LEFT_BUMPER", - "RIGHT_BUMPER", + "LEFT_BUMPER - LB/L1 - Delete last char", + "RIGHT_BUMPER - RB/R1 - Add space", # Triggers - "LEFT_TRIGGER", - "RIGHT_TRIGGER", + "LEFT_TRIGGER - LT/L2 - Page +", + "RIGHT_TRIGGER - RT/R2 - Page -", # Left stick directions - "JOYSTICK_LEFT_UP", - "JOYSTICK_LEFT_DOWN", - "JOYSTICK_LEFT_LEFT", - "JOYSTICK_LEFT_RIGHT", + "JOYSTICK_LEFT_UP - MOVE UP", + "JOYSTICK_LEFT_DOWN - MOVE DOWN", + "JOYSTICK_LEFT_LEFT - MOVE LEFT", + "JOYSTICK_LEFT_RIGHT - MOVE RIGHT", # Right stick directions - "JOYSTICK_RIGHT_UP", - "JOYSTICK_RIGHT_DOWN", - "JOYSTICK_RIGHT_LEFT", - "JOYSTICK_RIGHT_RIGHT", + "JOYSTICK_RIGHT_UP - MOVE U P", + "JOYSTICK_RIGHT_DOWN - MOVE DOWN", + "JOYSTICK_RIGHT_LEFT - MOVE LEFT", + "JOYSTICK_RIGHT_RIGHT - MOVE RIGHT", ]