Add comprehensive AI, diplomacy, and save system documentation
- 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>
This commit is contained in:
parent
076acd4812
commit
5e4235889a
677
docs/03-implementation/configuration/module-versioning.md
Normal file
677
docs/03-implementation/configuration/module-versioning.md
Normal file
@ -0,0 +1,677 @@
|
||||
# Module Versioning System
|
||||
|
||||
## Vue d'Ensemble
|
||||
|
||||
Le système de versioning des modules utilise un format **MAJOR.MINOR.PATCH.BUILD** automatiquement généré par CMake lors de la compilation. Ce système permet de :
|
||||
|
||||
- **Tracer les versions** : Identifier précisément quelle version du code a créé une save
|
||||
- **Détecter incompatibilités** : Alerter si une save utilise une version différente
|
||||
- **Debugging** : Reproduire bugs avec la version exacte du module
|
||||
- **Développement** : Versions incrémentales automatiques sans maintenance manuelle
|
||||
|
||||
## Format de Version
|
||||
|
||||
### Structure : MAJOR.MINOR.PATCH.BUILD
|
||||
|
||||
```
|
||||
0.1.15.3847
|
||||
│ │ │ │
|
||||
│ │ │ └─── BUILD : Auto-increment à chaque build (hash sources)
|
||||
│ │ └────── PATCH : Auto-increment à chaque commit
|
||||
│ └──────── MINOR : Manuel (fonctionnalités nouvelles)
|
||||
└────────── MAJOR : Manuel (breaking changes)
|
||||
```
|
||||
|
||||
### Composantes
|
||||
|
||||
#### MAJOR (Manuel)
|
||||
- **Incrémenté** : Breaking changes, incompatibilités majeures
|
||||
- **Exemples** : Refonte complète architecture, changement format save incompatible
|
||||
- **Gestion** : Modifié manuellement dans fichier `VERSION`
|
||||
- **Valeur initiale** : `0` (pré-release), `1` (première version stable)
|
||||
|
||||
#### MINOR (Manuel)
|
||||
- **Incrémenté** : Nouvelles fonctionnalités, ajouts non-breaking
|
||||
- **Exemples** : Nouveau type de tank, nouveau système économique
|
||||
- **Gestion** : Modifié manuellement dans fichier `VERSION`
|
||||
- **Valeur initiale** : `1`
|
||||
|
||||
#### PATCH (Automatique)
|
||||
- **Incrémenté** : À chaque commit Git
|
||||
- **Calcul** : Nombre de commits depuis le début du projet
|
||||
- **Exemples** : Bugfixes, optimisations, refactoring mineur
|
||||
- **Gestion** : Calculé automatiquement par CMake via `git rev-list --count HEAD`
|
||||
|
||||
#### BUILD (Automatique)
|
||||
- **Incrémenté** : À chaque rebuild (si sources modifiées)
|
||||
- **Calcul** : Hash MD5 des fichiers sources (`.cpp`, `.h`)
|
||||
- **But** : Distinguer builds de dev avec code non-commité
|
||||
- **Gestion** : Calculé automatiquement par CMake
|
||||
|
||||
### Exemples de Versions
|
||||
|
||||
```
|
||||
0.1.0.0 → Première version dev (MAJOR.MINOR manuel, PATCH/BUILD = 0)
|
||||
0.1.5.1234 → Après 5 commits, build 1234
|
||||
0.1.5.1235 → Même commit, sources modifiées → nouveau build
|
||||
0.1.6.1235 → Nouveau commit, sources identiques → même build (théorique)
|
||||
0.2.10.3847 → MINOR bump (nouvelle feature) + 10 commits
|
||||
1.0.0.0 → Release stable, reset PATCH/BUILD
|
||||
```
|
||||
|
||||
## Implémentation CMake
|
||||
|
||||
### Fichier VERSION
|
||||
|
||||
Chaque module contient un fichier `VERSION` à la racine :
|
||||
|
||||
```
|
||||
modules/tank/VERSION
|
||||
```
|
||||
|
||||
Contenu du fichier :
|
||||
```
|
||||
0.1
|
||||
```
|
||||
|
||||
Ce fichier contient uniquement `MAJOR.MINOR`. Les composantes `PATCH.BUILD` sont calculées automatiquement.
|
||||
|
||||
### CMakeLists.txt du Module
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(TankModule)
|
||||
|
||||
# ============================================================================
|
||||
# MODULE VERSIONING
|
||||
# ============================================================================
|
||||
|
||||
# 1. Lire MAJOR.MINOR depuis fichier VERSION
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION")
|
||||
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" BASE_VERSION)
|
||||
string(STRIP "${BASE_VERSION}" BASE_VERSION)
|
||||
else()
|
||||
set(BASE_VERSION "0.1")
|
||||
message(WARNING "No VERSION file found, using default: ${BASE_VERSION}")
|
||||
endif()
|
||||
|
||||
# 2. Calculer PATCH : nombre de commits Git
|
||||
execute_process(
|
||||
COMMAND git rev-list --count HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE PATCH_COUNT
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
RESULT_VARIABLE GIT_RESULT
|
||||
)
|
||||
|
||||
if(NOT GIT_RESULT EQUAL 0)
|
||||
set(PATCH_COUNT "0")
|
||||
message(WARNING "Git not available, PATCH set to 0")
|
||||
endif()
|
||||
|
||||
# 3. Calculer BUILD : hash des fichiers sources
|
||||
file(GLOB_RECURSE MODULE_SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include/*.h"
|
||||
)
|
||||
|
||||
# Concaténer contenu de tous les fichiers
|
||||
set(SOURCES_CONTENT "")
|
||||
foreach(SOURCE_FILE ${MODULE_SOURCES})
|
||||
file(READ ${SOURCE_FILE} FILE_CONTENT)
|
||||
string(APPEND SOURCES_CONTENT ${FILE_CONTENT})
|
||||
endforeach()
|
||||
|
||||
# Hash MD5 du contenu total
|
||||
string(MD5 SOURCES_HASH "${SOURCES_CONTENT}")
|
||||
|
||||
# Convertir hash hex en nombre décimal (4 premiers caractères)
|
||||
string(SUBSTRING ${SOURCES_HASH} 0 4 BUILD_HEX)
|
||||
# Calculer valeur numérique (modulo 10000 pour limiter à 4 chiffres)
|
||||
math(EXPR BUILD_NUM "0x${BUILD_HEX} % 10000")
|
||||
|
||||
# 4. Assembler version complète
|
||||
set(MODULE_VERSION "${BASE_VERSION}.${PATCH_COUNT}.${BUILD_NUM}")
|
||||
|
||||
message(STATUS "Module version: ${MODULE_VERSION}")
|
||||
message(STATUS " BASE: ${BASE_VERSION} (from VERSION file)")
|
||||
message(STATUS " PATCH: ${PATCH_COUNT} (git commits)")
|
||||
message(STATUS " BUILD: ${BUILD_NUM} (sources hash)")
|
||||
|
||||
# 5. Définir macro C++ pour accès au runtime
|
||||
add_definitions(-DMODULE_VERSION="${MODULE_VERSION}")
|
||||
|
||||
# ============================================================================
|
||||
# MODULE BUILD
|
||||
# ============================================================================
|
||||
|
||||
# Inclure sources
|
||||
file(GLOB_RECURSE TANK_SOURCES src/*.cpp)
|
||||
|
||||
# Créer bibliothèque partagée (.so)
|
||||
add_library(tank-module SHARED ${TANK_SOURCES})
|
||||
|
||||
# Configuration compilation
|
||||
target_include_directories(tank-module PRIVATE include)
|
||||
target_compile_features(tank-module PRIVATE cxx_std_20)
|
||||
|
||||
# Sortie
|
||||
set_target_properties(tank-module PROPERTIES
|
||||
OUTPUT_NAME "tank"
|
||||
PREFIX ""
|
||||
SUFFIX ".so"
|
||||
)
|
||||
|
||||
message(STATUS "Building TankModule v${MODULE_VERSION}")
|
||||
```
|
||||
|
||||
### Utilisation dans le Code C++
|
||||
|
||||
```cpp
|
||||
// src/TankModule.h
|
||||
#ifndef MODULE_VERSION
|
||||
#define MODULE_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
class TankModule : public IModule, public IModuleSave {
|
||||
public:
|
||||
std::string getVersion() const override {
|
||||
return MODULE_VERSION; // Défini par CMake
|
||||
}
|
||||
|
||||
nlohmann::json serialize() override {
|
||||
json data;
|
||||
data["module_name"] = "tank";
|
||||
data["module_version"] = getVersion(); // "0.1.15.3847"
|
||||
// ... reste de la sérialisation
|
||||
return data;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Affichage au Runtime
|
||||
|
||||
```cpp
|
||||
void TankModule::initialize() {
|
||||
LOG_INFO("TankModule v{} initializing...", getVersion());
|
||||
}
|
||||
```
|
||||
|
||||
Sortie console :
|
||||
```
|
||||
[INFO] TankModule v0.1.15.3847 initializing...
|
||||
```
|
||||
|
||||
## Workflow de Développement
|
||||
|
||||
### 1. Création Nouveau Module
|
||||
|
||||
```bash
|
||||
# Créer structure module
|
||||
mkdir -p modules/new_module/src
|
||||
cd modules/new_module
|
||||
|
||||
# Créer fichier VERSION
|
||||
echo "0.1" > VERSION
|
||||
|
||||
# Créer CMakeLists.txt avec système versioning
|
||||
# (copier template ci-dessus)
|
||||
```
|
||||
|
||||
**Première compilation** :
|
||||
```bash
|
||||
cmake . && make
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
-- Module version: 0.1.0.0
|
||||
-- BASE: 0.1 (from VERSION file)
|
||||
-- PATCH: 0 (git commits)
|
||||
-- BUILD: 0 (sources hash)
|
||||
-- Building NewModule v0.1.0.0
|
||||
```
|
||||
|
||||
### 2. Développement Itératif
|
||||
|
||||
**Scénario 1 : Modifier code sans commit**
|
||||
|
||||
```bash
|
||||
# Éditer src/NewModule.cpp
|
||||
nano src/NewModule.cpp
|
||||
|
||||
# Rebuild
|
||||
make
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
-- Module version: 0.1.0.4582
|
||||
-- PATCH: 0 (aucun commit)
|
||||
-- BUILD: 4582 (hash sources a changé)
|
||||
```
|
||||
|
||||
**Scénario 2 : Commit modifications**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add feature X"
|
||||
|
||||
# Rebuild
|
||||
make
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
-- Module version: 0.1.1.4582
|
||||
-- PATCH: 1 (nouveau commit)
|
||||
-- BUILD: 4582 (sources identiques au commit)
|
||||
```
|
||||
|
||||
**Scénario 3 : Rebuild sans modification**
|
||||
|
||||
```bash
|
||||
# Rebuild immédiat
|
||||
make
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
-- Module version: 0.1.1.4582
|
||||
(version identique, aucun changement)
|
||||
```
|
||||
|
||||
### 3. Release Mineure (Nouvelle Feature)
|
||||
|
||||
```bash
|
||||
# Bump MINOR version
|
||||
echo "0.2" > VERSION
|
||||
|
||||
# Commit
|
||||
git add VERSION
|
||||
git commit -m "Bump version to 0.2 (new feature: advanced tactics)"
|
||||
|
||||
# Rebuild
|
||||
cmake . && make
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
-- Module version: 0.2.2.5123
|
||||
-- BASE: 0.2 (nouvelle version)
|
||||
-- PATCH: 2 (commits depuis début projet)
|
||||
-- BUILD: 5123 (hash actuel)
|
||||
```
|
||||
|
||||
### 4. Release Majeure (Breaking Change)
|
||||
|
||||
```bash
|
||||
# Bump MAJOR version
|
||||
echo "1.0" > VERSION
|
||||
|
||||
git add VERSION
|
||||
git commit -m "Release 1.0.0 - Stable API"
|
||||
|
||||
cmake . && make
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
-- Module version: 1.0.3.5123
|
||||
-- BASE: 1.0 (version stable)
|
||||
-- PATCH: 3 (total commits)
|
||||
-- BUILD: 5123
|
||||
```
|
||||
|
||||
## Comparaison de Versions
|
||||
|
||||
### Dans le Système de Save
|
||||
|
||||
Lors du chargement d'une save, comparaison des versions :
|
||||
|
||||
```cpp
|
||||
bool SaveSystem::loadModule(const std::string& moduleName) {
|
||||
auto module = moduleSystem->getModule(moduleName);
|
||||
|
||||
json moduleData = readJsonFile(modulePath);
|
||||
std::string savedVersion = moduleData["module_version"];
|
||||
std::string currentVersion = module->getVersion();
|
||||
|
||||
if (savedVersion != currentVersion) {
|
||||
LOG_WARN("Module {} version mismatch:", moduleName);
|
||||
LOG_WARN(" Save version: {}", savedVersion);
|
||||
LOG_WARN(" Current version: {}", currentVersion);
|
||||
|
||||
// Analyser différences
|
||||
VersionInfo saved = parseVersion(savedVersion);
|
||||
VersionInfo current = parseVersion(currentVersion);
|
||||
|
||||
if (saved.major != current.major) {
|
||||
LOG_ERROR(" MAJOR version mismatch - likely incompatible!");
|
||||
return false; // Incompatibilité critique
|
||||
}
|
||||
else if (saved.minor != current.minor) {
|
||||
LOG_WARN(" MINOR version mismatch - some features may differ");
|
||||
// Continuer avec warning
|
||||
}
|
||||
else if (saved.patch != current.patch) {
|
||||
LOG_INFO(" PATCH version mismatch - minor changes only");
|
||||
// Sûr, juste bugfixes
|
||||
}
|
||||
else {
|
||||
LOG_DEBUG(" BUILD version mismatch - dev builds differ");
|
||||
// Totalement sûr
|
||||
}
|
||||
}
|
||||
|
||||
return module->deserialize(moduleData);
|
||||
}
|
||||
```
|
||||
|
||||
### Structure VersionInfo
|
||||
|
||||
```cpp
|
||||
struct VersionInfo {
|
||||
int major;
|
||||
int minor;
|
||||
int patch;
|
||||
int build;
|
||||
|
||||
static VersionInfo parse(const std::string& versionStr) {
|
||||
VersionInfo info;
|
||||
std::sscanf(versionStr.c_str(), "%d.%d.%d.%d",
|
||||
&info.major, &info.minor, &info.patch, &info.build);
|
||||
return info;
|
||||
}
|
||||
|
||||
bool isCompatibleWith(const VersionInfo& other) const {
|
||||
// Compatible si MAJOR identique
|
||||
return major == other.major;
|
||||
}
|
||||
|
||||
bool hasBreakingChanges(const VersionInfo& other) const {
|
||||
return major != other.major;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Gestion Multi-Modules
|
||||
|
||||
### Script CMake Global
|
||||
|
||||
Pour centraliser la logique de versioning, créer un fichier CMake réutilisable :
|
||||
|
||||
```cmake
|
||||
# cmake/ModuleVersioning.cmake
|
||||
|
||||
function(setup_module_versioning)
|
||||
# Lire VERSION
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION")
|
||||
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" BASE_VERSION)
|
||||
string(STRIP "${BASE_VERSION}" BASE_VERSION)
|
||||
else()
|
||||
set(BASE_VERSION "0.1")
|
||||
endif()
|
||||
|
||||
# Calculer PATCH
|
||||
execute_process(
|
||||
COMMAND git rev-list --count HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE PATCH_COUNT
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
RESULT_VARIABLE GIT_RESULT
|
||||
)
|
||||
if(NOT GIT_RESULT EQUAL 0)
|
||||
set(PATCH_COUNT "0")
|
||||
endif()
|
||||
|
||||
# Calculer BUILD
|
||||
file(GLOB_RECURSE MODULE_SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include/*.h"
|
||||
)
|
||||
set(SOURCES_CONTENT "")
|
||||
foreach(SOURCE_FILE ${MODULE_SOURCES})
|
||||
file(READ ${SOURCE_FILE} FILE_CONTENT)
|
||||
string(APPEND SOURCES_CONTENT ${FILE_CONTENT})
|
||||
endforeach()
|
||||
string(MD5 SOURCES_HASH "${SOURCES_CONTENT}")
|
||||
string(SUBSTRING ${SOURCES_HASH} 0 4 BUILD_HEX)
|
||||
math(EXPR BUILD_NUM "0x${BUILD_HEX} % 10000")
|
||||
|
||||
# Assembler version
|
||||
set(MODULE_VERSION "${BASE_VERSION}.${PATCH_COUNT}.${BUILD_NUM}" PARENT_SCOPE)
|
||||
|
||||
# Log
|
||||
message(STATUS "Module version: ${BASE_VERSION}.${PATCH_COUNT}.${BUILD_NUM}")
|
||||
endfunction()
|
||||
```
|
||||
|
||||
### Utilisation dans Module
|
||||
|
||||
```cmake
|
||||
# modules/tank/CMakeLists.txt
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(TankModule)
|
||||
|
||||
# Importer fonction versioning
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/ModuleVersioning.cmake)
|
||||
|
||||
# Setup version automatique
|
||||
setup_module_versioning()
|
||||
|
||||
# Définir macro
|
||||
add_definitions(-DMODULE_VERSION="${MODULE_VERSION}")
|
||||
|
||||
# ... reste du CMakeLists
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Quand Bumper MAJOR ?
|
||||
|
||||
**Situations justifiant MAJOR bump** :
|
||||
- Changement format save incompatible
|
||||
- Refonte API du module (signatures fonctions changent)
|
||||
- Suppression de fonctionnalités existantes
|
||||
- Changement architecture fondamentale
|
||||
|
||||
**Exemple** :
|
||||
```bash
|
||||
# Avant : TankModule v0.8.x utilise grille 10x10
|
||||
# Après : TankModule v1.0.0 utilise grille 16x16 (incompatible)
|
||||
echo "1.0" > VERSION
|
||||
```
|
||||
|
||||
### 2. Quand Bumper MINOR ?
|
||||
|
||||
**Situations justifiant MINOR bump** :
|
||||
- Ajout nouveau type de tank/composant
|
||||
- Nouvelle fonctionnalité (mode formation, tactiques AI)
|
||||
- Amélioration significative sans breaking change
|
||||
|
||||
**Exemple** :
|
||||
```bash
|
||||
# Ajout système de camouflage → 0.1 → 0.2
|
||||
echo "0.2" > VERSION
|
||||
```
|
||||
|
||||
### 3. PATCH et BUILD sont Automatiques
|
||||
|
||||
**Ne jamais modifier manuellement** :
|
||||
- PATCH suit automatiquement les commits
|
||||
- BUILD suit automatiquement les modifications code
|
||||
|
||||
### 4. Versioning pour Tests
|
||||
|
||||
En environnement de test, fixer une version stable :
|
||||
|
||||
```cmake
|
||||
if(DEFINED ENV{CI_BUILD})
|
||||
# Build CI : version fixe pour reproductibilité
|
||||
set(MODULE_VERSION "0.0.0.0")
|
||||
else()
|
||||
# Build normal : versioning automatique
|
||||
setup_module_versioning()
|
||||
endif()
|
||||
```
|
||||
|
||||
### 5. Affichage Version au Démarrage
|
||||
|
||||
```cpp
|
||||
void logModuleVersions() {
|
||||
LOG_INFO("=== Module Versions ===");
|
||||
for (auto& module : moduleSystem->getAllModules()) {
|
||||
LOG_INFO(" {}: v{}", module->getName(), module->getVersion());
|
||||
}
|
||||
LOG_INFO("=======================");
|
||||
}
|
||||
```
|
||||
|
||||
Sortie :
|
||||
```
|
||||
[INFO] === Module Versions ===
|
||||
[INFO] tank: v0.1.15.3847
|
||||
[INFO] economy: v0.2.8.1203
|
||||
[INFO] factory: v0.1.20.4512
|
||||
[INFO] transport: v0.1.5.982
|
||||
[INFO] =======================
|
||||
```
|
||||
|
||||
## Debugging avec Versions
|
||||
|
||||
### Reproduire un Bug
|
||||
|
||||
Si un bug est reporté avec une save :
|
||||
|
||||
1. **Lire version du module dans la save** :
|
||||
```bash
|
||||
cat saves/abc123/server_1/tank.json | grep module_version
|
||||
# "module_version": "0.1.15.3847"
|
||||
```
|
||||
|
||||
2. **Retrouver commit correspondant** :
|
||||
```bash
|
||||
# PATCH = 15 commits
|
||||
git log --oneline | head -n 15 | tail -n 1
|
||||
```
|
||||
|
||||
3. **Vérifier hash sources** :
|
||||
```bash
|
||||
# BUILD = 3847 correspond à un certain hash
|
||||
# Comparer avec hashes de commits proches
|
||||
```
|
||||
|
||||
4. **Checkout version exacte pour debugging** :
|
||||
```bash
|
||||
git checkout <commit_hash>
|
||||
cmake . && make
|
||||
# Version devrait matcher ou être proche
|
||||
```
|
||||
|
||||
### Logs de Version
|
||||
|
||||
```cpp
|
||||
void TankModule::initialize() {
|
||||
LOG_INFO("TankModule v{} initializing", getVersion());
|
||||
LOG_DEBUG(" Compiled: {} {}", __DATE__, __TIME__);
|
||||
LOG_DEBUG(" Compiler: {}", __VERSION__);
|
||||
}
|
||||
```
|
||||
|
||||
## Alternatives et Limitations
|
||||
|
||||
### Alternative 1 : Semantic Versioning Manuel
|
||||
|
||||
**Avantage** : Contrôle total sur versions
|
||||
**Inconvénient** : Maintenance manuelle, oublis fréquents
|
||||
|
||||
### Alternative 2 : Git Tags Only
|
||||
|
||||
```cmake
|
||||
execute_process(
|
||||
COMMAND git describe --tags --always
|
||||
OUTPUT_VARIABLE MODULE_VERSION
|
||||
)
|
||||
```
|
||||
|
||||
**Avantage** : Simple, suit git
|
||||
**Inconvénient** : Nécessite tags, pas de granularité build
|
||||
|
||||
### Limitation Actuelle : Hash Non-Déterministe
|
||||
|
||||
Le hash des sources peut varier selon :
|
||||
- Ordre de lecture des fichiers (filesystem dépendant)
|
||||
- Encodage des fichiers
|
||||
- Whitespace differences
|
||||
|
||||
**Solution future** : Hash déterministe avec tri des fichiers et normalisation.
|
||||
|
||||
## Migration depuis Système Existant
|
||||
|
||||
Si des modules existants utilisent un autre système :
|
||||
|
||||
```cmake
|
||||
# Détecter ancien système
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/version.txt")
|
||||
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" OLD_VERSION)
|
||||
message(WARNING "Found old version.txt, migrating to VERSION file")
|
||||
|
||||
# Extraire MAJOR.MINOR
|
||||
string(REGEX MATCH "^[0-9]+\\.[0-9]+" BASE_VERSION ${OLD_VERSION})
|
||||
file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" "${BASE_VERSION}")
|
||||
|
||||
message(STATUS "Migration complete: ${OLD_VERSION} -> ${BASE_VERSION}.x.x")
|
||||
endif()
|
||||
```
|
||||
|
||||
## Références Croisées
|
||||
|
||||
- `systeme-sauvegarde.md` : Utilisation des versions dans le système de save
|
||||
- `architecture-modulaire.md` : Interface IModule, contraintes modules
|
||||
- `claude-code-integration.md` : Optimisation builds pour développement AI
|
||||
|
||||
## Exemples Complets
|
||||
|
||||
### Module Complet avec Versioning
|
||||
|
||||
Voir structure complète dans :
|
||||
```
|
||||
modules/tank/
|
||||
├── VERSION # "0.1"
|
||||
├── CMakeLists.txt # Setup versioning + build
|
||||
├── src/
|
||||
│ ├── TankModule.cpp # Utilise MODULE_VERSION
|
||||
│ └── Tank.cpp
|
||||
└── include/
|
||||
└── TankModule.h # Déclare getVersion()
|
||||
```
|
||||
|
||||
### Test Versioning
|
||||
|
||||
```cpp
|
||||
#ifdef TESTING
|
||||
#include <cassert>
|
||||
|
||||
void testVersioning() {
|
||||
TankModule module;
|
||||
|
||||
std::string version = module.getVersion();
|
||||
assert(!version.empty());
|
||||
assert(version != "unknown");
|
||||
|
||||
// Parse version
|
||||
VersionInfo info = VersionInfo::parse(version);
|
||||
assert(info.major >= 0);
|
||||
assert(info.minor >= 0);
|
||||
assert(info.patch >= 0);
|
||||
assert(info.build >= 0);
|
||||
|
||||
std::cout << "Versioning test passed: " << version << std::endl;
|
||||
}
|
||||
#endif
|
||||
```
|
||||
1414
docs/ai-framework.md
Normal file
1414
docs/ai-framework.md
Normal file
File diff suppressed because it is too large
Load Diff
752
docs/calcul-menace.md
Normal file
752
docs/calcul-menace.md
Normal file
@ -0,0 +1,752 @@
|
||||
# Calcul de Menace Contextuelle
|
||||
|
||||
## Vue d'Ensemble
|
||||
|
||||
Le système de calcul de menace évalue la **dangerosité qu'une entité représente pour une autre** en analysant non seulement les forces brutes, mais aussi les capacités défensives, la qualité des équipements, et la production industrielle.
|
||||
|
||||
### Principes Fondamentaux
|
||||
|
||||
- **Contextuel** : Menace calculée selon capacités **des deux parties**
|
||||
- **Asymétrique** : A menace B ≠ B menace A
|
||||
- **Sword & Shield** : Qualité défenses peut annuler supériorité numérique
|
||||
- **Production** : Capacité industrielle = menace future
|
||||
- **Multi-domaines** : Terre, air, mer évalués séparément puis agrégés
|
||||
|
||||
## Échelle de Menace
|
||||
|
||||
**Valeurs** : `0` à `2 000 000+` (pas de maximum strict, type `int32_t`)
|
||||
|
||||
**Ordres de grandeur** :
|
||||
- `0` : Aucune menace
|
||||
- `100 - 1 000` : PMC petit
|
||||
- `5 000 - 50 000` : Company moyenne
|
||||
- `100 000` : État petit
|
||||
- `500 000` : État moyen (France, UK, Allemagne)
|
||||
- `900 000` : Superpower (USA, Chine, Russie)
|
||||
- `1 000 000+` : Menace existentielle
|
||||
|
||||
## Architecture Calcul
|
||||
|
||||
### Formule Globale
|
||||
|
||||
```cpp
|
||||
int calculateThreat(Entity attacker, Entity defender) {
|
||||
auto params = attacker.getThreatParams();
|
||||
|
||||
// Composantes
|
||||
int current_military = evaluateCurrentForces(attacker, defender);
|
||||
int production_capacity = evaluateProduction(attacker, defender, params.projection_months);
|
||||
|
||||
// Pondération configurable par entité
|
||||
return current_military * params.current_weight +
|
||||
production_capacity * params.production_weight;
|
||||
}
|
||||
|
||||
struct ThreatCalculationParams {
|
||||
int projection_months = 12; // Horizon projection (défaut 12 mois)
|
||||
float current_weight = 0.6f; // Poids forces actuelles (60%)
|
||||
float production_weight = 0.4f; // Poids production (40%)
|
||||
};
|
||||
```
|
||||
|
||||
**Personnalisation par entité** :
|
||||
- Company **agressive** : `current: 0.8, production: 0.2` (focus court-terme)
|
||||
- State **prudent** : `current: 0.4, production: 0.6` (focus long-terme)
|
||||
- PMC **réactif** : `current: 0.9, production: 0.1` (menace immédiate prioritaire)
|
||||
|
||||
## Évaluation Forces Actuelles
|
||||
|
||||
### Principe : Sword & Shield
|
||||
|
||||
Pour chaque domaine (terre, air, mer), on évalue :
|
||||
1. **Sword** (attaquant) : Capacités offensives
|
||||
2. **Shield** (défenseur) : Capacités défensives
|
||||
3. **Effectiveness** : Dans quelle mesure le shield arrête le sword
|
||||
|
||||
```cpp
|
||||
int evaluateCurrentForces(Entity attacker, Entity defender) {
|
||||
int land_threat = evaluateLandThreat(attacker, defender);
|
||||
int air_threat = evaluateAirThreat(attacker, defender);
|
||||
int naval_threat = evaluateNavalThreat(attacker, defender);
|
||||
|
||||
return land_threat + air_threat + naval_threat;
|
||||
}
|
||||
```
|
||||
|
||||
### Évaluation Domaine Terrestre
|
||||
|
||||
#### Inventaire Forces
|
||||
|
||||
**Attaquant** :
|
||||
```cpp
|
||||
struct LandForces {
|
||||
std::vector<Tank> tanks;
|
||||
std::vector<IFV> ifvs;
|
||||
std::vector<APC> apcs;
|
||||
std::vector<Artillery> artillery;
|
||||
std::vector<Infantry> infantry;
|
||||
};
|
||||
```
|
||||
|
||||
**Défenseur** :
|
||||
```cpp
|
||||
struct LandDefenses {
|
||||
std::vector<ATSystem> anti_tank; // Systèmes anti-char
|
||||
std::vector<Tank> counter_tanks; // Tanks défensifs
|
||||
std::vector<Infantry> anti_tank_inf; // Infanterie AT
|
||||
std::vector<Fortification> fortifications;
|
||||
};
|
||||
```
|
||||
|
||||
#### Couples Équipement ↔ Contre-mesures
|
||||
|
||||
**Principe** : Chaque type d'équipement offensif a des contre-mesures spécifiques.
|
||||
|
||||
```cpp
|
||||
struct EquipmentCounters {
|
||||
std::map<EquipmentType, std::vector<CounterType>> counters = {
|
||||
{TANK, {AT_MISSILE, AT_GUN, COUNTER_TANK, AT_INFANTRY}},
|
||||
{IFV, {AT_MISSILE, AT_GUN, AUTOCANNON}},
|
||||
{APC, {AUTOCANNON, MACHINE_GUN, RPG}},
|
||||
{INFANTRY, {MACHINE_GUN, ARTILLERY, AUTOCANNON}},
|
||||
{ARTILLERY, {COUNTER_BATTERY, AIR_STRIKE}}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### Évaluation Sword vs Shield
|
||||
|
||||
**Étape 1 : Match-making**
|
||||
|
||||
Pour chaque équipement offensif, identifier contre-mesures défensives applicables :
|
||||
|
||||
```cpp
|
||||
std::vector<Defense> findApplicableDefenses(
|
||||
Equipment sword,
|
||||
std::vector<Defense> available_defenses
|
||||
) {
|
||||
std::vector<Defense> applicable;
|
||||
|
||||
auto counters = EquipmentCounters::counters[sword.type];
|
||||
|
||||
for (auto& defense : available_defenses) {
|
||||
// Vérifier si défense peut contrer cet équipement
|
||||
if (std::find(counters.begin(), counters.end(), defense.type) != counters.end()) {
|
||||
// Vérifier compatibilité génération/qualité
|
||||
if (canCounter(defense, sword)) {
|
||||
applicable.push_back(defense);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return applicable;
|
||||
}
|
||||
|
||||
bool canCounter(Defense defense, Equipment sword) {
|
||||
// Exemple : AT missile Gen2 ne peut pas contrer tank Gen4 avec armure avancée
|
||||
if (defense.generation < sword.generation - 1) {
|
||||
return false; // Trop ancien
|
||||
}
|
||||
|
||||
// Vérifier spécificités
|
||||
if (sword.has_reactive_armor && defense.type == AT_MISSILE_OLD) {
|
||||
return false; // ERA bloque missiles anciens
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Étape 2 : Calcul Defensive Effectiveness**
|
||||
|
||||
```cpp
|
||||
float calculateDefensiveEffectiveness(
|
||||
std::vector<Equipment> swords,
|
||||
std::vector<Defense> shields
|
||||
) {
|
||||
float total_sword_value = 0;
|
||||
float neutralized_value = 0;
|
||||
|
||||
for (auto& sword : swords) {
|
||||
float sword_value = sword.quantity * sword.quality;
|
||||
total_sword_value += sword_value;
|
||||
|
||||
// Trouver défenses applicables
|
||||
auto applicable_shields = findApplicableDefenses(sword, shields);
|
||||
|
||||
// Calculer valeur défensive totale contre ce sword
|
||||
float shield_value = 0;
|
||||
for (auto& shield : applicable_shields) {
|
||||
float shield_effectiveness = calculateShieldEffectiveness(shield, sword);
|
||||
shield_value += shield.quantity * shield.quality * shield_effectiveness;
|
||||
}
|
||||
|
||||
// Neutralisation proportionnelle
|
||||
float neutralization = min(1.0f, shield_value / sword_value);
|
||||
neutralized_value += sword_value * neutralization;
|
||||
}
|
||||
|
||||
return total_sword_value > 0 ? (neutralized_value / total_sword_value) : 0.0f;
|
||||
}
|
||||
```
|
||||
|
||||
**Étape 3 : Score Final**
|
||||
|
||||
```cpp
|
||||
int evaluateLandThreat(Entity attacker, Entity defender) {
|
||||
auto swords = attacker.getLandForces();
|
||||
auto shields = defender.getLandDefenses();
|
||||
|
||||
// Menace brute (sans défenses)
|
||||
int raw_threat = 0;
|
||||
for (auto& sword : swords) {
|
||||
raw_threat += sword.quantity * sword.combat_value;
|
||||
}
|
||||
|
||||
// Réduction par défenses
|
||||
float defensive_effectiveness = calculateDefensiveEffectiveness(swords, shields);
|
||||
|
||||
// Menace finale
|
||||
int final_threat = raw_threat * (1.0f - defensive_effectiveness);
|
||||
|
||||
return final_threat;
|
||||
}
|
||||
```
|
||||
|
||||
### Exemples Concrets Terre
|
||||
|
||||
#### Exemple 1 : Qualité Domine Quantité
|
||||
|
||||
**Attaquant** :
|
||||
- 20 tanks Gen4 (Leopard 2A7)
|
||||
- Armure composite avancée
|
||||
- Génération 4
|
||||
- Combat value : 1000/tank
|
||||
- Menace brute : 20 × 1000 = 20 000
|
||||
|
||||
**Défenseur** :
|
||||
- 100 000 AT missiles Gen2
|
||||
- Anciens, inefficaces contre armure Gen4
|
||||
- `canCounter()` retourne `false`
|
||||
- Défense applicable : 0
|
||||
|
||||
**Résultat** :
|
||||
```
|
||||
defensive_effectiveness = 0 / 20000 = 0%
|
||||
final_threat = 20000 × (1 - 0) = 20 000
|
||||
```
|
||||
→ **Menace élevée** : Défenses obsolètes inefficaces
|
||||
|
||||
#### Exemple 2 : Quantité + Qualité Écrasent
|
||||
|
||||
**Attaquant** :
|
||||
- 1000 tanks Gen2
|
||||
- Armure standard
|
||||
- Combat value : 400/tank
|
||||
- Menace brute : 1000 × 400 = 400 000
|
||||
|
||||
**Défenseur** :
|
||||
- 500 AT missiles Gen4 (Javelin)
|
||||
- Top-attack, très efficaces
|
||||
- Combat value : 800/missile
|
||||
- `canCounter()` retourne `true`
|
||||
- Shield effectiveness : 0.9 (très efficace)
|
||||
- Shield value : 500 × 800 × 0.9 = 360 000
|
||||
|
||||
**Résultat** :
|
||||
```
|
||||
defensive_effectiveness = 360000 / 400000 = 90%
|
||||
final_threat = 400000 × (1 - 0.9) = 40 000
|
||||
```
|
||||
→ **Menace faible** : Défenses qualitatives réduisent massavement
|
||||
|
||||
#### Exemple 3 : Masse Insuffisante
|
||||
|
||||
**Attaquant** :
|
||||
- 1000 tanks Gen2
|
||||
- Combat value : 400/tank
|
||||
- Menace brute : 400 000
|
||||
|
||||
**Défenseur** :
|
||||
- 20 AT missiles Gen4
|
||||
- Très efficaces mais **pas assez nombreux**
|
||||
- Shield value : 20 × 800 × 0.9 = 14 400
|
||||
|
||||
**Résultat** :
|
||||
```
|
||||
defensive_effectiveness = 14400 / 400000 = 3.6%
|
||||
final_threat = 400000 × (1 - 0.036) = 385 440
|
||||
```
|
||||
→ **Menace reste élevée** : Pas assez de défenses pour couvrir la masse
|
||||
|
||||
### Évaluation Domaine Aérien
|
||||
|
||||
#### Spécificités Air
|
||||
|
||||
**Complexité supplémentaire** :
|
||||
- **Qualité prime** : Quelques jets furtifs Gen4 dominent des centaines AA Gen2
|
||||
- **ECM/ECCM** : Guerre électronique critique
|
||||
- **Compatibilité stricte** : AA anti-hélicoptère ne touche pas jets
|
||||
|
||||
```cpp
|
||||
struct AirForces {
|
||||
std::vector<Fighter> fighters;
|
||||
std::vector<Bomber> bombers;
|
||||
std::vector<Helicopter> helicopters;
|
||||
std::vector<Drone> drones;
|
||||
|
||||
// Capacités spéciales
|
||||
bool has_stealth;
|
||||
bool has_ecm; // Electronic Counter-Measures
|
||||
int electronic_warfare_level;
|
||||
};
|
||||
|
||||
struct AirDefenses {
|
||||
std::vector<AAMissile> aa_missiles;
|
||||
std::vector<AAGun> aa_guns;
|
||||
std::vector<Fighter> interceptors;
|
||||
|
||||
// Capacités
|
||||
bool has_eccm; // Electronic Counter-Counter-Measures
|
||||
int radar_quality;
|
||||
std::map<AASystem, TargetCapability> capabilities; // Quoi peut toucher quoi
|
||||
};
|
||||
|
||||
enum TargetCapability {
|
||||
HELICOPTER_ONLY, // Seulement hélicoptères
|
||||
LOW_ALTITUDE, // Avions basse altitude
|
||||
HIGH_ALTITUDE, // Avions haute altitude
|
||||
ALL_AIRCRAFT // Tous types
|
||||
};
|
||||
```
|
||||
|
||||
#### Compatibilité Systèmes
|
||||
|
||||
```cpp
|
||||
bool canEngageAircraft(AASystem aa, Aircraft aircraft) {
|
||||
// Vérifier compatibilité type
|
||||
switch (aa.capability) {
|
||||
case HELICOPTER_ONLY:
|
||||
return aircraft.type == HELICOPTER;
|
||||
|
||||
case LOW_ALTITUDE:
|
||||
return aircraft.altitude < 5000; // mètres
|
||||
|
||||
case HIGH_ALTITUDE:
|
||||
return aircraft.altitude > 5000;
|
||||
|
||||
case ALL_AIRCRAFT:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier guerre électronique
|
||||
if (aircraft.has_ecm && !aa.has_eccm) {
|
||||
// ECM peut brouiller AA sans ECCM
|
||||
float jam_probability = aircraft.ecm_power / (aa.radar_quality + 1);
|
||||
if (randomFloat() < jam_probability) {
|
||||
return false; // Brouillé
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier furtivité
|
||||
if (aircraft.has_stealth) {
|
||||
float detection_range = aa.radar_range * (1.0f - aircraft.stealth_rating);
|
||||
if (distance(aa, aircraft) > detection_range) {
|
||||
return false; // Pas détecté
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### Exemple Air : Furtivité Domine
|
||||
|
||||
**Attaquant** :
|
||||
- 20 jets furtifs Gen4 (F-35)
|
||||
- Stealth rating : 0.9 (réduit détection 90%)
|
||||
- ECM avancé
|
||||
- Combat value : 2000/jet
|
||||
- Menace brute : 40 000
|
||||
|
||||
**Défenseur** :
|
||||
- 100 000 AA missiles Gen2
|
||||
- Radar standard
|
||||
- Pas ECCM
|
||||
- Seulement 5% peuvent détecter/engager les furtifs
|
||||
- Shield value effectif : 100000 × 0.05 × 300 = 1 500 000... mais avec ECM :
|
||||
- Shield value final : 1 500 000 × 0.1 (jam rate) = 150 000
|
||||
|
||||
```
|
||||
defensive_effectiveness = 150000 / 40000 = ... wait, > 1.0 !
|
||||
→ Plafonné à min(1.0, value)
|
||||
defensive_effectiveness = min(1.0, 150000/40000) = 1.0...
|
||||
|
||||
ERREUR dans mon calcul !
|
||||
```
|
||||
|
||||
**Correction** : Le shield value doit être calculé par aircraft, pas globalement.
|
||||
|
||||
```cpp
|
||||
// Pour chaque jet
|
||||
for (auto& jet : jets) {
|
||||
float jet_value = jet.combat_value;
|
||||
|
||||
// Combien de AA peuvent l'engager ?
|
||||
int applicable_aa = 0;
|
||||
for (auto& aa : aa_systems) {
|
||||
if (canEngageAircraft(aa, jet)) {
|
||||
applicable_aa++;
|
||||
}
|
||||
}
|
||||
|
||||
// Shield value pour ce jet spécifique
|
||||
float shield_value = applicable_aa * aa_combat_value;
|
||||
float neutralization = min(1.0f, shield_value / jet_value);
|
||||
|
||||
threat_reduced += jet_value * neutralization;
|
||||
}
|
||||
```
|
||||
|
||||
**Résultat corrigé** :
|
||||
- 5000 AA peuvent engager (sur 100k)
|
||||
- Pour 1 jet : shield = 5000 × 300 = 1 500 000 vs jet = 2000
|
||||
- Neutralization = 100% par jet
|
||||
- Mais ils peuvent pas tous tirer simultanément !
|
||||
|
||||
**Contrainte simultanéité** :
|
||||
|
||||
```cpp
|
||||
// Limite engagement simultané
|
||||
int max_simultaneous = min(applicable_aa, ENGAGEMENT_LIMIT);
|
||||
// Exemple : Max 100 missiles simultanés par cible
|
||||
|
||||
float shield_value = max_simultaneous * aa_combat_value;
|
||||
```
|
||||
|
||||
Avec limite 100 simultanés :
|
||||
- Shield = 100 × 300 = 30 000 vs jet = 2000
|
||||
- Neutralization = 100% par jet
|
||||
- Mais 20 jets → seulement 2000 engagements simultanés total
|
||||
- Si AA rate coordination : menace reste
|
||||
|
||||
**C'est complexe !** Donc en pratique :
|
||||
|
||||
```cpp
|
||||
float calculateAirDefenseEffectiveness(
|
||||
std::vector<Aircraft> aircraft,
|
||||
std::vector<AASystem> aa_systems
|
||||
) {
|
||||
float total_threat = 0;
|
||||
float neutralized = 0;
|
||||
|
||||
for (auto& ac : aircraft) {
|
||||
total_threat += ac.combat_value;
|
||||
|
||||
// Compter AA applicables
|
||||
int applicable_count = 0;
|
||||
for (auto& aa : aa_systems) {
|
||||
if (canEngageAircraft(aa, ac)) {
|
||||
applicable_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Engagement limité
|
||||
int engaged = min(applicable_count, MAX_SIMULTANEOUS_PER_TARGET);
|
||||
|
||||
// Probabilité kill
|
||||
float kill_probability = engaged / float(ENGAGEMENTS_NEEDED_FOR_KILL);
|
||||
kill_probability = min(1.0f, kill_probability);
|
||||
|
||||
neutralized += ac.combat_value * kill_probability;
|
||||
}
|
||||
|
||||
return total_threat > 0 ? (neutralized / total_threat) : 0.0f;
|
||||
}
|
||||
```
|
||||
|
||||
### Évaluation Domaine Naval
|
||||
|
||||
**Similaire à terrestre/aérien** mais avec spécificités :
|
||||
- **Torpilles vs sonars** (submersibles)
|
||||
- **Anti-ship missiles vs CIWS** (Close-In Weapon Systems)
|
||||
- **Portée extrême** : Naval combat à 100+ km
|
||||
|
||||
Structure identique avec couples spécifiques.
|
||||
|
||||
## Évaluation Production
|
||||
|
||||
### Principe
|
||||
|
||||
La menace ne vient pas seulement des forces actuelles, mais aussi de la **capacité à produire** plus d'équipements.
|
||||
|
||||
```cpp
|
||||
int evaluateProduction(Entity attacker, Entity defender, int projection_months) {
|
||||
int production_threat = 0;
|
||||
|
||||
// Pour chaque type d'équipement
|
||||
for (auto& equipment_type : all_equipment_types) {
|
||||
// Taux production attaquant
|
||||
int attacker_rate = attacker.getProductionRate(equipment_type); // unités/mois
|
||||
|
||||
// Taux production défenses défenseur
|
||||
int defender_rate = defender.getProductionRate(getCounterType(equipment_type));
|
||||
|
||||
// Projection future
|
||||
int attacker_produced = attacker_rate * projection_months;
|
||||
int defender_produced = defender_rate * projection_months;
|
||||
|
||||
// Menace nette production
|
||||
int net_production = attacker_produced - defender_produced;
|
||||
if (net_production > 0) {
|
||||
int equipment_value = getEquipmentValue(equipment_type);
|
||||
production_threat += net_production * equipment_value;
|
||||
}
|
||||
}
|
||||
|
||||
return production_threat;
|
||||
}
|
||||
```
|
||||
|
||||
### Exemple Production
|
||||
|
||||
**État A** :
|
||||
- Production actuelle : 50 tanks/mois
|
||||
- Projection 12 mois : 600 tanks
|
||||
- Combat value : 400/tank
|
||||
- Production threat : 600 × 400 = 240 000
|
||||
|
||||
**État B** (défenseur) :
|
||||
- Production AT : 100 missiles/mois
|
||||
- Projection 12 mois : 1200 missiles
|
||||
- Combat value : 300/missile
|
||||
- Défense production : 1200 × 300 = 360 000
|
||||
|
||||
**Net production threat** :
|
||||
```
|
||||
Attacker gain : 240 000
|
||||
Defender gain : 360 000
|
||||
Net : 240 000 - 360 000 = -120 000 (négatif = défenseur gagne)
|
||||
|
||||
production_threat = max(0, net) = 0
|
||||
```
|
||||
|
||||
→ État B a **meilleure production**, donc menace production de A est nulle.
|
||||
|
||||
**Si inverse** :
|
||||
|
||||
**État A** :
|
||||
- Production : 200 tanks/mois → 2400 tanks/an
|
||||
- Threat : 2400 × 400 = 960 000
|
||||
|
||||
**État B** :
|
||||
- Production AT : 50 missiles/mois → 600 missiles/an
|
||||
- Défense : 600 × 300 = 180 000
|
||||
|
||||
**Net** : 960 000 - 180 000 = 780 000
|
||||
|
||||
→ État A a **production écrasante**, menace industrielle massive.
|
||||
|
||||
## Agrégation Finale
|
||||
|
||||
### Formule Complète
|
||||
|
||||
```cpp
|
||||
int calculateThreat(Entity attacker, Entity defender) {
|
||||
auto params = attacker.getThreatParams();
|
||||
|
||||
// Forces actuelles (sword & shield par domaine)
|
||||
int land_threat = evaluateLandThreat(attacker, defender);
|
||||
int air_threat = evaluateAirThreat(attacker, defender);
|
||||
int naval_threat = evaluateNavalThreat(attacker, defender);
|
||||
|
||||
int current_military = land_threat + air_threat + naval_threat;
|
||||
|
||||
// Production future
|
||||
int production_threat = evaluateProduction(
|
||||
attacker,
|
||||
defender,
|
||||
params.projection_months
|
||||
);
|
||||
|
||||
// Pondération finale
|
||||
int total_threat = current_military * params.current_weight +
|
||||
production_threat * params.production_weight;
|
||||
|
||||
return total_threat;
|
||||
}
|
||||
```
|
||||
|
||||
### Exemples Complets
|
||||
|
||||
#### Superpower vs État Moyen
|
||||
|
||||
**USA vs France**
|
||||
|
||||
**USA forces** :
|
||||
- Tanks : 8000 (Gen3-4) → 3 200 000
|
||||
- Aircraft : 2000 (Gen4, furtifs) → 4 000 000
|
||||
- Naval : 500 ships → 2 500 000
|
||||
- **Total brut** : 9 700 000
|
||||
|
||||
**France défenses** :
|
||||
- AT systems : 3000 (Gen3) → Shield 50% tanks
|
||||
- AA systems : 1500 (Gen3) → Shield 30% aircraft
|
||||
- Naval defenses : 200 → Shield 40% naval
|
||||
|
||||
**Après sword & shield** :
|
||||
- Land : 3 200 000 × 0.5 = 1 600 000
|
||||
- Air : 4 000 000 × 0.7 = 2 800 000
|
||||
- Naval : 2 500 000 × 0.6 = 1 500 000
|
||||
- **Current threat** : 5 900 000
|
||||
|
||||
**Production (12 mois)** :
|
||||
- USA : +600 tanks, +100 aircraft → 360 000
|
||||
- France : +100 tanks, +50 aircraft → 70 000
|
||||
- **Net production** : 290 000
|
||||
|
||||
**Total (60% current, 40% prod)** :
|
||||
```
|
||||
5 900 000 × 0.6 + 290 000 × 0.4 = 3 656 000
|
||||
```
|
||||
|
||||
→ **Menace colossale**, mais France peut tenir avec alliances
|
||||
|
||||
#### PMC vs Company
|
||||
|
||||
**PMC_Alpha vs RheinmetallCorp**
|
||||
|
||||
**PMC forces** :
|
||||
- Infantry : 500 (léger armement)
|
||||
- Vehicles : 20 APCs
|
||||
- **Total** : 50 000
|
||||
|
||||
**Rheinmetall défenses** :
|
||||
- Security : 100 guards
|
||||
- Fortifications : minimal
|
||||
- **Shield** : 5%
|
||||
|
||||
**Threat** : 50 000 × 0.95 = 47 500
|
||||
|
||||
→ PMC peut menacer installations Company, mais pas capacités production
|
||||
|
||||
## Cas Spéciaux
|
||||
|
||||
### Menace Économique Pure
|
||||
|
||||
Pour Companies sans capacités militaires :
|
||||
|
||||
```cpp
|
||||
int calculateEconomicThreat(Company attacker, Company defender) {
|
||||
// Part de marché
|
||||
float market_dominance = attacker.market_share / (defender.market_share + 0.01f);
|
||||
|
||||
// Capacité production
|
||||
float production_ratio = attacker.production_capacity / defender.production_capacity;
|
||||
|
||||
// Qualité produits
|
||||
float quality_advantage = attacker.avg_product_quality / defender.avg_product_quality;
|
||||
|
||||
// Menace économique
|
||||
int economic_threat = (market_dominance * 100000) +
|
||||
(production_ratio * 80000) +
|
||||
(quality_advantage * 50000);
|
||||
|
||||
return economic_threat;
|
||||
}
|
||||
```
|
||||
|
||||
### Menace Géographique
|
||||
|
||||
Distance géographique module menace :
|
||||
|
||||
```cpp
|
||||
float getGeographicModifier(Entity attacker, Entity defender) {
|
||||
float distance_km = calculateDistance(attacker.position, defender.position);
|
||||
|
||||
// Projection power diminue avec distance
|
||||
if (distance_km < 500) {
|
||||
return 1.0f; // Menace pleine
|
||||
}
|
||||
else if (distance_km < 2000) {
|
||||
return 0.7f; // 30% réduction
|
||||
}
|
||||
else if (distance_km < 5000) {
|
||||
return 0.4f; // 60% réduction
|
||||
}
|
||||
else {
|
||||
return 0.1f; // 90% réduction (trop loin)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance et Optimisation
|
||||
|
||||
### Cache Threat
|
||||
|
||||
```cpp
|
||||
class ThreatCache {
|
||||
private:
|
||||
struct CachedThreat {
|
||||
int value;
|
||||
int timestamp;
|
||||
int ttl = 3600; // 1 heure
|
||||
};
|
||||
|
||||
std::map<std::pair<EntityId, EntityId>, CachedThreat> cache;
|
||||
|
||||
public:
|
||||
int getThreat(EntityId attacker, EntityId defender) {
|
||||
auto key = std::make_pair(attacker, defender);
|
||||
|
||||
if (cache.contains(key)) {
|
||||
auto& cached = cache[key];
|
||||
if (getCurrentTime() - cached.timestamp < cached.ttl) {
|
||||
return cached.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculer
|
||||
int threat = calculateThreat(attacker, defender);
|
||||
cache[key] = {threat, getCurrentTime()};
|
||||
return threat;
|
||||
}
|
||||
|
||||
void invalidate(EntityId entity) {
|
||||
// Invalider tous les caches impliquant cette entité
|
||||
for (auto it = cache.begin(); it != cache.end();) {
|
||||
if (it->first.first == entity || it->first.second == entity) {
|
||||
it = cache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Invalidation Cache
|
||||
|
||||
Événements invalidant cache :
|
||||
- Production nouvelle unité
|
||||
- Perte unité au combat
|
||||
- Nouvelle recherche technologique
|
||||
- Changement doctrine militaire
|
||||
|
||||
### Calcul Lazy
|
||||
|
||||
```cpp
|
||||
// Ne calculer que si demandé par IA
|
||||
int getThreatOnDemand(EntityId attacker, EntityId defender) {
|
||||
return threatCache.getThreat(attacker, defender);
|
||||
}
|
||||
|
||||
// Pas de recalcul automatique chaque tick
|
||||
```
|
||||
|
||||
## Références Croisées
|
||||
|
||||
- `systeme-diplomatique.md` : Utilisation menace dans relations diplomatiques
|
||||
- `ai-framework.md` : Menace influence décisions IA (achats, alliances)
|
||||
- `systeme-militaire.md` : Valeurs combat équipements, générations
|
||||
- `economie-logistique.md` : Production rates, capacités industrielles
|
||||
1090
docs/systeme-diplomatique.md
Normal file
1090
docs/systeme-diplomatique.md
Normal file
File diff suppressed because it is too large
Load Diff
715
docs/systeme-sauvegarde.md
Normal file
715
docs/systeme-sauvegarde.md
Normal file
@ -0,0 +1,715 @@
|
||||
# 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;
|
||||
}
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user