forked from Mirrors/RGSX
Add support for Anbernic RG35XX controller and local custom sources ZIP handling
This commit is contained in:
@@ -219,6 +219,12 @@ else:
|
||||
logger.debug(f"Joysticks détectés: YES")
|
||||
for idx, name in enumerate(joystick_names):
|
||||
lname = name.lower()
|
||||
# Détection Anbernic RG35XX
|
||||
if ("rg35xx" in lname):
|
||||
config.anbernic_rg35xx_controller = True
|
||||
logger.debug(f"Anbernic Controller detected : {name}")
|
||||
print(f"Controller detected : {name}")
|
||||
# ne pas break ici pour permettre une détection plus spécifique (xbox elite) si nécessaire
|
||||
# Détection spécifique Elite AVANT la détection générique Xbox
|
||||
if ("microsoft xbox controller" in lname):
|
||||
config.xbox_elite_controller = True
|
||||
@@ -958,53 +964,83 @@ async def main():
|
||||
try:
|
||||
zip_path = os.path.join(config.SAVE_FOLDER, "data_download.zip")
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
# Déterminer l'URL à utiliser selon le mode (RGSX ou custom)
|
||||
sources_zip_url = get_sources_zip_url(OTA_data_ZIP)
|
||||
if sources_zip_url is None:
|
||||
# Mode custom sans URL valide -> pas de téléchargement, jeux vides
|
||||
logger.warning("Mode custom actif mais aucune URL valide fournie. Liste de jeux vide.")
|
||||
config.popup_message = _("sources_mode_custom_missing_url").format(config.RGSX_SETTINGS_PATH)
|
||||
config.popup_timer = 5000
|
||||
else:
|
||||
try:
|
||||
with requests.get(sources_zip_url, stream=True, headers=headers, timeout=30) as response:
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale du ZIP : {total_size} octets")
|
||||
downloaded = 0
|
||||
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
|
||||
with open(zip_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
config.download_progress[sources_zip_url] = {
|
||||
"downloaded_size": downloaded,
|
||||
"total_size": total_size,
|
||||
"status": "Téléchargement",
|
||||
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
|
||||
}
|
||||
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {zip_path}")
|
||||
# Support des sources custom locales: prioriser un ZIP présent dans SAVE_FOLDER
|
||||
try:
|
||||
from rgsx_settings import get_sources_mode
|
||||
from rgsx_settings import find_local_custom_sources_zip
|
||||
mode = get_sources_mode()
|
||||
except Exception:
|
||||
mode = "rgsx"
|
||||
find_local_custom_sources_zip = lambda: None # type: ignore
|
||||
|
||||
config.current_loading_system = _("loading_extracting_data")
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
dest_dir = config.SAVE_FOLDER
|
||||
success, message = extract_zip_data(zip_path, dest_dir, sources_zip_url)
|
||||
local_zip = find_local_custom_sources_zip() if mode == "custom" else None
|
||||
if local_zip and os.path.isfile(local_zip):
|
||||
# Extraire directement depuis le ZIP local
|
||||
config.current_loading_system = _("loading_extracting_data")
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
dest_dir = config.SAVE_FOLDER
|
||||
try:
|
||||
success, message = extract_zip_data(local_zip, dest_dir, local_zip)
|
||||
if success:
|
||||
logger.debug(f"Extraction réussie : {message}")
|
||||
logger.debug(f"Extraction locale réussie : {message}")
|
||||
config.loading_progress = 70.0
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
raise Exception(f"Échec de l'extraction : {message}")
|
||||
raise Exception(f"Échec de l'extraction locale : {message}")
|
||||
except Exception as de:
|
||||
logger.error(f"Erreur téléchargement custom source: {de}")
|
||||
logger.error(f"Erreur extraction ZIP local custom: {de}")
|
||||
config.popup_message = _("sources_mode_custom_download_error")
|
||||
config.popup_timer = 5000
|
||||
# Pas d'arrêt : continuer avec jeux vides
|
||||
# Continuer avec jeux vides
|
||||
else:
|
||||
# Déterminer l'URL à utiliser selon le mode (RGSX ou custom)
|
||||
sources_zip_url = get_sources_zip_url(OTA_data_ZIP)
|
||||
if sources_zip_url is None:
|
||||
# Mode custom sans fichier local ni URL valide -> pas de téléchargement, jeux vides
|
||||
logger.warning("Mode custom actif mais aucun ZIP local et aucune URL valide fournie. Liste de jeux vide.")
|
||||
config.popup_message = _("sources_mode_custom_missing_url").format(config.RGSX_SETTINGS_PATH)
|
||||
config.popup_timer = 5000
|
||||
else:
|
||||
try:
|
||||
with requests.get(sources_zip_url, stream=True, headers=headers, timeout=30) as response:
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
logger.debug(f"Taille totale du ZIP : {total_size} octets")
|
||||
downloaded = 0
|
||||
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
|
||||
with open(zip_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
config.download_progress[sources_zip_url] = {
|
||||
"downloaded_size": downloaded,
|
||||
"total_size": total_size,
|
||||
"status": "Téléchargement",
|
||||
"progress_percent": (downloaded / total_size * 100) if total_size > 0 else 0
|
||||
}
|
||||
config.loading_progress = 15.0 + (35.0 * downloaded / total_size) if total_size > 0 else 15.0
|
||||
config.needs_redraw = True
|
||||
await asyncio.sleep(0)
|
||||
logger.debug(f"ZIP téléchargé : {zip_path}")
|
||||
|
||||
config.current_loading_system = _("loading_extracting_data")
|
||||
config.loading_progress = 60.0
|
||||
config.needs_redraw = True
|
||||
dest_dir = config.SAVE_FOLDER
|
||||
success, message = extract_zip_data(zip_path, dest_dir, sources_zip_url)
|
||||
if success:
|
||||
logger.debug(f"Extraction réussie : {message}")
|
||||
config.loading_progress = 70.0
|
||||
config.needs_redraw = True
|
||||
else:
|
||||
raise Exception(f"Échec de l'extraction : {message}")
|
||||
except Exception as de:
|
||||
logger.error(f"Erreur téléchargement custom source: {de}")
|
||||
config.popup_message = _("sources_mode_custom_download_error")
|
||||
config.popup_timer = 5000
|
||||
# Pas d'arrêt : continuer avec jeux vides
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du téléchargement/extraction du Dossier Data : {str(e)}")
|
||||
# En mode custom on ne bloque pas le chargement ; en mode RGSX (sources_zip_url non None et OTA) on affiche une erreur
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"confirm": {
|
||||
"type": "button",
|
||||
"button": 3,
|
||||
"display": "A"
|
||||
},
|
||||
"cancel": {
|
||||
"type": "button",
|
||||
"button": 4,
|
||||
"display": "B"
|
||||
},
|
||||
"up": {
|
||||
"type": "hat",
|
||||
"value": [0, 1],
|
||||
"display": "↑"
|
||||
},
|
||||
"down": {
|
||||
"type": "hat",
|
||||
"value": [0, -1],
|
||||
"display": "↓"
|
||||
},
|
||||
"left": {
|
||||
"type": "hat",
|
||||
"value": [-1, 0],
|
||||
"display": "←"
|
||||
},
|
||||
"right": {
|
||||
"type": "hat",
|
||||
"value": [1, 0],
|
||||
"display": "→"
|
||||
},
|
||||
"start": {
|
||||
"type": "button",
|
||||
"button": 10,
|
||||
"display": "Start"
|
||||
},
|
||||
"filter": {
|
||||
"type": "button",
|
||||
"button": 9,
|
||||
"display": "Select"
|
||||
},
|
||||
"page_up": {
|
||||
"type": "button",
|
||||
"button": 7,
|
||||
"display": "LT"
|
||||
},
|
||||
"page_down": {
|
||||
"type": "button",
|
||||
"button": 8,
|
||||
"display": "RT"
|
||||
},
|
||||
"history": {
|
||||
"type": "button",
|
||||
"button": 6,
|
||||
"display": "Y"
|
||||
},
|
||||
"clear_history": {
|
||||
"type": "button",
|
||||
"button": 5,
|
||||
"display": "X"
|
||||
},
|
||||
"delete": {
|
||||
"type": "button",
|
||||
"button": 13,
|
||||
"display": "LB"
|
||||
},
|
||||
"space": {
|
||||
"type": "button",
|
||||
"button": 14,
|
||||
"display": "RB"
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,7 @@ steam_controller = False
|
||||
trimui_controller = False
|
||||
generic_controller = False
|
||||
xbox_elite_controller = False # Flag spécifique manette Xbox Elite
|
||||
anbernic_rg35xx_controller = False # Flag spécifique Anbernic RG3xxx
|
||||
|
||||
# --- Filtre plateformes (UI) ---
|
||||
selected_filter_index = 0 # index dans la liste visible triée
|
||||
|
||||
@@ -113,6 +113,8 @@ def load_controls_config(path=CONTROLS_CONFIG_PATH):
|
||||
candidates.append('nintendo_controller.json')
|
||||
if getattr(config, 'eightbitdo_controller', False):
|
||||
candidates.append('8bitdo_controller.json')
|
||||
if getattr(config, 'anbernic_rg35xx_controller', False):
|
||||
candidates.append('anbernic_rg34xx_sp_controller.json')
|
||||
# Fallbacks génériques
|
||||
if 'generic_controller.json' not in candidates:
|
||||
candidates.append('generic_controller.json')
|
||||
|
||||
@@ -197,6 +197,40 @@ def get_sources_zip_url(fallback_url):
|
||||
return None
|
||||
return fallback_url
|
||||
|
||||
def find_local_custom_sources_zip():
|
||||
"""Recherche un fichier ZIP local à la racine de SAVE_FOLDER pour le mode custom.
|
||||
|
||||
Priorité sur quelques noms courants afin d'éviter toute ambiguïté.
|
||||
Retourne le chemin absolu du ZIP si trouvé, sinon None.
|
||||
"""
|
||||
try:
|
||||
from config import SAVE_FOLDER
|
||||
candidates = [
|
||||
"games.zip",
|
||||
"custom_sources.zip",
|
||||
"rgsx_custom_sources.zip",
|
||||
"data.zip",
|
||||
]
|
||||
if not os.path.isdir(SAVE_FOLDER):
|
||||
return None
|
||||
for name in candidates:
|
||||
p = os.path.join(SAVE_FOLDER, name)
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
# Option avancée: prendre le plus récent *.zip si aucun nom connu trouvé
|
||||
try:
|
||||
zips = [os.path.join(SAVE_FOLDER, f) for f in os.listdir(SAVE_FOLDER) if f.lower().endswith('.zip')]
|
||||
zips = [z for z in zips if os.path.isfile(z)]
|
||||
if zips:
|
||||
newest = max(zips, key=lambda z: os.path.getmtime(z))
|
||||
return newest
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"find_local_custom_sources_zip error: {e}")
|
||||
return None
|
||||
|
||||
# ----------------------- Unsupported platforms toggle ----------------------- #
|
||||
|
||||
def get_show_unsupported_platforms(settings=None):
|
||||
|
||||
@@ -37,13 +37,9 @@ PROMPTS = [
|
||||
"JOYSTICK_LEFT_DOWN - MOVE DOWN",
|
||||
"JOYSTICK_LEFT_LEFT - MOVE LEFT",
|
||||
"JOYSTICK_LEFT_RIGHT - MOVE RIGHT",
|
||||
# Right stick directions
|
||||
"JOYSTICK_RIGHT_UP - MOVE U P",
|
||||
"JOYSTICK_RIGHT_DOWN - MOVE DOWN",
|
||||
"JOYSTICK_RIGHT_LEFT - MOVE LEFT",
|
||||
"JOYSTICK_RIGHT_RIGHT - MOVE RIGHT",
|
||||
]
|
||||
|
||||
INPUT_TIMEOUT_SECONDS = 10 # Temps max par entrée avant "ignored"
|
||||
|
||||
# --- Minimal on-screen console (Pygame window) ---
|
||||
SURFACE = None # type: ignore
|
||||
@@ -108,6 +104,8 @@ def init_joystick() -> pygame.joystick.Joystick:
|
||||
js.init()
|
||||
name = js.get_name()
|
||||
log(f"Using joystick 0: {name}")
|
||||
log("")
|
||||
log(f"Note: each input will auto-ignore after {INPUT_TIMEOUT_SECONDS}s if not present (e.g. missing L2/R2)")
|
||||
return js
|
||||
|
||||
|
||||
@@ -147,7 +145,7 @@ def wait_for_stable(js: pygame.joystick.Joystick, settle_ms: int = 250, deadband
|
||||
pygame.time.wait(10)
|
||||
|
||||
|
||||
def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_threshold: float = 0.6) -> Tuple[str, Any]:
|
||||
def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_threshold: float = 0.6, timeout_sec: int = INPUT_TIMEOUT_SECONDS) -> Tuple[str, Any]:
|
||||
"""Wait for a joystick event for the given logical control.
|
||||
|
||||
Returns a tuple of (kind, data):
|
||||
@@ -158,10 +156,18 @@ def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_thresho
|
||||
# Ensure prior motion has settled to avoid capturing a release
|
||||
wait_for_stable(js)
|
||||
log("")
|
||||
log(f"Press {logical_name} (ESC to skip, close window to quit)…")
|
||||
deadline = time.time() + max(1, int(timeout_sec))
|
||||
log(f"Press {logical_name} (Wait {timeout_sec}s to skip/ignore) if not present")
|
||||
# Flush old events
|
||||
pygame.event.clear()
|
||||
while True:
|
||||
# Update window title with countdown if we have a surface
|
||||
try:
|
||||
remaining = int(max(0, deadline - time.time()))
|
||||
if SURFACE is not None:
|
||||
pygame.display.set_caption(f"Controller Tester — {logical_name} — {remaining}s left")
|
||||
except Exception:
|
||||
pass
|
||||
for event in pygame.event.get():
|
||||
# Keyboard helpers
|
||||
if event.type == pygame.KEYDOWN:
|
||||
@@ -195,6 +201,10 @@ def wait_for_event(js: pygame.joystick.Joystick, logical_name: str, axis_thresho
|
||||
return ("axis", {"axis": axis, "direction": direction, "raw": value})
|
||||
|
||||
draw_log()
|
||||
# Timeout?
|
||||
if time.time() >= deadline:
|
||||
log(f"Ignored {logical_name} (timeout {timeout_sec}s)")
|
||||
return ("ignored", None)
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
@@ -213,6 +223,8 @@ def write_log(path: str, mapping: Dict[str, Tuple[str, Any]], device_name: str)
|
||||
lines.append(f"{name} = AXIS {ax} dir {direction}\n")
|
||||
elif kind == "skipped":
|
||||
lines.append(f"{name} = SKIPPED\n")
|
||||
elif kind == "ignored":
|
||||
lines.append(f"{name} = IGNORED\n")
|
||||
else:
|
||||
lines.append(f"{name} = UNKNOWN {data}\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user