- Add hybrid deployment modes: local_dev (MVP) and production_pwa (optional) - Integrate WarFactory engine reuse with hot-reload 0.4ms - Define multi-target compilation strategy (DLL/SO/WASM) - Detail both deployment modes with cost analysis - Add progressive roadmap: Phase 1 (local), Phase 2 (POC WASM), Phase 3 (cloud) - Budget clarified: $10-20/mois (local) vs $13-25/mois (cloud) - Document open questions for technical validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
716 lines
21 KiB
Markdown
716 lines
21 KiB
Markdown
# 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;
|
|
}
|
|
```
|