project-mobile-command/src/main.cpp
StillHammer 3dfaffc75a 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>
2025-12-01 11:05:51 +08:00

260 lines
8.5 KiB
C++

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