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:
StillHammer 2025-12-01 11:05:51 +08:00
parent d10b285a56
commit 3dfaffc75a
8 changed files with 674 additions and 48 deletions

66
.gitignore vendored
View File

@ -1,50 +1,40 @@
# Dependencies
node_modules/
vendor/
# Build outputs
dist/
# Build directories
build/
out/
build-*/
# CMake files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.cmake
!CMakeLists.txt
# Compiled binaries
*.exe
*.dll
*.out
*.so
*.dll
*.dylib
# IDE
# IDE files
.vscode/
.idea/
*.suo
*.user
*.userosscache
*.sln.docstates
# OS
.DS_Store
Thumbs.db
desktop.ini
*.swp
*.swo
*~
# Logs
*.log
logs/
# Environment
.env
.env.local
# Data files
data/
*.db
*.db-journal
*.db-wal
*.db-shm
# Temporary files
tmp/
temp/
*.tmp
*.temp
# Assets (work files)
*.psd
*.ai
*.blend1
*.blend2
# Game specific
saves/
cache/
screenshots/
# OS files
.DS_Store
Thumbs.db
external/GroveEngine

133
CLAUDE.md Normal file
View 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
View 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"
)

View File

@ -17,10 +17,68 @@ Le jeu force une évolution : De commandant infantry-heavy (2022) à opérateur
## Stack Technique
- **Engine** : GroveEngine (validation engine = objectif secondaire)
- **Engine** : GroveEngine (hot-reload C++17 module system)
- **Plateforme** : PC (Steam)
- **Rendu** : 2D (sprites ou vector)
- **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
@ -56,12 +114,15 @@ Le jeu force une évolution : De commandant infantry-heavy (2022) à opérateur
## Roadmap Développement
### Prototype (3-6 mois)
- Train basique (3 wagons)
- Craft simple (1 ressource → 1 drone)
- 1 mission combat Rimworld-style
- 3-5 events
- Loop complet minimal
### Prototype (3-6 mois) - EN COURS
- [x] Setup projet GroveEngine
- [x] Build system fonctionnel
- [x] Hot-reload validé
- [ ] Train basique (3 wagons)
- [ ] Craft simple (1 ressource → 1 drone)
- [ ] 1 mission combat Rimworld-style
- [ ] 3-5 events
- [ ] Loop complet minimal
### MVP (12-18 mois)
- Train complet (tous wagons)
@ -83,9 +144,12 @@ Le jeu force une évolution : De commandant infantry-heavy (2022) à opérateur
## Status
**CONCEPT** - Version 2.4 (1er décembre 2025)
- Setup initial du projet
- Prochaine étape : Prototype planning
**PROTOTYPE SETUP** - Version 0.1.0 (1er décembre 2025)
- ✅ Setup initial du projet
- ✅ GroveEngine intégré
- ✅ Build system configuré
- ✅ Hot-reload fonctionnel
- 🚧 Prochaine étape : Prototype gameplay
---

11
config/game.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.1.0",
"game": {
"name": "Mobile Command",
"targetFrameRate": 10
},
"debug": {
"logLevel": "debug",
"showFrameCount": true
}
}

3
run.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd build
./mobilecommand.exe

259
src/main.cpp Normal file
View 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;
}

View 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;
}
}