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

193 lines
8.6 KiB
Markdown

# 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 :
```text
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 :
```text
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.