Initial commit with RGSX project files
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
logs/
|
||||
images/
|
||||
games/
|
||||
_pycache_/
|
||||
sources.json
|
||||
gamelist.xml
|
||||
738
__main__.py
Normal file
738
__main__.py
Normal file
@@ -0,0 +1,738 @@
|
||||
import os
|
||||
os.environ["SDL_FBDEV"] = "/dev/fb0"
|
||||
import pygame
|
||||
import asyncio
|
||||
import platform
|
||||
import subprocess
|
||||
import math
|
||||
import logging
|
||||
import requests
|
||||
import sys
|
||||
import json
|
||||
from display import init_display, draw_loading_screen, draw_error_screen, draw_platform_grid, draw_progress_screen, draw_scrollbar, draw_confirm_dialog, draw_controls, draw_gradient, draw_virtual_keyboard, draw_popup_message, draw_extension_warning, draw_pause_menu, draw_controls_help
|
||||
from network import test_internet, download_rom, check_extension_before_download, extract_zip
|
||||
from controls import handle_controls
|
||||
from controls_mapper import load_controls_config, map_controls, draw_controls_mapping, ACTIONS
|
||||
from utils import truncate_text_end, load_system_image, load_games
|
||||
import config
|
||||
|
||||
# Configuration du logging
|
||||
log_dir = "/userdata/roms/ports/RGSX/logs"
|
||||
log_file = os.path.join(log_dir, "RGSX.log")
|
||||
try:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
filename=log_file,
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
except Exception as e:
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logging.error(f"Échec de la configuration du logging dans {log_file}: {str(e)}")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# URL du serveur OTA
|
||||
OTA_SERVER_URL = "https://retrogamesets.fr/softs"
|
||||
OTA_VERSION_ENDPOINT = f"{OTA_SERVER_URL}/version.json"
|
||||
OTA_UPDATE_SCRIPT = f"{OTA_SERVER_URL}/rgsx-update.sh"
|
||||
OTA_data_ZIP = f"{OTA_SERVER_URL}/rgsx-data.zip"
|
||||
|
||||
# Constantes pour la répétition automatique dans pause_menu
|
||||
REPEAT_DELAY = 300 # Délai initial avant répétition (ms)
|
||||
REPEAT_INTERVAL = 100 # Intervalle entre répétitions (ms)
|
||||
REPEAT_ACTION_DEBOUNCE = 50 # Délai anti-rebond pour répétitions (ms)
|
||||
|
||||
# Initialisation de Pygame
|
||||
pygame.init()
|
||||
pygame.joystick.init()
|
||||
pygame.mouse.set_visible(True)
|
||||
|
||||
# Détection système non-PC
|
||||
def detect_non_pc():
|
||||
arch = platform.machine()
|
||||
try:
|
||||
result = subprocess.run(["batocera-es-swissknife", "--arch"], capture_output=True, text=True, timeout=2)
|
||||
if result.returncode == 0:
|
||||
arch = result.stdout.strip()
|
||||
logger.debug(f"Architecture via batocera-es-swissknife: {arch}")
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
logger.debug(f"batocera-es-swissknife non disponible, utilisation de platform.machine(): {arch}")
|
||||
|
||||
is_non_pc = arch not in ["x86_64", "amd64"]
|
||||
logger.debug(f"Système détecté: {platform.system()}, architecture: {arch}, is_non_pc={is_non_pc}")
|
||||
return is_non_pc
|
||||
|
||||
config.is_non_pc = detect_non_pc()
|
||||
|
||||
# Initialisation de l’écran
|
||||
screen = init_display()
|
||||
pygame.display.set_caption("RGSX")
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
# Initialisation des polices
|
||||
try:
|
||||
config.font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 48)
|
||||
config.title_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 60)
|
||||
config.search_font = pygame.font.Font("/userdata/roms/ports/RGSX/assets/Pixel-UniCode.ttf", 60)
|
||||
logger.debug("Police Pixel-UniCode chargée")
|
||||
except:
|
||||
config.font = pygame.font.SysFont("arial", 48)
|
||||
config.title_font = pygame.font.SysFont("arial", 60)
|
||||
config.search_font = pygame.font.SysFont("arial", 60)
|
||||
logger.debug("Police Arial chargée")
|
||||
config.progress_font = pygame.font.SysFont("arial", 36)
|
||||
config.small_font = pygame.font.SysFont("arial", 24)
|
||||
|
||||
# Mise à jour de la résolution dans config
|
||||
config.screen_width, config.screen_height = pygame.display.get_surface().get_size()
|
||||
logger.debug(f"Résolution réelle : {config.screen_width}x{config.screen_height}")
|
||||
|
||||
# Initialisation des variables de grille
|
||||
config.current_page = 0
|
||||
config.selected_platform = 0
|
||||
config.selected_key = (0, 0)
|
||||
config.transition_state = "none"
|
||||
|
||||
# Initialisation des variables de répétition
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.repeat_last_action = 0
|
||||
|
||||
# Vérification et chargement de la configuration des contrôles
|
||||
config.controls_config = load_controls_config()
|
||||
if not config.controls_config:
|
||||
config.menu_state = "controls_mapping"
|
||||
else:
|
||||
config.menu_state = "loading"
|
||||
|
||||
# Initialisation du gamepad
|
||||
joystick = None
|
||||
if pygame.joystick.get_count() > 0:
|
||||
joystick = pygame.joystick.Joystick(0)
|
||||
joystick.init()
|
||||
logger.debug("Gamepad initialisé")
|
||||
|
||||
# Initialisation du mixer Pygame
|
||||
pygame.mixer.pre_init(44100, -16, 2, 4096)
|
||||
pygame.mixer.init()
|
||||
|
||||
# Dossier musique Batocera
|
||||
music_folder = "/userdata/roms/ports/RGSX/assets/music"
|
||||
music_files = [f for f in os.listdir(music_folder) if f.lower().endswith(('.ogg', '.mp3'))]
|
||||
if music_files:
|
||||
import random
|
||||
music_file = random.choice(music_files)
|
||||
music_path = os.path.join(music_folder, music_file)
|
||||
logger.debug(f"Lecture de la musique : {music_path}")
|
||||
pygame.mixer.music.load(music_path)
|
||||
pygame.mixer.music.set_volume(0.5)
|
||||
pygame.mixer.music.play(-1)
|
||||
else:
|
||||
logger.debug("Aucune musique trouvée dans /userdata/roms/ports/RGSX/assets/music")
|
||||
|
||||
# Fonction pour charger sources.json
|
||||
def load_sources():
|
||||
sources_path = "/userdata/roms/ports/RGSX/sources.json"
|
||||
logger.debug(f"Chargement de {sources_path}")
|
||||
try:
|
||||
with open(sources_path, 'r', encoding='utf-8') as f:
|
||||
sources = json.load(f)
|
||||
sources = sorted(sources, key=lambda x: x.get("nom", x.get("platform", "")).lower())
|
||||
config.platforms = [source["platform"] for source in sources]
|
||||
config.platform_dicts = sources
|
||||
config.platform_names = {source["platform"]: source["nom"] for source in sources}
|
||||
config.games_count = {platform: 0 for platform in config.platforms} # Initialiser à 0
|
||||
# Charger les jeux pour chaque plateforme
|
||||
for platform in config.platforms:
|
||||
games = load_games(platform)
|
||||
config.games_count[platform] = len(games)
|
||||
logger.debug(f"Jeux chargés pour {platform}: {len(games)} jeux")
|
||||
logger.debug(f"load_sources: platforms={config.platforms}, platform_names={config.platform_names}, games_count={config.games_count}")
|
||||
return sources
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de sources.json : {str(e)}")
|
||||
return []
|
||||
# Fonction pour vérifier et appliquer les mises à jour OTA
|
||||
async def check_for_updates():
|
||||
try:
|
||||
logger.debug("Vérification de la version disponible sur le serveur")
|
||||
config.current_loading_system = "Mise à jour en cours... Patientez l'ecran reste figé..Puis relancer l'application"
|
||||
config.loading_progress = 5.0
|
||||
config.needs_redraw = True
|
||||
response = requests.get(OTA_VERSION_ENDPOINT, timeout=5)
|
||||
response.raise_for_status()
|
||||
if response.headers.get("content-type") != "application/json":
|
||||
raise ValueError(f"Le fichier version.json n'est pas un JSON valide (type de contenu : {response.headers.get('content-type')})")
|
||||
version_data = response.json()
|
||||
latest_version = version_data.get("version")
|
||||
logger.debug(f"Version distante : {latest_version}, version locale : {config.app_version}")
|
||||
|
||||
if latest_version != config.app_version:
|
||||
config.current_loading_system = f"Mise à jour disponible : {latest_version}"
|
||||
config.loading_progress = 10.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Téléchargement du script de mise à jour : {OTA_UPDATE_SCRIPT}")
|
||||
|
||||
update_script_path = "/userdata/roms/ports/rgsx-update.sh"
|
||||
logger.debug(f"Téléchargement de {OTA_UPDATE_SCRIPT} vers {update_script_path}")
|
||||
with requests.get(OTA_UPDATE_SCRIPT, stream=True, timeout=10) as r:
|
||||
r.raise_for_status()
|
||||
with open(update_script_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
config.loading_progress = min(50.0, config.loading_progress + 5.0)
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
|
||||
config.current_loading_system = "Préparation de la mise à jour..."
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Rendre {update_script_path} exécutable")
|
||||
subprocess.run(["chmod", "+x", update_script_path], check=True)
|
||||
logger.debug(f"Script {update_script_path} rendu exécutable")
|
||||
|
||||
logger.debug(f"Vérification de l'existence et des permissions de {update_script_path}")
|
||||
if not os.path.isfile(update_script_path):
|
||||
logger.error(f"Le script {update_script_path} n'existe pas")
|
||||
return False, f"Erreur : le script {update_script_path} n'existe pas"
|
||||
if not os.access(update_script_path, os.X_OK):
|
||||
logger.error(f"Le script {update_script_path} n'est pas exécutable")
|
||||
return False, f"Erreur : le script {update_script_path} n'est pas exécutable"
|
||||
|
||||
wrapper_script_path = "/userdata/roms/ports/RGSX/update/run.update"
|
||||
logger.debug(f"Vérification de l'existence et des permissions de {wrapper_script_path}")
|
||||
if not os.path.isfile(wrapper_script_path):
|
||||
logger.error(f"Le script wrapper {wrapper_script_path} n'existe pas")
|
||||
return False, f"Erreur : le script wrapper {wrapper_script_path} n'existe pas"
|
||||
if not os.access(wrapper_script_path, os.X_OK):
|
||||
logger.error(f"Le script wrapper {wrapper_script_path} n'est pas exécutable")
|
||||
subprocess.run(["chmod", "+x", wrapper_script_path], check=True)
|
||||
logger.debug(f"Script wrapper {wrapper_script_path} rendu exécutable")
|
||||
|
||||
logger.debug("Désactivation des événements Pygame QUIT")
|
||||
pygame.event.set_blocked(pygame.QUIT)
|
||||
|
||||
config.current_loading_system = "Application de la mise à jour..."
|
||||
config.loading_progress = 80.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Exécution du script wrapper : {wrapper_script_path}")
|
||||
result = os.system(f"{wrapper_script_path} &")
|
||||
logger.debug(f"Résultat de os.system : {result}")
|
||||
if result != 0:
|
||||
logger.error(f"Échec du lancement du script wrapper : code de retour {result}")
|
||||
return False, f"Échec du lancement du script wrapper : code de retour {result}"
|
||||
|
||||
config.current_loading_system = "Mise à jour déclenchée, redémarrage..."
|
||||
config.loading_progress = 100.0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Mise à jour déclenchée, arrêt de l'application")
|
||||
config.update_triggered = True
|
||||
pygame.quit()
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.debug("Aucune mise à jour logicielle disponible")
|
||||
return True, "Aucune mise à jour disponible"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur OTA : {str(e)}")
|
||||
return False, f"Erreur lors de la vérification des mises à jour : {str(e)}"
|
||||
|
||||
# Boucle principale
|
||||
|
||||
async def main():
|
||||
logger.debug("Début main")
|
||||
running = True
|
||||
loading_step = "none"
|
||||
sources = []
|
||||
config.last_state_change_time = 0
|
||||
config.debounce_delay = 50
|
||||
config.update_triggered = False
|
||||
last_redraw_time = pygame.time.get_ticks()
|
||||
|
||||
screen = pygame.display.set_mode((1280, 720)) # Initialiser l'écran
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
while running:
|
||||
if config.update_triggered:
|
||||
logger.debug("Mise à jour déclenchée, arrêt de la boucle principale")
|
||||
break
|
||||
|
||||
current_time = pygame.time.get_ticks()
|
||||
|
||||
# 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
|
||||
last_redraw_time = current_time
|
||||
|
||||
# Gestion des événements
|
||||
events = pygame.event.get()
|
||||
for event in events:
|
||||
if event.type == pygame.QUIT:
|
||||
config.menu_state = "confirm_exit"
|
||||
config.confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Événement QUIT détecté, passage à confirm_exit")
|
||||
continue
|
||||
|
||||
start_config = config.controls_config.get("start", {})
|
||||
if start_config and (
|
||||
(event.type == pygame.KEYDOWN and start_config.get("type") == "key" and event.key == start_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and start_config.get("type") == "button" and event.button == start_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and start_config.get("type") == "axis" and event.axis == start_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == start_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and start_config.get("type") == "hat" and event.value == start_config.get("value")) or
|
||||
(event.type == pygame.MOUSEBUTTONDOWN and start_config.get("type") == "mouse" and event.button == start_config.get("value"))
|
||||
):
|
||||
if config.menu_state not in ["pause_menu", "controls_help", "controls_mapping"]:
|
||||
config.previous_menu_state = config.menu_state
|
||||
config.menu_state = "pause_menu"
|
||||
config.selected_pause_option = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Ouverture menu pause depuis {config.previous_menu_state}")
|
||||
continue
|
||||
|
||||
if config.menu_state == "pause_menu":
|
||||
current_time = pygame.time.get_ticks()
|
||||
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION):
|
||||
up_config = config.controls_config.get("up", {})
|
||||
down_config = config.controls_config.get("down", {})
|
||||
confirm_config = config.controls_config.get("confirm", {})
|
||||
cancel_config = config.controls_config.get("cancel", {})
|
||||
|
||||
if current_time - config.last_state_change_time < config.debounce_delay:
|
||||
continue
|
||||
|
||||
if (
|
||||
(event.type == pygame.KEYDOWN and up_config and event.key == up_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and up_config and up_config.get("type") == "button" and event.button == up_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and up_config and up_config.get("type") == "axis" and event.axis == up_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == up_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and up_config and up_config.get("type") == "hat" and event.value == up_config.get("value"))
|
||||
):
|
||||
config.selected_pause_option = max(0, config.selected_pause_option - 1)
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Menu pause: Haut, selected_option={config.selected_pause_option}, repeat_action={config.repeat_action}")
|
||||
elif (
|
||||
(event.type == pygame.KEYDOWN and down_config and event.key == down_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and down_config and down_config.get("type") == "button" and event.button == down_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and down_config and down_config.get("type") == "axis" and event.axis == down_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == down_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and down_config and down_config.get("type") == "hat" and event.value == down_config.get("value"))
|
||||
):
|
||||
config.selected_pause_option = min(2, config.selected_pause_option + 1)
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Menu pause: Bas, selected_option={config.selected_pause_option}, repeat_action={config.repeat_action}")
|
||||
elif (
|
||||
(event.type == pygame.KEYDOWN and confirm_config and event.key == confirm_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and confirm_config and confirm_config.get("type") == "button" and event.button == confirm_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and confirm_config and confirm_config.get("type") == "axis" and event.axis == confirm_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == confirm_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and confirm_config and confirm_config.get("type") == "hat" and event.value == confirm_config.get("value"))
|
||||
):
|
||||
if config.selected_pause_option == 0:
|
||||
config.menu_state = "controls_help"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Menu pause: Aide sélectionnée")
|
||||
elif config.selected_pause_option == 1:
|
||||
if map_controls(screen):
|
||||
config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "download_progress", "download_result", "confirm_exit", "extension_warning"] else "platform"
|
||||
config.controls_config = load_controls_config()
|
||||
logger.debug(f"Mappage des contrôles terminé, retour à {config.menu_state}")
|
||||
else:
|
||||
config.menu_state = "error"
|
||||
config.error_message = "Échec du mappage des contrôles"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Échec du mappage des contrôles")
|
||||
elif config.selected_pause_option == 2:
|
||||
running = False
|
||||
logger.debug("Menu pause: Quitter sélectionné")
|
||||
elif (
|
||||
(event.type == pygame.KEYDOWN and cancel_config and event.key == cancel_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and cancel_config and cancel_config.get("type") == "button" and event.button == cancel_config.get("value")) or
|
||||
(event.type == pygame.JOYAXISMOTION and cancel_config and cancel_config.get("type") == "axis" and event.axis == cancel_config.get("value")[0] and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == cancel_config.get("value")[1]) or
|
||||
(event.type == pygame.JOYHATMOTION and cancel_config and cancel_config.get("type") == "hat" and event.value == cancel_config.get("value"))
|
||||
):
|
||||
config.menu_state = config.previous_menu_state if config.previous_menu_state in ["platform", "game", "download_progress", "download_result", "confirm_exit", "extension_warning"] else "platform"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Menu pause: Annulation, retour à {config.menu_state}")
|
||||
|
||||
elif event.type in (pygame.KEYUP, pygame.JOYBUTTONUP):
|
||||
if (
|
||||
(event.type == pygame.KEYUP and is_input_matched(event, "up") or is_input_matched(event, "down")) or
|
||||
(event.type == pygame.JOYBUTTONUP and is_input_matched(event, "up") or is_input_matched(event, "down"))
|
||||
):
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Menu pause: Touche relâchée, répétition arrêtée")
|
||||
|
||||
if config.repeat_action in ["up", "down"] and current_time >= config.repeat_start_time:
|
||||
if current_time - config.repeat_last_action < REPEAT_ACTION_DEBOUNCE:
|
||||
continue
|
||||
config.repeat_last_action = current_time
|
||||
if config.repeat_action == "up":
|
||||
config.selected_pause_option = max(0, config.selected_pause_option - 1)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Menu pause: Répétition haut, selected_option={config.selected_pause_option}")
|
||||
elif config.repeat_action == "down":
|
||||
config.selected_pause_option = min(2, config.selected_pause_option + 1)
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Menu pause: Répétition bas, selected_option={config.selected_pause_option}")
|
||||
config.repeat_start_time = current_time + REPEAT_INTERVAL
|
||||
|
||||
continue
|
||||
|
||||
if config.menu_state == "controls_help":
|
||||
cancel_config = config.controls_config.get("cancel", {})
|
||||
if (
|
||||
(event.type == pygame.KEYDOWN and cancel_config and event.key == cancel_config.get("value")) or
|
||||
(event.type == pygame.JOYBUTTONDOWN and cancel_config and cancel_config.get("type") == "button" and event.button == cancel_config.get("value"))
|
||||
):
|
||||
config.menu_state = "pause_menu"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Controls_help: Annulation, retour à pause_menu")
|
||||
continue
|
||||
|
||||
if config.menu_state in ["platform", "game", "error", "confirm_exit", "download_progress", "download_result", "extension_warning"]:
|
||||
action = handle_controls(event, sources, joystick, screen)
|
||||
config.needs_redraw = True
|
||||
if action == "quit":
|
||||
running = False
|
||||
logger.debug("Action quit détectée, arrêt de l'application")
|
||||
elif action == "download" and config.menu_state == "game" and config.filtered_games:
|
||||
game = config.filtered_games[config.current_game]
|
||||
game_name = game[0] if isinstance(game, (list, tuple)) else game
|
||||
platform = config.platforms[config.current_platform]
|
||||
url = game[1] if isinstance(game, (list, tuple)) and len(game) > 1 else None
|
||||
if url:
|
||||
logger.debug(f"Vérification de l'extension pour {game_name}, URL: {url}")
|
||||
is_supported, message, is_zip_non_supported = check_extension_before_download(url, platform, game_name)
|
||||
if not is_supported:
|
||||
config.pending_download = (url, platform, game_name, is_zip_non_supported)
|
||||
config.menu_state = "extension_warning"
|
||||
config.extension_confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Extension non reconnue, passage à extension_warning pour {game_name}")
|
||||
else:
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported))
|
||||
config.download_tasks[task] = (task, url, game_name, platform)
|
||||
config.menu_state = "download_progress"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Téléchargement démarré pour {game_name}, passage à download_progress")
|
||||
|
||||
# Gestion des téléchargements
|
||||
if config.download_tasks:
|
||||
for task_id, (task, url, game_name, platform) in list(config.download_tasks.items()):
|
||||
if task.done():
|
||||
try:
|
||||
success, message = await task
|
||||
config.download_result_message = message
|
||||
config.download_result_error = not success
|
||||
config.download_result_start_time = pygame.time.get_ticks()
|
||||
config.menu_state = "download_result"
|
||||
config.download_progress.clear() # Réinitialiser download_progress
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
logger.debug(f"Téléchargement terminé: {game_name}, succès={success}, message={message}")
|
||||
except Exception as e:
|
||||
config.download_result_message = f"Erreur lors du téléchargement : {str(e)}"
|
||||
config.download_result_error = True
|
||||
config.download_result_start_time = pygame.time.get_ticks()
|
||||
config.menu_state = "download_result"
|
||||
config.download_progress.clear() # Réinitialiser download_progress
|
||||
config.needs_redraw = True
|
||||
del config.download_tasks[task_id]
|
||||
logger.error(f"Erreur dans tâche de téléchargement: {str(e)}")
|
||||
|
||||
# Gestion de la fin du popup download_result
|
||||
if config.menu_state == "download_result" and current_time - config.download_result_start_time > 3000:
|
||||
config.menu_state = "game"
|
||||
config.download_progress.clear() # Réinitialiser download_progress
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Fin popup download_result, retour à {config.menu_state}")
|
||||
|
||||
# Affichage
|
||||
if config.needs_redraw:
|
||||
draw_gradient(screen, (28, 37, 38), (47, 59, 61))
|
||||
if config.menu_state == "controls_mapping":
|
||||
draw_controls_mapping(screen, ACTIONS[0], None, False, 0.0)
|
||||
logger.debug("Rendu initial de draw_controls_mapping")
|
||||
elif config.menu_state == "loading":
|
||||
draw_loading_screen(screen)
|
||||
logger.debug("Rendu de draw_loading_screen")
|
||||
elif config.menu_state == "error":
|
||||
draw_error_screen(screen)
|
||||
logger.debug("Rendu de draw_error_screen")
|
||||
elif config.menu_state == "platform":
|
||||
platform = config.platforms[config.selected_platform]
|
||||
platform_name = config.platform_names.get(platform, platform)
|
||||
game_count = config.games_count.get(platform, 0)
|
||||
title_text = f"{platform_name} ({game_count} jeux)"
|
||||
title_surface = config.title_font.render(title_text, True, (255, 255, 255))
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 60))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), title_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), title_rect.inflate(40, 20), 2)
|
||||
screen.blit(title_surface, title_rect)
|
||||
draw_platform_grid(screen)
|
||||
elif config.menu_state == "game":
|
||||
platform = config.platforms[config.current_platform]
|
||||
platform_name = config.platform_names.get(platform, platform)
|
||||
games = config.filtered_games if config.filter_active or config.search_mode else config.games
|
||||
game_count = len(games)
|
||||
if not config.search_mode:
|
||||
title_text = f"{platform_name} ({game_count} jeux)"
|
||||
title_surface = config.title_font.render(title_text, True, (255, 255, 255))
|
||||
title_rect = title_surface.get_rect(center=(config.screen_width // 2, 60))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), title_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), title_rect.inflate(40, 20), 2)
|
||||
screen.blit(title_surface, title_rect)
|
||||
margin_top = 150
|
||||
line_height = config.font.get_height() + 10
|
||||
for i in range(config.scroll_offset, min(config.scroll_offset + config.visible_games, len(games))):
|
||||
game_name = games[i][0] if isinstance(games[i], (list, tuple)) else games[i]
|
||||
color = (0, 150, 255) if i == config.current_game else (255, 255, 255)
|
||||
game_text = truncate_text_end(game_name, config.font, config.screen_width - 40)
|
||||
text_surface = config.font.render(game_text, True, color)
|
||||
text_rect = text_surface.get_rect(center=(config.screen_width // 2, margin_top + (i - config.scroll_offset) * line_height))
|
||||
screen.blit(text_surface, text_rect)
|
||||
draw_scrollbar(screen)
|
||||
if config.search_mode:
|
||||
search_text = f"Filtrer : {config.search_query}_"
|
||||
search_surface = config.search_font.render(search_text, True, (255, 255, 255))
|
||||
search_rect = search_surface.get_rect(center=(config.screen_width // 2, 60))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), search_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), search_rect.inflate(40, 20), 2)
|
||||
screen.blit(search_surface, search_rect)
|
||||
if config.is_non_pc:
|
||||
draw_virtual_keyboard(screen)
|
||||
elif config.filter_active:
|
||||
filter_text = f"Filtre actif : {config.search_query}"
|
||||
filter_surface = config.small_font.render(filter_text, True, (255, 255, 255))
|
||||
filter_rect = filter_surface.get_rect(center=(config.screen_width // 2, 100))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), filter_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), filter_rect.inflate(40, 20), 2)
|
||||
screen.blit(filter_surface, filter_rect)
|
||||
elif config.menu_state == "download_progress":
|
||||
draw_progress_screen(screen)
|
||||
logger.debug("Rendu de draw_progress_screen")
|
||||
elif config.menu_state == "download_result":
|
||||
draw_popup_message(screen, config.download_result_message, config.download_result_error)
|
||||
logger.debug("Rendu de draw_popup_message")
|
||||
elif config.menu_state == "confirm_exit":
|
||||
draw_confirm_dialog(screen)
|
||||
logger.debug("Rendu de draw_confirm_dialog")
|
||||
elif config.menu_state == "extension_warning":
|
||||
draw_extension_warning(screen)
|
||||
logger.debug("Rendu de draw_extension_warning")
|
||||
elif config.menu_state == "pause_menu":
|
||||
draw_pause_menu(screen, config.selected_pause_option)
|
||||
logger.debug("Rendu de draw_pause_menu")
|
||||
elif config.menu_state == "controls_help":
|
||||
draw_controls_help(screen, config.previous_menu_state)
|
||||
logger.debug("Rendu de draw_controls_help")
|
||||
|
||||
draw_controls(screen, config.menu_state)
|
||||
pygame.display.flip()
|
||||
config.needs_redraw = False
|
||||
|
||||
# Gestion de l'état controls_mapping
|
||||
if config.menu_state == "controls_mapping":
|
||||
logger.debug("Avant appel de map_controls")
|
||||
try:
|
||||
success = map_controls(screen)
|
||||
logger.debug(f"map_controls terminé, succès={success}")
|
||||
if success:
|
||||
config.controls_config = load_controls_config()
|
||||
config.menu_state = "loading"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Passage à l'état loading après mappage")
|
||||
else:
|
||||
config.menu_state = "error"
|
||||
config.error_message = "Échec du mappage des contrôles"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Échec du mappage, passage à l'état error")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'appel de map_controls : {str(e)}")
|
||||
config.menu_state = "error"
|
||||
config.error_message = f"Erreur dans map_controls: {str(e)}"
|
||||
config.needs_redraw = True
|
||||
|
||||
# Gestion de l'état loading
|
||||
elif config.menu_state == "loading":
|
||||
logger.debug(f"Étape chargement : {loading_step}")
|
||||
if loading_step == "none":
|
||||
loading_step = "test_internet"
|
||||
config.current_loading_system = "Test de connexion..."
|
||||
config.loading_progress = 0.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
elif loading_step == "test_internet":
|
||||
logger.debug("Exécution de test_internet()")
|
||||
if test_internet():
|
||||
loading_step = "check_ota"
|
||||
config.current_loading_system = "Mise à jour en cours..."
|
||||
config.loading_progress = 5.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
else:
|
||||
config.menu_state = "error"
|
||||
config.error_message = "Pas de connexion Internet. Vérifiez votre réseau."
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Erreur : {config.error_message}")
|
||||
elif loading_step == "check_ota":
|
||||
logger.debug("Exécution de check_for_updates()")
|
||||
success, message = await check_for_updates()
|
||||
logger.debug(f"Résultat de check_for_updates : success={success}, message={message}")
|
||||
if not success:
|
||||
config.menu_state = "error"
|
||||
config.error_message = message
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Erreur OTA : {message}")
|
||||
else:
|
||||
loading_step = "check_data"
|
||||
config.current_loading_system = "Téléchargement des données ..."
|
||||
config.loading_progress = 10.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
elif loading_step == "check_data":
|
||||
games_data_dir = "/userdata/roms/ports/RGSX/games"
|
||||
is_data_empty = not os.path.exists(games_data_dir) or not any(os.scandir(games_data_dir))
|
||||
logger.debug(f"Dossier Data directory {games_data_dir} is {'empty' if is_data_empty else 'not empty'}")
|
||||
|
||||
if is_data_empty:
|
||||
config.current_loading_system = "Téléchargement du Dossier Data initial..."
|
||||
config.loading_progress = 15.0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Dossier Data vide, début du téléchargement du ZIP")
|
||||
|
||||
try:
|
||||
zip_path = "/userdata/roms/ports/RGSX.zip"
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
with requests.get(OTA_data_ZIP, stream=True, headers=headers, timeout=30) as response:
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale du ZIP : {total_size} octets")
|
||||
downloaded = 0
|
||||
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
|
||||
with open(zip_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
config.download_progress[OTA_data_ZIP] = {
|
||||
"downloaded_size": downloaded,
|
||||
"total_size": total_size,
|
||||
"status": "Téléchargement",
|
||||
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
|
||||
}
|
||||
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {zip_path}")
|
||||
|
||||
config.current_loading_system = "Extraction du Dossier Data initial..."
|
||||
config.loading_progress = 50.0
|
||||
config.needs_redraw = True
|
||||
dest_dir = "/userdata/roms/ports/RGSX"
|
||||
success, message = extract_zip(zip_path, dest_dir, OTA_data_ZIP)
|
||||
if success:
|
||||
logger.debug(f"Extraction réussie : {message}")
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
raise Exception(f"Échec de l'extraction : {message}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du téléchargement/extraction du Dossier Data : {str(e)}")
|
||||
config.menu_state = "error"
|
||||
config.error_message = f"Échec du téléchargement/extraction du Dossier Data : {str(e)}"
|
||||
config.needs_redraw = True
|
||||
loading_step = "load_sources"
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
continue
|
||||
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
logger.debug(f"Fichier ZIP {zip_path} supprimé")
|
||||
|
||||
loading_step = "load_sources"
|
||||
config.current_loading_system = "Chargement des systèmes..."
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Étape chargement : {loading_step}, progress={config.loading_progress}")
|
||||
else:
|
||||
loading_step = "load_sources"
|
||||
config.current_loading_system = "Chargement des systèmes..."
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Dossier Data non vide, passage à {loading_step}")
|
||||
elif loading_step == "load_sources":
|
||||
sources = load_sources()
|
||||
if not sources:
|
||||
config.menu_state = "error"
|
||||
config.error_message = "Échec du chargement de sources.json"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Erreur : Échec du chargement de sources.json")
|
||||
else:
|
||||
config.menu_state = "platform"
|
||||
config.loading_progress = 0.0
|
||||
config.current_loading_system = ""
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Fin chargement, passage à platform, progress={config.loading_progress}")
|
||||
|
||||
# Gestion de l'état de transition
|
||||
if config.transition_state == "to_game":
|
||||
config.transition_progress += 1
|
||||
if config.transition_progress >= config.transition_duration:
|
||||
config.menu_state = "game"
|
||||
config.transition_state = "idle"
|
||||
config.transition_progress = 0.0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Transition terminée, passage à game")
|
||||
|
||||
clock.tick(60)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
pygame.mixer.music.stop()
|
||||
pygame.quit()
|
||||
logger.debug("Application terminée")
|
||||
|
||||
|
||||
# Fonction pour vérifier si un événement correspond à une action
|
||||
def is_input_matched(event, action_name):
|
||||
if not config.controls_config.get(action_name):
|
||||
return False
|
||||
mapping = config.controls_config[action_name]
|
||||
input_type = mapping["type"]
|
||||
input_value = mapping["value"]
|
||||
|
||||
if input_type == "key" and event.type == pygame.KEYDOWN:
|
||||
return event.key == input_value
|
||||
elif input_type == "button" and event.type == pygame.JOYBUTTONDOWN:
|
||||
return event.button == input_value
|
||||
elif input_type == "axis" and event.type == pygame.JOYAXISMOTION:
|
||||
axis, direction = input_value
|
||||
return event.axis == axis and abs(event.value) > 0.5 and (1 if event.value > 0 else -1) == direction
|
||||
elif input_type == "hat" and event.type == pygame.JOYHATMOTION:
|
||||
return event.value == input_value
|
||||
elif input_type == "mouse" and event.type == pygame.MOUSEBUTTONDOWN:
|
||||
return event.button == input_value
|
||||
return False
|
||||
|
||||
if platform.system() == "Emscripten":
|
||||
asyncio.ensure_future(main())
|
||||
else:
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
59
config.py
Normal file
59
config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import pygame
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Version actuelle de l'application
|
||||
app_version = "1.4.0"
|
||||
|
||||
|
||||
# Variables d'état
|
||||
platforms = []
|
||||
current_platform = 0
|
||||
platform_names = {} # {platform_id: platform_name}
|
||||
games = []
|
||||
current_game = 0
|
||||
menu_state = "popup"
|
||||
confirm_choice = False
|
||||
scroll_offset = 0
|
||||
visible_games = 15
|
||||
popup_start_time = 0
|
||||
last_progress_update = 0
|
||||
needs_redraw = True
|
||||
transition_state = "idle"
|
||||
transition_progress = 0.0
|
||||
transition_duration = 18
|
||||
games_count = {}
|
||||
download_tasks = {}
|
||||
download_progress = {}
|
||||
download_result_message = ""
|
||||
download_result_error = False
|
||||
download_result_start_time = 0
|
||||
loading_progress = 0.0
|
||||
current_loading_system = ""
|
||||
error_message = ""
|
||||
repeat_action = None
|
||||
repeat_start_time = 0
|
||||
repeat_last_action = 0
|
||||
repeat_key = None
|
||||
filtered_games = []
|
||||
search_mode = False
|
||||
search_query = ""
|
||||
filter_active = False
|
||||
extension_confirm_selection = 0
|
||||
pending_download = None
|
||||
controls_config = {}
|
||||
selected_pause_option = 0
|
||||
previous_menu_state = None
|
||||
|
||||
# Résolution de l'écran
|
||||
screen_width = 800
|
||||
screen_height = 600
|
||||
|
||||
# Polices
|
||||
font = None
|
||||
progress_font = None
|
||||
title_font = None
|
||||
search_font = None
|
||||
small_font = None
|
||||
527
controls.py
Normal file
527
controls.py
Normal file
@@ -0,0 +1,527 @@
|
||||
import pygame
|
||||
import config
|
||||
import asyncio
|
||||
import math
|
||||
from display import draw_validation_transition
|
||||
from network import download_rom, check_extension_before_download
|
||||
from controls_mapper import get_readable_input_name
|
||||
from utils import load_games # Ajout de l'import
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constantes pour la répétition automatique
|
||||
REPEAT_DELAY = 300 # Délai initial avant répétition (ms)
|
||||
REPEAT_INTERVAL = 100 # Intervalle entre répétitions (ms)
|
||||
JOYHAT_DEBOUNCE = 200 # Délai anti-rebond pour JOYHATMOTION (ms)
|
||||
JOYAXIS_DEBOUNCE = 50 # Délai anti-rebond pour JOYAXISMOTION (ms)
|
||||
REPEAT_ACTION_DEBOUNCE = 50 # Délai anti-rebond pour répétitions up/down/left/right (ms)
|
||||
|
||||
def is_input_matched(event, action_name):
|
||||
"""Vérifie si l'événement correspond à l'action configurée."""
|
||||
if not config.controls_config.get(action_name):
|
||||
return False
|
||||
mapping = config.controls_config[action_name]
|
||||
input_type = mapping["type"]
|
||||
input_value = mapping["value"]
|
||||
|
||||
event_type = event["type"] if isinstance(event, dict) else event.type
|
||||
event_key = event.get("key") if isinstance(event, dict) else getattr(event, "key", None)
|
||||
event_button = event.get("button") if isinstance(event, dict) else getattr(event, "button", None)
|
||||
event_axis = event.get("axis") if isinstance(event, dict) else getattr(event, "axis", None)
|
||||
event_value = event.get("value") if isinstance(event, dict) else getattr(event, "value", None)
|
||||
|
||||
if input_type == "key" and event_type in (pygame.KEYDOWN, pygame.KEYUP):
|
||||
logger.debug(f"Vérification key: event_key={event_key}, input_value={input_value}")
|
||||
return event_key == input_value
|
||||
elif input_type == "button" and event_type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP):
|
||||
logger.debug(f"Vérification button: event_button={event_button}, input_value={input_value}")
|
||||
return event_button == input_value
|
||||
elif input_type == "axis" and event_type == pygame.JOYAXISMOTION:
|
||||
axis, direction = input_value
|
||||
result = event_axis == axis and abs(event_value) > 0.5 and (1 if event_value > 0 else -1) == direction
|
||||
logger.debug(f"Vérification axis: event_axis={event_axis}, event_value={event_value}, input_value={input_value}, result={result}")
|
||||
return result
|
||||
elif input_type == "hat" and event_type == pygame.JOYHATMOTION:
|
||||
# Convertir input_value en tuple pour comparaison
|
||||
input_value_tuple = tuple(input_value) if isinstance(input_value, list) else input_value
|
||||
logger.debug(f"Vérification hat: event_value={event_value}, input_value={input_value_tuple}")
|
||||
return event_value == input_value_tuple
|
||||
elif input_type == "mouse" and event_type == pygame.MOUSEBUTTONDOWN:
|
||||
logger.debug(f"Vérification mouse: event_button={event_button}, input_value={input_value}")
|
||||
return event_button == input_value
|
||||
return False
|
||||
|
||||
def handle_controls(event, sources, joystick, screen):
|
||||
"""Gère un événement clavier/joystick/souris et la répétition automatique.
|
||||
Retourne 'quit', 'download', ou None.
|
||||
"""
|
||||
action = None
|
||||
current_time = pygame.time.get_ticks()
|
||||
|
||||
# Debounce général
|
||||
if current_time - config.last_state_change_time < config.debounce_delay:
|
||||
return action
|
||||
|
||||
# Log des événements reçus
|
||||
logger.debug(f"Événement reçu: type={event.type}, value={getattr(event, 'value', None)}")
|
||||
|
||||
# --- CLAVIER, MANETTE, SOURIS ---
|
||||
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN):
|
||||
# Débouncer les événements JOYHATMOTION
|
||||
if event.type == pygame.JOYHATMOTION:
|
||||
logger.debug(f"JOYHATMOTION détecté: hat={event.hat}, value={event.value}")
|
||||
if event.value == (0, 0): # Ignorer les relâchements
|
||||
return action
|
||||
if current_time - config.repeat_last_action < JOYHAT_DEBOUNCE:
|
||||
return action
|
||||
|
||||
# Débouncer les événements JOYAXISMOTION
|
||||
if event.type == pygame.JOYAXISMOTION and current_time - config.repeat_last_action < JOYAXIS_DEBOUNCE:
|
||||
return action
|
||||
|
||||
# Quitter l'appli
|
||||
if event.type == pygame.QUIT:
|
||||
logger.debug("Événement pygame.QUIT détecté")
|
||||
return "quit"
|
||||
|
||||
# Vérification des actions mappées
|
||||
for action_name in ["up", "down", "left", "right"]:
|
||||
if is_input_matched(event, action_name):
|
||||
logger.debug(f"Action mappée détectée: {action_name}, input={get_readable_input_name(event)}")
|
||||
|
||||
# Erreur
|
||||
if config.menu_state == "error":
|
||||
if is_input_matched(event, "confirm"):
|
||||
config.menu_state = "loading"
|
||||
logger.debug("Sortie erreur avec Confirm")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "confirm_exit"
|
||||
config.confirm_selection = 0
|
||||
|
||||
# Plateformes
|
||||
elif config.menu_state == "platform":
|
||||
max_index = min(9, len(config.platforms) - config.current_page * 9) - 1
|
||||
current_grid_index = config.selected_platform - config.current_page * 9
|
||||
row = current_grid_index // 3
|
||||
if is_input_matched(event, "down"):
|
||||
if current_grid_index + 3 <= max_index:
|
||||
config.selected_platform += 3
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "up"):
|
||||
if current_grid_index - 3 >= 0:
|
||||
config.selected_platform -= 3
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "left"):
|
||||
if current_grid_index % 3 != 0:
|
||||
config.selected_platform -= 1
|
||||
config.repeat_action = "left"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif config.current_page > 0:
|
||||
config.current_page -= 1
|
||||
config.selected_platform = config.current_page * 9 + row * 3 + 2
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
config.repeat_action = "left"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "right"):
|
||||
if current_grid_index % 3 != 2 and current_grid_index < max_index:
|
||||
config.selected_platform += 1
|
||||
config.repeat_action = "right"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif (config.current_page + 1) * 9 < len(config.platforms):
|
||||
config.current_page += 1
|
||||
config.selected_platform = config.current_page * 9 + row * 3
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
config.repeat_action = "right"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
if (config.current_page + 1) * 9 < len(config.platforms):
|
||||
config.current_page += 1
|
||||
config.selected_platform = config.current_page * 9 + row * 3
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
config.repeat_action = None # Réinitialiser la répétition
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.repeat_last_action = current_time
|
||||
config.needs_redraw = True
|
||||
logger.debug("Page suivante, répétition réinitialisée")
|
||||
elif is_input_matched(event, "progress"):
|
||||
if config.download_tasks:
|
||||
config.menu_state = "download_progress"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à download_progress depuis platform")
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.platforms:
|
||||
config.current_platform = config.selected_platform
|
||||
config.games = load_games(config.platforms[config.current_platform]) # Appel à load_games depuis utils
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
draw_validation_transition(screen, config.current_platform) # Animation de transition
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Plateforme sélectionnée: {config.platforms[config.current_platform]}, {len(config.games)} jeux chargés")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "confirm_exit"
|
||||
config.confirm_selection = 0
|
||||
|
||||
# Jeux
|
||||
elif config.menu_state == "game":
|
||||
if config.search_mode:
|
||||
if config.is_non_pc:
|
||||
keyboard_layout = [
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
|
||||
['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'],
|
||||
['W', 'X', 'C', 'V', 'B', 'N']
|
||||
]
|
||||
row, col = config.selected_key
|
||||
max_row = len(keyboard_layout) - 1
|
||||
max_col = len(keyboard_layout[row]) - 1
|
||||
if is_input_matched(event, "up"):
|
||||
if row > 0:
|
||||
config.selected_key = (row - 1, min(col, len(keyboard_layout[row - 1]) - 1))
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "down"):
|
||||
if row < max_row:
|
||||
config.selected_key = (row + 1, min(col, len(keyboard_layout[row + 1]) - 1))
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "left"):
|
||||
if col > 0:
|
||||
config.selected_key = (row, col - 1)
|
||||
config.repeat_action = "left"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "right"):
|
||||
if col < max_col:
|
||||
config.selected_key = (row, col + 1)
|
||||
config.repeat_action = "right"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
key = keyboard_layout[row][col]
|
||||
if len(config.search_query) < 50:
|
||||
config.search_query += key.lower()
|
||||
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "delete"):
|
||||
config.search_query = config.search_query[:-1]
|
||||
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "space"):
|
||||
config.search_query += " "
|
||||
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.search_mode = False
|
||||
config.search_query = ""
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Filtre annulé")
|
||||
elif is_input_matched(event, "filter"):
|
||||
config.search_mode = False
|
||||
config.filter_active = bool(config.search_query)
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
if is_input_matched(event, "confirm"):
|
||||
config.search_mode = False
|
||||
config.filter_active = bool(config.search_query)
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.search_mode = False
|
||||
config.search_query = ""
|
||||
config.filtered_games = config.games
|
||||
config.filter_active = False
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Filtre annulé")
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_BACKSPACE:
|
||||
config.search_query = config.search_query[:-1]
|
||||
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif event.key == pygame.K_SPACE:
|
||||
config.search_query += " "
|
||||
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
elif event.unicode.isprintable() and len(config.search_query) < 50:
|
||||
config.search_query += event.unicode
|
||||
config.filtered_games = [game for game in config.games if config.search_query.lower() in game[0].lower()] if config.search_query else config.games
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
if is_input_matched(event, "down"):
|
||||
config.current_game = min(config.current_game + 1, len(config.filtered_games) - 1)
|
||||
if config.current_game >= config.scroll_offset + config.visible_games:
|
||||
config.scroll_offset += 1
|
||||
config.repeat_action = "down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "up"):
|
||||
config.current_game = max(config.current_game - 1, 0)
|
||||
if config.current_game < config.scroll_offset:
|
||||
config.scroll_offset -= 1
|
||||
config.repeat_action = "up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_up"):
|
||||
config.current_game = max(config.current_game - config.visible_games, 0)
|
||||
config.scroll_offset = max(config.scroll_offset - config.visible_games, 0)
|
||||
config.repeat_action = "page_up"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "page_down"):
|
||||
config.current_game = min(config.current_game + config.visible_games, len(config.filtered_games) - 1)
|
||||
config.scroll_offset = min(config.scroll_offset + config.visible_games, len(config.filtered_games) - config.visible_games)
|
||||
config.repeat_action = "page_down"
|
||||
config.repeat_start_time = current_time + REPEAT_DELAY
|
||||
config.repeat_last_action = current_time
|
||||
config.repeat_key = event.key if event.type == pygame.KEYDOWN else event.button if event.type == pygame.JOYBUTTONDOWN else (event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION else event.value
|
||||
config.needs_redraw = True
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.filtered_games:
|
||||
action = "download"
|
||||
elif is_input_matched(event, "filter"):
|
||||
config.search_mode = True
|
||||
config.search_query = ""
|
||||
config.filtered_games = config.games
|
||||
config.selected_key = (0, 0)
|
||||
config.needs_redraw = True
|
||||
logger.debug("Entrée en mode recherche")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "platform"
|
||||
config.current_game = 0
|
||||
config.scroll_offset = 0
|
||||
config.filter_active = False
|
||||
config.filtered_games = config.games
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à platform, filtre réinitialisé")
|
||||
elif is_input_matched(event, "progress"):
|
||||
if config.download_tasks:
|
||||
config.menu_state = "download_progress"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à download_progress depuis game")
|
||||
|
||||
# Download progress
|
||||
elif config.menu_state == "download_progress":
|
||||
if is_input_matched(event, "cancel"):
|
||||
if config.download_tasks:
|
||||
task = list(config.download_tasks.keys())[0]
|
||||
config.download_tasks[task][0].cancel()
|
||||
url = config.download_tasks[task][1]
|
||||
game_name = config.download_tasks[task][2]
|
||||
if url in config.download_progress:
|
||||
del config.download_progress[url]
|
||||
del config.download_tasks[task]
|
||||
config.download_result_message = f"Téléchargement annulé : {game_name}"
|
||||
config.download_result_error = True
|
||||
config.download_result_start_time = pygame.time.get_ticks()
|
||||
config.menu_state = "download_result"
|
||||
elif is_input_matched(event, "progress"):
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à game depuis download_progress")
|
||||
|
||||
# Confirmation de sortie
|
||||
elif config.menu_state == "confirm_exit":
|
||||
if is_input_matched(event, "left"):
|
||||
config.confirm_selection = 1
|
||||
config.needs_redraw = True
|
||||
logger.debug("Sélection Oui")
|
||||
elif is_input_matched(event, "right"):
|
||||
config.confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Sélection Non")
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.confirm_selection == 1:
|
||||
logger.debug("Retour de 'quit' pour fermer l'application")
|
||||
return "quit"
|
||||
else:
|
||||
config.menu_state = "platform"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à platform depuis confirm_exit")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "platform"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation confirm_exit")
|
||||
|
||||
# Avertissement d'extension
|
||||
elif config.menu_state == "extension_warning":
|
||||
if is_input_matched(event, "left"):
|
||||
config.extension_confirm_selection = 1
|
||||
config.needs_redraw = True
|
||||
logger.debug("Sélection Oui (extension_warning)")
|
||||
elif is_input_matched(event, "right"):
|
||||
config.extension_confirm_selection = 0
|
||||
config.needs_redraw = True
|
||||
logger.debug("Sélection Non (extension_warning)")
|
||||
elif is_input_matched(event, "confirm"):
|
||||
if config.extension_confirm_selection == 1:
|
||||
if config.pending_download:
|
||||
url, platform, game_name, is_zip_non_supported = config.pending_download
|
||||
task = asyncio.create_task(download_rom(url, platform, game_name, is_zip_non_supported=is_zip_non_supported))
|
||||
config.download_tasks[task] = (task, url, game_name, platform)
|
||||
config.menu_state = "download_progress"
|
||||
config.pending_download = None
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
config.menu_state = "game"
|
||||
config.pending_download = None
|
||||
config.needs_redraw = True
|
||||
logger.debug("Téléchargement annulé (extension_warning)")
|
||||
elif is_input_matched(event, "cancel"):
|
||||
config.menu_state = "game"
|
||||
config.pending_download = None
|
||||
config.needs_redraw = True
|
||||
logger.debug("Annulation extension_warning")
|
||||
|
||||
# Résultat téléchargement
|
||||
elif config.menu_state == "download_result":
|
||||
if is_input_matched(event, "confirm"):
|
||||
config.menu_state = "game"
|
||||
config.needs_redraw = True
|
||||
logger.debug("Retour à game depuis download_result")
|
||||
|
||||
# Enregistrer la touche pour la répétition
|
||||
if config.repeat_action in ["up", "down", "page_up", "page_down", "left", "right"]:
|
||||
if event.type == pygame.KEYDOWN:
|
||||
config.repeat_key = event.key
|
||||
elif event.type == pygame.JOYBUTTONDOWN:
|
||||
config.repeat_key = event.button
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
config.repeat_key = (event.axis, 1 if event.value > 0 else -1)
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
config.repeat_key = event.value
|
||||
config.repeat_last_action = current_time
|
||||
|
||||
elif event.type in (pygame.KEYUP, pygame.JOYBUTTONUP):
|
||||
if config.menu_state in ("game", "platform") and is_input_matched(event, config.repeat_action):
|
||||
config.repeat_action = None
|
||||
config.repeat_key = None
|
||||
config.repeat_start_time = 0
|
||||
config.needs_redraw = True
|
||||
|
||||
# Gestion de la répétition automatique
|
||||
if config.menu_state in ("game", "platform") and config.repeat_action:
|
||||
if current_time >= config.repeat_start_time:
|
||||
if config.repeat_action in ["up", "down", "left", "right"] and current_time - config.repeat_last_action < REPEAT_ACTION_DEBOUNCE:
|
||||
return action
|
||||
|
||||
last_repeat_time = config.repeat_start_time - REPEAT_INTERVAL
|
||||
config.repeat_last_action = current_time
|
||||
if config.menu_state == "game":
|
||||
if config.repeat_action == "down":
|
||||
config.current_game = min(config.current_game + 1, len(config.filtered_games) - 1)
|
||||
if config.current_game >= config.scroll_offset + config.visible_games:
|
||||
config.scroll_offset += 1
|
||||
config.needs_redraw = True
|
||||
elif config.repeat_action == "up":
|
||||
config.current_game = max(config.current_game - 1, 0)
|
||||
if config.current_game < config.scroll_offset:
|
||||
config.scroll_offset -= 1
|
||||
config.needs_redraw = True
|
||||
elif config.repeat_action == "page_down":
|
||||
config.current_game = min(config.current_game + config.visible_games, len(config.filtered_games) - 1)
|
||||
config.scroll_offset = min(config.scroll_offset + config.visible_games, len(config.filtered_games) - config.visible_games)
|
||||
config.needs_redraw = True
|
||||
elif config.repeat_action == "page_up":
|
||||
config.current_game = max(config.current_game - config.visible_games, 0)
|
||||
config.scroll_offset = max(config.scroll_offset - config.visible_games, 0)
|
||||
config.needs_redraw = True
|
||||
elif config.menu_state == "platform":
|
||||
max_index = min(9, len(config.platforms) - config.current_page * 9) - 1
|
||||
current_grid_index = config.selected_platform - config.current_page * 9
|
||||
row = current_grid_index // 3
|
||||
if config.repeat_action == "down":
|
||||
if current_grid_index + 3 <= max_index:
|
||||
config.selected_platform += 3
|
||||
config.needs_redraw = True
|
||||
elif config.repeat_action == "up":
|
||||
if current_grid_index - 3 >= 0:
|
||||
config.selected_platform -= 3
|
||||
config.needs_redraw = True
|
||||
elif config.repeat_action == "left":
|
||||
if current_grid_index % 3 != 0:
|
||||
config.selected_platform -= 1
|
||||
config.needs_redraw = True
|
||||
elif config.current_page > 0:
|
||||
config.current_page -= 1
|
||||
config.selected_platform = config.current_page * 9 + row * 3 + 2
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
config.needs_redraw = True
|
||||
elif config.repeat_action == "right":
|
||||
if current_grid_index % 3 != 2 and current_grid_index < max_index:
|
||||
config.selected_platform += 1
|
||||
config.needs_redraw = True
|
||||
elif (config.current_page + 1) * 9 < len(config.platforms):
|
||||
config.current_page += 1
|
||||
config.selected_platform = config.current_page * 9 + row * 3
|
||||
if config.selected_platform >= len(config.platforms):
|
||||
config.selected_platform = len(config.platforms) - 1
|
||||
config.needs_redraw = True
|
||||
config.repeat_start_time = last_repeat_time + REPEAT_INTERVAL
|
||||
if config.repeat_start_time < current_time:
|
||||
config.repeat_start_time = current_time + REPEAT_INTERVAL
|
||||
|
||||
return action
|
||||
476
controls_mapper.py
Normal file
476
controls_mapper.py
Normal file
@@ -0,0 +1,476 @@
|
||||
import pygame
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import config
|
||||
from display import draw_gradient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Chemin du fichier de configuration des contrôles
|
||||
CONTROLS_CONFIG_PATH = "/userdata/saves/ports/rgsx/controls.json"
|
||||
|
||||
# Actions internes de RGSX à mapper
|
||||
ACTIONS = [
|
||||
{"name": "confirm", "display": "Confirmer", "description": "Valider (ex: A, Entrée)"},
|
||||
{"name": "cancel", "display": "Annuler", "description": "Annuler/Retour (ex: B, RetourArrière)"},
|
||||
{"name": "up", "display": "Haut", "description": "Naviguer vers le haut"},
|
||||
{"name": "down", "display": "Bas", "description": "Naviguer vers le bas"},
|
||||
{"name": "left", "display": "Gauche", "description": "Naviguer à gauche"},
|
||||
{"name": "right", "display": "Droite", "description": "Naviguer à droite"},
|
||||
{"name": "page_up", "display": "Page Précédente", "description": "Page précédente (ex: PageUp, LB)"},
|
||||
{"name": "page_down", "display": "Page Suivante", "description": "Page suivante (ex: PageDown, RB)"},
|
||||
{"name": "progress", "display": "Progression", "description": "Voir progression (ex: X)"},
|
||||
{"name": "filter", "display": "Filtrer", "description": "Ouvrir filtre (ex: F, Select)"},
|
||||
{"name": "delete", "display": "Supprimer", "description": "Supprimer caractère (ex: LT, Suppr)"},
|
||||
{"name": "space", "display": "Espace", "description": "Ajouter espace (ex: RT, Espace)"},
|
||||
{"name": "start", "display": "Start", "description": "Ouvrir le menu pause (ex: Start, AltGr)"},
|
||||
]
|
||||
|
||||
# Mappage des valeurs SDL vers les constantes Pygame
|
||||
SDL_TO_PYGAME_KEY = {
|
||||
1073741906: pygame.K_UP, # Flèche Haut
|
||||
1073741905: pygame.K_DOWN, # Flèche Bas
|
||||
1073741904: pygame.K_LEFT, # Flèche Gauche
|
||||
1073741903: pygame.K_RIGHT, # Flèche Droite
|
||||
1073742050: pygame.K_LALT, # Alt gauche
|
||||
1073742051: pygame.K_RSHIFT, # Alt droit
|
||||
1073742049: pygame.K_LCTRL, # Ctrl gauche
|
||||
1073742053: pygame.K_RCTRL, # Ctrl droit
|
||||
1073742048: pygame.K_LSHIFT, # Shift gauche
|
||||
1073742054: pygame.K_RALT, # Shift droit
|
||||
}
|
||||
|
||||
# Noms lisibles pour les touches clavier
|
||||
KEY_NAMES = {
|
||||
pygame.K_RETURN: "Entrée",
|
||||
pygame.K_ESCAPE: "Échap",
|
||||
pygame.K_SPACE: "Espace",
|
||||
pygame.K_UP: "Flèche Haut",
|
||||
pygame.K_DOWN: "Flèche Bas",
|
||||
pygame.K_LEFT: "Flèche Gauche",
|
||||
pygame.K_RIGHT: "Flèche Droite",
|
||||
pygame.K_BACKSPACE: "Retour Arrière",
|
||||
pygame.K_TAB: "Tab",
|
||||
pygame.K_LALT: "Alt",
|
||||
pygame.K_RALT: "AltGR",
|
||||
pygame.K_LCTRL: "LCtrl",
|
||||
pygame.K_RCTRL: "RCtrl",
|
||||
pygame.K_LSHIFT: "LShift",
|
||||
pygame.K_RSHIFT: "RShift",
|
||||
pygame.K_LMETA: "LMeta",
|
||||
pygame.K_RMETA: "RMeta",
|
||||
pygame.K_CAPSLOCK: "Verr Maj",
|
||||
pygame.K_NUMLOCK: "Verr Num",
|
||||
pygame.K_SCROLLOCK: "Verr Déf",
|
||||
pygame.K_a: "A",
|
||||
pygame.K_b: "B",
|
||||
pygame.K_c: "C",
|
||||
pygame.K_d: "D",
|
||||
pygame.K_e: "E",
|
||||
pygame.K_f: "F",
|
||||
pygame.K_g: "G",
|
||||
pygame.K_h: "H",
|
||||
pygame.K_i: "I",
|
||||
pygame.K_j: "J",
|
||||
pygame.K_k: "K",
|
||||
pygame.K_l: "L",
|
||||
pygame.K_m: "M",
|
||||
pygame.K_n: "N",
|
||||
pygame.K_o: "O",
|
||||
pygame.K_p: "P",
|
||||
pygame.K_q: "Q",
|
||||
pygame.K_r: "R",
|
||||
pygame.K_s: "S",
|
||||
pygame.K_t: "T",
|
||||
pygame.K_u: "U",
|
||||
pygame.K_v: "V",
|
||||
pygame.K_w: "W",
|
||||
pygame.K_x: "X",
|
||||
pygame.K_y: "Y",
|
||||
pygame.K_z: "Z",
|
||||
pygame.K_0: "0",
|
||||
pygame.K_1: "1",
|
||||
pygame.K_2: "2",
|
||||
pygame.K_3: "3",
|
||||
pygame.K_4: "4",
|
||||
pygame.K_5: "5",
|
||||
pygame.K_6: "6",
|
||||
pygame.K_7: "7",
|
||||
pygame.K_8: "8",
|
||||
pygame.K_9: "9",
|
||||
pygame.K_KP0: "Pavé 0",
|
||||
pygame.K_KP1: "Pavé 1",
|
||||
pygame.K_KP2: "Pavé 2",
|
||||
pygame.K_KP3: "Pavé 3",
|
||||
pygame.K_KP4: "Pavé 4",
|
||||
pygame.K_KP5: "Pavé 5",
|
||||
pygame.K_KP6: "Pavé 6",
|
||||
pygame.K_KP7: "Pavé 7",
|
||||
pygame.K_KP8: "Pavé 8",
|
||||
pygame.K_KP9: "Pavé 9",
|
||||
pygame.K_KP_PERIOD: "Pavé .",
|
||||
pygame.K_KP_DIVIDE: "Pavé /",
|
||||
pygame.K_KP_MULTIPLY: "Pavé *",
|
||||
pygame.K_KP_MINUS: "Pavé -",
|
||||
pygame.K_KP_PLUS: "Pavé +",
|
||||
pygame.K_KP_ENTER: "Pavé Entrée",
|
||||
pygame.K_KP_EQUALS: "Pavé =",
|
||||
pygame.K_F1: "F1",
|
||||
pygame.K_F2: "F2",
|
||||
pygame.K_F3: "F3",
|
||||
pygame.K_F4: "F4",
|
||||
pygame.K_F5: "F5",
|
||||
pygame.K_F6: "F6",
|
||||
pygame.K_F7: "F7",
|
||||
pygame.K_F8: "F8",
|
||||
pygame.K_F9: "F9",
|
||||
pygame.K_F10: "F10",
|
||||
pygame.K_F11: "F11",
|
||||
pygame.K_F12: "F12",
|
||||
pygame.K_F13: "F13",
|
||||
pygame.K_F14: "F14",
|
||||
pygame.K_F15: "F15",
|
||||
pygame.K_INSERT: "Inser",
|
||||
pygame.K_DELETE: "Suppr",
|
||||
pygame.K_HOME: "Début",
|
||||
pygame.K_END: "Fin",
|
||||
pygame.K_PAGEUP: "Page Haut",
|
||||
pygame.K_PAGEDOWN: "Page Bas",
|
||||
pygame.K_PRINT: "Impr Écran",
|
||||
pygame.K_SYSREQ: "SysReq",
|
||||
pygame.K_BREAK: "Pause",
|
||||
pygame.K_PAUSE: "Pause",
|
||||
pygame.K_BACKQUOTE: "`",
|
||||
pygame.K_MINUS: "-",
|
||||
pygame.K_EQUALS: "=",
|
||||
pygame.K_LEFTBRACKET: "[",
|
||||
pygame.K_RIGHTBRACKET: "]",
|
||||
pygame.K_BACKSLASH: "\\",
|
||||
pygame.K_SEMICOLON: ";",
|
||||
pygame.K_QUOTE: "'",
|
||||
pygame.K_COMMA: ",",
|
||||
pygame.K_PERIOD: ".",
|
||||
pygame.K_SLASH: "/",
|
||||
}
|
||||
|
||||
# Noms lisibles pour les boutons de manette
|
||||
BUTTON_NAMES = {
|
||||
0: "A",
|
||||
1: "B",
|
||||
2: "X",
|
||||
3: "Y",
|
||||
4: "LB",
|
||||
5: "RB",
|
||||
6: "LT",
|
||||
7: "RT",
|
||||
8: "Select",
|
||||
9: "Start",
|
||||
}
|
||||
|
||||
# Noms pour les axes de joystick
|
||||
AXIS_NAMES = {
|
||||
(0, 1): "Joy G Haut",
|
||||
(0, -1): "Joy G Bas",
|
||||
(1, 1): "Joy G Gauche",
|
||||
(1, -1): "Joy G Droite",
|
||||
(2, 1): "Joy D Haut",
|
||||
(2, -1): "Joy D Bas",
|
||||
(3, 1): "Joy D Gauche",
|
||||
(3, -1): "Joy D Droite",
|
||||
}
|
||||
|
||||
# Noms pour la croix directionnelle
|
||||
HAT_NAMES = {
|
||||
(0, 1): "D-Pad Haut",
|
||||
(0, -1): "D-Pad Bas",
|
||||
(-1, 0): "D-Pad Gauche",
|
||||
(1, 0): "D-Pad Droite",
|
||||
}
|
||||
|
||||
# Noms pour les boutons de souris
|
||||
MOUSE_BUTTON_NAMES = {
|
||||
1: "Clic Gauche",
|
||||
2: "Clic Milieu",
|
||||
3: "Clic Droit",
|
||||
}
|
||||
|
||||
# Durée de maintien pour valider une entrée (en millisecondes)
|
||||
HOLD_DURATION = 1000
|
||||
|
||||
def load_controls_config():
|
||||
"""Charge la configuration des contrôles depuis controls.json."""
|
||||
try:
|
||||
if os.path.exists(CONTROLS_CONFIG_PATH):
|
||||
with open(CONTROLS_CONFIG_PATH, "r") as f:
|
||||
config = json.load(f)
|
||||
logger.debug(f"Configuration des contrôles chargée : {config}")
|
||||
return config
|
||||
else:
|
||||
logger.debug("Aucun fichier controls.json trouvé, configuration par défaut.")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de controls.json : {e}")
|
||||
return {}
|
||||
|
||||
def save_controls_config(controls_config):
|
||||
"""Enregistre la configuration des contrôles dans controls.json."""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(CONTROLS_CONFIG_PATH), exist_ok=True)
|
||||
with open(CONTROLS_CONFIG_PATH, "w") as f:
|
||||
json.dump(controls_config, f, indent=4)
|
||||
logger.debug(f"Configuration des contrôles enregistrée : {controls_config}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'enregistrement de controls.json : {e}")
|
||||
|
||||
def get_readable_input_name(event):
|
||||
"""Retourne un nom lisible pour une entrée (touche, bouton, axe, hat, ou souris)."""
|
||||
if event.type == pygame.KEYDOWN:
|
||||
key_value = SDL_TO_PYGAME_KEY.get(event.key, event.key)
|
||||
return KEY_NAMES.get(key_value, pygame.key.name(key_value) or f"Touche {key_value}")
|
||||
elif event.type == pygame.JOYBUTTONDOWN:
|
||||
return BUTTON_NAMES.get(event.button, f"Bouton {event.button}")
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
if abs(event.value) > 0.5: # Seuil pour détecter un mouvement significatif
|
||||
return AXIS_NAMES.get((event.axis, 1 if event.value > 0 else -1), f"Axe {event.axis}")
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
return HAT_NAMES.get(event.value, f"D-Pad {event.value}")
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||
return MOUSE_BUTTON_NAMES.get(event.button, f"Souris Bouton {event.button}")
|
||||
return "Inconnu"
|
||||
|
||||
def map_controls(screen):
|
||||
"""Interface de mappage des contrôles avec validation par maintien de 3 secondes."""
|
||||
controls_config = load_controls_config()
|
||||
current_action_index = 0
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
last_frame_time = pygame.time.get_ticks()
|
||||
config.needs_redraw = True
|
||||
|
||||
# Initialiser l'état des boutons et axes pour suivre les relâchements
|
||||
held_keys = set()
|
||||
held_buttons = set()
|
||||
held_axes = {} # {axis: direction}
|
||||
held_hats = {} # {hat: value}
|
||||
held_mouse_buttons = set()
|
||||
|
||||
while current_action_index < len(ACTIONS):
|
||||
if config.needs_redraw:
|
||||
progress = min(input_held_time / HOLD_DURATION, 1.0) if current_input else 0.0
|
||||
draw_controls_mapping(screen, ACTIONS[current_action_index], last_input_name, current_input is not None, progress)
|
||||
pygame.display.flip()
|
||||
config.needs_redraw = False
|
||||
|
||||
current_time = pygame.time.get_ticks()
|
||||
delta_time = current_time - last_frame_time
|
||||
last_frame_time = current_time
|
||||
|
||||
events = pygame.event.get()
|
||||
for event in events:
|
||||
if event.type == pygame.QUIT:
|
||||
return False
|
||||
|
||||
# Détecter les relâchements pour réinitialiser
|
||||
if event.type == pygame.KEYUP:
|
||||
if event.key in held_keys:
|
||||
held_keys.remove(event.key)
|
||||
if current_input and current_input["type"] == "key" and current_input["value"] == event.key:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Touche relâchée: {event.key}")
|
||||
elif event.type == pygame.JOYBUTTONUP:
|
||||
if event.button in held_buttons:
|
||||
held_buttons.remove(event.button)
|
||||
if current_input and current_input["type"] == "button" and current_input["value"] == event.button:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Bouton relâché: {event.button}")
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
if event.button in held_mouse_buttons:
|
||||
held_mouse_buttons.remove(event.button)
|
||||
if current_input and current_input["type"] == "mouse" and current_input["value"] == event.button:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Bouton souris relâché: {event.button}")
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
if abs(event.value) < 0.5: # Axe revenu à la position neutre
|
||||
if event.axis in held_axes:
|
||||
del held_axes[event.axis]
|
||||
if current_input and current_input["type"] == "axis" and current_input["value"][0] == event.axis:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Axe relâché: {event.axis}")
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
if event.value == (0, 0): # D-Pad revenu à la position neutre
|
||||
if event.hat in held_hats:
|
||||
del held_hats[event.hat]
|
||||
if current_input and current_input["type"] == "hat" and current_input["value"] == event.value:
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"D-Pad relâché: {event.hat}")
|
||||
|
||||
# Détecter les nouvelles entrées
|
||||
if event.type in (pygame.KEYDOWN, pygame.JOYBUTTONDOWN, pygame.JOYAXISMOTION, pygame.JOYHATMOTION, pygame.MOUSEBUTTONDOWN):
|
||||
input_name = get_readable_input_name(event)
|
||||
if input_name != "Inconnu":
|
||||
input_type = {
|
||||
pygame.KEYDOWN: "key",
|
||||
pygame.JOYBUTTONDOWN: "button",
|
||||
pygame.JOYAXISMOTION: "axis",
|
||||
pygame.JOYHATMOTION: "hat",
|
||||
pygame.MOUSEBUTTONDOWN: "mouse",
|
||||
}[event.type]
|
||||
input_value = (
|
||||
SDL_TO_PYGAME_KEY.get(event.key, event.key) if event.type == pygame.KEYDOWN else
|
||||
event.button if event.type == pygame.JOYBUTTONDOWN else
|
||||
(event.axis, 1 if event.value > 0 else -1) if event.type == pygame.JOYAXISMOTION and abs(event.value) > 0.5 else
|
||||
event.value if event.type == pygame.JOYHATMOTION else
|
||||
event.button
|
||||
)
|
||||
|
||||
# Vérifier si l'entrée est nouvelle ou différente
|
||||
if (current_input is None or
|
||||
(input_type == "key" and current_input["value"] != input_value) or
|
||||
(input_type == "button" and current_input["value"] != input_value) or
|
||||
(input_type == "axis" and current_input["value"] != input_value) or
|
||||
(input_type == "hat" and current_input["value"] != input_value) or
|
||||
(input_type == "mouse" and current_input["value"] != input_value)):
|
||||
current_input = {"type": input_type, "value": input_value}
|
||||
input_held_time = 0
|
||||
last_input_name = input_name
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Nouvelle entrée détectée: {input_type}:{input_value} ({input_name})")
|
||||
|
||||
# Mettre à jour les entrées maintenues
|
||||
if input_type == "key":
|
||||
held_keys.add(input_value)
|
||||
elif input_type == "button":
|
||||
held_buttons.add(input_value)
|
||||
elif input_type == "axis":
|
||||
held_axes[input_value[0]] = input_value[1]
|
||||
elif input_type == "hat":
|
||||
held_hats[event.hat] = input_value
|
||||
elif input_type == "mouse":
|
||||
held_mouse_buttons.add(input_value)
|
||||
|
||||
# Sauter à l'action suivante avec Échap
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
||||
action_name = ACTIONS[current_action_index]["name"]
|
||||
controls_config[action_name] = {} # Marquer comme non mappé
|
||||
current_action_index += 1
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
logger.debug(f"Action {action_name} ignorée avec Échap, passage à l'action suivante: {ACTIONS[current_action_index]['name'] if current_action_index < len(ACTIONS) else 'fin'}")
|
||||
|
||||
# Mettre à jour le temps de maintien
|
||||
if current_input:
|
||||
input_held_time += delta_time
|
||||
if input_held_time >= HOLD_DURATION:
|
||||
action_name = ACTIONS[current_action_index]["name"]
|
||||
logger.debug(f"Entrée validée pour {action_name}: {current_input['type']}:{current_input['value']} ({last_input_name})")
|
||||
controls_config[action_name] = {
|
||||
"type": current_input["type"],
|
||||
"value": current_input["value"],
|
||||
"display": last_input_name
|
||||
}
|
||||
current_action_index += 1
|
||||
current_input = None
|
||||
input_held_time = 0
|
||||
last_input_name = None
|
||||
config.needs_redraw = True
|
||||
config.needs_redraw = True
|
||||
|
||||
pygame.time.wait(10)
|
||||
|
||||
save_controls_config(controls_config)
|
||||
config.controls_config = controls_config
|
||||
return True
|
||||
|
||||
def draw_controls_mapping(screen, action, last_input, waiting_for_input, hold_progress):
|
||||
"""Affiche l'interface de mappage des contrôles avec une barre de progression pour le maintien."""
|
||||
draw_gradient(screen, (28, 37, 38), (47, 59, 61))
|
||||
|
||||
max_width = config.screen_width // 1.2
|
||||
padding_horizontal = 40
|
||||
padding_vertical = 30
|
||||
padding_between = 10
|
||||
border_radius = 24
|
||||
border_width = 4
|
||||
shadow_offset = 8
|
||||
|
||||
# Instructions
|
||||
instruction_text = f"Maintenez une touche/bouton pendant 3s pour '{action['display']}'"
|
||||
description_text = action['description']
|
||||
skip_text = "Appuyez sur Échap pour passer"
|
||||
instruction_surface = config.font.render(instruction_text, True, (255, 255, 255))
|
||||
description_surface = config.font.render(description_text, True, (200, 200, 200))
|
||||
skip_surface = config.font.render(skip_text, True, (255, 255, 255))
|
||||
instruction_width, instruction_height = instruction_surface.get_size()
|
||||
description_width, description_height = description_surface.get_size()
|
||||
skip_width, skip_height = skip_surface.get_size()
|
||||
|
||||
# Input détecté
|
||||
input_text = last_input or (f"En attente d'une entrée..." if waiting_for_input else "Maintenez une touche/bouton")
|
||||
input_surface = config.font.render(input_text, True, (0, 255, 0) if last_input else (255, 255, 255))
|
||||
input_width, input_height = input_surface.get_size()
|
||||
|
||||
# Dimensions de la popup
|
||||
text_width = max(instruction_width, description_width, input_width, skip_width)
|
||||
text_height = instruction_height + description_height + input_height + skip_height + 3 * padding_between
|
||||
popup_width = text_width + 2 * padding_horizontal
|
||||
popup_height = text_height + 40 + 2 * padding_vertical # +40 pour la barre de progression
|
||||
popup_x = (config.screen_width - popup_width) // 2
|
||||
popup_y = (config.screen_height - popup_height) // 2
|
||||
|
||||
# Ombre portée
|
||||
shadow_rect = pygame.Rect(popup_x + shadow_offset, popup_y + shadow_offset, popup_width, popup_height)
|
||||
shadow_surface = pygame.Surface((popup_width, popup_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(shadow_surface, (0, 0, 0, 100), shadow_surface.get_rect(), border_radius=border_radius)
|
||||
screen.blit(shadow_surface, shadow_rect.topleft)
|
||||
|
||||
# Fond semi-transparent
|
||||
popup_rect = pygame.Rect(popup_x, popup_y, popup_width, popup_height)
|
||||
popup_surface = pygame.Surface((popup_width, popup_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(popup_surface, (30, 30, 30, 220), popup_surface.get_rect(), border_radius=border_radius)
|
||||
screen.blit(popup_surface, popup_rect.topleft)
|
||||
|
||||
# Bordure blanche
|
||||
pygame.draw.rect(screen, (255, 255, 255), popup_rect, border_width, border_radius=border_radius)
|
||||
|
||||
# Afficher les textes
|
||||
start_y = popup_y + padding_vertical
|
||||
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, start_y + instruction_height // 2))
|
||||
screen.blit(instruction_surface, instruction_rect)
|
||||
start_y += instruction_height + padding_between
|
||||
description_rect = description_surface.get_rect(center=(config.screen_width // 2, start_y + description_height // 2))
|
||||
screen.blit(description_surface, description_rect)
|
||||
start_y += description_height + padding_between
|
||||
input_rect = input_surface.get_rect(center=(config.screen_width // 2, start_y + input_height // 2))
|
||||
screen.blit(input_surface, input_rect)
|
||||
start_y += input_height + padding_between
|
||||
skip_rect = skip_surface.get_rect(center=(config.screen_width // 2, start_y + skip_height // 2))
|
||||
screen.blit(skip_surface, skip_rect)
|
||||
|
||||
# Barre de progression pour le maintien
|
||||
bar_width = 200
|
||||
bar_height = 20
|
||||
bar_x = (config.screen_width - bar_width) // 2
|
||||
bar_y = start_y + skip_height + 20
|
||||
pygame.draw.rect(screen, (100, 100, 100), (bar_x, bar_y, bar_width, bar_height))
|
||||
progress_width = bar_width * hold_progress
|
||||
pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, progress_width, bar_height))
|
||||
pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 2)
|
||||
548
display.py
Normal file
548
display.py
Normal file
@@ -0,0 +1,548 @@
|
||||
import pygame
|
||||
import config
|
||||
import math
|
||||
from utils import truncate_text_end, wrap_text, load_system_image, load_games
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def init_display():
|
||||
"""Initialise l’écran Pygame."""
|
||||
screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
|
||||
return screen
|
||||
|
||||
def draw_gradient(screen, top_color, bottom_color):
|
||||
"""Dessine un fond dégradé vertical."""
|
||||
height = screen.get_height()
|
||||
top_color = pygame.Color(*top_color)
|
||||
bottom_color = pygame.Color(*bottom_color)
|
||||
for y in range(height):
|
||||
ratio = y / height
|
||||
color = top_color.lerp(bottom_color, ratio)
|
||||
pygame.draw.line(screen, color, (0, y), (screen.get_width(), y))
|
||||
|
||||
def draw_loading_screen(screen):
|
||||
"""Affiche l’écran de chargement avec le disclaimer en haut, le texte de chargement et la barre de progression."""
|
||||
disclaimer_lines = [
|
||||
"Bienvenue dans RGSX",
|
||||
"It's dangerous to go alone, take all you need!",
|
||||
"Mais ne téléchargez que des jeux",
|
||||
"dont vous possédez les originaux !"
|
||||
]
|
||||
|
||||
margin_horizontal = 20
|
||||
padding_vertical = 20
|
||||
padding_between = 8
|
||||
border_radius = 16
|
||||
border_width = 3
|
||||
shadow_offset = 6
|
||||
|
||||
line_height = config.font.get_height() + padding_between
|
||||
total_height = line_height * len(disclaimer_lines) - padding_between
|
||||
rect_width = config.screen_width - 2 * margin_horizontal
|
||||
rect_height = total_height + 2 * padding_vertical
|
||||
rect_x = margin_horizontal
|
||||
rect_y = 20
|
||||
|
||||
shadow_rect = pygame.Rect(rect_x + shadow_offset, rect_y + shadow_offset, rect_width, rect_height)
|
||||
shadow_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(shadow_surface, (0, 0, 0, 100), shadow_surface.get_rect(), border_radius=border_radius)
|
||||
screen.blit(shadow_surface, shadow_rect.topleft)
|
||||
|
||||
disclaimer_rect = pygame.Rect(rect_x, rect_y, rect_width, rect_height)
|
||||
disclaimer_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(disclaimer_surface, (30, 30, 30, 220), disclaimer_surface.get_rect(), border_radius=border_radius)
|
||||
screen.blit(disclaimer_surface, disclaimer_rect.topleft)
|
||||
|
||||
pygame.draw.rect(screen, (255, 255, 255), disclaimer_rect, border_width, border_radius=border_radius)
|
||||
|
||||
for i, line in enumerate(disclaimer_lines):
|
||||
text_surface = config.font.render(line, True, (255, 255, 255))
|
||||
text_rect = text_surface.get_rect(center=(
|
||||
config.screen_width // 2,
|
||||
rect_y + padding_vertical + (i + 0.5) * line_height - padding_between // 2
|
||||
))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
loading_y = rect_y + rect_height + 100
|
||||
text = config.font.render(f"{config.current_loading_system}", True, (255, 255, 255))
|
||||
text_rect = text.get_rect(center=(config.screen_width // 2, loading_y))
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
progress_text = config.font.render(f"Progression : {int(config.loading_progress)}%", True, (255, 255, 255))
|
||||
progress_rect = progress_text.get_rect(center=(config.screen_width // 2, loading_y + 50))
|
||||
screen.blit(progress_text, progress_rect)
|
||||
|
||||
bar_width = 400
|
||||
bar_height = 40
|
||||
progress_width = (bar_width * config.loading_progress) / 100
|
||||
pygame.draw.rect(screen, (100, 100, 100), (config.screen_width // 2 - bar_width // 2, loading_y + 100, bar_width, bar_height))
|
||||
pygame.draw.rect(screen, (0, 255, 0), (config.screen_width // 2 - bar_width // 2, loading_y + 100, progress_width, bar_height))
|
||||
|
||||
def draw_error_screen(screen):
|
||||
"""Affiche l’écran d’erreur."""
|
||||
error_font = pygame.font.SysFont("arial", 28)
|
||||
text = error_font.render(config.error_message, True, (255, 0, 0))
|
||||
text_rect = text.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
|
||||
screen.blit(text, text_rect)
|
||||
retry_text = config.font.render(f"{config.controls_config.get('confirm', {}).get('display', 'Entrée/A')} : retenter, {config.controls_config.get('cancel', {}).get('display', 'Échap/B')} : quitter", True, (255, 255, 255))
|
||||
retry_rect = retry_text.get_rect(center=(config.screen_width // 2, config.screen_height // 2 + 100))
|
||||
screen.blit(retry_text, retry_rect)
|
||||
|
||||
def draw_platform_grid(screen):
|
||||
"""Affiche la grille des plateformes."""
|
||||
margin_left = 50
|
||||
margin_right = 50
|
||||
margin_top = 120
|
||||
margin_bottom = 70
|
||||
num_cols = 3
|
||||
num_rows = 3
|
||||
systems_per_page = num_cols * num_rows
|
||||
|
||||
available_width = config.screen_width - margin_left - margin_right
|
||||
available_height = config.screen_height - margin_top - margin_bottom
|
||||
|
||||
col_width = available_width // num_cols
|
||||
row_height = available_height // num_rows
|
||||
|
||||
x_positions = [margin_left + col_width * i + col_width // 2 for i in range(num_cols)]
|
||||
y_positions = [margin_top + row_height * i + row_height // 2 for i in range(num_rows)]
|
||||
|
||||
start_idx = config.current_page * systems_per_page
|
||||
logger.debug(f"Page {config.current_page}, start_idx: {start_idx}, total_platforms: {len(config.platforms)}")
|
||||
|
||||
for idx in range(start_idx, start_idx + systems_per_page):
|
||||
if idx >= len(config.platforms):
|
||||
break
|
||||
grid_idx = idx - start_idx
|
||||
row = grid_idx // num_cols
|
||||
col = grid_idx % num_cols
|
||||
x = x_positions[col]
|
||||
y = y_positions[row]
|
||||
scale = 1.5 if idx == config.selected_platform else 1.0
|
||||
platform_dict = config.platform_dicts[idx]
|
||||
image = load_system_image(platform_dict)
|
||||
if image:
|
||||
# Calculer la taille en respectant le ratio d'origine
|
||||
orig_width, orig_height = image.get_width(), image.get_height()
|
||||
max_size = int(min(col_width, row_height) * scale * 0.9)
|
||||
ratio = min(max_size / orig_width, max_size / orig_height)
|
||||
new_width = int(orig_width * ratio)
|
||||
new_height = int(orig_height * ratio)
|
||||
image = pygame.transform.smoothscale(image, (new_width, new_height))
|
||||
image_rect = image.get_rect(center=(x, y))
|
||||
|
||||
if idx == config.selected_platform:
|
||||
neon_color = (0, 255, 255)
|
||||
border_radius = 24
|
||||
padding = 24
|
||||
rect_width = image_rect.width + 2 * padding
|
||||
rect_height = image_rect.height + 2 * padding
|
||||
neon_surface = pygame.Surface((rect_width, rect_height), pygame.SRCALPHA)
|
||||
pygame.draw.rect(
|
||||
neon_surface,
|
||||
neon_color + (60,),
|
||||
neon_surface.get_rect(),
|
||||
width=1,
|
||||
border_radius=border_radius + 8
|
||||
)
|
||||
pygame.draw.rect(
|
||||
neon_surface,
|
||||
neon_color + (180,),
|
||||
neon_surface.get_rect().inflate(-8, -8),
|
||||
width=2,
|
||||
border_radius=border_radius
|
||||
)
|
||||
screen.blit(neon_surface, (image_rect.left - padding, image_rect.top - padding), special_flags=pygame.BLEND_RGBA_ADD)
|
||||
|
||||
screen.blit(image, image_rect)
|
||||
|
||||
def draw_virtual_keyboard(screen):
|
||||
"""Affiche un clavier virtuel pour la saisie dans search_mode, centré verticalement."""
|
||||
keyboard_layout = [
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
|
||||
['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'],
|
||||
['W', 'X', 'C', 'V', 'B', 'N']
|
||||
]
|
||||
key_width = 60
|
||||
key_height = 60
|
||||
key_spacing = 10
|
||||
keyboard_width = len(keyboard_layout[0]) * (key_width + key_spacing) - key_spacing
|
||||
keyboard_height = len(keyboard_layout) * (key_height + key_spacing) - key_spacing
|
||||
start_x = (config.screen_width - keyboard_width) // 2
|
||||
search_bottom_y = 120 + (config.search_font.get_height() + 40) // 2
|
||||
controls_y = config.screen_height - 20
|
||||
available_height = controls_y - search_bottom_y
|
||||
start_y = search_bottom_y + (available_height - keyboard_height - 40) // 2
|
||||
|
||||
keyboard_rect = pygame.Rect(start_x - 20, start_y - 20, keyboard_width + 40, keyboard_height + 40)
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), keyboard_rect, border_radius=10)
|
||||
pygame.draw.rect(screen, (255, 255, 255), keyboard_rect, 2, border_radius=10)
|
||||
|
||||
for row_idx, row in enumerate(keyboard_layout):
|
||||
for col_idx, key in enumerate(row):
|
||||
x = start_x + col_idx * (key_width + key_spacing)
|
||||
y = start_y + row_idx * (key_height + key_spacing)
|
||||
key_rect = pygame.Rect(x, y, key_width, key_height)
|
||||
if (row_idx, col_idx) == config.selected_key:
|
||||
pygame.draw.rect(screen, (0, 150, 255, 150), key_rect, border_radius=5)
|
||||
else:
|
||||
pygame.draw.rect(screen, (80, 80, 80, 255), key_rect, border_radius=5)
|
||||
pygame.draw.rect(screen, (255, 255, 255), key_rect, 1, border_radius=5)
|
||||
text = config.font.render(key, True, (255, 255, 255))
|
||||
text_rect = text.get_rect(center=key_rect.center)
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
def draw_progress_screen(screen):
|
||||
"""Affiche l'écran de progression des téléchargements avec taille en Mo, et un message spécifique pour la conversion ISO."""
|
||||
logger.debug("Début de draw_progress_screen")
|
||||
|
||||
if not config.download_tasks:
|
||||
logger.debug("Aucune tâche de téléchargement active")
|
||||
return
|
||||
|
||||
task = list(config.download_tasks.keys())[0]
|
||||
game_name = config.download_tasks[task][2]
|
||||
url = config.download_tasks[task][1]
|
||||
progress = config.download_progress.get(url, {"downloaded_size": 0, "total_size": 0, "status": "Téléchargement", "progress_percent": 0})
|
||||
status = progress.get("status", "Téléchargement")
|
||||
downloaded_size = progress["downloaded_size"]
|
||||
total_size = progress["total_size"]
|
||||
progress_percent = progress["progress_percent"]
|
||||
logger.debug(f"Progression : game_name={game_name}, url={url}, status={status}, progress_percent={progress_percent}, downloaded_size={downloaded_size}, total_size={total_size}")
|
||||
|
||||
overlay = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 180))
|
||||
screen.blit(overlay, (0, 0))
|
||||
|
||||
if status == "Converting ISO":
|
||||
title_text = f"Converting : {truncate_text_end(game_name, config.font, config.screen_width - 200)}"
|
||||
else:
|
||||
title_text = f"{status} : {truncate_text_end(game_name, config.font, config.screen_width - 200)}"
|
||||
title_render = config.font.render(title_text, True, (255, 255, 255))
|
||||
title_rect = title_render.get_rect(center=(config.screen_width // 2, config.screen_height // 2 - 100))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), title_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), title_rect.inflate(40, 20), 2)
|
||||
screen.blit(title_render, title_rect)
|
||||
logger.debug(f"Titre affiché : texte={title_text}, position={title_rect}, taille={title_render.get_size()}")
|
||||
|
||||
if status == "Converting ISO":
|
||||
conversion_text = config.font.render("Conversion de l'ISO en dossier .ps3 en cours...", True, (255, 255, 255))
|
||||
conversion_rect = conversion_text.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), conversion_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), conversion_rect.inflate(40, 20), 2)
|
||||
screen.blit(conversion_text, conversion_rect)
|
||||
logger.debug(f"Message de conversion affiché : position={conversion_rect}, taille={conversion_text.get_size()}")
|
||||
else:
|
||||
bar_width = config.screen_width // 2
|
||||
bar_height = 30
|
||||
bar_x = (config.screen_width - bar_width) // 2
|
||||
bar_y = config.screen_height // 2
|
||||
progress_width = 0
|
||||
pygame.draw.rect(screen, (100, 100, 100), (bar_x, bar_y, bar_width, bar_height))
|
||||
if total_size > 0:
|
||||
progress_width = int(bar_width * (progress_percent / 100))
|
||||
pygame.draw.rect(screen, (0, 150, 255), (bar_x, bar_y, progress_width, bar_height))
|
||||
pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 2)
|
||||
logger.debug(f"Barre de progression affichée : position=({bar_x}, {bar_y}), taille=({bar_width}, {bar_height}), progress_width={progress_width}")
|
||||
|
||||
downloaded_mb = downloaded_size / (1024 * 1024)
|
||||
total_mb = total_size / (1024 * 1024)
|
||||
size_text = f"{downloaded_mb:.1f} Mo / {total_mb:.1f} Mo"
|
||||
percent_text = f"{int(progress_percent)}% {size_text}"
|
||||
percent_render = config.font.render(percent_text, True, (255, 255, 255))
|
||||
text_y = bar_y + bar_height // 2 + config.font.get_height() + 20
|
||||
percent_rect = percent_render.get_rect(center=(config.screen_width // 2, text_y))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), percent_rect.inflate(20, 10))
|
||||
pygame.draw.rect(screen, (255, 255, 255), percent_rect.inflate(20, 10), 2)
|
||||
screen.blit(percent_render, percent_rect)
|
||||
logger.debug(f"Texte de progression affiché : texte={percent_text}, position={percent_rect}, taille={percent_render.get_size()}")
|
||||
|
||||
def draw_scrollbar(screen):
|
||||
"""Affiche la barre de défilement à droite de l’écran."""
|
||||
if len(config.filtered_games) <= config.visible_games:
|
||||
return
|
||||
|
||||
game_area_height = config.screen_height - 150
|
||||
scrollbar_height = game_area_height * (config.visible_games / len(config.filtered_games))
|
||||
scrollbar_y = 120 + (game_area_height - scrollbar_height) * (config.scroll_offset / max(1, len(config.filtered_games) - config.visible_games))
|
||||
pygame.draw.rect(screen, (255, 255, 255), (config.screen_width - 25, scrollbar_y, 15, scrollbar_height))
|
||||
|
||||
def draw_confirm_dialog(screen):
|
||||
"""Affiche la boîte de dialogue de confirmation pour quitter."""
|
||||
overlay = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 180))
|
||||
screen.blit(overlay, (0, 0))
|
||||
|
||||
message = "Voulez-vous vraiment quitter ?"
|
||||
text = config.font.render(message, True, (255, 255, 255))
|
||||
text_rect = text.get_rect(center=(config.screen_width // 2, config.screen_height // 2 - 50))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), text_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), text_rect.inflate(40, 20), 2)
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
yes_text = config.font.render("Oui", True, (255, 255, 255))
|
||||
no_text = config.font.render("Non", True, (255, 255, 255))
|
||||
yes_rect = yes_text.get_rect(center=(config.screen_width // 2 - 100, config.screen_height // 2 + 50))
|
||||
no_rect = no_text.get_rect(center=(config.screen_width // 2 + 100, config.screen_height // 2 + 50))
|
||||
|
||||
if config.confirm_selection == 1:
|
||||
pygame.draw.rect(screen, (0, 150, 255, 150), yes_rect.inflate(40, 20))
|
||||
else:
|
||||
pygame.draw.rect(screen, (0, 150, 255, 150), no_rect.inflate(40, 20))
|
||||
|
||||
pygame.draw.rect(screen, (255, 255, 255), yes_rect.inflate(40, 20), 2)
|
||||
pygame.draw.rect(screen, (255, 255, 255), no_rect.inflate(40, 20), 2)
|
||||
screen.blit(yes_text, yes_rect)
|
||||
screen.blit(no_text, no_rect)
|
||||
|
||||
def draw_popup_message(screen, message, is_error):
|
||||
"""Affiche une popup avec un message de résultat."""
|
||||
overlay = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 180))
|
||||
screen.blit(overlay, (0, 0))
|
||||
|
||||
text = config.font.render(message, True, (255, 0, 0) if is_error else (0, 255, 0))
|
||||
text_rect = text.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), text_rect.inflate(40, 20))
|
||||
pygame.draw.rect(screen, (255, 255, 255), text_rect.inflate(40, 20), 2)
|
||||
screen.blit(text, text_rect)
|
||||
|
||||
def draw_extension_warning(screen):
|
||||
"""Affiche un avertissement pour une extension non reconnue ou un fichier ZIP."""
|
||||
#logger.debug("Début de draw_extension_warning")
|
||||
|
||||
if not config.pending_download:
|
||||
#logger.error("config.pending_download est None ou vide dans extension_warning")
|
||||
message = "Erreur : Aucun téléchargement en attente."
|
||||
is_zip = False
|
||||
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}")
|
||||
is_zip = is_zip_non_supported
|
||||
if not game_name:
|
||||
game_name = "Inconnu"
|
||||
logger.warning("game_name vide, utilisation de 'Inconnu'")
|
||||
|
||||
if is_zip:
|
||||
message = f"Le fichier '{game_name}' 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 ?"
|
||||
else:
|
||||
message = f"L'extension du fichier '{game_name}' n'est pas supportée par Batocera d'après le fichier info.txt. Voulez-vous continuer ?"
|
||||
|
||||
max_width = config.screen_width - 80
|
||||
lines = wrap_text(message, config.font, max_width)
|
||||
#logger.debug(f"Lignes générées : {lines}")
|
||||
|
||||
try:
|
||||
# Calcul de la hauteur de ligne
|
||||
line_height = config.font.get_height() + 5
|
||||
# Hauteur pour les lignes de texte
|
||||
text_height = len(lines) * line_height
|
||||
# Hauteur pour les boutons (1 ligne + marges)
|
||||
button_height = line_height + 20
|
||||
# Marges en haut et en bas
|
||||
margin_top_bottom = 20
|
||||
# Hauteur totale du rectangle
|
||||
rect_height = text_height + button_height + 2 * margin_top_bottom
|
||||
# Largeur du rectangle
|
||||
max_text_width = max([config.font.size(line)[0] for line in lines], default=300)
|
||||
rect_width = max_text_width + 40
|
||||
# Position du rectangle
|
||||
rect_x = (config.screen_width - rect_width) // 2
|
||||
rect_y = (config.screen_height - rect_height) // 2
|
||||
|
||||
# Dessiner le rectangle de fond
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10)
|
||||
pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10)
|
||||
|
||||
# Afficher les lignes de texte
|
||||
text_surfaces = [config.font.render(line, True, (255, 255, 255)) for line in lines]
|
||||
text_rects = [
|
||||
surface.get_rect(center=(config.screen_width // 2, rect_y + margin_top_bottom + i * line_height + line_height // 2))
|
||||
for i, surface in enumerate(text_surfaces)
|
||||
]
|
||||
for surface, rect in zip(text_surfaces, text_rects):
|
||||
screen.blit(surface, rect)
|
||||
#logger.debug(f"Lignes affichées : {[(rect.center, surface.get_size()) for rect, surface in zip(text_rects, text_surfaces)]}")
|
||||
|
||||
# Afficher les boutons Oui/Non
|
||||
yes_text = "[Oui]" if config.extension_confirm_selection == 1 else "Oui"
|
||||
no_text = "[Non]" if config.extension_confirm_selection == 0 else "Non"
|
||||
yes_surface = config.font.render(yes_text, True, (0, 150, 255) if config.extension_confirm_selection == 1 else (255, 255, 255))
|
||||
no_surface = config.font.render(no_text, True, (0, 150, 255) if config.extension_confirm_selection == 0 else (255, 255, 255))
|
||||
|
||||
button_y = rect_y + text_height + margin_top_bottom + line_height // 2
|
||||
yes_rect = yes_surface.get_rect(center=(config.screen_width // 2 - 100, button_y))
|
||||
no_rect = no_surface.get_rect(center=(config.screen_width // 2 + 100, button_y))
|
||||
|
||||
screen.blit(yes_surface, yes_rect)
|
||||
screen.blit(no_surface, no_rect)
|
||||
#logger.debug(f"Boutons affichés : Oui={yes_rect}, Non={no_rect}, selection={config.extension_confirm_selection}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du rendu de extension_warning : {str(e)}")
|
||||
error_message = "Erreur d'affichage de l'avertissement."
|
||||
error_surface = config.font.render(error_message, True, (255, 0, 0))
|
||||
error_rect = error_surface.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), error_rect.inflate(40, 20), border_radius=10)
|
||||
pygame.draw.rect(screen, (255, 255, 255), error_rect.inflate(40, 20), 2, border_radius=10)
|
||||
screen.blit(error_surface, error_rect)
|
||||
|
||||
def draw_controls(screen, menu_state):
|
||||
"""Affiche les contrôles sur une seule ligne en bas de l’écran pour tous les états du menu."""
|
||||
start_button = config.controls_config.get('start', {}).get('display', 'START')
|
||||
control_text = f"{start_button} : Menu - Controls
|
||||
max_width = config.screen_width - 40
|
||||
control_text = truncate_text_end(control_text, config.font, max_width)
|
||||
text_surface = config.font.render(control_text, True, (255, 255, 255))
|
||||
text_width, text_height = text_surface.get_size()
|
||||
|
||||
rect_width = text_width + 40
|
||||
rect_height = text_height + 20
|
||||
rect_x = (config.screen_width - rect_width) // 2
|
||||
rect_y = config.screen_height - rect_height - 20
|
||||
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), (rect_x, rect_y, rect_width, rect_height), border_radius=10)
|
||||
pygame.draw.rect(screen, (255, 255, 255), (rect_x, rect_y, rect_width, rect_height), 2, border_radius=10)
|
||||
|
||||
text_rect = text_surface.get_rect(center=(config.screen_width // 2, rect_y + rect_height // 2))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
def draw_validation_transition(screen, platform_index):
|
||||
"""Affiche une animation de transition pour la sélection d’une plateforme."""
|
||||
platform_dict = config.platform_dicts[platform_index] # Use platform dictionary
|
||||
image = load_system_image(platform_dict)
|
||||
if not image:
|
||||
return
|
||||
orig_width, orig_height = image.get_width(), image.get_height()
|
||||
base_size = 150
|
||||
start_time = pygame.time.get_ticks()
|
||||
duration = 500
|
||||
while pygame.time.get_ticks() - start_time < duration:
|
||||
draw_gradient(screen, (28, 37, 38), (47, 59, 61))
|
||||
elapsed = pygame.time.get_ticks() - start_time
|
||||
scale = 2.0 + (2.0 * elapsed / duration) if elapsed < duration / 2 else 3.0 - (2.0 * elapsed / duration)
|
||||
new_width = int(base_size * scale)
|
||||
new_height = int(base_size * scale)
|
||||
scaled_image = pygame.transform.smoothscale(image, (new_width, new_height))
|
||||
image_rect = scaled_image.get_rect(center=(config.screen_width // 2, config.screen_height // 2))
|
||||
screen.blit(scaled_image, image_rect)
|
||||
pygame.display.flip()
|
||||
pygame.time.wait(10)
|
||||
|
||||
def draw_pause_menu(screen, selected_option):
|
||||
"""Dessine le menu pause avec les options Aide, Configurer contrôles, Quitter."""
|
||||
overlay = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 180))
|
||||
screen.blit(overlay, (0, 0))
|
||||
|
||||
options = [
|
||||
"Controls",
|
||||
"Remap controls",
|
||||
"Quit"
|
||||
]
|
||||
|
||||
menu_width = 400
|
||||
menu_height = len(options) * 60 + 40
|
||||
menu_x = (config.screen_width - menu_width) // 2
|
||||
menu_y = (config.screen_height - menu_height) // 2
|
||||
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), (menu_x, menu_y, menu_width, menu_height))
|
||||
pygame.draw.rect(screen, (255, 255, 255), (menu_x, menu_y, menu_width, menu_height), 2)
|
||||
|
||||
for i, option in enumerate(options):
|
||||
color = (0, 150, 255) if i == selected_option else (255, 255, 255)
|
||||
text_surface = config.font.render(option, True, color)
|
||||
text_rect = text_surface.get_rect(center=(config.screen_width // 2, menu_y + 30 + i * 60))
|
||||
screen.blit(text_surface, text_rect)
|
||||
|
||||
def get_control_display(action, default):
|
||||
"""Récupère le nom d'affichage d'une action depuis controls_config."""
|
||||
return config.controls_config.get(action, {}).get('display', default)
|
||||
|
||||
def draw_controls_help(screen, previous_state):
|
||||
"""Affiche la liste des contrôles pour l'état précédent du menu."""
|
||||
# Dictionnaire des contrôles par état
|
||||
state_controls = {
|
||||
"error": [
|
||||
f"{get_control_display('confirm', 'A')} : Retenter",
|
||||
f"{get_control_display('cancel', 'Échap/B')} : Quitter"
|
||||
],
|
||||
"platform": [
|
||||
f"{get_control_display('confirm', 'Entrée/A')} : Sélectionner",
|
||||
f"{get_control_display('cancel', 'Échap/B')} : Quitter",
|
||||
f"{get_control_display('start', 'Start')} : Menu",
|
||||
*( [f"{get_control_display('progress', 'X')} : Progression"] if config.download_tasks else [])
|
||||
],
|
||||
"game": [
|
||||
f"{get_control_display('confirm', 'Entrée/A')} : {'Valider' if config.search_mode else 'Télécharger'}",
|
||||
f"{get_control_display('cancel', 'Échap/B')} : {'Annuler' if config.search_mode else 'Retour'}",
|
||||
*( [
|
||||
f"{get_control_display('delete', 'Retour Arrière')} : Supprimer" if config.controls_config.get('delete', {}) else [],
|
||||
f"{get_control_display('space', 'Espace')} : Espace" if config.controls_config.get('space', {}) else []
|
||||
] if config.search_mode and config.is_non_pc else []),
|
||||
*( [
|
||||
"Saisir texte : Filtrer" if config.search_mode else
|
||||
f"{get_control_display('up', 'Flèche Haut')} / {get_control_display('down', 'Flèche Bas')} : Naviguer",
|
||||
f"{get_control_display('page_up', 'Q/LB')} / {get_control_display('page_down', 'E/RB')} : Page",
|
||||
f"{get_control_display('filter', 'Select')} : Filtrer"
|
||||
] if not config.is_non_pc or not config.search_mode else []),
|
||||
f"{get_control_display('start', 'Start')} : Menu",
|
||||
*( [f"{get_control_display('progress', 'X')} : Progression"] if config.download_tasks and not config.search_mode else [])
|
||||
],
|
||||
"download_progress": [
|
||||
f"{get_control_display('cancel', 'Échap/B')} : Annuler le téléchargement",
|
||||
f"{get_control_display('progress', 'X')} : Arrière plan",
|
||||
f"{get_control_display('start', 'Start')} : Menu"
|
||||
],
|
||||
"download_result": [
|
||||
f"{get_control_display('confirm', 'Entrée/A')} : Retour"
|
||||
],
|
||||
"confirm_exit": [
|
||||
f"{get_control_display('confirm', 'Entrée/A')} : Confirmer"
|
||||
],
|
||||
"extension_warning": [
|
||||
f"{get_control_display('confirm', 'Entrée/A')} : Confirmer"
|
||||
]
|
||||
}
|
||||
|
||||
# Récupérer les contrôles pour l'état donné
|
||||
controls = state_controls.get(previous_state, [])
|
||||
|
||||
# Créer un overlay semi-transparent
|
||||
overlay = pygame.Surface((config.screen_width, config.screen_height), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 180))
|
||||
screen.blit(overlay, (0, 0))
|
||||
|
||||
# Envelopper les textes pour respecter la largeur maximale
|
||||
max_width = config.screen_width - 80
|
||||
wrapped_controls = []
|
||||
current_line = ""
|
||||
for control in controls:
|
||||
test_line = f"{current_line} | {control}" if current_line else control
|
||||
if config.font.size(test_line)[0] <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
wrapped_controls.append(current_line)
|
||||
current_line = control
|
||||
if current_line:
|
||||
wrapped_controls.append(current_line)
|
||||
|
||||
# Calculer les dimensions de la popup
|
||||
line_height = config.font.get_height() + 10
|
||||
popup_width = max_width + 40
|
||||
popup_height = len(wrapped_controls) * line_height + 60
|
||||
popup_x = (config.screen_width - popup_width) // 2
|
||||
popup_y = (config.screen_height - popup_height) // 2
|
||||
|
||||
# Dessiner la popup
|
||||
pygame.draw.rect(screen, (50, 50, 50, 200), (popup_x, popup_y, popup_width, popup_height), border_radius=16)
|
||||
pygame.draw.rect(screen, (255, 255, 255), (popup_x, popup_y, popup_width, popup_height), 2, border_radius=16)
|
||||
|
||||
# Afficher les textes
|
||||
for i, line in enumerate(wrapped_controls):
|
||||
text = config.font.render(line, True, (255, 255, 255))
|
||||
text_rect = text.get_rect(center=(config.screen_width // 2, popup_y + 40 + i * line_height))
|
||||
screen.blit(text, text_rect)
|
||||
381
network.py
Normal file
381
network.py
Normal file
@@ -0,0 +1,381 @@
|
||||
import requests
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
import threading
|
||||
import pygame
|
||||
import zipfile
|
||||
import json
|
||||
from urllib.parse import urljoin, unquote
|
||||
import asyncio
|
||||
import config
|
||||
from utils import sanitize_filename
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
JSON_EXTENSIONS = "/userdata/roms/ports/RGSX/rom_extensions.json"
|
||||
|
||||
def test_internet():
|
||||
logger.debug("Test de connexion Internet")
|
||||
try:
|
||||
result = subprocess.run(['ping', '-c', '4', '8.8.8.8'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
logger.debug("Connexion Internet OK")
|
||||
return True
|
||||
else:
|
||||
logger.debug("Échec ping 8.8.8.8")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Erreur test Internet: {str(e)}")
|
||||
return False
|
||||
|
||||
def load_extensions_json():
|
||||
"""Charge le fichier JSON contenant les extensions supportées."""
|
||||
try:
|
||||
with open(JSON_EXTENSIONS, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la lecture de {JSON_EXTENSIONS}: {e}")
|
||||
return []
|
||||
|
||||
def is_extension_supported(filename, platform, extensions_data):
|
||||
"""Vérifie si l'extension du fichier est supportée pour la plateforme donnée."""
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict["platform"] == platform:
|
||||
dest_dir = 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("/userdata/roms", platform)
|
||||
for system in extensions_data:
|
||||
if system["folder"] == dest_dir:
|
||||
return extension in system["extensions"]
|
||||
logger.warning(f"Aucun système trouvé pour le dossier {dest_dir}")
|
||||
return False
|
||||
|
||||
def extract_zip(zip_path, dest_dir, url):
|
||||
"""Extrait le contenu du fichier ZIP dans le dossier cible avec un suivi progressif de la progression."""
|
||||
try:
|
||||
lock = threading.Lock()
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
total_size = sum(info.file_size for info in zip_ref.infolist() if not info.is_dir())
|
||||
logger.info(f"Taille totale à extraire: {total_size} octets")
|
||||
if total_size == 0:
|
||||
logger.warning("ZIP vide ou ne contenant que des dossiers")
|
||||
return True, "ZIP vide extrait avec succès"
|
||||
|
||||
extracted_size = 0
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
chunk_size = 8192
|
||||
for info in zip_ref.infolist():
|
||||
if info.is_dir():
|
||||
continue
|
||||
file_path = os.path.join(dest_dir, info.filename)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with zip_ref.open(info) as source, open(file_path, 'wb') as dest:
|
||||
file_size = info.file_size
|
||||
file_extracted = 0
|
||||
while True:
|
||||
chunk = source.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
dest.write(chunk)
|
||||
file_extracted += len(chunk)
|
||||
extracted_size += len(chunk)
|
||||
with lock:
|
||||
config.download_progress[url]["downloaded_size"] = extracted_size
|
||||
config.download_progress[url]["total_size"] = total_size
|
||||
config.download_progress[url]["status"] = "Extracting"
|
||||
config.download_progress[url]["progress_percent"] = (extracted_size / total_size * 100) if total_size > 0 else 0
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
logger.debug(f"Extraction {info.filename}, chunk: {len(chunk)}, file_extracted: {file_extracted}/{file_size}, total_extracted: {extracted_size}/{total_size}, progression: {(extracted_size/total_size*100):.1f}%")
|
||||
os.chmod(file_path, 0o644)
|
||||
|
||||
for root, dirs, _ in os.walk(dest_dir):
|
||||
for dir_name in dirs:
|
||||
os.chmod(os.path.join(root, dir_name), 0o755)
|
||||
|
||||
os.remove(zip_path)
|
||||
logger.info(f"Fichier ZIP {zip_path} extrait dans {dest_dir} et supprimé")
|
||||
return True, "ZIP extrait avec succès"
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'extraction de {zip_path}: {e}")
|
||||
return False, str(e)
|
||||
|
||||
def extract_rar(rar_path, dest_dir, url):
|
||||
"""Extrait le contenu du fichier RAR dans le dossier cible, préservant la structure des dossiers."""
|
||||
try:
|
||||
lock = threading.Lock()
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
result = subprocess.run(['unrar'], capture_output=True, text=True)
|
||||
if result.returncode not in [0, 1]:
|
||||
logger.error("Commande unrar non disponible")
|
||||
return False, "Commande unrar non disponible"
|
||||
|
||||
result = subprocess.run(['unrar', 'l', '-v', rar_path], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.strip()
|
||||
logger.error(f"Erreur lors de la liste des fichiers RAR: {error_msg}")
|
||||
return False, f"Échec de la liste des fichiers RAR: {error_msg}"
|
||||
|
||||
logger.debug(f"Sortie brute de 'unrar l -v {rar_path}':\n{result.stdout}")
|
||||
|
||||
total_size = 0
|
||||
files_to_extract = []
|
||||
root_dirs = set()
|
||||
lines = result.stdout.splitlines()
|
||||
in_file_list = False
|
||||
for line in lines:
|
||||
if line.startswith("----"):
|
||||
in_file_list = not in_file_list
|
||||
continue
|
||||
if in_file_list:
|
||||
match = re.match(r'^\s*(\S+)\s+(\d+)\s+\d*\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(.+)$', line)
|
||||
if match:
|
||||
attrs = match.group(1)
|
||||
file_size = int(match.group(2))
|
||||
file_date = match.group(3)
|
||||
file_name = match.group(4).strip()
|
||||
if 'D' not in attrs:
|
||||
files_to_extract.append((file_name, file_size))
|
||||
total_size += file_size
|
||||
root_dir = file_name.split('/')[0] if '/' in file_name else ''
|
||||
if root_dir:
|
||||
root_dirs.add(root_dir)
|
||||
logger.debug(f"Ligne parsée: {file_name}, taille: {file_size}, date: {file_date}")
|
||||
else:
|
||||
logger.debug(f"Dossier ignoré: {file_name}")
|
||||
else:
|
||||
logger.debug(f"Ligne ignorée (format inattendu): {line}")
|
||||
|
||||
logger.info(f"Taille totale à extraire (RAR): {total_size} octets")
|
||||
logger.debug(f"Fichiers à extraire: {files_to_extract}")
|
||||
logger.debug(f"Dossiers racines détectés: {root_dirs}")
|
||||
if total_size == 0:
|
||||
logger.warning("RAR vide, ne contenant que des dossiers, ou erreur de parsing")
|
||||
return False, "RAR vide ou erreur lors de la liste des fichiers"
|
||||
|
||||
with lock:
|
||||
config.download_progress[url]["downloaded_size"] = 0
|
||||
config.download_progress[url]["total_size"] = total_size
|
||||
config.download_progress[url]["status"] = "Extracting"
|
||||
config.download_progress[url]["progress_percent"] = 0
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
|
||||
escaped_rar_path = rar_path.replace(" ", "\\ ")
|
||||
escaped_dest_dir = dest_dir.replace(" ", "\\ ")
|
||||
process = subprocess.Popen(['unrar', 'x', '-y', escaped_rar_path, escaped_dest_dir],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.error(f"Erreur lors de l'extraction de {rar_path}: {stderr}")
|
||||
return False, f"Erreur lors de l'extraction: {stderr}"
|
||||
|
||||
extracted_size = 0
|
||||
extracted_files = []
|
||||
for root, _, files in os.walk(dest_dir):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(file_path, dest_dir).replace(os.sep, '/')
|
||||
for expected_file, file_size in files_to_extract:
|
||||
if rel_path == expected_file:
|
||||
extracted_size += file_size
|
||||
extracted_files.append(expected_file)
|
||||
os.chmod(file_path, 0o644)
|
||||
logger.debug(f"Fichier extrait: {expected_file}, taille: {file_size}, chemin: {file_path}")
|
||||
break
|
||||
|
||||
missing_files = [f for f, _ in files_to_extract if f not in extracted_files]
|
||||
if missing_files:
|
||||
logger.warning(f"Fichiers non extraits: {', '.join(missing_files)}")
|
||||
return False, f"Fichiers non extraits: {', '.join(missing_files)}"
|
||||
|
||||
with lock:
|
||||
config.download_progress[url]["downloaded_size"] = extracted_size
|
||||
config.download_progress[url]["total_size"] = total_size
|
||||
config.download_progress[url]["status"] = "Extracting"
|
||||
config.download_progress[url]["progress_percent"] = 100 if total_size > 0 else 0
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
|
||||
if dest_dir == "/userdata/roms/ps3" and len(root_dirs) == 1:
|
||||
root_dir = root_dirs.pop()
|
||||
old_path = os.path.join(dest_dir, root_dir)
|
||||
new_path = os.path.join(dest_dir, f"{root_dir}.ps3")
|
||||
if os.path.isdir(old_path):
|
||||
try:
|
||||
os.rename(old_path, new_path)
|
||||
logger.info(f"Dossier renommé: {old_path} -> {new_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du renommage de {old_path} en {new_path}: {str(e)}")
|
||||
return False, f"Erreur lors du renommage du dossier: {str(e)}"
|
||||
else:
|
||||
logger.warning(f"Dossier racine {old_path} non trouvé après extraction")
|
||||
elif dest_dir == "/userdata/roms/ps3" and len(root_dirs) > 1:
|
||||
logger.warning(f"Plusieurs dossiers racines détectés dans l'archive: {root_dirs}. Aucun renommage effectué.")
|
||||
|
||||
for root, dirs, _ in os.walk(dest_dir):
|
||||
for dir_name in dirs:
|
||||
os.chmod(os.path.join(root, dir_name), 0o755)
|
||||
|
||||
os.remove(rar_path)
|
||||
logger.info(f"Fichier RAR {rar_path} extrait dans {dest_dir} et supprimé")
|
||||
return True, "RAR extrait avec succès"
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'extraction de {rar_path}: {str(e)}")
|
||||
return False, str(e)
|
||||
finally:
|
||||
if os.path.exists(rar_path):
|
||||
try:
|
||||
os.remove(rar_path)
|
||||
logger.info(f"Fichier RAR {rar_path} supprimé après échec de l'extraction")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression de {rar_path}: {str(e)}")
|
||||
|
||||
async def download_rom(url, platform, game_name, is_zip_non_supported=False):
|
||||
logger.debug(f"Début téléchargement: {game_name} depuis {url}, is_zip_non_supported={is_zip_non_supported}")
|
||||
result = [None, None]
|
||||
|
||||
def download_thread():
|
||||
logger.debug(f"Thread téléchargement démarré pour {url}")
|
||||
try:
|
||||
dest_dir = None
|
||||
for platform_dict in config.platform_dicts:
|
||||
if platform_dict["platform"] == platform:
|
||||
dest_dir = 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("/userdata/roms", platform)
|
||||
|
||||
logger.debug(f"Vérification répertoire destination: {dest_dir}")
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
if not os.access(dest_dir, os.W_OK):
|
||||
raise PermissionError(f"Pas de permission d'écriture dans {dest_dir}")
|
||||
|
||||
sanitized_name = sanitize_filename(game_name)
|
||||
dest_path = os.path.join(dest_dir, f"{sanitized_name}")
|
||||
logger.debug(f"Chemin destination: {dest_path}")
|
||||
|
||||
lock = threading.Lock()
|
||||
with lock:
|
||||
config.download_progress[url] = {
|
||||
"downloaded_size": 0,
|
||||
"total_size": 0,
|
||||
"status": "Téléchargement",
|
||||
"progress_percent": 0,
|
||||
"game_name": game_name
|
||||
}
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
logger.debug(f"Progression initialisée pour {url}")
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
logger.debug(f"Envoi requête GET à {url}")
|
||||
response = requests.get(url, stream=True, headers=headers, timeout=30)
|
||||
logger.debug(f"Réponse reçue, status: {response.status_code}")
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale: {total_size} octets")
|
||||
with lock:
|
||||
config.download_progress[url]["total_size"] = total_size
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
|
||||
downloaded = 0
|
||||
with open(dest_path, 'wb') as f:
|
||||
logger.debug(f"Ouverture fichier: {dest_path}")
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
with lock:
|
||||
config.download_progress[url]["downloaded_size"] = downloaded
|
||||
config.download_progress[url]["status"] = "Téléchargement"
|
||||
config.download_progress[url]["progress_percent"] = (downloaded / total_size * 100) if total_size > 0 else 0
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
logger.debug(f"Progression: {downloaded}/{total_size} octets, {config.download_progress[url]['progress_percent']:.1f}%")
|
||||
|
||||
if is_zip_non_supported:
|
||||
with lock:
|
||||
config.download_progress[url]["downloaded_size"] = 0
|
||||
config.download_progress[url]["total_size"] = 0
|
||||
config.download_progress[url]["status"] = "Extracting"
|
||||
config.download_progress[url]["progress_percent"] = 0
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
extension = os.path.splitext(dest_path)[1].lower()
|
||||
if extension == ".zip":
|
||||
success, msg = extract_zip(dest_path, dest_dir, url)
|
||||
elif extension == ".rar":
|
||||
success, msg = extract_rar(dest_path, dest_dir, url)
|
||||
else:
|
||||
raise Exception(f"Type d'archive non supporté: {extension}")
|
||||
if not success:
|
||||
raise Exception(f"Échec de l'extraction de l'archive: {msg}")
|
||||
result[0] = True
|
||||
result[1] = f"Téléchargé et extrait : {game_name}"
|
||||
else:
|
||||
os.chmod(dest_path, 0o644)
|
||||
logger.debug(f"Téléchargement terminé: {dest_path}")
|
||||
result[0] = True
|
||||
result[1] = f"Téléchargé : {game_name}"
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur téléchargement {url}: {str(e)}")
|
||||
if url in config.download_progress:
|
||||
with lock:
|
||||
del config.download_progress[url]
|
||||
if os.path.exists(dest_path):
|
||||
os.remove(dest_path)
|
||||
result[0] = False
|
||||
result[1] = str(e)
|
||||
finally:
|
||||
logger.debug(f"Thread téléchargement terminé pour {url}")
|
||||
with lock:
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
|
||||
thread = threading.Thread(target=download_thread)
|
||||
logger.debug(f"Démarrage thread pour {url}")
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
pygame.event.pump()
|
||||
await asyncio.sleep(0.1)
|
||||
thread.join()
|
||||
logger.debug(f"Thread rejoint pour {url}")
|
||||
|
||||
with threading.Lock():
|
||||
config.download_result_message = result[1]
|
||||
config.download_result_error = not result[0]
|
||||
config.download_result_start_time = pygame.time.get_ticks()
|
||||
config.menu_state = "download_result"
|
||||
config.needs_redraw = True # Forcer le redraw
|
||||
logger.debug(f"Transition vers download_result, message={result[1]}, erreur={not result[0]}")
|
||||
return result[0], result[1]
|
||||
|
||||
def check_extension_before_download(url, platform, game_name):
|
||||
"""Vérifie l'extension avant de lancer le téléchargement."""
|
||||
try:
|
||||
sanitized_name = sanitize_filename(game_name)
|
||||
extensions_data = load_extensions_json()
|
||||
if not extensions_data:
|
||||
logger.error(f"Fichier {JSON_EXTENSIONS} vide ou introuvable")
|
||||
return False, "Fichier de configuration des extensions introuvable", False
|
||||
|
||||
is_supported = is_extension_supported(sanitized_name, platform, extensions_data)
|
||||
extension = os.path.splitext(sanitized_name)[1].lower()
|
||||
is_archive = extension in (".zip", ".rar")
|
||||
|
||||
if is_supported:
|
||||
logger.debug(f"L'extension de {sanitized_name} est supportée pour {platform}")
|
||||
return True, "", False
|
||||
else:
|
||||
if is_archive:
|
||||
logger.debug(f"Fichier {extension.upper()} détecté pour {sanitized_name}, extraction automatique prévue")
|
||||
return False, f"Fichiers {extension.upper()} non supportés par cette plateforme, extraction automatique après le téléchargement.", True
|
||||
logger.debug(f"L'extension de {sanitized_name} n'est pas supportée pour {platform}")
|
||||
return False, f"L'extension de {sanitized_name} n'est pas supportée pour {platform}", False
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur vérification extension {url}: {str(e)}")
|
||||
return False, str(e), False
|
||||
78
update_gamelist.py
Normal file
78
update_gamelist.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
import xml.dom.minidom
|
||||
import xml.etree.ElementTree as ET
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GAMELIST_FILE = "/userdata/roms/ports/gamelist.xml"
|
||||
RGSX_ENTRY = {
|
||||
"path": "./RGSX/RGSX.sh",
|
||||
"name": "RGSX",
|
||||
"desc": "Retro Games Sets X - Games Downloader",
|
||||
"image": "./images/RGSX.png",
|
||||
"marquee": "./images/RGSX.png",
|
||||
"thumbnail": "./images/RGSX.png",
|
||||
"fanart": "./images/RGSX.png",
|
||||
"rating": "1",
|
||||
"releasedate": "20250620T165718",
|
||||
"developer": "RetroGameSets.fr",
|
||||
"genre": "Compilation, Various / Utilities",
|
||||
"playcount": "251",
|
||||
"lastplayed": "20250621T234656",
|
||||
"gametime": "30830",
|
||||
"lang": "fr"
|
||||
}
|
||||
|
||||
def update_gamelist():
|
||||
try:
|
||||
# Si le fichier n'existe pas, est vide ou non valide, créer une nouvelle structure
|
||||
if not os.path.exists(GAMELIST_FILE) or os.path.getsize(GAMELIST_FILE) == 0:
|
||||
logger.info(f"Création de {GAMELIST_FILE}")
|
||||
root = ET.Element("gameList")
|
||||
else:
|
||||
try:
|
||||
logger.info(f"Lecture de {GAMELIST_FILE}")
|
||||
tree = ET.parse(GAMELIST_FILE)
|
||||
root = tree.getroot()
|
||||
if root.tag != "gameList":
|
||||
logger.info(f"{GAMELIST_FILE} n'a pas de balise <gameList>, création d'une nouvelle structure")
|
||||
root = ET.Element("gameList")
|
||||
except ET.ParseError:
|
||||
logger.info(f"{GAMELIST_FILE} est invalide, création d'une nouvelle structure")
|
||||
root = ET.Element("gameList")
|
||||
|
||||
# Supprimer l'ancienne entrée RGSX
|
||||
for game in root.findall("game"):
|
||||
path = game.find("path")
|
||||
if path is not None and path.text == "./RGSX/RGSX.sh":
|
||||
root.remove(game)
|
||||
logger.info("Ancienne entrée RGSX supprimée")
|
||||
|
||||
# Ajouter la nouvelle entrée
|
||||
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")
|
||||
|
||||
# Générer le XML avec minidom pour une indentation correcte
|
||||
rough_string = '<?xml version="1.0" encoding="UTF-8"?>\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_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write(pretty_xml)
|
||||
logger.info(f"{GAMELIST_FILE} mis à jour avec succès")
|
||||
|
||||
# Définir les permissions
|
||||
os.chmod(GAMELIST_FILE, 0o644)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la mise à jour de {GAMELIST_FILE}: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_gamelist()
|
||||
112
utils.py
Normal file
112
utils.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import pygame
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
import config
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_placeholder(width=400):
|
||||
"""Crée une image de substitution pour les jeux sans vignette."""
|
||||
logger.debug(f"Création placeholder: largeur={width}")
|
||||
if config.font is None:
|
||||
# Police de secours si config.font n’est pas initialisé
|
||||
fallback_font = pygame.font.SysFont("arial", 24)
|
||||
text = fallback_font.render("No Image", True, (255, 255, 255))
|
||||
else:
|
||||
text = config.font.render("No Image", True, (255, 255, 255))
|
||||
|
||||
height = int(150 * (width / 200))
|
||||
placeholder = pygame.Surface((width, height))
|
||||
placeholder.fill((50, 50, 50))
|
||||
text_rect = text.get_rect(center=(width // 2, height // 2))
|
||||
placeholder.blit(text, text_rect)
|
||||
return placeholder
|
||||
|
||||
def truncate_text_middle(text, font, max_width):
|
||||
"""Tronque le texte en insérant '...' au milieu."""
|
||||
text_width = font.size(text)[0]
|
||||
if text_width <= max_width:
|
||||
return text
|
||||
ellipsis = "..."
|
||||
ellipsis_width = font.size(ellipsis)[0]
|
||||
max_text_width = max_width - ellipsis_width
|
||||
while text_width > max_text_width and len(text) > 0:
|
||||
text = text[:-1]
|
||||
text_width = font.size(text)[0]
|
||||
mid = len(text) // 2
|
||||
return text[:mid] + ellipsis + text[mid:]
|
||||
|
||||
def truncate_text_end(text, font, max_width):
|
||||
"""Tronque le texte à la fin pour qu'il tienne dans max_width avec la police donnée."""
|
||||
if not isinstance(text, str):
|
||||
logger.error(f"Texte non valide: {text}")
|
||||
return ""
|
||||
if not isinstance(font, pygame.font.Font):
|
||||
logger.error("Police non valide dans truncate_text_end")
|
||||
return text # Retourne le texte brut si la police est invalide
|
||||
|
||||
try:
|
||||
if font.size(text)[0] <= max_width:
|
||||
return text
|
||||
|
||||
truncated = text
|
||||
while len(truncated) > 0 and font.size(truncated + "...")[0] > max_width:
|
||||
truncated = truncated[:-1]
|
||||
return truncated + "..." if len(truncated) < len(text) else text
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du rendu du texte '{text}': {str(e)}")
|
||||
return text # Retourne le texte brut en cas d'erreur
|
||||
|
||||
def sanitize_filename(name):
|
||||
"""Sanitise les noms de fichiers en remplaçant les caractères interdits."""
|
||||
return re.sub(r'[<>:"/\/\\|?*]', '_', name).strip()
|
||||
|
||||
def wrap_text(text, font, max_width):
|
||||
"""Divise le texte en lignes pour respecter la largeur maximale."""
|
||||
words = text.split(' ')
|
||||
lines = []
|
||||
current_line = ''
|
||||
|
||||
for word in words:
|
||||
# Tester si ajouter le mot dépasse la largeur
|
||||
test_line = current_line + (' ' if current_line else '') + word
|
||||
test_surface = font.render(test_line, True, (255, 255, 255))
|
||||
if test_surface.get_width() <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
def load_system_image(platform_dict):
|
||||
"""Charge une image système depuis le chemin spécifié dans system_image."""
|
||||
image_path = platform_dict.get("system_image")
|
||||
platform_name = platform_dict.get("platform", "unknown")
|
||||
#logger.debug(f"Chargement de l'image système pour {platform_name} depuis {image_path}")
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
logger.error(f"Image introuvable pour {platform_name} à {image_path}")
|
||||
return None
|
||||
return pygame.image.load(image_path).convert_alpha()
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement de l'image pour {platform_name} : {str(e)}")
|
||||
return None
|
||||
|
||||
def load_games(platform_id):
|
||||
"""Charge les jeux pour une plateforme donnée en utilisant platform_id."""
|
||||
games_path = f"/userdata/roms/ports/RGSX/games/{platform_id}.json"
|
||||
#logger.debug(f"Chargement des jeux pour {platform_id} depuis {games_path}")
|
||||
try:
|
||||
with open(games_path, 'r', encoding='utf-8') as f:
|
||||
games = json.load(f)
|
||||
return games
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement des jeux pour {platform_id} : {str(e)}")
|
||||
return []
|
||||
Reference in New Issue
Block a user