2025-07-06 19:47:21 +02:00
import pygame
import json
import os
import logging
import config
2025-07-06 20:57:36 +02:00
from config import CONTROLS_CONFIG_PATH
2025-07-06 19:47:21 +02:00
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 "
2025-07-06 20:57:36 +02:00
ACTIONS = [ " start " , " confirm " , " cancel " ]
2025-07-06 19:47:21 +02:00
def map_controls ( screen ) :
2025-07-06 20:57:36 +02:00
mapping = True
current_action = 0
clock = pygame . time . Clock ( )
while mapping :
clock . tick ( 100 ) # 100 FPS
for event in pygame . event . get ( ) :
""" 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
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
# 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 ( )
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
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
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
current_time = pygame . time . get_ticks ( )
delta_time = current_time - last_frame_time
last_frame_time = current_time
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
events = pygame . event . get ( )
for event in events :
if event . type == pygame . QUIT :
return False
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
# 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
2025-07-06 19:47:21 +02:00
current_input = None
input_held_time = 0
last_input_name = None
config . needs_redraw = True
2025-07-06 20:57:36 +02:00
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
2025-07-06 19:47:21 +02:00
current_input = None
input_held_time = 0
last_input_name = None
config . needs_redraw = True
2025-07-06 20:57:36 +02:00
config . needs_redraw = True
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
pygame . time . wait ( 10 )
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
save_controls_config ( controls_config )
config . controls_config = controls_config
return True
pass
2025-07-06 19:47:21 +02:00
2025-07-06 20:57:36 +02:00
def save_controls_config ( config ) :
""" Enregistre la configuration des contrôles dans un fichier JSON. """
try :
with open ( CONTROLS_CONFIG_PATH , " w " ) as f :
json . dump ( config , f , indent = 4 )
logging . debug ( " Configuration des contrôles enregistrée " )
except Exception as e :
logging . error ( f " Erreur lors de l ' enregistrement de controls.json : { e } " )
return False
2025-07-06 19:47:21 +02:00
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 )