- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| assets | ||
| demo | ||
| src/metasave | ||
| tests | ||
| .gitignore | ||
| API_PLUGIN.md | ||
| launcher.sh | ||
| main.py | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
MetaSave
MetaSave est une application de bureau PyQt6 qui surveille des fichiers de sauvegarde, attend la fin de leur écriture, travaille sur une copie temporaire et produit un JSON consultable dans l'interface. Les fichiers originaux ne sont jamais modifiés.
Fonctionnalités
- démarrage discret dans la zone de notification avec notification système ;
- menu de l'icône : Ouvrir l'interface, Parser maintenant, Quitter ;
- ajout et retrait de sauvegardes (le retrait ne supprime jamais l'original) ;
- fenêtre de gestion des surveillances avec ajout de plusieurs fichiers ou d'un
dossier associé à un wildcard (
slot*.sav,save?.hg, etc.) ; - surveillance simultanée de tous les fichiers correspondant à un même motif ;
- surveillance temps réel, pause, reprise et arrêt ;
- anti-rebond et contrôle de stabilité avant chaque copie temporaire ;
- analyse asynchrone pour garder l'interface réactive ;
- explorateur JSON paresseux et vue du JSON brut ;
- exports JSON atomiques conservés dans le dossier de données de l'application ;
- plugin No Man's Sky avec résumé métier : joueur, position, galaxie, planète, bâtiment, multi-outils, vaisseaux et inventaires ;
- plugin Pokémon Génération 1 pour les sauvegardes Rouge, Bleu et Jaune : joueur, argent, position, sac et PC, équipe, Pension, 12 boîtes, IV/DV, EV, statistiques, attaques, badges, Conseil des 4 et Pokédex ;
- résolution des 256 galaxies, dont Odyalutai, et des noms d'objets d'inventaire ;
- plugins intégrés supplémentaires pour JSON, texte et binaire inconnu ;
- chargement de plugins Python externes.
- transmission optionnelle et asynchrone des informations vers l'API externe de Twitchiou.
- interfaçage en lecture seule avec Azahar par RPC UDP, ou avec un pont TCP compatible ;
- aperçu actualisé du JSON de l’émulateur dans sa fenêtre de configuration et affichage de l’émulateur et du jeu courant dans la liste principale ;
- détection du jeu lancé par des plugins d'émulateur, sans remontée pour les jeux non pris en charge ;
- plugin Mario Kart 7 : type et mode de course, circuit, classement courant et objets détenus par chaque joueur ;
- API Web locale configurable pour lire une valeur, compter une collection ou récupérer le JSON courant.
Le premier onglet affiche uniquement les informations utiles demandées. Le second affiche le même résultat au format JSON ; le document technique décompressé n'est ni exporté ni injecté dans l'interface.
Pour une sauvegarde No Man's Sky, le JSON commence directement par les cinq
sections joueur, position, multi_outils, vaisseaux et inventaires.
La section position.coordonnees_glyphes contient les coordonnées voxel,
les index du système et de la planète, ainsi que l'adresse portail hexadécimale
de 12 glyphes au format PSSSYYZZZXXX.
Pour Pokémon Rouge, Bleu et Jaune, ajouter directement le fichier SRAM de
l'émulateur (.sav, .srm ou .ram, 32 Kio ou davantage). Le JSON ne contient
que jeu, joueur, position, inventaire, pokemons, badges,
conseil_des_4 et pokedex. L'équipe, la Pension et les 12 boîtes PC sont lues.
Les IV sont les DV natifs de la Génération 1 (0 à 15) et les EV sont
l'expérience statistique native (0 à 65535), avec son bonus calculé.
Une SRAM Pokémon Gen 1 ne contient pas le nom de l'édition. MetaSave le déduit
du nom du fichier s'il contient rouge/red, bleu/blue ou jaune/yellow ;
sinon le résultat indique que l'édition est indéterminée. Ces jeux étant en 2D,
la coordonnée z est null.
Le plugin No Man's Sky décompresse et lit le document dans le worker d'arrière-plan, puis ne transmet à l'interface que les cinq sections utiles.
Lors de la première analyse No Man's Sky, MetaSave met en cache le mapping de
clés et le catalogue de noms d'objets de NMS Toolkit dans nms-reference/.
Sans réseau, le résumé continue de fonctionner avec le mapping embarqué ; seuls
certains noms d'objets peuvent alors rester sous forme d'identifiants NMS.
Installation et lancement
Python 3.10 ou plus récent est requis.
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
python main.py
Par défaut, la fenêtre reste cachée et l'icône apparaît dans la zone de notification. Pour ouvrir directement la fenêtre :
python main.py --show
Pour ajouter et parser un fichier dès le lancement :
python main.py --add "C:\chemin\save.hg" --parse-now --show
Gérer plusieurs sauvegardes
Ouvrir Fichier → Gérer les surveillances…. La fenêtre permet :
- d'ajouter plusieurs fichiers en une seule sélection ;
- d'ajouter un dossier et un wildcard portant sur les noms de fichiers de ce
dossier, par exemple
slot*.savousave?.hg; - de supprimer une ou plusieurs surveillances sans supprimer les fichiers.
En surveillance temps réel, chaque fichier correspondant qui termine une écriture est copié temporairement puis analysé. Les événements de plusieurs fichiers d'une même surveillance sont conservés dans une file afin qu'une paire de sauvegardes soit traitée intégralement. Pour Parser maintenant et donc déclencher une transmission manuelle, MetaSave sélectionne le fichier correspondant dont la date de modification est la plus récente.
Une surveillance wildcard conserve un seul JSON courant : chaque analyse le remplace par le résultat du dernier fichier traité. Le chemin de ce fichier est mémorisé et affiché comme dernière correspondance.
Données locales
Qt choisit le dossier local adapté à la plateforme. Sous Windows, il se trouve
normalement dans %LOCALAPPDATA%\MetaSave\MetaSave. Il contient :
saves.json registre des chemins surveillés
exports/ un JSON courant par sauvegarde
plugins/ plugins Python personnels
emulator-plugins/ plugins Python personnels pour les émulateurs
nms-reference/ mapping et catalogue NMS mis en cache
twitchiou.json configuration locale de la transmission Twitchiou
emulator.json configuration locale de l’interfaçage émulateur
web-api.json configuration locale du serveur HTTP
La suppression d'une ligne depuis l'interface modifie uniquement saves.json.
L'export déjà créé reste disponible, et l'original n'est jamais touché.
Configuration Twitchiou
Ouvrir Paramètres → Configurer Twitchiou… puis renseigner :
- l'URL de destination, par défaut
https://{bot_ip}:5000/api/external/events; - la clé externe
twi_ext_…; - le nom d'événement autorisé pour cette clé ;
- l'adresse IP du bot Twitchiou ;
- l'autorisation du certificat SSL autosigné local ;
- l'activation ou la désactivation de la transmission.
Après un parsing réussi, MetaSave envoie en arrière-plan un POST dont le champ
data contient exactement le JSON affiché. La clé est placée dans l'en-tête
Authorization: Bearer et n'est jamais ajoutée à l'URL ou aux messages de log.
L'option de certificat autosigné désactive la vérification TLS et ne doit être
utilisée qu'avec une instance locale de confiance.
Lors d'une remontée issue d'un émulateur, le payload contient
"source": "emulator.azahar" au lieu du chemin d'une sauvegarde. Aucun POST
n'est déclenché si le processus lancé ne correspond à aucun plugin émulateur.
Configuration d’Azahar
- Dans Azahar, ouvrir les paramètres de débogage et activer Enable RPC server.
- Dans MetaSave, ouvrir Paramètres → Configurer l’émulateur….
- Choisir Azahar, saisir l’adresse IP et le port, puis activer l’interfaçage.
Le serveur RPC officiel d’Azahar utilise UDP et écoute par défaut sur le port
45987. L’option TCP vise un pont compatible utilisant le même cadrage de
paquets ; Azahar lui-même doit normalement être configuré en UDP.
MetaSave interroge la liste des processus 3DS et choisit un plugin à partir du
Title ID. Le plugin intégré Mario Kart 7 reconnaît les éditions Europe,
Amérique et Japon. Pendant une course, le JSON live sépare les informations
sur le jeu lancé dans emulator et les données de Mario Kart 7 dans data.
data.course décrit la course et chaque entrée de data.classement expose le
pseudonymme affiché dans le classement, le personnage, le rang, le tour actuel,
les tours effectués (tour en cours inclus) et les objets détenus.
data.course.nombre_tours_effectues donne également le nombre de tours
effectués par le joueur principal, tour en cours inclus.
data.notification indique si une course est active, l’écran courant et la
position finale du joueur principal quand la course est terminée. Le détecteur
lit la page réellement active de Mario Kart 7 pour distinguer les sélections de
mode, cylindrée, personnage, kart, coupe ou circuit, les écrans en ligne, les
communautés, la chaîne Mario Kart, la course, l’arrivée et les résultats. Le
champ page expose aussi le nom technique de cette page pour le diagnostic.
Le classement final reprend directement la valeur position de la ligne du
joueur principal, après trois lectures identiques, et reste disponible jusqu’au
départ de la course suivante. En ligne, cette ligne est choisie avec
NetworkEngine.m_local_player_id, l’identifiant effectif du joueur local, et
non avec l’identifiant du maître/hôte réseau. La correspondance entre joueur et
position vient de ModeManagerBase.m_player_id_to_rank et est normalisée en
positions publiques 1 à 8. CRaceInfo.m_race_rank n’est pas utilisé. Le champ
notification.joueur_principal_id expose l’identifiant joueur de la ligne
effectivement utilisée.
data.classement est vide dès que data.notification.ecran est différent de
course, ce qui permet aux overlays de masquer le classement hors course.
Les données identiques ne sont pas renvoyées plusieurs fois.
L’interfaçage est strictement en lecture seule. Si Azahar est fermé, si aucune course n’est active ou si le jeu n’est pas supporté, aucune donnée n’est envoyée à Twitchiou.
API Web locale
Ouvrir Paramètres → Configurer l’API Web…, choisir l’adresse IP et le port d’écoute, puis activer le serveur.
GET / renvoie le catalogue des sources. saves contient toutes les
sauvegardes configurées avec leur identifiant stable et json_available.
emulators contient uniquement les émulateurs actuellement connectés, avec
leur état, le jeu reconnu et la disponibilité du JSON.
Pour une API à l’adresse http://127.0.0.1:8765 :
GET /value/saves/<id>/joueur/nomrenvoie une valeur de cette sauvegarde ;GET /count/saves/<id>/vaisseaux/__countcompte ses vaisseaux ;GET /raw/saves/<id>/renvoie tout son JSON ;GET /value/emulators/azahar/data/course/circuitlit la télémétrie Azahar ;GET /value/emulators/azahar/data/course/nombre_tours_effectueslit les tours terminés ;GET /value/emulators/azahar/data/notification/ecranlit l’écran courant ;GET /value/emulators/azahar/data/classement/0/pseudonymmelit le nom affiché ;GET /count/emulators/azahar/data/classement/__countcompte les concurrents ;GET /raw/emulators/azahar/renvoie tout le JSON de l’émulateur ;GET /events/emulators/azahardiffuse ce JSON en temps réel avec SSE ;GET /count/saves/__countetGET /count/emulators/__countcomptent les sources disponibles.
Le suffixe __coun est accepté comme alias de __count. Le chemin spécial
__meta permet de lire les métadonnées d’une source, par exemple
/value/saves/<id>/__meta/name. Les segments numériques parcourent les
tableaux, comme /value/emulators/azahar/data/classement/0/personnage.
Une connexion SSE déjà ouverte reçoit une invalidation avec
emulator.available: false et un classement vide si la lecture Azahar est
interrompue. L’overlay ne conserve donc pas un ancien état de course pendant
une coupure ou une transition, puis les données reprennent automatiquement.
Les anciennes routes sans préfixe restent compatibles avec le dernier JSON
reçu : /value/position/galaxie/nom, /count/vaisseaux/__count et
/raw/. Les réponses comportent X-MetaSave-Source, désactivent le cache
et autorisent le CORS.
L’adresse 127.0.0.1 limite l’accès à la machine locale. Utiliser
0.0.0.0 ou une adresse du réseau expose les données sans authentification.
Écrire un plugin
Le contrat des documents JSON produits par les plugins de sauvegarde et
d’émulateur est détaillé dans API_PLUGIN.md.
Copier un fichier .py dans le sous-dossier plugins. Un plugin est du code
Python exécuté localement : n'installer que du code de confiance. Le module doit
exposer PLUGIN (instance ou classe) ou create_plugin() :
from pathlib import Path
from metasave.domain import PluginResult
from metasave.plugins.base import SaveParserPlugin
class MySavePlugin(SaveParserPlugin):
name = "Mon jeu"
version = "1.0"
priority = 50
def probe(self, path: Path, header: bytes) -> int:
return 95 if header.startswith(b"MYGAME") else 0
def parse(self, path: Path) -> PluginResult:
raw = path.read_bytes()
return PluginResult(
data={"payload_hex": raw[6:].hex()},
metadata={"format": "MYGAME save", "bytes": len(raw)},
)
PLUGIN = MySavePlugin
Le score de probe va de 0 à 100. Le plugin ayant le meilleur score est choisi,
puis parse reçoit uniquement le chemin de la copie temporaire.
Plugin d’émulateur
Copier le module dans emulator-plugins. Il doit exposer
EMULATOR_PLUGIN ou create_emulator_plugin() et hériter de
EmulatorGamePlugin. probe(process) reconnaît le jeu, idéalement par son
title_id, puis read(memory, process) renvoie un EmulatorPluginResult
ou None lorsqu’il n’y a rien à publier :
from metasave.emulators.base import EmulatorGamePlugin, EmulatorPluginResult
class MyEmulatorPlugin(EmulatorGamePlugin):
name = "Mon jeu (live)"
game_name = "Mon jeu"
def probe(self, process):
return 100 if process.title_id == 0x0004000000000000 else 0
def read(self, memory, process):
score = int.from_bytes(memory.read_memory(0x123456, 4), "little")
return EmulatorPluginResult({"score": score})
EMULATOR_PLUGIN = MyEmulatorPlugin
Les plugins sont chargés au démarrage. Une exception dans un plugin n’empêche pas le chargement des autres.
Tests
python -m pytest
Référence du format .hg
L'implémentation suit le format de blocs publié par les projets communautaires
libNOM.io et
NMS Toolkit : en-tête little-endian
0xFEEDA1E5, taille compressée, taille décompressée, quatre octets réservés,
puis données LZ4. MetaSave est volontairement en lecture seule.
Référence du format Pokémon Génération 1
Les offsets SRAM, structures de Pokémon, tables d'espèces, cartes, objets et attaques suivent les désassemblages documentés de pret/pokered et pret/pokeyellow. Le checksum principal est vérifié avant toute extraction. Les libellés français d'objets et d'attaques proviennent des ressources de localisation de PKHeX. MetaSave ne modifie jamais la sauvegarde.