Files
Ransom-s-wake/docs/adr/0007-versioned-json-saves.md

8.6 KiB

0007 — Sauvegardes JSON versionnées et remplaçables sans perte

  • Statut : accepté
  • Date : 2026-08-10
  • Remplace : aucun

Contexte

La campagne contient un état métier durable : argent, dette, position du navire, contrats, archives, épaves connues, cargaison et améliorations. Une plongée contient en plus un état physique très fréquent et temporaire. Tenter de sérialiser les nœuds, les collisions, le maillage sonar ou la position arbitraire du sous-marin couplerait la sauvegarde aux scènes et rendrait les migrations fragiles.

Le joueur doit pouvoir conserver plusieurs étapes d'une même campagne, utiliser des sauvegardes manuelles et reprendre après un crash sans perdre un sous-marin à cause d'une écriture interrompue. Les réglages de plateforme et d'accessibilité ne doivent pas être dupliqués dans chaque état de campagne.

Décision

Format et enveloppe

Chaque sauvegarde est un document JSON explicite. Son enveloppe contient au minimum :

schema_version  entier positif
campaign_id     identifiant stable de la partie
save_id         identifiant stable de cette sauvegarde
created_at_utc  date ISO 8601 utilisée comme métadonnée d'affichage
kind            before_dive | after_dive | campaign | manual
display_name    libellé facultatif d'une sauvegarde manuelle
payload         état de campagne versionné

Les identifiants sont générés et validés par l'application. Un libellé saisi par le joueur n'est jamais utilisé comme nom de fichier ou fragment de chemin. Les horodatages viennent d'une horloge injectée ; ils n'influencent aucune règle de gameplay.

Le codec JSON appartient à l'infrastructure. Il convertit entre les dictionnaires du format et des objets de transfert typés, puis entre ces objets et le domaine. Aucun dictionnaire anonyme, chemin de scène, Node, Resource arbitraire ou objet exécutable ne traverse cette frontière.

Le payload contient au minimum :

  • difficulté, argent et dette ;
  • VesselLocation, disponibilité du sous-marin et ressources non remplies ;
  • contrats, objectifs connus et révélations ;
  • zones d'écho, épaves localisées, identités et observations sécurisées ;
  • états persistants d'ouverture et d'interaction ;
  • panier, cale, stockage et objets narratifs ;
  • archives, plans et améliorations ;
  • dernier résultat de plongée.

Les références de contenu utilisent des identifiants stables validés au chargement. Une valeur dérivée est recalculée à partir de ses sources lorsqu'il est possible de le faire sans perte d'information.

Organisation des fichiers

Les campagnes sont rangées sous :

user://saves/<campaign_id>/
  autosave_before_dive.json
  autosave_after_dive.json
  autosave_campaign.json
  manual/<save_id>.json
  active_dive.marker.json

Les fichiers temporaires et de secours utilisent des suffixes techniques dans le même dossier que leur cible. Ils ne sont pas affichés comme des sauvegardes. Le campaign_id identifie la lignée créée par une nouvelle partie ; il ne représente pas une seconde campagne narrative.

Les réglages du joueur sont versionnés et stockés séparément sous user://settings.json. Ils comprennent notamment commandes, accessibilité, audio, affichage et qualité Forward+. Charger une autre sauvegarde de campagne ne les remplace pas.

Politique des emplacements

Trois emplacements automatiques ont une signification fixe :

  • before_dive est écrit après validation de la préparation et avant le chargement de la scène sous-marine ;
  • after_dive est écrit après application unique du résultat de plongée au navire et à la campagne ;
  • campaign est écrit après une action durable de progression, notamment transfert, analyse, remise, vente, entretien ou achat.

Chaque autosauvegarde conserve sa dernière version valide comme secours invisible. Une sauvegarde manuelle remplacée conserve également sa précédente version valide comme secours technique.

Le joueur peut créer jusqu'à dix sauvegardes manuelles nommées, uniquement lorsqu'aucune plongée n'est active. Atteindre la limite exige de choisir et de confirmer un remplacement ou une suppression ; aucune sauvegarde visible n'est effacée automatiquement.

Continuer charge la sauvegarde principale valide la plus récente parmi tous les emplacements visibles. Si elle est invalide, son secours est essayé, puis la recherche continue avec les autres sauvegardes valides. Charger affiche les sauvegardes visibles avec leur statut sans supprimer ni réécrire les fichiers invalides.

Écriture récupérable

Une écriture suit toujours cette procédure :

  1. sérialiser vers un fichier temporaire créé dans le dossier de la cible ;
  2. fermer le fichier, puis le relire intégralement ;
  3. décoder, migrer si nécessaire et valider le document relu ;
  4. préserver la cible valide actuelle comme secours ;
  5. remplacer la cible par renommage sur le même volume ;
  6. restaurer le secours si le remplacement final échoue ;
  7. signaler explicitement le succès ou l'erreur à l'application.

La dernière sauvegarde valide n'est jamais supprimée avant qu'une nouvelle copie complète ait été écrite et validée. Un échec d'I/O ne devient ni une réussite silencieuse, ni un nouvel état de campagne.

Au démarrage, l'infrastructure peut supprimer uniquement les temporaires orphelins dont la cible ou le secours valide a été identifié. Elle ne supprime jamais automatiquement une sauvegarde visible invalide.

Plongée interrompue

Après réussite de l'autosauvegarde before_dive, le démarrage effectif de la plongée crée active_dive.marker.json. Ce marqueur contient seulement le campaign_id, le save_id de reprise et un identifiant de session. Il ne contient aucune position, ressource, collision ou donnée sonar.

Une fin de plongée traitée avec succès supprime le marqueur après écriture de l'état after_dive. Si le jeu redémarre avec un marqueur valide, il propose ou effectue la reprise depuis l'autosauvegarde before_dive référencée. Le crash n'inflige ni perte automatique du véhicule, ni consommation intermédiaire.

La position arbitraire du sous-marin, les vitesses, particules, collisions, échantillons sonar, couleurs et maillages dynamiques ne sont jamais sauvegardés.

Versions et migrations

schema_version existe dès la V1. Le chargement applique des migrations explicites, successives et déterministes d'une version prise en charge vers la version courante, puis valide l'ensemble des invariants et références de contenu.

Une fixture anonymisée de chaque version prise en charge est conservée sous tests/fixtures/. Les tests vérifient migration, aller-retour, sauvegarde tronquée, référence absente, secours et refus d'une version inconnue.

Une version plus récente que celle du jeu est refusée proprement. Le fichier original n'est ni modifié, ni rétrogradé. Une migration réussie en mémoire ne réécrit pas non plus l'original avant une nouvelle sauvegarde explicitement déclenchée.

Conséquences

  • Les sauvegardes survivent aux changements de scènes et peuvent être testées sans lancer une plongée.
  • Plusieurs étapes d'une partie coexistent sans transformer les libellés du joueur en chemins dangereux.
  • Une plongée ne peut pas être reprise à la seconde exacte de l'interruption ; elle recommence depuis sa préparation validée.
  • Les sauvegardes et leurs secours consomment davantage d'espace, mais elles ne contiennent pas les données physiques ou sonar volumineuses.
  • Chaque modification du format exige une migration et des fixtures de contrat. Toute exception nécessite un nouvel ADR.
  • Les réglages restent disponibles même lorsqu'aucune campagne valide n'existe.
  • Les erreurs de disque ou de validation restent récupérables et visibles pour le joueur.

Alternatives considérées

  • Sérialiser l'arbre de scènes Godot : rejeté, car les chemins, nœuds et états physiques ne constituent pas un contrat de sauvegarde stable.
  • Sauvegarder pendant la plongée : rejeté pour la V1, car restaurer une simulation cohérente coûterait beaucoup plus que reprendre avant le départ.
  • Une seule sauvegarde écrasée en place : rejeté, car une interruption d'écriture pourrait détruire toute la progression.
  • Un format binaire opaque : rejeté pour la V1, car il compliquerait inspection, fixtures et migrations sans besoin de taille démontré.
  • Inclure les réglages dans chaque sauvegarde : rejeté, car ils appartiennent au joueur et à la plateforme, pas à une étape de campagne.
  • Supprimer automatiquement les sauvegardes invalides ou excédentaires : rejeté, car la récupération ou le choix doit rester sous contrôle du joueur.