From 3dfaffc75a748bf57e716be8cc4cd89b7abf2b79 Mon Sep 17 00:00:00 2001 From: StillHammer Date: Mon, 1 Dec 2025 11:05:51 +0800 Subject: [PATCH] Setup initial: Mobile Command project with GroveEngine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 66 ++++------ CLAUDE.md | 133 +++++++++++++++++++ CMakeLists.txt | 77 +++++++++++ README.md | 84 ++++++++++-- config/game.json | 11 ++ run.sh | 3 + src/main.cpp | 259 +++++++++++++++++++++++++++++++++++++ src/modules/GameModule.cpp | 89 +++++++++++++ 8 files changed, 674 insertions(+), 48 deletions(-) create mode 100644 CLAUDE.md create mode 100644 CMakeLists.txt create mode 100644 config/game.json create mode 100644 run.sh create mode 100644 src/main.cpp create mode 100644 src/modules/GameModule.cpp diff --git a/.gitignore b/.gitignore index 4dc8123..bda3083 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..59a861b --- /dev/null +++ b/CLAUDE.md @@ -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::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/` + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e898e46 --- /dev/null +++ b/CMakeLists.txt @@ -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 $ + DEPENDS mobilecommand modules + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running Mobile Command" +) diff --git a/README.md b/README.md index d33c95a..46c07fa 100644 --- a/README.md +++ b/README.md @@ -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 --- diff --git a/config/game.json b/config/game.json new file mode 100644 index 0000000..4d4e1ae --- /dev/null +++ b/config/game.json @@ -0,0 +1,11 @@ +{ + "version": "0.1.0", + "game": { + "name": "Mobile Command", + "targetFrameRate": 10 + }, + "debug": { + "logLevel": "debug", + "showFrameCount": true + } +} diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..f2e3d8f --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd build +./mobilecommand.exe diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..7f6ad53 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,259 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +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 m_lastModified; +}; + +// Load JSON config file +std::unique_ptr loadConfig(const std::string& path) { + if (fs::exists(path)) { + std::ifstream file(path); + nlohmann::json j; + file >> j; + auto config = std::make_unique("config", j); + spdlog::info("Config chargee: {}", path); + return config; + } else { + spdlog::warn("Config non trouvee: {}, utilisation des defauts", path); + return std::make_unique("config"); + } +} + +// Module entry in our simple manager +struct ModuleEntry { + std::string name; + std::string configFile; + std::string path; + std::unique_ptr loader; + std::unique_ptr 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 modules; + FileWatcher watcher; + + // Liste des modules a charger + std::vector> 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(); + 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(frameStart - lastFrame).count(); + auto gameTime = std::chrono::duration(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 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(gameTime / 60.0f); + int seconds = static_cast(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; +} diff --git a/src/modules/GameModule.cpp b/src/modules/GameModule.cpp new file mode 100644 index 0000000..7ac3996 --- /dev/null +++ b/src/modules/GameModule.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include + +/** + * 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(&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 getState() override { + auto state = std::make_unique("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 getHealthStatus() override { + auto health = std::make_unique("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; + } +}