- 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>
260 lines
8.5 KiB
C++
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;
|
|
}
|