Parseur de fichiers de sauvegarde local sur le PC : Sert de base au Bot Twitchio pour télétransmettre des informations en temps réel sur fichier de sauvegarde de jeu.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-20 11:36:47 +02:00
assets Ajout du plugin Mario Kart 7 / Metasave 2026-09-19 15:09:41 +02:00
demo Firt release 2026-08-20 17:15:51 +02:00
src/metasave Ajout alerte carapace bleue 2026-09-20 11:36:47 +02:00
tests Ajout alerte carapace bleue 2026-09-20 11:36:47 +02:00
.gitignore Firt release 2026-08-20 17:15:51 +02:00
API_PLUGIN.md Ajout alerte carapace bleue 2026-09-20 11:36:47 +02:00
launcher.sh Ajout de notification par e-mail et configuration mail destinataire de ces dites notifications 2026-09-11 19:28:53 +02:00
main.py Firt release 2026-08-20 17:15:51 +02:00
pyproject.toml Ajout du plugin Mario Kart 7 / Metasave 2026-09-19 15:09:41 +02:00
README.md Ajout alerte carapace bleue 2026-09-20 11:36:47 +02:00
requirements.txt Firt release 2026-08-20 17:15:51 +02:00

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*.sav ou save?.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 linterfaç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 dAzahar

  1. Dans Azahar, ouvrir les paramètres de débogage et activer Enable RPC server.
  2. Dans MetaSave, ouvrir Paramètres → Configurer lémulateur….
  3. Choisir Azahar, saisir ladresse IP et le port, puis activer linterfaçage.

Le serveur RPC officiel dAzahar utilise UDP et écoute par défaut sur le port 45987. Loption 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, larrivé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 jusquau départ de la course suivante. En ligne, cette ligne est choisie avec NetworkEngine.m_local_player_id, lidentifiant effectif du joueur local, et non avec lidentifiant 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 nest pas utilisé. Le champ notification.joueur_principal_id expose lidentifiant 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.

Linterfaçage est strictement en lecture seule. Si Azahar est fermé, si aucune course nest active ou si le jeu nest pas supporté, aucune donnée nest envoyée à Twitchiou.

API Web locale

Ouvrir Paramètres → Configurer lAPI Web…, choisir ladresse 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 à ladresse http://127.0.0.1:8765 :

  • GET /value/saves/<id>/joueur/nom renvoie une valeur de cette sauvegarde ;
  • GET /count/saves/<id>/vaisseaux/__count compte ses vaisseaux ;
  • GET /raw/saves/<id>/ renvoie tout son JSON ;
  • GET /value/emulators/azahar/data/course/circuit lit la télémétrie Azahar ;
  • GET /value/emulators/azahar/data/course/nombre_tours_effectues lit les tours terminés ;
  • GET /value/emulators/azahar/data/notification/ecran lit lécran courant ;
  • GET /value/emulators/azahar/data/classement/0/pseudonymme lit le nom affiché ;
  • GET /count/emulators/azahar/data/classement/__count compte les concurrents ;
  • GET /raw/emulators/azahar/ renvoie tout le JSON de lémulateur ;
  • GET /events/emulators/azahar diffuse ce JSON en temps réel avec SSE ;
  • GET /count/saves/__count et GET /count/emulators/__count comptent les sources disponibles.

Le suffixe __coun est accepté comme alias de __count. Le chemin spécial __meta permet de lire les métadonnées dune 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. Loverlay 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.

Ladresse 127.0.0.1 limite laccè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 lorsquil ny 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 nempê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.