Setup initial: Mobile Command project with GroveEngine
- Architecture basée sur GroveEngine (hot-reload C++17) - Structure du projet (src, config, external) - CMakeLists.txt avec support MinGW - GameModule de base (hot-reloadable) - Main loop 10Hz avec file watcher - Configuration via JSON - Documentation README et CLAUDE.md ✅ Build fonctionnel ✅ Hot-reload validé 🚧 Prochaine étape: Prototype gameplay 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d10b285a56
commit
3dfaffc75a
66
.gitignore
vendored
66
.gitignore
vendored
@ -1,50 +1,40 @@
|
|||||||
# Dependencies
|
# Build directories
|
||||||
node_modules/
|
|
||||||
vendor/
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
dist/
|
|
||||||
build/
|
build/
|
||||||
out/
|
build-*/
|
||||||
|
|
||||||
|
# CMake files
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
Makefile
|
||||||
|
*.cmake
|
||||||
|
!CMakeLists.txt
|
||||||
|
|
||||||
|
# Compiled binaries
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
*.out
|
||||||
*.so
|
*.so
|
||||||
|
*.dll
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
# IDE
|
# IDE files
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.suo
|
*.swp
|
||||||
*.user
|
*.swo
|
||||||
*.userosscache
|
*~
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
desktop.ini
|
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
logs/
|
|
||||||
|
|
||||||
# Environment
|
# Data files
|
||||||
.env
|
data/
|
||||||
.env.local
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
# Temporary files
|
# OS files
|
||||||
tmp/
|
.DS_Store
|
||||||
temp/
|
Thumbs.db
|
||||||
*.tmp
|
external/GroveEngine
|
||||||
*.temp
|
|
||||||
|
|
||||||
# Assets (work files)
|
|
||||||
*.psd
|
|
||||||
*.ai
|
|
||||||
*.blend1
|
|
||||||
*.blend2
|
|
||||||
|
|
||||||
# Game specific
|
|
||||||
saves/
|
|
||||||
cache/
|
|
||||||
screenshots/
|
|
||||||
|
|||||||
133
CLAUDE.md
Normal file
133
CLAUDE.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# Claude - Mobile Command Development
|
||||||
|
|
||||||
|
## Rôle
|
||||||
|
Je suis l'assistant de développement pour le projet Mobile Command.
|
||||||
|
|
||||||
|
## Contexte du Projet
|
||||||
|
|
||||||
|
**Mobile Command** est un jeu de gestion/survie où le joueur commande un train blindé mobile à travers l'Ukraine en guerre (2022-2025).
|
||||||
|
|
||||||
|
### Stack Technique
|
||||||
|
- **Engine**: GroveEngine (C++17 hot-reload module system)
|
||||||
|
- **Build**: CMake 3.20+ avec MinGW/GCC
|
||||||
|
- **Architecture**: Main loop + modules hot-reloadable
|
||||||
|
|
||||||
|
### Structure du Projet
|
||||||
|
```
|
||||||
|
mobilecommand/
|
||||||
|
├── external/GroveEngine/ # Lien symbolique vers ../groveengine
|
||||||
|
├── src/
|
||||||
|
│ ├── main.cpp # Boucle principale (10Hz)
|
||||||
|
│ └── modules/ # Modules hot-reloadable (.dll)
|
||||||
|
│ └── GameModule.* # Module de jeu principal
|
||||||
|
├── config/
|
||||||
|
│ └── game.json # Configuration
|
||||||
|
└── build/ # Dossier de build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow de Développement
|
||||||
|
|
||||||
|
### Build Initial
|
||||||
|
```bash
|
||||||
|
cmake -B build -G "MinGW Makefiles"
|
||||||
|
cmake --build build -j4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot-Reload Workflow
|
||||||
|
1. Le jeu tourne: `./build/mobilecommand.exe`
|
||||||
|
2. Éditer un module: `src/modules/GameModule.cpp`
|
||||||
|
3. Rebuild: `cmake --build build --target modules`
|
||||||
|
4. Le module se recharge automatiquement avec préservation d'état
|
||||||
|
|
||||||
|
### Commandes Utiles
|
||||||
|
```bash
|
||||||
|
# Build complet
|
||||||
|
cmake --build build -j4
|
||||||
|
|
||||||
|
# Build modules seulement (rapide)
|
||||||
|
cmake --build build --target modules
|
||||||
|
|
||||||
|
# Run
|
||||||
|
cd build && ./mobilecommand.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Principes GroveEngine
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
- Héritent de `grove::IModule`
|
||||||
|
- Implémentent: `process()`, `getState()`, `setState()`, `shutdown()`
|
||||||
|
- Chargés dynamiquement (.dll/.so)
|
||||||
|
- Hot-reload < 1ms avec état préservé
|
||||||
|
|
||||||
|
### Communication
|
||||||
|
- `grove::IIO` pour pub/sub entre modules
|
||||||
|
- Messages via `grove::IDataNode` (JSON)
|
||||||
|
- Topics: `module:event` (ex: `game:tick`, `train:damaged`)
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Fichiers JSON dans `config/`
|
||||||
|
- Chargés via `grove::JsonDataNode`
|
||||||
|
- Hot-reload de config supporté
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 - Setup (COMPLÉTÉ)
|
||||||
|
- [x] Structure projet
|
||||||
|
- [x] GroveEngine intégré
|
||||||
|
- [x] Build system
|
||||||
|
- [x] Hot-reload validé
|
||||||
|
|
||||||
|
### Phase 2 - Prototype Gameplay (EN COURS)
|
||||||
|
- [ ] Train basique (3 wagons)
|
||||||
|
- [ ] Système de craft simple
|
||||||
|
- [ ] 1 mission de combat
|
||||||
|
- [ ] 3-5 événements
|
||||||
|
- [ ] Boucle de jeu complète
|
||||||
|
|
||||||
|
### Phase 3 - MVP
|
||||||
|
- [ ] Train complet
|
||||||
|
- [ ] Système d'expéditions
|
||||||
|
- [ ] Campagne Act 1
|
||||||
|
|
||||||
|
## Conventions de Code
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
- Nom: `XyzModule` (PascalCase)
|
||||||
|
- Fichiers: `XyzModule.cpp` + `XyzModule.h` (optionnel)
|
||||||
|
- Exports: `createModule()` et `destroyModule()`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
```cpp
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
spdlog::info("[ModuleName] Message");
|
||||||
|
spdlog::debug("[ModuleName] Debug info");
|
||||||
|
spdlog::error("[ModuleName] Error message");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
```cpp
|
||||||
|
auto config = loadConfig("config/module.json");
|
||||||
|
int value = config->getInt("key", defaultValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectifs du Projet
|
||||||
|
|
||||||
|
### Primaire
|
||||||
|
Créer un jeu de gestion/survie autour d'un train blindé mobile
|
||||||
|
|
||||||
|
### Secondaire
|
||||||
|
Valider GroveEngine en conditions réelles de production
|
||||||
|
|
||||||
|
## Notes Importantes
|
||||||
|
|
||||||
|
- Le projet est en phase prototype
|
||||||
|
- Focus sur la validation des mécaniques de base
|
||||||
|
- Hot-reload est essentiel au workflow
|
||||||
|
- Garder les modules simples et focalisés
|
||||||
|
|
||||||
|
## Ressources
|
||||||
|
|
||||||
|
- **Concept complet**: `../couple-repo/Projects/CONCEPT/mobile_command_v2.md`
|
||||||
|
- **GroveEngine docs**: `external/GroveEngine/README.md`
|
||||||
|
- **Architecture GroveEngine**: `external/GroveEngine/docs/architecture/`
|
||||||
|
|
||||||
77
CMakeLists.txt
Normal file
77
CMakeLists.txt
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MobileCommand VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# Export compile commands for IDE support
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# GroveEngine Integration
|
||||||
|
# ============================================================================
|
||||||
|
set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE)
|
||||||
|
set(GROVE_BUILD_MODULES OFF CACHE BOOL "Disable GroveEngine modules" FORCE)
|
||||||
|
add_subdirectory(external/GroveEngine)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Dependencies
|
||||||
|
# ============================================================================
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Executable
|
||||||
|
# ============================================================================
|
||||||
|
add_executable(mobilecommand
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(mobilecommand PRIVATE
|
||||||
|
GroveEngine::impl
|
||||||
|
spdlog::spdlog
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(mobilecommand PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Hot-Reloadable Modules (.so/.dll) - Game modules
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# GameModule - Core game loop
|
||||||
|
add_library(GameModule SHARED
|
||||||
|
src/modules/GameModule.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(GameModule PRIVATE
|
||||||
|
GroveEngine::impl
|
||||||
|
spdlog::spdlog
|
||||||
|
)
|
||||||
|
set_target_properties(GameModule PROPERTIES
|
||||||
|
PREFIX "lib"
|
||||||
|
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Copy config files to build directory
|
||||||
|
# ============================================================================
|
||||||
|
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/
|
||||||
|
DESTINATION ${CMAKE_BINARY_DIR}/config)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Development targets
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Quick rebuild of modules only (for hot-reload workflow)
|
||||||
|
add_custom_target(modules
|
||||||
|
DEPENDS GameModule
|
||||||
|
COMMENT "Building hot-reloadable modules only"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run MobileCommand
|
||||||
|
add_custom_target(run
|
||||||
|
COMMAND $<TARGET_FILE:mobilecommand>
|
||||||
|
DEPENDS mobilecommand modules
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
COMMENT "Running Mobile Command"
|
||||||
|
)
|
||||||
84
README.md
84
README.md
@ -17,10 +17,68 @@ Le jeu force une évolution : De commandant infantry-heavy (2022) à opérateur
|
|||||||
|
|
||||||
## Stack Technique
|
## Stack Technique
|
||||||
|
|
||||||
- **Engine** : GroveEngine (validation engine = objectif secondaire)
|
- **Engine** : GroveEngine (hot-reload C++17 module system)
|
||||||
- **Plateforme** : PC (Steam)
|
- **Plateforme** : PC (Steam)
|
||||||
- **Rendu** : 2D (sprites ou vector)
|
- **Rendu** : 2D (sprites ou vector)
|
||||||
- **Public** : 16+ (Thèmes de guerre, violence, choix moraux)
|
- **Public** : 16+ (Thèmes de guerre, violence, choix moraux)
|
||||||
|
- **Build** : CMake 3.20+, MinGW/GCC
|
||||||
|
|
||||||
|
## Architecture Projet
|
||||||
|
|
||||||
|
```
|
||||||
|
mobilecommand/
|
||||||
|
├── external/
|
||||||
|
│ └── GroveEngine/ # GroveEngine (symlink vers ../groveengine)
|
||||||
|
├── src/
|
||||||
|
│ ├── main.cpp # Main application loop
|
||||||
|
│ └── modules/
|
||||||
|
│ └── GameModule.* # Core game loop (hot-reloadable)
|
||||||
|
├── config/
|
||||||
|
│ └── game.json # Game configuration
|
||||||
|
├── build/ # Build directory
|
||||||
|
│ ├── mobilecommand.exe # Main executable
|
||||||
|
│ └── modules/ # Hot-reloadable modules (.dll)
|
||||||
|
└── CMakeLists.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- CMake 3.20+
|
||||||
|
- C++17 compiler (MinGW/GCC on Windows)
|
||||||
|
- GroveEngine (included via symlink)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure (first time)
|
||||||
|
cmake -B build -G "MinGW Makefiles"
|
||||||
|
|
||||||
|
# Build everything
|
||||||
|
cmake --build build -j4
|
||||||
|
|
||||||
|
# Build modules only (for hot-reload workflow)
|
||||||
|
cmake --build build --target modules
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run from build directory
|
||||||
|
cd build
|
||||||
|
./mobilecommand.exe
|
||||||
|
|
||||||
|
# Or use the run script
|
||||||
|
./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot-Reload Workflow
|
||||||
|
|
||||||
|
1. Start Mobile Command: `./build/mobilecommand.exe`
|
||||||
|
2. Edit a module: `src/modules/GameModule.cpp`
|
||||||
|
3. Rebuild: `cmake --build build --target modules`
|
||||||
|
4. **Module reloads automatically with state preserved**
|
||||||
|
|
||||||
## Piliers de Design
|
## Piliers de Design
|
||||||
|
|
||||||
@ -56,12 +114,15 @@ Le jeu force une évolution : De commandant infantry-heavy (2022) à opérateur
|
|||||||
|
|
||||||
## Roadmap Développement
|
## Roadmap Développement
|
||||||
|
|
||||||
### Prototype (3-6 mois)
|
### Prototype (3-6 mois) - EN COURS
|
||||||
- Train basique (3 wagons)
|
- [x] Setup projet GroveEngine
|
||||||
- Craft simple (1 ressource → 1 drone)
|
- [x] Build system fonctionnel
|
||||||
- 1 mission combat Rimworld-style
|
- [x] Hot-reload validé
|
||||||
- 3-5 events
|
- [ ] Train basique (3 wagons)
|
||||||
- Loop complet minimal
|
- [ ] Craft simple (1 ressource → 1 drone)
|
||||||
|
- [ ] 1 mission combat Rimworld-style
|
||||||
|
- [ ] 3-5 events
|
||||||
|
- [ ] Loop complet minimal
|
||||||
|
|
||||||
### MVP (12-18 mois)
|
### MVP (12-18 mois)
|
||||||
- Train complet (tous wagons)
|
- Train complet (tous wagons)
|
||||||
@ -83,9 +144,12 @@ Le jeu force une évolution : De commandant infantry-heavy (2022) à opérateur
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**CONCEPT** - Version 2.4 (1er décembre 2025)
|
**PROTOTYPE SETUP** - Version 0.1.0 (1er décembre 2025)
|
||||||
- Setup initial du projet
|
- ✅ Setup initial du projet
|
||||||
- Prochaine étape : Prototype planning
|
- ✅ GroveEngine intégré
|
||||||
|
- ✅ Build system configuré
|
||||||
|
- ✅ Hot-reload fonctionnel
|
||||||
|
- 🚧 Prochaine étape : Prototype gameplay
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
11
config/game.json
Normal file
11
config/game.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"game": {
|
||||||
|
"name": "Mobile Command",
|
||||||
|
"targetFrameRate": 10
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"logLevel": "debug",
|
||||||
|
"showFrameCount": true
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/main.cpp
Normal file
259
src/main.cpp
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
#include <grove/ModuleLoader.h>
|
||||||
|
#include <grove/JsonDataNode.h>
|
||||||
|
#include <grove/IOFactory.h>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <csignal>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// Global flag for clean shutdown
|
||||||
|
static volatile bool g_running = true;
|
||||||
|
|
||||||
|
void signalHandler(int signal) {
|
||||||
|
spdlog::info("Signal {} recu, arret en cours...", signal);
|
||||||
|
g_running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple file watcher for hot-reload
|
||||||
|
class FileWatcher {
|
||||||
|
public:
|
||||||
|
void watch(const std::string& path) {
|
||||||
|
if (fs::exists(path)) {
|
||||||
|
m_lastModified[path] = fs::last_write_time(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasChanged(const std::string& path) {
|
||||||
|
if (!fs::exists(path)) return false;
|
||||||
|
|
||||||
|
auto currentTime = fs::last_write_time(path);
|
||||||
|
auto it = m_lastModified.find(path);
|
||||||
|
|
||||||
|
if (it == m_lastModified.end()) {
|
||||||
|
m_lastModified[path] = currentTime;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime != it->second) {
|
||||||
|
it->second = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, fs::file_time_type> m_lastModified;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load JSON config file
|
||||||
|
std::unique_ptr<grove::JsonDataNode> loadConfig(const std::string& path) {
|
||||||
|
if (fs::exists(path)) {
|
||||||
|
std::ifstream file(path);
|
||||||
|
nlohmann::json j;
|
||||||
|
file >> j;
|
||||||
|
auto config = std::make_unique<grove::JsonDataNode>("config", j);
|
||||||
|
spdlog::info("Config chargee: {}", path);
|
||||||
|
return config;
|
||||||
|
} else {
|
||||||
|
spdlog::warn("Config non trouvee: {}, utilisation des defauts", path);
|
||||||
|
return std::make_unique<grove::JsonDataNode>("config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module entry in our simple manager
|
||||||
|
struct ModuleEntry {
|
||||||
|
std::string name;
|
||||||
|
std::string configFile;
|
||||||
|
std::string path;
|
||||||
|
std::unique_ptr<grove::ModuleLoader> loader;
|
||||||
|
std::unique_ptr<grove::IIO> io;
|
||||||
|
grove::IModule* module = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
// Setup logging
|
||||||
|
auto console = spdlog::stdout_color_mt("MobileCommand");
|
||||||
|
spdlog::set_default_logger(console);
|
||||||
|
spdlog::set_level(spdlog::level::debug);
|
||||||
|
spdlog::set_pattern("[%H:%M:%S.%e] [%n] [%^%l%$] %v");
|
||||||
|
|
||||||
|
spdlog::info("========================================");
|
||||||
|
spdlog::info(" MOBILE COMMAND");
|
||||||
|
spdlog::info(" Survival Management / Base Building");
|
||||||
|
spdlog::info(" Powered by GroveEngine");
|
||||||
|
spdlog::info("========================================");
|
||||||
|
|
||||||
|
// Signal handling
|
||||||
|
std::signal(SIGINT, signalHandler);
|
||||||
|
std::signal(SIGTERM, signalHandler);
|
||||||
|
|
||||||
|
// Paths - try ./modules/ first, fallback to ./build/modules/
|
||||||
|
std::string modulesDir = "./modules/";
|
||||||
|
if (!fs::exists(modulesDir) || fs::is_empty(modulesDir)) {
|
||||||
|
if (fs::exists("./build/modules/")) {
|
||||||
|
modulesDir = "./build/modules/";
|
||||||
|
spdlog::info("Using modules from: {}", modulesDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const std::string configDir = "./config/";
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Hot-Reloadable Modules
|
||||||
|
// =========================================================================
|
||||||
|
std::map<std::string, ModuleEntry> modules;
|
||||||
|
FileWatcher watcher;
|
||||||
|
|
||||||
|
// Liste des modules a charger
|
||||||
|
std::vector<std::pair<std::string, std::string>> moduleList = {
|
||||||
|
{"GameModule", "game.json"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Charger les modules
|
||||||
|
for (const auto& [moduleName, configFile] : moduleList) {
|
||||||
|
std::string modulePath = modulesDir + "lib" + moduleName + ".so";
|
||||||
|
|
||||||
|
if (!fs::exists(modulePath)) {
|
||||||
|
spdlog::warn("{} non trouve: {}", moduleName, modulePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleEntry entry;
|
||||||
|
entry.name = moduleName;
|
||||||
|
entry.configFile = configFile;
|
||||||
|
entry.path = modulePath;
|
||||||
|
entry.loader = std::make_unique<grove::ModuleLoader>();
|
||||||
|
entry.io = grove::IOFactory::create("intra", moduleName);
|
||||||
|
|
||||||
|
auto modulePtr = entry.loader->load(modulePath, moduleName);
|
||||||
|
if (!modulePtr) {
|
||||||
|
spdlog::error("Echec du chargement: {}", moduleName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
auto config = loadConfig(configDir + configFile);
|
||||||
|
modulePtr->setConfiguration(*config, entry.io.get(), nullptr);
|
||||||
|
|
||||||
|
entry.module = modulePtr.release();
|
||||||
|
watcher.watch(modulePath);
|
||||||
|
|
||||||
|
spdlog::info("{} charge et configure", moduleName);
|
||||||
|
modules[moduleName] = std::move(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modules.empty()) {
|
||||||
|
spdlog::warn("Aucun module charge! Build les modules: cmake --build build --target modules");
|
||||||
|
spdlog::info("Demarrage sans modules (mode minimal)...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Main Loop
|
||||||
|
// =========================================================================
|
||||||
|
spdlog::info("Demarrage de la boucle principale (Ctrl+C pour quitter)");
|
||||||
|
|
||||||
|
using Clock = std::chrono::high_resolution_clock;
|
||||||
|
auto startTime = Clock::now();
|
||||||
|
auto lastFrame = Clock::now();
|
||||||
|
const auto targetFrameTime = std::chrono::milliseconds(100); // 10 Hz
|
||||||
|
|
||||||
|
grove::JsonDataNode frameInput("frame");
|
||||||
|
uint64_t frameCount = 0;
|
||||||
|
|
||||||
|
while (g_running) {
|
||||||
|
auto frameStart = Clock::now();
|
||||||
|
|
||||||
|
// Calculate times
|
||||||
|
auto deltaTime = std::chrono::duration<float>(frameStart - lastFrame).count();
|
||||||
|
auto gameTime = std::chrono::duration<float>(frameStart - startTime).count();
|
||||||
|
lastFrame = frameStart;
|
||||||
|
|
||||||
|
// Prepare frame input
|
||||||
|
frameInput.setDouble("deltaTime", deltaTime);
|
||||||
|
frameInput.setInt("frameCount", frameCount);
|
||||||
|
frameInput.setDouble("gameTime", gameTime);
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Hot-Reload Check (every 10 frames = ~1 second)
|
||||||
|
// =====================================================================
|
||||||
|
if (frameCount % 10 == 0) {
|
||||||
|
for (auto& [name, entry] : modules) {
|
||||||
|
if (watcher.hasChanged(entry.path)) {
|
||||||
|
spdlog::info("Modification detectee: {}, hot-reload...", name);
|
||||||
|
|
||||||
|
// Get state before reload
|
||||||
|
std::unique_ptr<grove::IDataNode> state;
|
||||||
|
if (entry.module) {
|
||||||
|
state = entry.module->getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload
|
||||||
|
auto reloaded = entry.loader->load(entry.path, name, true);
|
||||||
|
if (reloaded) {
|
||||||
|
auto config = loadConfig(configDir + entry.configFile);
|
||||||
|
reloaded->setConfiguration(*config, entry.io.get(), nullptr);
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
reloaded->setState(*state);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.module = reloaded.release();
|
||||||
|
spdlog::info("{} recharge avec succes!", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Process Modules (hot-reloadable)
|
||||||
|
// =====================================================================
|
||||||
|
for (auto& [name, entry] : modules) {
|
||||||
|
if (entry.module) {
|
||||||
|
entry.module->process(frameInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Frame timing
|
||||||
|
// =====================================================================
|
||||||
|
frameCount++;
|
||||||
|
|
||||||
|
auto frameEnd = Clock::now();
|
||||||
|
auto frameDuration = frameEnd - frameStart;
|
||||||
|
|
||||||
|
if (frameDuration < targetFrameTime) {
|
||||||
|
std::this_thread::sleep_for(targetFrameTime - frameDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status log every 30 seconds
|
||||||
|
if (frameCount % 300 == 0) {
|
||||||
|
int minutes = static_cast<int>(gameTime / 60.0f);
|
||||||
|
int seconds = static_cast<int>(gameTime) % 60;
|
||||||
|
spdlog::debug("Session: {}m{}s, {} modules actifs",
|
||||||
|
minutes, seconds, modules.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Shutdown
|
||||||
|
// =========================================================================
|
||||||
|
spdlog::info("Arret en cours...");
|
||||||
|
|
||||||
|
// Shutdown modules
|
||||||
|
for (auto& [name, entry] : modules) {
|
||||||
|
if (entry.module) {
|
||||||
|
entry.module->shutdown();
|
||||||
|
spdlog::info("{} arrete", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spdlog::info("A bientot!");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
89
src/modules/GameModule.cpp
Normal file
89
src/modules/GameModule.cpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#include <grove/IModule.h>
|
||||||
|
#include <grove/JsonDataNode.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GameModule - Core game loop module
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Main game state management
|
||||||
|
* - Game loop coordination
|
||||||
|
* - Event dispatching
|
||||||
|
*/
|
||||||
|
class GameModule : public grove::IModule {
|
||||||
|
public:
|
||||||
|
GameModule() = default;
|
||||||
|
~GameModule() override = default;
|
||||||
|
|
||||||
|
void setConfiguration(const grove::IDataNode& config, grove::IIO* io, grove::ITaskScheduler* scheduler) override {
|
||||||
|
m_io = io;
|
||||||
|
m_scheduler = scheduler;
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
auto* jsonNode = dynamic_cast<const grove::JsonDataNode*>(&config);
|
||||||
|
if (jsonNode) {
|
||||||
|
spdlog::info("[GameModule] Configuration loaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void process(const grove::IDataNode& input) override {
|
||||||
|
// Main game loop processing
|
||||||
|
m_frameCount++;
|
||||||
|
|
||||||
|
if (m_frameCount % 100 == 0) {
|
||||||
|
spdlog::debug("[GameModule] Frame {}", m_frameCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown() override {
|
||||||
|
spdlog::info("[GameModule] Shutdown");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<grove::IDataNode> getState() override {
|
||||||
|
auto state = std::make_unique<grove::JsonDataNode>("state");
|
||||||
|
state->setInt("frameCount", m_frameCount);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setState(const grove::IDataNode& state) override {
|
||||||
|
m_frameCount = state.getInt("frameCount", 0);
|
||||||
|
spdlog::info("[GameModule] State restored: frame {}", m_frameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grove::IDataNode& getConfiguration() override {
|
||||||
|
return m_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<grove::IDataNode> getHealthStatus() override {
|
||||||
|
auto health = std::make_unique<grove::JsonDataNode>("health");
|
||||||
|
health->setString("status", "healthy");
|
||||||
|
health->setInt("frameCount", m_frameCount);
|
||||||
|
return health;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getType() const override {
|
||||||
|
return "GameModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIdle() const override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
grove::IIO* m_io = nullptr;
|
||||||
|
grove::ITaskScheduler* m_scheduler = nullptr;
|
||||||
|
int m_frameCount = 0;
|
||||||
|
grove::JsonDataNode m_config{"config"};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Module factory function
|
||||||
|
extern "C" {
|
||||||
|
grove::IModule* createModule() {
|
||||||
|
return new GameModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroyModule(grove::IModule* module) {
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user