feat: Implement complete IDataNode/IDataTree system with JSON backend
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 <noreply@anthropic.com>
This commit is contained in:
parent
c01e00559b
commit
fad105afb2
@ -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)
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
class SerializationRegistry;
|
||||
|
||||
@ -26,4 +26,4 @@ protected:
|
||||
void unregisterFromSerialization();
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <memory>
|
||||
#include "IDataTree.h"
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
/**
|
||||
* @brief Factory for creating data tree instances
|
||||
@ -20,4 +20,4 @@ public:
|
||||
static std::unique_ptr<IDataTree> create(const std::string& type, const std::string& sourcePath);
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -6,7 +6,7 @@
|
||||
#include <functional>
|
||||
#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<IDataNode> 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
|
||||
} // namespace grove
|
||||
@ -5,12 +5,18 @@
|
||||
#include <functional>
|
||||
#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<IDataNode> 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<IDataNode> 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<IDataNode> 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<IDataNode> 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<IDataNode> 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<void()> callback) = 0;
|
||||
|
||||
@ -66,4 +126,4 @@ public:
|
||||
virtual std::string getType() = 0;
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
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
|
||||
|
||||
@ -4,12 +4,12 @@
|
||||
#include <memory>
|
||||
|
||||
// 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
|
||||
} // namespace grove
|
||||
@ -6,7 +6,7 @@
|
||||
#include <memory>
|
||||
#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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
||||
} // namespace grove
|
||||
@ -3,7 +3,7 @@
|
||||
#include "IDataNode.h"
|
||||
#include <memory>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
class ISerializable {
|
||||
public:
|
||||
@ -13,4 +13,4 @@ public:
|
||||
virtual void deserialize(const IDataNode& data) = 0;
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <memory>
|
||||
#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<IDataNode> getCompletedTask() = 0;
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@ -126,4 +126,4 @@ public:
|
||||
virtual void setState(const json& state) = 0;
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@ -337,4 +337,4 @@ constexpr const char* toString(Orientation orient) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -13,7 +13,7 @@
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
/**
|
||||
* @brief ImGui implementation of IUI interface
|
||||
@ -704,4 +704,4 @@ private:
|
||||
void renderLogConsole();
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
108
include/grove/JsonDataNode.h
Normal file
108
include/grove/JsonDataNode.h
Normal file
@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include "IDataNode.h"
|
||||
#include "JsonDataValue.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
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<IDataNode> getChild(const std::string& name) override;
|
||||
std::vector<std::string> getChildNames() override;
|
||||
bool hasChildren() override;
|
||||
|
||||
// Exact search in children
|
||||
std::vector<IDataNode*> 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<IDataNode*> 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<IDataNode*> queryByProperty(const std::string& propName,
|
||||
const std::function<bool(const IDataValue&)>& predicate) override;
|
||||
|
||||
// Node's own data
|
||||
std::unique_ptr<IDataValue> getData() const override;
|
||||
bool hasData() const override;
|
||||
void setData(std::unique_ptr<IDataValue> 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<IDataNode> 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<std::string, std::unique_ptr<JsonDataNode>>& getChildren() const { return m_children; }
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
json m_data;
|
||||
JsonDataNode* m_parent;
|
||||
bool m_readOnly;
|
||||
std::map<std::string, std::unique_ptr<JsonDataNode>> m_children;
|
||||
|
||||
// Helper methods
|
||||
bool matchesPattern(const std::string& text, const std::string& pattern) const;
|
||||
void collectMatchingNodes(const std::string& pattern, std::vector<IDataNode*>& results);
|
||||
void collectNodesByProperty(const std::string& propName,
|
||||
const std::function<bool(const IDataValue&)>& predicate,
|
||||
std::vector<IDataNode*>& results);
|
||||
std::string computeHash(const std::string& input) const;
|
||||
void checkReadOnly() const;
|
||||
};
|
||||
|
||||
} // namespace grove
|
||||
86
include/grove/JsonDataTree.h
Normal file
86
include/grove/JsonDataTree.h
Normal file
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include "IDataTree.h"
|
||||
#include "JsonDataNode.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
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<IDataNode> getRoot() override;
|
||||
std::unique_ptr<IDataNode> getNode(const std::string& path) override;
|
||||
|
||||
// Separate roots
|
||||
std::unique_ptr<IDataNode> getConfigRoot() override;
|
||||
std::unique_ptr<IDataNode> getDataRoot() override;
|
||||
std::unique_ptr<IDataNode> 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<void()> callback) override;
|
||||
|
||||
// Metadata
|
||||
std::string getType() override;
|
||||
|
||||
private:
|
||||
std::string m_basePath;
|
||||
std::unique_ptr<JsonDataNode> m_root;
|
||||
std::unique_ptr<JsonDataNode> m_configRoot;
|
||||
std::unique_ptr<JsonDataNode> m_dataRoot;
|
||||
std::unique_ptr<JsonDataNode> m_runtimeRoot;
|
||||
|
||||
std::map<std::string, std::filesystem::file_time_type> m_configFileTimes;
|
||||
std::vector<std::function<void()>> 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
|
||||
53
include/grove/JsonDataValue.h
Normal file
53
include/grove/JsonDataValue.h
Normal file
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "IDataValue.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<IDataValue> get(size_t index) const override;
|
||||
std::unique_ptr<IDataValue> 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
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -3,7 +3,7 @@
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
/**
|
||||
* @brief Centralized random number generator singleton
|
||||
@ -86,4 +86,4 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -6,7 +6,7 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
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
|
||||
} // namespace grove
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
class ASerializable;
|
||||
|
||||
@ -35,4 +35,4 @@ public:
|
||||
void clear();
|
||||
};
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
15
src/DataTreeFactory.cpp
Normal file
15
src/DataTreeFactory.cpp
Normal file
@ -0,0 +1,15 @@
|
||||
#include "grove/DataTreeFactory.h"
|
||||
#include "grove/JsonDataTree.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace grove {
|
||||
|
||||
std::unique_ptr<IDataTree> DataTreeFactory::create(const std::string& type, const std::string& sourcePath) {
|
||||
if (type == "json") {
|
||||
return std::make_unique<JsonDataTree>(sourcePath);
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown data tree type: " + type);
|
||||
}
|
||||
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
|
||||
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
|
||||
} // namespace grove
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
std::unique_ptr<IEngine> 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
|
||||
} // namespace grove
|
||||
@ -11,7 +11,7 @@
|
||||
// #include "LocalIO.h"
|
||||
// #include "NetworkIO.h"
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
std::unique_ptr<IIO> 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
|
||||
} // namespace grove
|
||||
@ -3,7 +3,7 @@
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
// ========================================
|
||||
// IUI INTERFACE IMPLEMENTATION - REQUESTS & EVENTS
|
||||
@ -543,4 +543,4 @@ void ImGuiUI::renderLogConsole() {
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -6,7 +6,7 @@
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
// Factory function for IntraIOManager to avoid circular include
|
||||
std::shared_ptr<IntraIO> createIntraIOInstance(const std::string& instanceId) {
|
||||
@ -481,4 +481,4 @@ const std::string& IntraIO::getInstanceId() const {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -4,7 +4,7 @@
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
IntraIOManager::IntraIOManager() {
|
||||
// Create logger
|
||||
@ -266,4 +266,4 @@ IntraIOManager& IntraIOManager::getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
343
src/JsonDataNode.cpp
Normal file
343
src/JsonDataNode.cpp
Normal file
@ -0,0 +1,343 @@
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <openssl/sha.h>
|
||||
#include <stdexcept>
|
||||
|
||||
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<IDataNode> 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<JsonDataNode>(it->second->getName(),
|
||||
it->second->getJsonData(),
|
||||
this,
|
||||
it->second->m_readOnly);
|
||||
}
|
||||
|
||||
std::vector<std::string> JsonDataNode::getChildNames() {
|
||||
std::vector<std::string> 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<IDataNode*> JsonDataNode::getChildrenByName(const std::string& name) {
|
||||
std::vector<IDataNode*> 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<IDataNode*>& 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<IDataNode*> JsonDataNode::getChildrenByNameMatch(const std::string& pattern) {
|
||||
std::vector<IDataNode*> 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<JsonDataNode*>(this);
|
||||
std::vector<IDataNode*> results;
|
||||
mutableThis->collectMatchingNodes(pattern, results);
|
||||
return !results.empty();
|
||||
}
|
||||
|
||||
IDataNode* JsonDataNode::getFirstChildByNameMatch(const std::string& pattern) {
|
||||
std::vector<IDataNode*> results;
|
||||
collectMatchingNodes(pattern, results);
|
||||
return results.empty() ? nullptr : results[0];
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// QUERY BY PROPERTIES
|
||||
// ========================================
|
||||
|
||||
void JsonDataNode::collectNodesByProperty(const std::string& propName,
|
||||
const std::function<bool(const IDataValue&)>& predicate,
|
||||
std::vector<IDataNode*>& 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<IDataNode*> JsonDataNode::queryByProperty(const std::string& propName,
|
||||
const std::function<bool(const IDataValue&)>& predicate) {
|
||||
std::vector<IDataNode*> results;
|
||||
collectNodesByProperty(propName, predicate, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NODE'S OWN DATA
|
||||
// ========================================
|
||||
|
||||
std::unique_ptr<IDataValue> JsonDataNode::getData() const {
|
||||
return std::make_unique<JsonDataValue>(m_data);
|
||||
}
|
||||
|
||||
bool JsonDataNode::hasData() const {
|
||||
return !m_data.is_null() && !m_data.empty();
|
||||
}
|
||||
|
||||
void JsonDataNode::setData(std::unique_ptr<IDataValue> data) {
|
||||
checkReadOnly();
|
||||
|
||||
// Extract JSON from JsonDataValue
|
||||
if (auto* jsonValue = dynamic_cast<JsonDataValue*>(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<std::string>() : 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<int>() : 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<double>() : 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<bool>() : 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<const unsigned char*>(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<int>(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<IDataNode> node) {
|
||||
checkReadOnly();
|
||||
|
||||
// Extract JsonDataNode
|
||||
if (auto* jsonNode = dynamic_cast<JsonDataNode*>(node.get())) {
|
||||
auto newNode = std::make_unique<JsonDataNode>(
|
||||
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<JsonDataNode>(
|
||||
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
|
||||
352
src/JsonDataTree.cpp
Normal file
352
src/JsonDataTree.cpp
Normal file
@ -0,0 +1,352 @@
|
||||
#include "grove/JsonDataTree.h"
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
|
||||
namespace grove {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
JsonDataTree::JsonDataTree(const std::string& basePath)
|
||||
: m_basePath(basePath) {
|
||||
|
||||
// Create root node
|
||||
m_root = std::make_unique<JsonDataNode>("", 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<IDataNode> JsonDataTree::getRoot() {
|
||||
// Return copy of root
|
||||
return std::make_unique<JsonDataNode>("", m_root->getJsonData(), nullptr, false);
|
||||
}
|
||||
|
||||
std::unique_ptr<IDataNode> 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<JsonDataNode*>(child);
|
||||
}
|
||||
|
||||
return std::make_unique<JsonDataNode>(current->getName(),
|
||||
current->getJsonData(),
|
||||
nullptr,
|
||||
false);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SEPARATE ROOTS
|
||||
// ========================================
|
||||
|
||||
std::unique_ptr<IDataNode> JsonDataTree::getConfigRoot() {
|
||||
auto configNode = m_root->getFirstChildByName("config");
|
||||
if (!configNode) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* jsonNode = static_cast<JsonDataNode*>(configNode);
|
||||
return std::make_unique<JsonDataNode>(jsonNode->getName(),
|
||||
jsonNode->getJsonData(),
|
||||
nullptr,
|
||||
true); // Read-only
|
||||
}
|
||||
|
||||
std::unique_ptr<IDataNode> JsonDataTree::getDataRoot() {
|
||||
auto dataNode = m_root->getFirstChildByName("data");
|
||||
if (!dataNode) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* jsonNode = static_cast<JsonDataNode*>(dataNode);
|
||||
return std::make_unique<JsonDataNode>(jsonNode->getName(),
|
||||
jsonNode->getJsonData(),
|
||||
nullptr,
|
||||
false);
|
||||
}
|
||||
|
||||
std::unique_ptr<IDataNode> JsonDataTree::getRuntimeRoot() {
|
||||
auto runtimeNode = m_root->getFirstChildByName("runtime");
|
||||
if (!runtimeNode) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* jsonNode = static_cast<JsonDataNode*>(runtimeNode);
|
||||
return std::make_unique<JsonDataNode>(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<JsonDataNode*>(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<JsonDataNode*>(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<void()> 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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>(
|
||||
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<JsonDataNode>(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
|
||||
97
src/JsonDataValue.cpp
Normal file
97
src/JsonDataValue.cpp
Normal file
@ -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<bool>();
|
||||
}
|
||||
|
||||
int JsonDataValue::asInt(int defaultValue) const {
|
||||
if (!m_value.is_number()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return m_value.get<int>();
|
||||
}
|
||||
|
||||
double JsonDataValue::asDouble(double defaultValue) const {
|
||||
if (!m_value.is_number()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return m_value.get<double>();
|
||||
}
|
||||
|
||||
std::string JsonDataValue::asString(const std::string& defaultValue) const {
|
||||
if (!m_value.is_string()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return m_value.get<std::string>();
|
||||
}
|
||||
|
||||
// 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<IDataValue> JsonDataValue::get(size_t index) const {
|
||||
if (!m_value.is_array() || index >= m_value.size()) {
|
||||
return std::make_unique<JsonDataValue>(json(nullptr));
|
||||
}
|
||||
return std::make_unique<JsonDataValue>(m_value[index]);
|
||||
}
|
||||
|
||||
std::unique_ptr<IDataValue> JsonDataValue::get(const std::string& key) const {
|
||||
if (!m_value.is_object() || !m_value.contains(key)) {
|
||||
return std::make_unique<JsonDataValue>(json(nullptr));
|
||||
}
|
||||
return std::make_unique<JsonDataValue>(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
|
||||
@ -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
|
||||
} // namespace grove
|
||||
@ -10,7 +10,7 @@
|
||||
// #include "ThreadPoolModuleSystem.h"
|
||||
// #include "ClusterModuleSystem.h"
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
std::unique_ptr<IModuleSystem> ModuleSystemFactory::create(const std::string& strategy) {
|
||||
auto logger = getFactoryLogger();
|
||||
@ -236,4 +236,4 @@ int ModuleSystemFactory::detectCpuCores() {
|
||||
return cores;
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -1,7 +1,7 @@
|
||||
#include <grove/ResourceRegistry.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
// Static member initialization
|
||||
std::unique_ptr<ResourceRegistry> ResourceRegistry::instance = nullptr;
|
||||
@ -117,4 +117,4 @@ void ResourceRegistry::clear() {
|
||||
next_id = 1;
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
@ -3,7 +3,7 @@
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
|
||||
namespace warfactory {
|
||||
namespace grove {
|
||||
|
||||
SequentialModuleSystem::SequentialModuleSystem() {
|
||||
// Create logger with file and console output
|
||||
@ -273,4 +273,4 @@ void SequentialModuleSystem::validateModule() const {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace warfactory
|
||||
} // namespace grove
|
||||
Loading…
Reference in New Issue
Block a user