- ai-framework.md: Unified decision framework with scoring system, RL integration, doctrines - systeme-diplomatique.md: Relations (shared), intentions (bilateral), threat, reliability systems - calcul-menace.md: Contextual threat calculation with sword & shield mechanics - systeme-sauvegarde.md: V1 save system with JSON, metachunks, module autonomy - module-versioning.md: Automatic MAJOR.MINOR.PATCH.BUILD versioning via CMake 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
21 KiB
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 :
{
"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 savemodules: État de chaque module sauvéversion: Version du module (voirmodule-versioning.md)load_status:"ok","load_pending","failed"
metachunks_count: Nombre de metachunks sauvésworld_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
{
"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
{
"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 savesave_timestamp: Timestamp de sauvegardedata: 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
{
"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
// 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 :
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
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 :
saveSystem->saveGame("abc123");
En multi-serveur : Le coordinateur broadcast une clé de save :
// 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
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
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 :
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
// 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
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 :
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.
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 :
- Pas de migration format : Si le format JSON d'un module change, incompatibilité
- JSON verbeux : Fichiers volumineux comparé à format binaire (acceptable pour debug)
- Pas de compression : Taille disque non optimale
- Pas de checksums : Corruption de données détectée tard (au parsing JSON)
- 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 modulesarchitecture-modulaire.md: Interface IModule, contraintes autonomiesystemes-techniques.md: Architecture chunks multi-échellemap-system.md: Génération procédurale, resource patchesmetriques-joueur.md: Métriques à sauvegarder (3.1GB analytics)
Exemples Pratiques
Créer une Nouvelle Save
// Single-process
saveSystem->saveGame("my_first_base");
// Multi-serveur
coordinator->broadcastSaveCommand("my_first_base_1728226320");
Charger une Save Existante
saveSystem->loadGame("my_first_base_1728226320");
Éditer une Save Manuellement
# 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
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;
}