# 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 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::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 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 SaveSystem::listSaves() { std::vector 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; } ```