# Système de Sauvegarde

## Philosophie

Le système de sauvegarde suit la philosophie du projet : **évolution itérative avec versioning des systèmes**. Chaque version améliore progressivement les performances et fonctionnalités tout en maintenant la compatibilité.

Le système est conçu pour être **modulaire et autonome**, chaque module gérant sa propre persistence de manière indépendante.

## Version 1 : Save JSON Local Sans Versioning de Format

### Principes de Base

- **Format** : JSON pur pour lisibilité, debugging et édition manuelle
- **Granularité** : Un fichier par module
- **Distribution** : Chaque serveur sauvegarde localement (multi-serveur ready)
- **Coordination** : Clé de save distribuée par le coordinateur
- **Autonomie** : Chaque module implémente son propre `serialize()` / `deserialize()`
- **Chunks** : Seulement les chunks modifiés (metachunks 512x512)
- **Objectif** : Système fonctionnel et suffisant pour tests et développement

### Architecture de Fichiers

```
saves/
└── abc123_1728226320/          # Clé de save : [id]_[timestamp]
    ├── save_metadata.json       # Métadonnées globales de la save
    ├── server_1/                # Un dossier par serveur (future-proof)
    │   ├── tank.json
    │   └── combat.json
    ├── server_2/
    │   ├── economy.json
    │   └── factory.json
    └── metachunks/
        ├── 0_0.json             # Metachunk coords (x, y) en 512x512
        ├── 0_1.json
        └── ...
```

**Note** : En configuration single-process V1, un seul dossier serveur existe (ex: `server_1/`), mais la structure supporte déjà le multi-serveur.

### Format save_metadata.json

Fichier global décrivant la sauvegarde complète :

```json
{
  "save_key": "abc123_1728226320",
  "timestamp": "2025-10-06T14:32:00Z",
  "game_version": "0.1.0-alpha",
  "modules": {
    "tank": {
      "version": "0.1.15.3847",
      "load_status": "ok"
    },
    "economy": {
      "version": "0.2.8.1203",
      "load_status": "ok"
    },
    "factory": {
      "version": "0.1.20.4512",
      "load_status": "load_pending"
    },
    "transport": {
      "version": "0.1.5.982",
      "load_status": "ok"
    }
  },
  "metachunks_count": 42,
  "world_seed": 1847293847
}
```

**Champs** :
- `save_key` : Identifiant unique de la save (format : `[id]_[timestamp_unix]`)
- `timestamp` : Date/heure de sauvegarde (ISO 8601)
- `game_version` : Version du jeu ayant créé la save
- `modules` : État de chaque module sauvé
  - `version` : Version du module (voir `module-versioning.md`)
  - `load_status` : `"ok"`, `"load_pending"`, `"failed"`
- `metachunks_count` : Nombre de metachunks sauvés
- `world_seed` : Seed de génération procédurale

### Format Fichiers Modules

Chaque module implémente `IModuleSave` et définit son propre format JSON.

#### Exemple : tank.json

```json
{
  "module_name": "tank",
  "module_version": "0.1.15.3847",
  "save_timestamp": "2025-10-06T14:32:00Z",
  "data": {
    "tanks": [
      {
        "id": "tank_001",
        "chassis_type": "m1_abrams",
        "position": {"x": 1250.5, "y": 3480.2},
        "rotation": 45.0,
        "components": [
          {
            "type": "turret_120mm",
            "grid_pos": [2, 3],
            "health": 100,
            "ammo_loaded": "apfsds"
          },
          {
            "type": "engine_turbine",
            "grid_pos": [1, 1],
            "health": 85,
            "fuel": 0.75
          }
        ],
        "inventory": {
          "120mm_apfsds": 32,
          "120mm_heat": 18
        },
        "crew_status": "operational"
      }
    ]
  }
}
```

#### Exemple : economy.json

```json
{
  "module_name": "economy",
  "module_version": "0.2.8.1203",
  "save_timestamp": "2025-10-06T14:32:00Z",
  "data": {
    "market_prices": {
      "iron_ore": 2.5,
      "copper_ore": 3.2,
      "steel_plate": 8.0
    },
    "player_balance": 125000,
    "pending_transactions": [
      {
        "id": "tx_4829",
        "type": "buy",
        "resource": "steel_plate",
        "quantity": 1000,
        "price_per_unit": 8.0,
        "timestamp": "2025-10-06T14:30:15Z"
      }
    ]
  }
}
```

**Convention** : Chaque fichier module contient :
- `module_name` : Nom du module (vérification cohérence)
- `module_version` : Version du module ayant créé la save
- `save_timestamp` : Timestamp de sauvegarde
- `data` : Objet contenant toutes les données spécifiques au module

### Format Metachunks (512x512)

Pour réduire le nombre de fichiers, les chunks 64x64 sont regroupés en **metachunks 512x512** (8x8 chunks par metachunk = 64 chunks).

#### Structure Metachunk

```json
{
  "metachunk_coords": {"x": 0, "y": 0},
  "metachunk_size": 512,
  "chunk_size": 64,
  "chunks": {
    "0_0": {
      "terrain": {
        "land_ids_base64": "AQIDBAUGBwg...",
        "roof_ids_base64": "CQAKAA..."
      },
      "buildings": [
        {
          "id": "building_factory_001",
          "type": "assembler_mk1",
          "position": {"x": 10, "y": 15},
          "recipe": "ammunition_7.62mm",
          "progress": 0.45,
          "inventory": {
            "iron_plate": 50,
            "copper_wire": 30
          }
        }
      ],
      "resources": [
        {
          "type": "iron_ore",
          "patch_id": "iron_patch_042",
          "positions": [[5,5], [5,6], [6,5], [6,6]],
          "amounts": [1000, 950, 980, 1020]
        }
      ]
    },
    "0_1": {
      "terrain": { "land_ids_base64": "..." },
      "buildings": [],
      "resources": []
    }
  }
}
```

**Optimisations** :
- **Base64 compression** : Données terrain denses compressées
- **Sparse storage** : Chunks vides omis du metachunk
- **Dirty tracking** : Seulement metachunks avec chunks modifiés sont sauvés

#### Calcul Coords Metachunk

```cpp
// Chunk 64x64 coords → Metachunk 512x512 coords
MetachunkCoords getMetachunkCoords(int chunkX, int chunkY) {
    return {
        chunkX / 8,  // 512 / 64 = 8 chunks par metachunk
        chunkY / 8
    };
}

// Coords locales dans le metachunk
ChunkLocalCoords getLocalCoords(int chunkX, int chunkY) {
    return {
        chunkX % 8,
        chunkY % 8
    };
}
```

### Interface IModuleSave

Chaque module implémente cette interface pour gérer sa persistence :

```cpp
class IModuleSave {
public:
    virtual ~IModuleSave() = default;

    // Sérialise l'état complet du module en JSON
    virtual nlohmann::json serialize() = 0;

    // Restaure l'état du module depuis JSON
    // Retourne true si succès, false si erreur (→ load_pending)
    virtual bool deserialize(const nlohmann::json& data) = 0;

    // Version du module (auto-générée, voir module-versioning.md)
    virtual std::string getVersion() const = 0;
};
```

#### Exemple Implémentation

```cpp
class TankModule : public IModule, public IModuleSave {
private:
    std::vector<Tank> tanks;

public:
    nlohmann::json serialize() override {
        json data;
        data["module_name"] = "tank";
        data["module_version"] = getVersion();
        data["save_timestamp"] = getCurrentTimestamp();

        json tanksArray = json::array();
        for (const auto& tank : tanks) {
            tanksArray.push_back(tank.toJson());
        }
        data["data"]["tanks"] = tanksArray;

        return data;
    }

    bool deserialize(const json& data) override {
        try {
            // Validation version
            if (data["module_name"] != "tank") {
                LOG_ERROR("Invalid module name in save file");
                return false;
            }

            // Charge les tanks
            tanks.clear();
            for (const auto& tankJson : data["data"]["tanks"]) {
                tanks.push_back(Tank::fromJson(tankJson));
            }

            return true;
        }
        catch (const std::exception& e) {
            LOG_ERROR("Deserialization failed: {}", e.what());
            return false;
        }
    }

    std::string getVersion() const override {
        return MODULE_VERSION;  // Défini par CMake
    }
};
```

### Workflow de Sauvegarde

#### Déclenchement

**En single-process** :
```cpp
saveSystem->saveGame("abc123");
```

**En multi-serveur** :
Le coordinateur broadcast une clé de save :
```cpp
// Coordinateur
coordinator->broadcastSaveCommand("abc123_1728226320");

// Chaque serveur reçoit et exécute
void Server::onSaveCommand(const std::string& saveKey) {
    saveSystem->saveGame(saveKey);
}
```

#### Processus de Sauvegarde

```cpp
void SaveSystem::saveGame(const std::string& saveKey) {
    auto startTime = std::chrono::steady_clock::now();

    // 1. Créer structure de dossiers
    std::string savePath = "saves/" + saveKey + "/";
    fs::create_directories(savePath + "server_1/");
    fs::create_directories(savePath + "metachunks/");

    // 2. Sauvegarder chaque module
    json metadata;
    metadata["save_key"] = saveKey;
    metadata["timestamp"] = getCurrentTimestamp();
    metadata["game_version"] = GAME_VERSION;

    for (auto& module : moduleSystem->getAllModules()) {
        std::string moduleName = module->getName();

        try {
            // Sérialiser module
            json moduleData = module->serialize();

            // Écrire fichier
            std::string modulePath = savePath + "server_1/" +
                                     moduleName + ".json";
            writeJsonFile(modulePath, moduleData);

            // Mettre à jour metadata
            metadata["modules"][moduleName] = {
                {"version", module->getVersion()},
                {"load_status", "ok"}
            };

            LOG_INFO("Module {} saved successfully", moduleName);
        }
        catch (const std::exception& e) {
            LOG_ERROR("Failed to save module {}: {}", moduleName, e.what());
            metadata["modules"][moduleName] = {
                {"version", module->getVersion()},
                {"load_status", "failed"}
            };
        }
    }

    // 3. Sauvegarder metachunks modifiés
    int metachunkCount = 0;
    for (auto& [coords, metachunk] : dirtyMetachunks) {
        json metachunkData = metachunk->serialize();
        std::string filename = std::to_string(coords.x) + "_" +
                               std::to_string(coords.y) + ".json";
        writeJsonFile(savePath + "metachunks/" + filename, metachunkData);
        metachunkCount++;
    }
    metadata["metachunks_count"] = metachunkCount;

    // 4. Écrire metadata
    writeJsonFile(savePath + "save_metadata.json", metadata);

    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::steady_clock::now() - startTime
    );

    LOG_INFO("Save completed in {}ms: {} modules, {} metachunks",
             duration.count(), metadata["modules"].size(), metachunkCount);
}
```

### Workflow de Chargement

```cpp
void SaveSystem::loadGame(const std::string& saveKey) {
    std::string savePath = "saves/" + saveKey + "/";

    // 1. Charger metadata
    json metadata = readJsonFile(savePath + "save_metadata.json");

    // Validation game version (warning seulement en V1)
    if (metadata["game_version"] != GAME_VERSION) {
        LOG_WARN("Save created with different game version: {} vs {}",
                 metadata["game_version"], GAME_VERSION);
    }

    // 2. Charger world data (seed, params)
    worldGenerator->setSeed(metadata["world_seed"]);

    // 3. Charger chaque module
    for (auto& module : moduleSystem->getAllModules()) {
        std::string moduleName = module->getName();
        std::string modulePath = savePath + "server_1/" +
                                 moduleName + ".json";

        if (!fs::exists(modulePath)) {
            LOG_WARN("No save file for module {}, using defaults", moduleName);
            continue;
        }

        try {
            json moduleData = readJsonFile(modulePath);

            // Vérification version module
            std::string savedVersion = moduleData["module_version"];
            std::string currentVersion = module->getVersion();

            if (savedVersion != currentVersion) {
                LOG_WARN("Module {} version mismatch: save={}, current={}",
                         moduleName, savedVersion, currentVersion);
            }

            // Désérialiser
            bool success = module->deserialize(moduleData);

            if (!success) {
                LOG_ERROR("Module {} deserialization failed, marked as load_pending",
                          moduleName);
                metadata["modules"][moduleName]["load_status"] = "load_pending";
            }
            else {
                LOG_INFO("Module {} loaded successfully", moduleName);
            }
        }
        catch (const std::exception& e) {
            LOG_ERROR("Failed to load module {}: {}", moduleName, e.what());
            metadata["modules"][moduleName]["load_status"] = "load_pending";
        }
    }

    // 4. Metachunks chargés à la demande (streaming)
    // Voir Chunk Streaming ci-dessous

    LOG_INFO("Save loaded: {}", saveKey);
}
```

### Chunk Streaming

Les metachunks ne sont **pas tous chargés au démarrage** pour économiser la mémoire. Système de chargement à la demande :

```cpp
Chunk* ChunkManager::getChunk(int x, int y) {
    ChunkCoords coords{x, y};

    // 1. Vérifier si déjà en RAM
    if (loadedChunks.contains(coords)) {
        return loadedChunks[coords];
    }

    // 2. Calculer metachunk parent
    MetachunkCoords metaCoords = getMetachunkCoords(x, y);
    ChunkLocalCoords localCoords = getLocalCoords(x, y);

    // 3. Charger metachunk si nécessaire
    if (!loadedMetachunks.contains(metaCoords)) {
        loadMetachunk(metaCoords);
    }

    // 4. Extraire chunk du metachunk
    Metachunk* metachunk = loadedMetachunks[metaCoords];
    Chunk* chunk = metachunk->getChunk(localCoords);

    if (chunk) {
        loadedChunks[coords] = chunk;
        return chunk;
    }

    // 5. Si chunk n'existe pas dans save → génération procédurale
    return generateNewChunk(coords);
}

void ChunkManager::loadMetachunk(MetachunkCoords coords) {
    std::string path = "saves/" + currentSave + "/metachunks/" +
                       std::to_string(coords.x) + "_" +
                       std::to_string(coords.y) + ".json";

    if (!fs::exists(path)) {
        // Metachunk pas encore exploré/modifié
        loadedMetachunks[coords] = new Metachunk(coords);
        return;
    }

    // Charger et désérialiser metachunk
    json metachunkData = readJsonFile(path);
    Metachunk* metachunk = new Metachunk(coords);
    metachunk->deserialize(metachunkData);

    loadedMetachunks[coords] = metachunk;
    LOG_DEBUG("Metachunk loaded: ({}, {})", coords.x, coords.y);
}
```

### Gestion des Erreurs : load_pending

Lorsqu'un module échoue à charger (JSON corrompu, incompatibilité version, etc.), il est marqué `load_pending` dans le metadata.

#### Workflow de Récupération

```cpp
// Au chargement, si erreur détectée
if (!module->deserialize(moduleData)) {
    metadata["modules"][moduleName]["load_status"] = "load_pending";

    // Sauvegarder metadata mis à jour
    writeJsonFile(savePath + "save_metadata.json", metadata);

    // Continuer le chargement des autres modules
}
```

#### Interface Utilisateur

```cpp
void UI::displayLoadPendingModules() {
    for (const auto& [moduleName, info] : metadata["modules"].items()) {
        if (info["load_status"] == "load_pending") {
            std::cout << "[WARNING] Module '" << moduleName
                      << "' failed to load. Check logs and fix JSON file.\n";
            std::cout << "  File: saves/" << saveKey << "/server_1/"
                      << moduleName << ".json\n";
            std::cout << "  Version: " << info["version"] << "\n\n";
        }
    }
}
```

#### Retry Manuel

Après avoir corrigé le fichier JSON manuellement :

```cpp
void SaveSystem::retryLoadModule(const std::string& moduleName) {
    auto module = moduleSystem->getModule(moduleName);
    std::string modulePath = savePath + "server_1/" + moduleName + ".json";

    try {
        json moduleData = readJsonFile(modulePath);
        bool success = module->deserialize(moduleData);

        if (success) {
            metadata["modules"][moduleName]["load_status"] = "ok";
            writeJsonFile(savePath + "save_metadata.json", metadata);
            LOG_INFO("Module {} successfully reloaded", moduleName);
        }
    }
    catch (const std::exception& e) {
        LOG_ERROR("Retry failed for module {}: {}", moduleName, e.what());
    }
}
```

### Dirty Tracking

Seuls les chunks/metachunks **modifiés** sont sauvegardés pour optimiser la taille des saves.

```cpp
class ChunkManager {
private:
    std::unordered_set<MetachunkCoords> dirtyMetachunks;

public:
    void markChunkDirty(int chunkX, int chunkY) {
        MetachunkCoords metaCoords = getMetachunkCoords(chunkX, chunkY);
        dirtyMetachunks.insert(metaCoords);
    }

    void onBuildingPlaced(int x, int y) {
        int chunkX = x / 64;
        int chunkY = y / 64;
        markChunkDirty(chunkX, chunkY);
    }

    void onResourceMined(int x, int y) {
        int chunkX = x / 64;
        int chunkY = y / 64;
        markChunkDirty(chunkX, chunkY);
    }
};
```

### Performance Targets V1

- **Autosave** : < 100ms pause perceptible (background thread recommandé)
- **Save manuelle** : < 500ms acceptable (avec feedback UI)
- **Load game** : < 3 secondes pour metadata + modules
- **Chunk streaming** : < 16ms par metachunk (1 frame @60fps)
- **Taille save** : ~10-50MB pour partie moyenne (dépend exploration)

### Limitations V1

Ces limitations seront résolues dans les versions futures :

1. **Pas de migration format** : Si le format JSON d'un module change, incompatibilité
2. **JSON verbeux** : Fichiers volumineux comparé à format binaire (acceptable pour debug)
3. **Pas de compression** : Taille disque non optimale
4. **Pas de checksums** : Corruption de données détectée tard (au parsing JSON)
5. **Concurrence limitée** : Save/load synchrones (pas de multi-threading)

## Évolution Future

### Version 2 : Compression et Migration

- **Compression LZ4/Zstd** : Réduction 60-80% taille fichiers
- **Format binaire optionnel** : MessagePack ou Protobuf pour données volumineuses
- **Migration automatique** : Système de conversion entre versions de format
- **Checksums** : Validation intégrité (CRC32, SHA256)
- **Async I/O** : Save/load en background threads

### Version 3 : Incremental Saves

- **Delta encoding** : Sauvegardes différentielles (snapshot + journal de changements)
- **Rollback temporel** : Restaurer état à timestamp spécifique
- **Compression inter-saves** : Déduplication entre sauvegardes
- **Hot-save** : Sauvegarde pendant le jeu sans pause

### Version 4 : Cloud et Multiplayer

- **Synchronisation cloud** : Steam Cloud, Google Drive, etc.
- **Save distribué** : Réplication multi-serveur automatique
- **Conflict resolution** : Merge intelligent pour multiplayer
- **Versioning git-like** : Branches, merge, rollback

## Références Croisées

- `module-versioning.md` : Système de versioning automatique des modules
- `architecture-modulaire.md` : Interface IModule, contraintes autonomie
- `systemes-techniques.md` : Architecture chunks multi-échelle
- `map-system.md` : Génération procédurale, resource patches
- `metriques-joueur.md` : Métriques à sauvegarder (3.1GB analytics)

## Exemples Pratiques

### Créer une Nouvelle Save

```cpp
// Single-process
saveSystem->saveGame("my_first_base");

// Multi-serveur
coordinator->broadcastSaveCommand("my_first_base_1728226320");
```

### Charger une Save Existante

```cpp
saveSystem->loadGame("my_first_base_1728226320");
```

### Éditer une Save Manuellement

```bash
# Ouvrir fichier module
nano saves/abc123_1728226320/server_1/economy.json

# Modifier prix du marché
# "iron_ore": 2.5 → "iron_ore": 5.0

# Recharger le module spécifique
saveSystem->retryLoadModule("economy");
```

### Lister les Saves Disponibles

```cpp
std::vector<SaveInfo> SaveSystem::listSaves() {
    std::vector<SaveInfo> saves;

    for (const auto& entry : fs::directory_iterator("saves/")) {
        if (!entry.is_directory()) continue;

        std::string metadataPath = entry.path().string() + "/save_metadata.json";
        if (!fs::exists(metadataPath)) continue;

        json metadata = readJsonFile(metadataPath);
        saves.push_back({
            .saveKey = metadata["save_key"],
            .timestamp = metadata["timestamp"],
            .gameVersion = metadata["game_version"],
            .moduleCount = metadata["modules"].size(),
            .metachunkCount = metadata["metachunks_count"]
        });
    }

    // Trier par timestamp décroissant (plus récent en premier)
    std::sort(saves.begin(), saves.end(), [](const auto& a, const auto& b) {
        return a.timestamp > b.timestamp;
    });

    return saves;
}
```
