From fad105afb213372fbd76cf77da404d3a4880b340 Mon Sep 17 00:00:00 2001 From: StillHammer Date: Tue, 28 Oct 2025 15:36:25 +0800 Subject: [PATCH] feat: Implement complete IDataNode/IDataTree system with JSON backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major feature: Unified config/data/runtime tree system **New System Architecture:** - Unified data tree for config, persistent data, and runtime state - Three separate roots: config/ (read-only + hot-reload), data/ (read-write + save), runtime/ (temporary) - Support for modding, saves, and hot-reload in single system **Interfaces:** - IDataValue: Abstract data value interface (type-safe access) - IDataNode: Tree node with navigation, search, and modification - IDataTree: Root container with config/data/runtime management **Concrete Implementations:** - JsonDataValue: nlohmann::json backed value - JsonDataNode: Full tree navigation with pattern matching & queries - JsonDataTree: File-based JSON storage with hot-reload **Features:** - Pattern matching search (wildcards support) - Property-based queries with predicates - SHA256 hashing for validation/sync - Hot-reload for config/ directory - Save operations for data/ persistence - Read-only enforcement for config/ **API Changes:** - All namespaces changed from 'warfactory' to 'grove' - IDataTree: Added getConfigRoot(), getDataRoot(), getRuntimeRoot() - IDataTree: Added saveData(), saveNode() for persistence - IDataNode: Added setChild(), removeChild(), clearChildren() - CMakeLists.txt: Added OpenSSL dependency for hashing **Usage:** ```cpp auto tree = DataTreeFactory::create("json", "./gamedata"); auto config = tree->getConfigRoot(); // Read-only game config auto data = tree->getDataRoot(); // Player saves auto runtime = tree->getRuntimeRoot(); // Temporary state // Hot-reload config on file changes if (tree->reloadIfChanged()) { /* refresh modules */ } // Save player progress data->setChild("progress", progressNode); tree->saveData(); ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CMakeLists.txt | 8 + include/grove/ASerializable.h | 4 +- include/grove/DataTreeFactory.h | 4 +- include/grove/DebugEngine.h | 4 +- include/grove/EngineFactory.h | 4 +- include/grove/ICoordinationModule.h | 6 +- include/grove/IDataNode.h | 34 ++- include/grove/IDataTree.h | 84 +++++- include/grove/IDataValue.h | 4 +- include/grove/IEngine.h | 6 +- include/grove/IIO.h | 4 +- include/grove/IModule.h | 6 +- include/grove/IModuleSystem.h | 6 +- include/grove/IOFactory.h | 4 +- include/grove/IRegion.h | 4 +- include/grove/ISerializable.h | 4 +- include/grove/ITaskScheduler.h | 4 +- include/grove/IUI.h | 4 +- include/grove/IUI_Enums.h | 4 +- include/grove/ImGuiUI.h | 4 +- include/grove/IntraIO.h | 4 +- include/grove/IntraIOManager.h | 4 +- include/grove/JsonDataNode.h | 108 ++++++++ include/grove/JsonDataTree.h | 86 ++++++ include/grove/JsonDataValue.h | 53 ++++ include/grove/ModuleFactory.h | 4 +- include/grove/ModuleSystemFactory.h | 4 +- include/grove/RandomGenerator.h | 4 +- include/grove/Resource.h | 4 +- include/grove/ResourceRegistry.h | 4 +- include/grove/SequentialModuleSystem.h | 4 +- include/grove/SerializationRegistry.h | 4 +- src/DataTreeFactory.cpp | 15 ++ src/DebugEngine.cpp | 4 +- src/EngineFactory.cpp | 4 +- src/IOFactory.cpp | 4 +- src/ImGuiUI.cpp | 4 +- src/IntraIO.cpp | 4 +- src/IntraIOManager.cpp | 4 +- src/JsonDataNode.cpp | 343 ++++++++++++++++++++++++ src/JsonDataTree.cpp | 352 +++++++++++++++++++++++++ src/JsonDataValue.cpp | 97 +++++++ src/ModuleFactory.cpp | 4 +- src/ModuleSystemFactory.cpp | 4 +- src/ResourceRegistry.cpp | 4 +- src/SequentialModuleSystem.cpp | 4 +- 46 files changed, 1242 insertions(+), 90 deletions(-) create mode 100644 include/grove/JsonDataNode.h create mode 100644 include/grove/JsonDataTree.h create mode 100644 include/grove/JsonDataValue.h create mode 100644 src/DataTreeFactory.cpp create mode 100644 src/JsonDataNode.cpp create mode 100644 src/JsonDataTree.cpp create mode 100644 src/JsonDataValue.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 507def3..33f2cac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,12 +37,20 @@ if(GROVE_BUILD_IMPLEMENTATIONS) add_library(grove_impl STATIC src/ImGuiUI.cpp src/ResourceRegistry.cpp + src/JsonDataValue.cpp + src/JsonDataNode.cpp + src/JsonDataTree.cpp + src/DataTreeFactory.cpp ) target_link_libraries(grove_impl PUBLIC GroveEngine::core + OpenSSL::Crypto ) + # Find OpenSSL for SHA256 hashing + find_package(OpenSSL REQUIRED) + # If imgui is available from parent project, link it if(TARGET imgui_backends) target_link_libraries(grove_impl PUBLIC imgui_backends) diff --git a/include/grove/ASerializable.h b/include/grove/ASerializable.h index 5e6ca86..42cd04b 100644 --- a/include/grove/ASerializable.h +++ b/include/grove/ASerializable.h @@ -4,7 +4,7 @@ #include #include -namespace warfactory { +namespace grove { class SerializationRegistry; @@ -26,4 +26,4 @@ protected: void unregisterFromSerialization(); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/DataTreeFactory.h b/include/grove/DataTreeFactory.h index ed08da7..1cb5b29 100644 --- a/include/grove/DataTreeFactory.h +++ b/include/grove/DataTreeFactory.h @@ -4,7 +4,7 @@ #include #include "IDataTree.h" -namespace warfactory { +namespace grove { /** * @brief Factory for creating data tree instances @@ -20,4 +20,4 @@ public: static std::unique_ptr create(const std::string& type, const std::string& sourcePath); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/DebugEngine.h b/include/grove/DebugEngine.h index 067b6c9..1df257d 100644 --- a/include/grove/DebugEngine.h +++ b/include/grove/DebugEngine.h @@ -15,7 +15,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { /** * @brief Debug engine implementation with comprehensive logging @@ -86,4 +86,4 @@ public: void setLogLevel(spdlog::level::level_enum level); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/EngineFactory.h b/include/grove/EngineFactory.h index 37c0679..5d82773 100644 --- a/include/grove/EngineFactory.h +++ b/include/grove/EngineFactory.h @@ -8,7 +8,7 @@ #include "IEngine.h" #include "DebugEngine.h" -namespace warfactory { +namespace grove { /** * @brief Factory for creating engine implementations @@ -102,4 +102,4 @@ private: static std::string toLowercase(const std::string& str); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/ICoordinationModule.h b/include/grove/ICoordinationModule.h index 0967fa6..562c89e 100644 --- a/include/grove/ICoordinationModule.h +++ b/include/grove/ICoordinationModule.h @@ -8,12 +8,12 @@ #include "IDataTree.h" // Forward declarations -namespace warfactory { +namespace grove { class IEngine; class IModuleSystem; } -namespace warfactory { +namespace grove { /** * @brief Global system orchestrator - First launched, last shutdown @@ -158,4 +158,4 @@ public: virtual json getSystemHealthReport() = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IDataNode.h b/include/grove/IDataNode.h index 2f42733..2e553e0 100644 --- a/include/grove/IDataNode.h +++ b/include/grove/IDataNode.h @@ -6,7 +6,7 @@ #include #include "IDataValue.h" -namespace warfactory { +namespace grove { /** * @brief Interface for a single node in the data tree @@ -226,6 +226,36 @@ public: * @return Node type identifier */ virtual std::string getNodeType() const = 0; + + // ======================================== + // TREE MODIFICATION (For data/ and runtime/ nodes) + // ======================================== + + /** + * @brief Add or update a child node + * @param name Child name + * @param node Child node to add/replace + * + * If a child with this name already exists, it will be replaced. + * Only works for data/ and runtime/ nodes. Config nodes are read-only. + */ + virtual void setChild(const std::string& name, std::unique_ptr node) = 0; + + /** + * @brief Remove a child node + * @param name Child name to remove + * @return true if child was found and removed + * + * Only works for data/ and runtime/ nodes. Config nodes are read-only. + */ + virtual bool removeChild(const std::string& name) = 0; + + /** + * @brief Clear all children from this node + * + * Only works for data/ and runtime/ nodes. Config nodes are read-only. + */ + virtual void clearChildren() = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IDataTree.h b/include/grove/IDataTree.h index cb3548a..3885af6 100644 --- a/include/grove/IDataTree.h +++ b/include/grove/IDataTree.h @@ -5,12 +5,18 @@ #include #include "IDataNode.h" -namespace warfactory { +namespace grove { /** * @brief Interface for the root data tree container * - * Manages the entire tree structure and provides hot-reload capabilities + * Unified system for configuration, persistent data, and runtime state. + * Supports hot-reload for config and persistence for data. + * + * Tree Structure: + * - config/ : Read-only game configuration (hot-reload enabled, moddable) + * - data/ : Persistent player data (read-write, saved to disk) + * - runtime/ : Temporary runtime state (read-write, never saved) */ class IDataTree { public: @@ -21,37 +27,91 @@ public: // ======================================== /** - * @brief Get root node of the tree - * @return Root node + * @brief Get root node of the entire tree + * @return Root node containing config/, data/, runtime/ + * + * WARNING: This gives access to everything. Use getConfigRoot(), + * getDataRoot(), or getRuntimeRoot() for isolated access. */ virtual std::unique_ptr getRoot() = 0; /** * @brief Get node by path from root - * @param path Path from root (e.g., "vehicles/tanks/heavy") + * @param path Path from root (e.g., "config/vehicles/tanks/heavy") * @return Node at path or nullptr if not found */ virtual std::unique_ptr getNode(const std::string& path) = 0; // ======================================== - // MANUAL HOT-RELOAD (SIMPLE & EFFECTIVE) + // SEPARATE ROOTS (Recommended Access Pattern) // ======================================== /** - * @brief Check if source files have changed - * @return true if changes detected + * @brief Get config tree root (read-only, hot-reload enabled) + * @return Config root node (config/) + * + * Use for: Game configuration, unit stats, modding + */ + virtual std::unique_ptr getConfigRoot() = 0; + + /** + * @brief Get persistent data root (read-write, saved to disk) + * @return Data root node (data/) + * + * Use for: Campaign progress, unlocks, player statistics + */ + virtual std::unique_ptr getDataRoot() = 0; + + /** + * @brief Get runtime data root (read-write, never saved) + * @return Runtime root node (runtime/) + * + * Use for: Current game state, temporary calculations, caches + */ + virtual std::unique_ptr getRuntimeRoot() = 0; + + // ======================================== + // SAVE OPERATIONS + // ======================================== + + /** + * @brief Save all persistent data to disk + * @return true if save succeeded + * + * Saves the entire data/ subtree to disk. Does not affect config/ or runtime/. + */ + virtual bool saveData() = 0; + + /** + * @brief Save specific node and its subtree + * @param path Path to node to save (e.g., "data/campaign/progress") + * @return true if save succeeded + * + * Allows granular saves for performance. Only works for data/ paths. + */ + virtual bool saveNode(const std::string& path) = 0; + + // ======================================== + // HOT-RELOAD (Config Only) + // ======================================== + + /** + * @brief Check if config files have changed + * @return true if changes detected in config/ */ virtual bool checkForChanges() = 0; /** - * @brief Reload entire tree if changes detected + * @brief Reload config tree if files changed * @return true if reload was performed + * + * Only reloads config/. Does not affect data/ or runtime/. */ virtual bool reloadIfChanged() = 0; /** - * @brief Register callback for when tree is reloaded - * @param callback Function called after successful reload + * @brief Register callback for when config is reloaded + * @param callback Function called after successful config reload */ virtual void onTreeReloaded(std::function callback) = 0; @@ -66,4 +126,4 @@ public: virtual std::string getType() = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IDataValue.h b/include/grove/IDataValue.h index b361281..43b8563 100644 --- a/include/grove/IDataValue.h +++ b/include/grove/IDataValue.h @@ -4,7 +4,7 @@ #include #include -namespace warfactory { +namespace grove { /** * @brief Interface for data values - abstracts underlying data format @@ -40,4 +40,4 @@ public: virtual std::string toString() const = 0; }; -} // namespace warfactory +} // namespace grove diff --git a/include/grove/IEngine.h b/include/grove/IEngine.h index c3e40e0..656cb09 100644 --- a/include/grove/IEngine.h +++ b/include/grove/IEngine.h @@ -4,12 +4,12 @@ #include // Forward declarations to avoid circular dependencies -namespace warfactory { +namespace grove { class IModuleSystem; class IIO; } -namespace warfactory { +namespace grove { enum class EngineType { DEBUG = 0, @@ -120,4 +120,4 @@ public: virtual EngineType getType() const = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IIO.h b/include/grove/IIO.h index f3fddbe..391704f 100644 --- a/include/grove/IIO.h +++ b/include/grove/IIO.h @@ -6,7 +6,7 @@ #include #include "IDataNode.h" -namespace warfactory { +namespace grove { enum class IOType { INTRA = 0, // Same process @@ -98,4 +98,4 @@ public: virtual IOType getType() const = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IModule.h b/include/grove/IModule.h index 2fdf792..78a6521 100644 --- a/include/grove/IModule.h +++ b/include/grove/IModule.h @@ -6,11 +6,11 @@ #include "ITaskScheduler.h" // Forward declarations -namespace warfactory { +namespace grove { class IIO; } -namespace warfactory { +namespace grove { @@ -108,4 +108,4 @@ public: virtual std::string getType() const = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IModuleSystem.h b/include/grove/IModuleSystem.h index 911a018..2ed5c02 100644 --- a/include/grove/IModuleSystem.h +++ b/include/grove/IModuleSystem.h @@ -5,12 +5,12 @@ #include "ITaskScheduler.h" // Forward declarations to avoid circular dependencies -namespace warfactory { +namespace grove { class IModule; class IIO; } -namespace warfactory { +namespace grove { enum class ModuleSystemType { SEQUENTIAL = 0, @@ -89,4 +89,4 @@ public: virtual ModuleSystemType getType() const = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IOFactory.h b/include/grove/IOFactory.h index 10e1551..396ef88 100644 --- a/include/grove/IOFactory.h +++ b/include/grove/IOFactory.h @@ -11,7 +11,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { /** * @brief Factory for creating IO transport implementations @@ -131,4 +131,4 @@ private: static std::string generateEndpoint(IOType ioType); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IRegion.h b/include/grove/IRegion.h index 7a6f8f4..416398c 100644 --- a/include/grove/IRegion.h +++ b/include/grove/IRegion.h @@ -2,7 +2,7 @@ #include -namespace warfactory { +namespace grove { /** * @brief Interface for geological regions during world generation @@ -47,4 +47,4 @@ public: virtual bool canFuseWith(const IRegion* other) const = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/ISerializable.h b/include/grove/ISerializable.h index 78a5a2a..2ee56d9 100644 --- a/include/grove/ISerializable.h +++ b/include/grove/ISerializable.h @@ -3,7 +3,7 @@ #include "IDataNode.h" #include -namespace warfactory { +namespace grove { class ISerializable { public: @@ -13,4 +13,4 @@ public: virtual void deserialize(const IDataNode& data) = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/ITaskScheduler.h b/include/grove/ITaskScheduler.h index bbdbb0a..8f39f24 100644 --- a/include/grove/ITaskScheduler.h +++ b/include/grove/ITaskScheduler.h @@ -4,7 +4,7 @@ #include #include "IDataNode.h" -namespace warfactory { +namespace grove { /** * @brief Task scheduling interface for module delegation to execution system @@ -100,4 +100,4 @@ public: virtual std::unique_ptr getCompletedTask() = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IUI.h b/include/grove/IUI.h index b9a30af..b23fb14 100644 --- a/include/grove/IUI.h +++ b/include/grove/IUI.h @@ -4,7 +4,7 @@ #include #include -namespace warfactory { +namespace grove { using json = nlohmann::json; @@ -126,4 +126,4 @@ public: virtual void setState(const json& state) = 0; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IUI_Enums.h b/include/grove/IUI_Enums.h index 78b76fa..52ff3c9 100644 --- a/include/grove/IUI_Enums.h +++ b/include/grove/IUI_Enums.h @@ -4,7 +4,7 @@ #include #include -namespace warfactory { +namespace grove { using json = nlohmann::json; @@ -337,4 +337,4 @@ constexpr const char* toString(Orientation orient) { } } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/ImGuiUI.h b/include/grove/ImGuiUI.h index dbb2524..0ff2154 100644 --- a/include/grove/ImGuiUI.h +++ b/include/grove/ImGuiUI.h @@ -13,7 +13,7 @@ #include #include -namespace warfactory { +namespace grove { /** * @brief ImGui implementation of IUI interface @@ -704,4 +704,4 @@ private: void renderLogConsole(); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IntraIO.h b/include/grove/IntraIO.h index 981fa5f..3ca2a06 100644 --- a/include/grove/IntraIO.h +++ b/include/grove/IntraIO.h @@ -17,7 +17,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { // Interface for message delivery to avoid circular include class IIntraIODelivery { @@ -132,4 +132,4 @@ public: const std::string& getInstanceId() const; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/IntraIOManager.h b/include/grove/IntraIOManager.h index 3bb2b9e..ffeb036 100644 --- a/include/grove/IntraIOManager.h +++ b/include/grove/IntraIOManager.h @@ -13,7 +13,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { class IntraIO; // Forward declaration class IIntraIODelivery; // Forward declaration @@ -88,4 +88,4 @@ public: static IntraIOManager& getInstance(); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/JsonDataNode.h b/include/grove/JsonDataNode.h new file mode 100644 index 0000000..0f081ad --- /dev/null +++ b/include/grove/JsonDataNode.h @@ -0,0 +1,108 @@ +#pragma once + +#include "IDataNode.h" +#include "JsonDataValue.h" +#include +#include +#include +#include +#include +#include + +namespace grove { + +using json = nlohmann::json; + +/** + * @brief Concrete implementation of IDataNode backed by JSON + * + * Represents a node in the hierarchical data tree. Can have: + * - Children nodes (map of name -> node) + * - Own data (JSON value) + * - Path in the tree for identification + */ +class JsonDataNode : public IDataNode { +public: + /** + * @brief Create a node with name and optional data + * @param name Node name + * @param data Optional JSON data for this node + * @param parent Optional parent node (for path tracking) + * @param readOnly Whether this node is read-only (for config/) + */ + JsonDataNode(const std::string& name, + const json& data = json::object(), + JsonDataNode* parent = nullptr, + bool readOnly = false); + + virtual ~JsonDataNode() = default; + + // Tree navigation + std::unique_ptr getChild(const std::string& name) override; + std::vector getChildNames() override; + bool hasChildren() override; + + // Exact search in children + std::vector getChildrenByName(const std::string& name) override; + bool hasChildrenByName(const std::string& name) const override; + IDataNode* getFirstChildByName(const std::string& name) override; + + // Pattern matching search + std::vector getChildrenByNameMatch(const std::string& pattern) override; + bool hasChildrenByNameMatch(const std::string& pattern) const override; + IDataNode* getFirstChildByNameMatch(const std::string& pattern) override; + + // Query by properties + std::vector queryByProperty(const std::string& propName, + const std::function& predicate) override; + + // Node's own data + std::unique_ptr getData() const override; + bool hasData() const override; + void setData(std::unique_ptr data) override; + + // Typed data access + std::string getString(const std::string& name, const std::string& defaultValue = "") const override; + int getInt(const std::string& name, int defaultValue = 0) const override; + double getDouble(const std::string& name, double defaultValue = 0.0) const override; + bool getBool(const std::string& name, bool defaultValue = false) const override; + bool hasProperty(const std::string& name) const override; + + // Hash system + std::string getDataHash() override; + std::string getTreeHash() override; + std::string getSubtreeHash(const std::string& childPath) override; + + // Metadata + std::string getPath() const override; + std::string getName() const override; + std::string getNodeType() const override; + + // Tree modification + void setChild(const std::string& name, std::unique_ptr node) override; + bool removeChild(const std::string& name) override; + void clearChildren() override; + + // Direct JSON access (for internal use by JsonDataTree) + const json& getJsonData() const { return m_data; } + json& getJsonData() { return m_data; } + const std::map>& getChildren() const { return m_children; } + +private: + std::string m_name; + json m_data; + JsonDataNode* m_parent; + bool m_readOnly; + std::map> m_children; + + // Helper methods + bool matchesPattern(const std::string& text, const std::string& pattern) const; + void collectMatchingNodes(const std::string& pattern, std::vector& results); + void collectNodesByProperty(const std::string& propName, + const std::function& predicate, + std::vector& results); + std::string computeHash(const std::string& input) const; + void checkReadOnly() const; +}; + +} // namespace grove diff --git a/include/grove/JsonDataTree.h b/include/grove/JsonDataTree.h new file mode 100644 index 0000000..75d2f67 --- /dev/null +++ b/include/grove/JsonDataTree.h @@ -0,0 +1,86 @@ +#pragma once + +#include "IDataTree.h" +#include "JsonDataNode.h" +#include +#include +#include +#include +#include +#include + +namespace grove { + +/** + * @brief Concrete implementation of IDataTree backed by JSON files + * + * Manages three separate trees: + * - config/ : Read-only configuration loaded from files (hot-reload enabled) + * - data/ : Persistent player data (read-write, saved to disk) + * - runtime/ : Temporary runtime state (read-write, never saved) + * + * File structure: + * basePath/ + * ├─ config/ + * │ ├─ tanks.json + * │ ├─ weapons.json + * │ └─ ... + * ├─ data/ + * │ ├─ campaign.json + * │ ├─ unlocks.json + * │ └─ ... + * └─ runtime/ (in-memory only, not on disk) + */ +class JsonDataTree : public IDataTree { +public: + /** + * @brief Create a data tree from a base directory + * @param basePath Base directory containing config/, data/ subdirs + */ + explicit JsonDataTree(const std::string& basePath); + virtual ~JsonDataTree() = default; + + // Tree access + std::unique_ptr getRoot() override; + std::unique_ptr getNode(const std::string& path) override; + + // Separate roots + std::unique_ptr getConfigRoot() override; + std::unique_ptr getDataRoot() override; + std::unique_ptr getRuntimeRoot() override; + + // Save operations + bool saveData() override; + bool saveNode(const std::string& path) override; + + // Hot-reload + bool checkForChanges() override; + bool reloadIfChanged() override; + void onTreeReloaded(std::function callback) override; + + // Metadata + std::string getType() override; + +private: + std::string m_basePath; + std::unique_ptr m_root; + std::unique_ptr m_configRoot; + std::unique_ptr m_dataRoot; + std::unique_ptr m_runtimeRoot; + + std::map m_configFileTimes; + std::vector> m_reloadCallbacks; + + // Helper methods + void loadConfigTree(); + void loadDataTree(); + void initializeRuntimeTree(); + void scanDirectory(const std::string& dirPath, JsonDataNode* parentNode, bool readOnly); + json loadJsonFile(const std::string& filePath); + bool saveJsonFile(const std::string& filePath, const json& data); + void buildNodeFromJson(const std::string& name, const json& data, JsonDataNode* parentNode, bool readOnly); + json nodeToJson(const JsonDataNode* node); + void updateFileTimestamps(const std::string& dirPath); +}; + +} // namespace grove diff --git a/include/grove/JsonDataValue.h b/include/grove/JsonDataValue.h new file mode 100644 index 0000000..6961090 --- /dev/null +++ b/include/grove/JsonDataValue.h @@ -0,0 +1,53 @@ +#pragma once + +#include "IDataValue.h" +#include +#include +#include +#include + +namespace grove { + +using json = nlohmann::json; + +/** + * @brief Concrete implementation of IDataValue backed by nlohmann::json + */ +class JsonDataValue : public IDataValue { +public: + explicit JsonDataValue(const json& value); + explicit JsonDataValue(json&& value); + virtual ~JsonDataValue() = default; + + // Type checking + bool isNull() const override; + bool isBool() const override; + bool isNumber() const override; + bool isString() const override; + bool isArray() const override; + bool isObject() const override; + + // Value access with defaults + bool asBool(bool defaultValue = false) const override; + int asInt(int defaultValue = 0) const override; + double asDouble(double defaultValue = 0.0) const override; + std::string asString(const std::string& defaultValue = "") const override; + + // Array/Object access + size_t size() const override; + std::unique_ptr get(size_t index) const override; + std::unique_ptr get(const std::string& key) const override; + bool has(const std::string& key) const override; + + // Serialization + std::string toString() const override; + + // Direct JSON access (for internal use) + const json& getJson() const { return m_value; } + json& getJson() { return m_value; } + +private: + json m_value; +}; + +} // namespace grove diff --git a/include/grove/ModuleFactory.h b/include/grove/ModuleFactory.h index 5c7cc29..aa04365 100644 --- a/include/grove/ModuleFactory.h +++ b/include/grove/ModuleFactory.h @@ -11,7 +11,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { /** * @brief Factory for loading and creating modules from shared libraries (.so files) @@ -99,4 +99,4 @@ private: void logModuleError(const std::string& operation, const std::string& details) const; }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/ModuleSystemFactory.h b/include/grove/ModuleSystemFactory.h index 9ccd84a..379f663 100644 --- a/include/grove/ModuleSystemFactory.h +++ b/include/grove/ModuleSystemFactory.h @@ -11,7 +11,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { /** * @brief Factory for creating ModuleSystem implementations @@ -113,4 +113,4 @@ private: static int detectCpuCores(); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/RandomGenerator.h b/include/grove/RandomGenerator.h index 8d62bb5..6a7dd6b 100644 --- a/include/grove/RandomGenerator.h +++ b/include/grove/RandomGenerator.h @@ -3,7 +3,7 @@ #include #include -namespace warfactory { +namespace grove { /** * @brief Centralized random number generator singleton @@ -86,4 +86,4 @@ public: } }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/Resource.h b/include/grove/Resource.h index 7ffcc8e..4d11253 100644 --- a/include/grove/Resource.h +++ b/include/grove/Resource.h @@ -5,7 +5,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { class Resource { private: @@ -34,4 +34,4 @@ public: static Resource loadFromJson(const std::string& resource_id, const json& resource_data); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/ResourceRegistry.h b/include/grove/ResourceRegistry.h index f1fb853..e82736b 100644 --- a/include/grove/ResourceRegistry.h +++ b/include/grove/ResourceRegistry.h @@ -6,7 +6,7 @@ #include #include -namespace warfactory { +namespace grove { /** * @brief Singleton registry for all game resources with fast uint32_t ID lookup @@ -110,4 +110,4 @@ public: #define GET_RESOURCE(id) warfactory::ResourceRegistry::getInstance().getResource(id) #define VALID_RESOURCE(id) warfactory::ResourceRegistry::getInstance().isValidResourceId(id) -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/SequentialModuleSystem.h b/include/grove/SequentialModuleSystem.h index 709eec9..65f1e48 100644 --- a/include/grove/SequentialModuleSystem.h +++ b/include/grove/SequentialModuleSystem.h @@ -12,7 +12,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { /** * @brief Sequential module system implementation for debug and testing @@ -84,4 +84,4 @@ public: void setLogLevel(spdlog::level::level_enum level); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/include/grove/SerializationRegistry.h b/include/grove/SerializationRegistry.h index fdd7eb6..842c29e 100644 --- a/include/grove/SerializationRegistry.h +++ b/include/grove/SerializationRegistry.h @@ -7,7 +7,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { class ASerializable; @@ -35,4 +35,4 @@ public: void clear(); }; -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/DataTreeFactory.cpp b/src/DataTreeFactory.cpp new file mode 100644 index 0000000..baa2737 --- /dev/null +++ b/src/DataTreeFactory.cpp @@ -0,0 +1,15 @@ +#include "grove/DataTreeFactory.h" +#include "grove/JsonDataTree.h" +#include + +namespace grove { + +std::unique_ptr DataTreeFactory::create(const std::string& type, const std::string& sourcePath) { + if (type == "json") { + return std::make_unique(sourcePath); + } + + throw std::runtime_error("Unknown data tree type: " + type); +} + +} // namespace grove diff --git a/src/DebugEngine.cpp b/src/DebugEngine.cpp index b70a80c..b213b73 100644 --- a/src/DebugEngine.cpp +++ b/src/DebugEngine.cpp @@ -4,7 +4,7 @@ #include #include -namespace warfactory { +namespace grove { DebugEngine::DebugEngine() { // Create comprehensive logger with multiple sinks @@ -484,4 +484,4 @@ void DebugEngine::validateConfiguration() { logger->trace("🚧 TODO: Implement comprehensive config validation"); } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/EngineFactory.cpp b/src/EngineFactory.cpp index fc7a586..bc015f0 100644 --- a/src/EngineFactory.cpp +++ b/src/EngineFactory.cpp @@ -6,7 +6,7 @@ using json = nlohmann::json; -namespace warfactory { +namespace grove { std::unique_ptr EngineFactory::createEngine(const std::string& engineType) { auto logger = getFactoryLogger(); @@ -204,4 +204,4 @@ std::string EngineFactory::toLowercase(const std::string& str) { return result; } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/IOFactory.cpp b/src/IOFactory.cpp index 0b71072..2491030 100644 --- a/src/IOFactory.cpp +++ b/src/IOFactory.cpp @@ -11,7 +11,7 @@ // #include "LocalIO.h" // #include "NetworkIO.h" -namespace warfactory { +namespace grove { std::unique_ptr IOFactory::create(const std::string& transportType, const std::string& instanceId) { auto logger = getFactoryLogger(); @@ -308,4 +308,4 @@ std::string IOFactory::generateEndpoint(IOType ioType) { } } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/ImGuiUI.cpp b/src/ImGuiUI.cpp index 43a216d..1091d36 100644 --- a/src/ImGuiUI.cpp +++ b/src/ImGuiUI.cpp @@ -3,7 +3,7 @@ #include #include -namespace warfactory { +namespace grove { // ======================================== // IUI INTERFACE IMPLEMENTATION - REQUESTS & EVENTS @@ -543,4 +543,4 @@ void ImGuiUI::renderLogConsole() { ImGui::End(); } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/IntraIO.cpp b/src/IntraIO.cpp index bd27225..a878f13 100644 --- a/src/IntraIO.cpp +++ b/src/IntraIO.cpp @@ -6,7 +6,7 @@ #include #include -namespace warfactory { +namespace grove { // Factory function for IntraIOManager to avoid circular include std::shared_ptr createIntraIOInstance(const std::string& instanceId) { @@ -481,4 +481,4 @@ const std::string& IntraIO::getInstanceId() const { return instanceId; } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/IntraIOManager.cpp b/src/IntraIOManager.cpp index b7b1be8..d73fb59 100644 --- a/src/IntraIOManager.cpp +++ b/src/IntraIOManager.cpp @@ -4,7 +4,7 @@ #include #include -namespace warfactory { +namespace grove { IntraIOManager::IntraIOManager() { // Create logger @@ -266,4 +266,4 @@ IntraIOManager& IntraIOManager::getInstance() { return instance; } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/JsonDataNode.cpp b/src/JsonDataNode.cpp new file mode 100644 index 0000000..b7fe91a --- /dev/null +++ b/src/JsonDataNode.cpp @@ -0,0 +1,343 @@ +#include "grove/JsonDataNode.h" +#include +#include +#include +#include +#include + +namespace grove { + +JsonDataNode::JsonDataNode(const std::string& name, const json& data, JsonDataNode* parent, bool readOnly) + : m_name(name), m_data(data), m_parent(parent), m_readOnly(readOnly) { +} + +// ======================================== +// TREE NAVIGATION +// ======================================== + +std::unique_ptr JsonDataNode::getChild(const std::string& name) { + auto it = m_children.find(name); + if (it == m_children.end()) { + return nullptr; + } + // Return a copy wrapped in unique_ptr + return std::make_unique(it->second->getName(), + it->second->getJsonData(), + this, + it->second->m_readOnly); +} + +std::vector JsonDataNode::getChildNames() { + std::vector names; + names.reserve(m_children.size()); + for (const auto& [name, _] : m_children) { + names.push_back(name); + } + return names; +} + +bool JsonDataNode::hasChildren() { + return !m_children.empty(); +} + +// ======================================== +// EXACT SEARCH IN CHILDREN +// ======================================== + +std::vector JsonDataNode::getChildrenByName(const std::string& name) { + std::vector results; + auto it = m_children.find(name); + if (it != m_children.end()) { + results.push_back(it->second.get()); + } + return results; +} + +bool JsonDataNode::hasChildrenByName(const std::string& name) const { + return m_children.find(name) != m_children.end(); +} + +IDataNode* JsonDataNode::getFirstChildByName(const std::string& name) { + auto it = m_children.find(name); + if (it != m_children.end()) { + return it->second.get(); + } + return nullptr; +} + +// ======================================== +// PATTERN MATCHING SEARCH +// ======================================== + +bool JsonDataNode::matchesPattern(const std::string& text, const std::string& pattern) const { + // Convert wildcard pattern to regex + std::string regexPattern = pattern; + + // Escape special regex characters except * + regexPattern = std::regex_replace(regexPattern, std::regex("\\."), "\\."); + regexPattern = std::regex_replace(regexPattern, std::regex("\\+"), "\\+"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\?"), "\\?"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\["), "\\["); + regexPattern = std::regex_replace(regexPattern, std::regex("\\]"), "\\]"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\^"), "\\^"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\$"), "\\$"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\("), "\\("); + regexPattern = std::regex_replace(regexPattern, std::regex("\\)"), "\\)"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\{"), "\\{"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\}"), "\\}"); + regexPattern = std::regex_replace(regexPattern, std::regex("\\|"), "\\|"); + + // Convert * to .* + regexPattern = std::regex_replace(regexPattern, std::regex("\\*"), ".*"); + + // Match entire string + std::regex re("^" + regexPattern + "$"); + return std::regex_match(text, re); +} + +void JsonDataNode::collectMatchingNodes(const std::string& pattern, std::vector& results) { + // Check this node + if (matchesPattern(m_name, pattern)) { + results.push_back(this); + } + + // Recursively check children + for (auto& [name, child] : m_children) { + child->collectMatchingNodes(pattern, results); + } +} + +std::vector JsonDataNode::getChildrenByNameMatch(const std::string& pattern) { + std::vector results; + collectMatchingNodes(pattern, results); + return results; +} + +bool JsonDataNode::hasChildrenByNameMatch(const std::string& pattern) const { + // Cast away const for search (doesn't modify state) + JsonDataNode* mutableThis = const_cast(this); + std::vector results; + mutableThis->collectMatchingNodes(pattern, results); + return !results.empty(); +} + +IDataNode* JsonDataNode::getFirstChildByNameMatch(const std::string& pattern) { + std::vector results; + collectMatchingNodes(pattern, results); + return results.empty() ? nullptr : results[0]; +} + +// ======================================== +// QUERY BY PROPERTIES +// ======================================== + +void JsonDataNode::collectNodesByProperty(const std::string& propName, + const std::function& predicate, + std::vector& results) { + // Check this node + if (hasProperty(propName)) { + auto value = getData(); + if (value->has(propName)) { + auto propValue = value->get(propName); + if (predicate(*propValue)) { + results.push_back(this); + } + } + } + + // Recursively check children + for (auto& [name, child] : m_children) { + child->collectNodesByProperty(propName, predicate, results); + } +} + +std::vector JsonDataNode::queryByProperty(const std::string& propName, + const std::function& predicate) { + std::vector results; + collectNodesByProperty(propName, predicate, results); + return results; +} + +// ======================================== +// NODE'S OWN DATA +// ======================================== + +std::unique_ptr JsonDataNode::getData() const { + return std::make_unique(m_data); +} + +bool JsonDataNode::hasData() const { + return !m_data.is_null() && !m_data.empty(); +} + +void JsonDataNode::setData(std::unique_ptr data) { + checkReadOnly(); + + // Extract JSON from JsonDataValue + if (auto* jsonValue = dynamic_cast(data.get())) { + m_data = jsonValue->getJson(); + } else { + throw std::runtime_error("JsonDataNode requires JsonDataValue"); + } +} + +// ======================================== +// TYPED DATA ACCESS +// ======================================== + +std::string JsonDataNode::getString(const std::string& name, const std::string& defaultValue) const { + if (!m_data.is_object() || !m_data.contains(name)) { + return defaultValue; + } + const auto& val = m_data[name]; + return val.is_string() ? val.get() : defaultValue; +} + +int JsonDataNode::getInt(const std::string& name, int defaultValue) const { + if (!m_data.is_object() || !m_data.contains(name)) { + return defaultValue; + } + const auto& val = m_data[name]; + return val.is_number() ? val.get() : defaultValue; +} + +double JsonDataNode::getDouble(const std::string& name, double defaultValue) const { + if (!m_data.is_object() || !m_data.contains(name)) { + return defaultValue; + } + const auto& val = m_data[name]; + return val.is_number() ? val.get() : defaultValue; +} + +bool JsonDataNode::getBool(const std::string& name, bool defaultValue) const { + if (!m_data.is_object() || !m_data.contains(name)) { + return defaultValue; + } + const auto& val = m_data[name]; + return val.is_boolean() ? val.get() : defaultValue; +} + +bool JsonDataNode::hasProperty(const std::string& name) const { + return m_data.is_object() && m_data.contains(name); +} + +// ======================================== +// HASH SYSTEM +// ======================================== + +std::string JsonDataNode::computeHash(const std::string& input) const { + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256(reinterpret_cast(input.c_str()), input.length(), hash); + + std::stringstream ss; + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { + ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); + } + return ss.str(); +} + +std::string JsonDataNode::getDataHash() { + std::string dataStr = m_data.dump(); + return computeHash(dataStr); +} + +std::string JsonDataNode::getTreeHash() { + // Combine data hash with all children hashes + std::string combined = getDataHash(); + for (const auto& [name, child] : m_children) { + combined += name + ":" + child->getTreeHash(); + } + return computeHash(combined); +} + +std::string JsonDataNode::getSubtreeHash(const std::string& childPath) { + // Parse path and navigate + size_t pos = childPath.find('/'); + if (pos == std::string::npos) { + // Direct child + auto it = m_children.find(childPath); + if (it != m_children.end()) { + return it->second->getTreeHash(); + } + return ""; + } else { + // Nested path + std::string firstPart = childPath.substr(0, pos); + std::string rest = childPath.substr(pos + 1); + auto it = m_children.find(firstPart); + if (it != m_children.end()) { + return it->second->getSubtreeHash(rest); + } + return ""; + } +} + +// ======================================== +// METADATA +// ======================================== + +std::string JsonDataNode::getPath() const { + if (m_parent == nullptr) { + return m_name; + } + std::string parentPath = m_parent->getPath(); + return parentPath.empty() ? m_name : parentPath + "/" + m_name; +} + +std::string JsonDataNode::getName() const { + return m_name; +} + +std::string JsonDataNode::getNodeType() const { + return "JsonDataNode"; +} + +// ======================================== +// TREE MODIFICATION +// ======================================== + +void JsonDataNode::checkReadOnly() const { + if (m_readOnly) { + throw std::runtime_error("Cannot modify read-only node: " + getPath()); + } +} + +void JsonDataNode::setChild(const std::string& name, std::unique_ptr node) { + checkReadOnly(); + + // Extract JsonDataNode + if (auto* jsonNode = dynamic_cast(node.get())) { + auto newNode = std::make_unique( + jsonNode->getName(), + jsonNode->getJsonData(), + this, + m_readOnly // Inherit read-only status + ); + + // Copy children recursively + for (const auto& [childName, child] : jsonNode->getChildren()) { + newNode->setChild(childName, std::make_unique( + child->getName(), + child->getJsonData(), + newNode.get(), + m_readOnly + )); + } + + m_children[name] = std::move(newNode); + } else { + throw std::runtime_error("JsonDataNode requires JsonDataNode child"); + } +} + +bool JsonDataNode::removeChild(const std::string& name) { + checkReadOnly(); + return m_children.erase(name) > 0; +} + +void JsonDataNode::clearChildren() { + checkReadOnly(); + m_children.clear(); +} + +} // namespace grove diff --git a/src/JsonDataTree.cpp b/src/JsonDataTree.cpp new file mode 100644 index 0000000..3e93b08 --- /dev/null +++ b/src/JsonDataTree.cpp @@ -0,0 +1,352 @@ +#include "grove/JsonDataTree.h" +#include +#include +#include + +namespace grove { + +namespace fs = std::filesystem; + +JsonDataTree::JsonDataTree(const std::string& basePath) + : m_basePath(basePath) { + + // Create root node + m_root = std::make_unique("", json::object(), nullptr, false); + + // Load three sub-trees + loadConfigTree(); + loadDataTree(); + initializeRuntimeTree(); + + // Attach to root + m_root->setChild("config", std::move(m_configRoot)); + m_root->setChild("data", std::move(m_dataRoot)); + m_root->setChild("runtime", std::move(m_runtimeRoot)); +} + +// ======================================== +// TREE ACCESS +// ======================================== + +std::unique_ptr JsonDataTree::getRoot() { + // Return copy of root + return std::make_unique("", m_root->getJsonData(), nullptr, false); +} + +std::unique_ptr JsonDataTree::getNode(const std::string& path) { + if (path.empty()) { + return getRoot(); + } + + // Split path and navigate + JsonDataNode* current = m_root.get(); + std::string remaining = path; + + while (!remaining.empty()) { + size_t pos = remaining.find('/'); + std::string part = (pos == std::string::npos) ? remaining : remaining.substr(0, pos); + remaining = (pos == std::string::npos) ? "" : remaining.substr(pos + 1); + + auto child = current->getFirstChildByName(part); + if (!child) { + return nullptr; + } + current = static_cast(child); + } + + return std::make_unique(current->getName(), + current->getJsonData(), + nullptr, + false); +} + +// ======================================== +// SEPARATE ROOTS +// ======================================== + +std::unique_ptr JsonDataTree::getConfigRoot() { + auto configNode = m_root->getFirstChildByName("config"); + if (!configNode) { + return nullptr; + } + auto* jsonNode = static_cast(configNode); + return std::make_unique(jsonNode->getName(), + jsonNode->getJsonData(), + nullptr, + true); // Read-only +} + +std::unique_ptr JsonDataTree::getDataRoot() { + auto dataNode = m_root->getFirstChildByName("data"); + if (!dataNode) { + return nullptr; + } + auto* jsonNode = static_cast(dataNode); + return std::make_unique(jsonNode->getName(), + jsonNode->getJsonData(), + nullptr, + false); +} + +std::unique_ptr JsonDataTree::getRuntimeRoot() { + auto runtimeNode = m_root->getFirstChildByName("runtime"); + if (!runtimeNode) { + return nullptr; + } + auto* jsonNode = static_cast(runtimeNode); + return std::make_unique(jsonNode->getName(), + jsonNode->getJsonData(), + nullptr, + false); +} + +// ======================================== +// SAVE OPERATIONS +// ======================================== + +bool JsonDataTree::saveData() { + try { + std::string dataPath = m_basePath + "/data"; + auto dataNode = m_root->getFirstChildByName("data"); + if (!dataNode) { + return false; + } + + auto* jsonNode = static_cast(dataNode); + json dataJson = nodeToJson(jsonNode); + + // Save each top-level child as separate file + for (const auto& [name, child] : jsonNode->getChildren()) { + std::string filePath = dataPath + "/" + name + ".json"; + json childJson = nodeToJson(child.get()); + if (!saveJsonFile(filePath, childJson)) { + return false; + } + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to save data: " << e.what() << std::endl; + return false; + } +} + +bool JsonDataTree::saveNode(const std::string& path) { + // Only allow saving data/ paths + if (path.find("data/") != 0) { + std::cerr << "Can only save nodes under data/: " << path << std::endl; + return false; + } + + try { + auto node = getNode(path); + if (!node) { + return false; + } + + // Extract filename from path + size_t lastSlash = path.find_last_of('/'); + std::string filename = (lastSlash == std::string::npos) ? path : path.substr(lastSlash + 1); + std::string filePath = m_basePath + "/" + path + ".json"; + + auto* jsonNode = dynamic_cast(node.get()); + if (!jsonNode) { + return false; + } + + json nodeJson = nodeToJson(jsonNode); + return saveJsonFile(filePath, nodeJson); + } catch (const std::exception& e) { + std::cerr << "Failed to save node " << path << ": " << e.what() << std::endl; + return false; + } +} + +// ======================================== +// HOT-RELOAD +// ======================================== + +bool JsonDataTree::checkForChanges() { + std::string configPath = m_basePath + "/config"; + + try { + for (const auto& [filePath, lastTime] : m_configFileTimes) { + if (!fs::exists(filePath)) { + return true; // File deleted + } + + auto currentTime = fs::last_write_time(filePath); + if (currentTime != lastTime) { + return true; // File modified + } + } + + // Check for new files + if (fs::exists(configPath)) { + for (const auto& entry : fs::directory_iterator(configPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + if (m_configFileTimes.find(entry.path().string()) == m_configFileTimes.end()) { + return true; // New file + } + } + } + } + + return false; + } catch (const std::exception& e) { + std::cerr << "Error checking for changes: " << e.what() << std::endl; + return false; + } +} + +bool JsonDataTree::reloadIfChanged() { + if (!checkForChanges()) { + return false; + } + + try { + loadConfigTree(); + + // Trigger callbacks + for (auto& callback : m_reloadCallbacks) { + callback(); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to reload config: " << e.what() << std::endl; + return false; + } +} + +void JsonDataTree::onTreeReloaded(std::function callback) { + m_reloadCallbacks.push_back(callback); +} + +// ======================================== +// METADATA +// ======================================== + +std::string JsonDataTree::getType() { + return "JsonDataTree"; +} + +// ======================================== +// HELPER METHODS +// ======================================== + +void JsonDataTree::loadConfigTree() { + std::string configPath = m_basePath + "/config"; + m_configRoot = std::make_unique("config", json::object(), nullptr, true); + + if (fs::exists(configPath) && fs::is_directory(configPath)) { + scanDirectory(configPath, m_configRoot.get(), true); + updateFileTimestamps(configPath); + } +} + +void JsonDataTree::loadDataTree() { + std::string dataPath = m_basePath + "/data"; + m_dataRoot = std::make_unique("data", json::object(), nullptr, false); + + if (fs::exists(dataPath) && fs::is_directory(dataPath)) { + scanDirectory(dataPath, m_dataRoot.get(), false); + } else { + // Create data directory if it doesn't exist + fs::create_directories(dataPath); + } +} + +void JsonDataTree::initializeRuntimeTree() { + m_runtimeRoot = std::make_unique("runtime", json::object(), nullptr, false); +} + +void JsonDataTree::scanDirectory(const std::string& dirPath, JsonDataNode* parentNode, bool readOnly) { + for (const auto& entry : fs::directory_iterator(dirPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + std::string filename = entry.path().stem().string(); + json data = loadJsonFile(entry.path().string()); + buildNodeFromJson(filename, data, parentNode, readOnly); + } else if (entry.is_directory()) { + // Create child node for subdirectory + auto childNode = std::make_unique( + entry.path().filename().string(), + json::object(), + parentNode, + readOnly + ); + scanDirectory(entry.path().string(), childNode.get(), readOnly); + parentNode->setChild(entry.path().filename().string(), std::move(childNode)); + } + } +} + +json JsonDataTree::loadJsonFile(const std::string& filePath) { + std::ifstream file(filePath); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filePath); + } + + json data; + file >> data; + return data; +} + +bool JsonDataTree::saveJsonFile(const std::string& filePath, const json& data) { + try { + // Ensure directory exists + fs::path path(filePath); + if (path.has_parent_path()) { + fs::create_directories(path.parent_path()); + } + + std::ofstream file(filePath); + if (!file.is_open()) { + return false; + } + + file << data.dump(2); // Pretty print with 2-space indent + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to save JSON file: " << e.what() << std::endl; + return false; + } +} + +void JsonDataTree::buildNodeFromJson(const std::string& name, const json& data, JsonDataNode* parentNode, bool readOnly) { + auto node = std::make_unique(name, data, parentNode, readOnly); + + // If data is an object with children, create child nodes + if (data.is_object()) { + for (auto& [key, value] : data.items()) { + if (value.is_object() || value.is_array()) { + buildNodeFromJson(key, value, node.get(), readOnly); + } + } + } + + parentNode->setChild(name, std::move(node)); +} + +json JsonDataTree::nodeToJson(const JsonDataNode* node) { + json result = node->getJsonData(); + + // Add children + for (const auto& [name, child] : node->getChildren()) { + result[name] = nodeToJson(child.get()); + } + + return result; +} + +void JsonDataTree::updateFileTimestamps(const std::string& dirPath) { + m_configFileTimes.clear(); + + for (const auto& entry : fs::directory_iterator(dirPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + m_configFileTimes[entry.path().string()] = fs::last_write_time(entry.path()); + } + } +} + +} // namespace grove diff --git a/src/JsonDataValue.cpp b/src/JsonDataValue.cpp new file mode 100644 index 0000000..e691e99 --- /dev/null +++ b/src/JsonDataValue.cpp @@ -0,0 +1,97 @@ +#include "grove/JsonDataValue.h" + +namespace grove { + +JsonDataValue::JsonDataValue(const json& value) : m_value(value) {} + +JsonDataValue::JsonDataValue(json&& value) : m_value(std::move(value)) {} + +// Type checking +bool JsonDataValue::isNull() const { + return m_value.is_null(); +} + +bool JsonDataValue::isBool() const { + return m_value.is_boolean(); +} + +bool JsonDataValue::isNumber() const { + return m_value.is_number(); +} + +bool JsonDataValue::isString() const { + return m_value.is_string(); +} + +bool JsonDataValue::isArray() const { + return m_value.is_array(); +} + +bool JsonDataValue::isObject() const { + return m_value.is_object(); +} + +// Value access with defaults +bool JsonDataValue::asBool(bool defaultValue) const { + if (!m_value.is_boolean()) { + return defaultValue; + } + return m_value.get(); +} + +int JsonDataValue::asInt(int defaultValue) const { + if (!m_value.is_number()) { + return defaultValue; + } + return m_value.get(); +} + +double JsonDataValue::asDouble(double defaultValue) const { + if (!m_value.is_number()) { + return defaultValue; + } + return m_value.get(); +} + +std::string JsonDataValue::asString(const std::string& defaultValue) const { + if (!m_value.is_string()) { + return defaultValue; + } + return m_value.get(); +} + +// Array/Object access +size_t JsonDataValue::size() const { + if (m_value.is_array() || m_value.is_object()) { + return m_value.size(); + } + return 0; +} + +std::unique_ptr JsonDataValue::get(size_t index) const { + if (!m_value.is_array() || index >= m_value.size()) { + return std::make_unique(json(nullptr)); + } + return std::make_unique(m_value[index]); +} + +std::unique_ptr JsonDataValue::get(const std::string& key) const { + if (!m_value.is_object() || !m_value.contains(key)) { + return std::make_unique(json(nullptr)); + } + return std::make_unique(m_value[key]); +} + +bool JsonDataValue::has(const std::string& key) const { + if (!m_value.is_object()) { + return false; + } + return m_value.contains(key); +} + +// Serialization +std::string JsonDataValue::toString() const { + return m_value.dump(); +} + +} // namespace grove diff --git a/src/ModuleFactory.cpp b/src/ModuleFactory.cpp index 0df0228..3b1fab4 100644 --- a/src/ModuleFactory.cpp +++ b/src/ModuleFactory.cpp @@ -7,7 +7,7 @@ namespace fs = std::filesystem; -namespace warfactory { +namespace grove { ModuleFactory::ModuleFactory() { // Create logger with file and console output @@ -506,4 +506,4 @@ void ModuleFactory::logModuleError(const std::string& operation, const std::stri logger->error("❌ Module {} error: {}", operation, details); } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/ModuleSystemFactory.cpp b/src/ModuleSystemFactory.cpp index ab31fdb..a8e3b0b 100644 --- a/src/ModuleSystemFactory.cpp +++ b/src/ModuleSystemFactory.cpp @@ -10,7 +10,7 @@ // #include "ThreadPoolModuleSystem.h" // #include "ClusterModuleSystem.h" -namespace warfactory { +namespace grove { std::unique_ptr ModuleSystemFactory::create(const std::string& strategy) { auto logger = getFactoryLogger(); @@ -236,4 +236,4 @@ int ModuleSystemFactory::detectCpuCores() { return cores; } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/ResourceRegistry.cpp b/src/ResourceRegistry.cpp index cf3e35c..923988d 100644 --- a/src/ResourceRegistry.cpp +++ b/src/ResourceRegistry.cpp @@ -1,7 +1,7 @@ #include #include -namespace warfactory { +namespace grove { // Static member initialization std::unique_ptr ResourceRegistry::instance = nullptr; @@ -117,4 +117,4 @@ void ResourceRegistry::clear() { next_id = 1; } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file diff --git a/src/SequentialModuleSystem.cpp b/src/SequentialModuleSystem.cpp index 81def4a..398c3fc 100644 --- a/src/SequentialModuleSystem.cpp +++ b/src/SequentialModuleSystem.cpp @@ -3,7 +3,7 @@ #include #include -namespace warfactory { +namespace grove { SequentialModuleSystem::SequentialModuleSystem() { // Create logger with file and console output @@ -273,4 +273,4 @@ void SequentialModuleSystem::validateModule() const { } } -} // namespace warfactory \ No newline at end of file +} // namespace grove \ No newline at end of file