From 0fd8deaf534f1234bdca0a2c46b1dfa44cf40e43 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 30 Apr 2026 20:21:18 +0300 Subject: [PATCH] direct save/load action database --- src/features/editScene/CMakeLists.txt | 3 + src/features/editScene/EditorApp.cpp | 11 + .../editScene/components/ActionDatabase.cpp | 351 ++++++++++++++++++ .../editScene/components/ActionDatabase.hpp | 37 ++ .../editScene/systems/EditorUISystem.cpp | 21 ++ src/features/editScene/tests/Ogre.h | 35 +- 6 files changed, 457 insertions(+), 1 deletion(-) diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index aca1c59..bff77ec 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -403,6 +403,7 @@ add_executable(action_db_lua_test target_link_libraries(action_db_lua_test lua flecs::flecs_static + nlohmann_json::nlohmann_json ) target_include_directories(action_db_lua_test PRIVATE @@ -411,6 +412,8 @@ target_include_directories(action_db_lua_test PRIVATE ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src ) + + # Copy local resources (materials, etc.) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources" diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 30fd3d1..8035ada 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -509,6 +509,17 @@ void EditorApp::setup() // Run late setup: load data.lua and initial scripts. m_lua.lateSetup(); + + // Auto-load Action Database from "actions.json" in the General + // resource group. If the file is missing or contains errors + // the error is logged and the database stays empty (scene + // components will still be processed below). + ActionDatabase::loadFromJson("actions.json"); + + // Re-process scene components so that any + // ActionDatabaseComponent entities in the scene override or + // append actions from the file. + ActionDatabase::reloadFromSceneComponents(m_world); } // Game mode can be set externally before setup() is called diff --git a/src/features/editScene/components/ActionDatabase.cpp b/src/features/editScene/components/ActionDatabase.cpp index de98b6c..1200a56 100644 --- a/src/features/editScene/components/ActionDatabase.cpp +++ b/src/features/editScene/components/ActionDatabase.cpp @@ -1,4 +1,13 @@ #include "ActionDatabase.hpp" +#ifndef OGRE_STUB_H +#include +#include +#endif + +#include +#include +#include +#include // --------------------------------------------------------------------------- // Singleton @@ -162,3 +171,345 @@ void ActionDatabaseComponent::syncToSingleton() const for (const auto &goal : goals) db.addOrReplaceGoal(goal); } + +// --------------------------------------------------------------------------- +// JSON serialization helpers (local to this translation unit) +// --------------------------------------------------------------------------- + +static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb) +{ + nlohmann::json json; + json["bits"] = (uint64_t)bb.bits; + json["mask"] = (uint64_t)bb.mask; + if (bb.bitmask != ~0ULL) + json["bitmask"] = (uint64_t)bb.bitmask; + if (!bb.values.empty()) { + json["values"] = nlohmann::json::object(); + for (const auto &pair : bb.values) + json["values"][pair.first] = pair.second; + } + if (!bb.floatValues.empty()) { + json["floatValues"] = nlohmann::json::object(); + for (const auto &pair : bb.floatValues) + json["floatValues"][pair.first] = pair.second; + } + if (!bb.vec3Values.empty()) { + json["vec3Values"] = nlohmann::json::object(); + for (const auto &pair : bb.vec3Values) { + nlohmann::json v; + v.push_back(pair.second.x); + v.push_back(pair.second.y); + v.push_back(pair.second.z); + json["vec3Values"][pair.first] = v; + } + } + return json; +} + +static void deserializeGoapBlackboard(GoapBlackboard &bb, + const nlohmann::json &json) +{ + bb.bits = json.value("bits", (uint64_t)0); + bb.mask = json.value("mask", (uint64_t)0); + bb.bitmask = json.value("bitmask", ~0ULL); + bb.values.clear(); + if (json.contains("values") && json["values"].is_object()) { + for (auto &[key, val] : json["values"].items()) + bb.values[key] = val.get(); + } + bb.floatValues.clear(); + if (json.contains("floatValues") && json["floatValues"].is_object()) { + for (auto &[key, val] : json["floatValues"].items()) + bb.floatValues[key] = val.get(); + } + bb.vec3Values.clear(); + if (json.contains("vec3Values") && json["vec3Values"].is_object()) { + for (auto &[key, val] : json["vec3Values"].items()) { + if (val.is_array() && val.size() >= 3) + bb.vec3Values[key] = + Ogre::Vector3(val[0].get(), + val[1].get(), + val[2].get()); + } + } +} + +static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node) +{ + nlohmann::json json; + json["type"] = node.type; + if (!node.name.empty()) + json["name"] = node.name; + if (!node.params.empty()) + json["params"] = node.params; + if (!node.children.empty()) { + json["children"] = nlohmann::json::array(); + for (const auto &child : node.children) + json["children"].push_back( + serializeBehaviorTreeNode(child)); + } + return json; +} + +static void deserializeBehaviorTreeNode(BehaviorTreeNode &node, + const nlohmann::json &json) +{ + node.type = json.value("type", "task"); + node.name = json.value("name", ""); + node.params = json.value("params", ""); + node.children.clear(); + if (json.contains("children") && json["children"].is_array()) { + for (const auto &childJson : json["children"]) { + BehaviorTreeNode child; + deserializeBehaviorTreeNode(child, childJson); + node.children.push_back(child); + } + } +} + +static nlohmann::json serializeGoapAction(const GoapAction &action) +{ + nlohmann::json json; + json["name"] = action.name; + json["cost"] = action.cost; + json["preconditions"] = serializeGoapBlackboard(action.preconditions); + json["effects"] = serializeGoapBlackboard(action.effects); + if (action.preconditionMask != ~0ULL) + json["preconditionMask"] = action.preconditionMask; + json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree); + if (!action.behaviorTreeName.empty()) + json["behaviorTreeName"] = action.behaviorTreeName; + return json; +} + +static void deserializeGoapAction(GoapAction &action, + const nlohmann::json &json) +{ + action.name = json.value("name", "Unnamed"); + action.cost = json.value("cost", 1); + if (json.contains("preconditions")) + deserializeGoapBlackboard(action.preconditions, + json["preconditions"]); + if (json.contains("effects")) + deserializeGoapBlackboard(action.effects, json["effects"]); + action.preconditionMask = json.value("preconditionMask", ~0ULL); + if (json.contains("behaviorTree")) + deserializeBehaviorTreeNode(action.behaviorTree, + json["behaviorTree"]); + action.behaviorTreeName = json.value("behaviorTreeName", ""); +} + +static nlohmann::json serializeGoapGoal(const GoapGoal &goal) +{ + nlohmann::json json; + json["name"] = goal.name; + json["priority"] = goal.priority; + json["target"] = serializeGoapBlackboard(goal.target); + if (!goal.condition.empty()) + json["condition"] = goal.condition; + return json; +} + +static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json) +{ + goal.name = json.value("name", "Unnamed"); + goal.priority = json.value("priority", 1); + if (json.contains("target")) + deserializeGoapBlackboard(goal.target, json["target"]); + goal.condition = json.value("condition", ""); +} + +// --------------------------------------------------------------------------- +// saveToJson / loadFromJson +// --------------------------------------------------------------------------- + +bool ActionDatabase::saveToJson(const std::string &filename) +{ + try { + // Resolve the filesystem path from the "General" resource group + Ogre::ResourceGroupManager &rgm = + Ogre::ResourceGroupManager::getSingleton(); + const Ogre::ResourceGroupManager::LocationList &locations = + rgm.getResourceLocationList("General"); + if (locations.empty()) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::saveToJson: " + "no resource locations for group 'General'"); + return false; + } + // Use the first location's path + std::string dir = locations.begin()->archive->getName(); + + std::string filepath = dir + "/" + filename; + + // Backup existing file + if (std::filesystem::exists(filepath)) { + std::string backup = filepath + ".bak"; + try { + std::filesystem::copy_file( + filepath, backup, + std::filesystem::copy_options:: + overwrite_existing); + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::saveToJson: " + "backup failed: " + + Ogre::String(e.what())); + } + } + + const ActionDatabase &db = getSingleton(); + nlohmann::json root; + + root["actions"] = nlohmann::json::array(); + for (const auto &action : db.actions) + root["actions"].push_back(serializeGoapAction(action)); + + root["goals"] = nlohmann::json::array(); + for (const auto &goal : db.goals) + root["goals"].push_back(serializeGoapGoal(goal)); + + // Save bit names + nlohmann::json bitNames = nlohmann::json::array(); + for (int i = 0; i < 64; i++) { + const char *name = GoapBlackboard::getBitName(i); + if (name) { + nlohmann::json entry; + entry["index"] = i; + entry["name"] = name; + bitNames.push_back(entry); + } + } + if (!bitNames.empty()) + root["bitNames"] = bitNames; + + std::ofstream file(filepath); + if (!file.is_open()) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::saveToJson: " + "failed to open " + + filepath); + return false; + } + file << root.dump(4); + file.close(); + + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase saved to " + filepath); + return true; + + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::saveToJson error: " + + Ogre::String(e.what())); + return false; + } +} + +bool ActionDatabase::loadFromJson(const std::string &filename) +{ + try { + // Resolve the filesystem path from the "General" resource group + Ogre::ResourceGroupManager &rgm = + Ogre::ResourceGroupManager::getSingleton(); + const Ogre::ResourceGroupManager::LocationList &locations = + rgm.getResourceLocationList("General"); + if (locations.empty()) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::loadFromJson: " + "no resource locations for group 'General'"); + return false; + } + std::string dir = locations.begin()->archive->getName(); + + std::string filepath = dir + "/" + filename; + + if (!std::filesystem::exists(filepath)) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::loadFromJson: " + "file not found: " + + filepath); + return false; + } + + std::ifstream file(filepath); + if (!file.is_open()) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::loadFromJson: " + "failed to open " + + filepath); + return false; + } + + nlohmann::json root; + try { + file >> root; + } catch (const nlohmann::json::parse_error &e) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::loadFromJson: " + "JSON parse error in " + + filepath + ": " + Ogre::String(e.what())); + return false; + } + file.close(); + + ActionDatabase &db = getSingleton(); + + // Load actions (add/replace) + if (root.contains("actions") && root["actions"].is_array()) { + for (const auto &actionJson : root["actions"]) { + GoapAction action; + deserializeGoapAction(action, actionJson); + db.addOrReplaceAction(action); + } + } + + // Load goals (add/replace) + if (root.contains("goals") && root["goals"].is_array()) { + for (const auto &goalJson : root["goals"]) { + GoapGoal goal; + deserializeGoapGoal(goal, goalJson); + db.addOrReplaceGoal(goal); + } + } + + // Load bit names + if (root.contains("bitNames") && root["bitNames"].is_array()) { + for (const auto &entry : root["bitNames"]) { + if (entry.contains("index") && + entry.contains("name")) + GoapBlackboard::setBitName( + entry["index"].get(), + entry["name"] + .get()); + } + } + + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase loaded from " + filepath); + return true; + + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "ActionDatabase::loadFromJson error: " + + Ogre::String(e.what())); + return false; + } +} + +// --------------------------------------------------------------------------- +// reloadFromSceneComponents +// --------------------------------------------------------------------------- + +void ActionDatabase::reloadFromSceneComponents(flecs::world &world) +{ + // First, load from file (if available) — this is done by the caller + // before calling this function. Here we just re-sync from scene + // entities so that scene-defined actions are applied on top. + + // Iterate all entities with ActionDatabaseComponent + world.each([](flecs::entity e, ActionDatabaseComponent &dbComp) { + (void)e; + dbComp.syncToSingleton(); + }); +} diff --git a/src/features/editScene/components/ActionDatabase.hpp b/src/features/editScene/components/ActionDatabase.hpp index 7abf569..47131fa 100644 --- a/src/features/editScene/components/ActionDatabase.hpp +++ b/src/features/editScene/components/ActionDatabase.hpp @@ -6,6 +6,13 @@ #include "GoapGoal.hpp" #include #include +#include + +// Forward declaration for reloadFromSceneComponents +namespace flecs +{ +class world; +} /** * Global action database singleton. @@ -55,6 +62,36 @@ public: // Clear all actions and goals void clear(); + /** + * Save the action database to a JSON file. + * Creates a backup of the existing file (if any) by appending ".bak". + * The file is written to the filesystem path resolved from the + * "General" resource group. + * + * @param filename The filename (e.g. "actions.json"). + * @return true on success. + */ + static bool saveToJson(const std::string &filename); + + /** + * Load the action database from a JSON file. + * The file is located via the "General" resource group. + * On failure (file not found, parse error) the error is logged + * and the database is left unchanged. + * + * @param filename The filename (e.g. "actions.json"). + * @return true on success. + */ + static bool loadFromJson(const std::string &filename); + + /** + * Re-process all ActionDatabaseComponent entities in the given + * Flecs world: clear the singleton and re-sync from every entity + * that carries the component. This is used after a reload so + * scene-defined actions are re-applied on top of the file. + */ + static void reloadFromSceneComponents(flecs::world &world); + private: ActionDatabase() = default; ~ActionDatabase() = default; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 7093c6f..5eead5a 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -371,6 +371,27 @@ void EditorUISystem::renderHierarchyWindow() "Ctrl+O")) { showFileDialog(false); } + ImGui::Separator(); + if (ImGui::MenuItem("Save Action DB", + nullptr)) { + ActionDatabase::saveToJson( + "actions.json"); + } + if (ImGui::MenuItem("Load Action DB", + nullptr)) { + ActionDatabase::loadFromJson( + "actions.json"); + } + if (ImGui::MenuItem("Reload Action DB", + nullptr)) { + // Reload from file, then re-sync + // scene components on top + ActionDatabase::getSingleton().clear(); + ActionDatabase::loadFromJson( + "actions.json"); + ActionDatabase::reloadFromSceneComponents( + m_world); + } ImGui::EndMenu(); } diff --git a/src/features/editScene/tests/Ogre.h b/src/features/editScene/tests/Ogre.h index dac39ca..48d2376 100644 --- a/src/features/editScene/tests/Ogre.h +++ b/src/features/editScene/tests/Ogre.h @@ -63,7 +63,7 @@ struct StringConverter { } }; -// Minimal LogManager stub (used by LuaActionApi) +// Minimal LogManager stub (used by LuaActionApi and ActionDatabase) class LogManager { public: static LogManager &getSingleton() @@ -84,6 +84,39 @@ public: { return Stream(); } + + void logMessage(const String &, int = 0, bool = false) + { + } +}; + +// Minimal ResourceGroupManager stub (used by ActionDatabase save/load) +class ResourceGroupManager { +public: + // LocationList is a vector of shared_ptr to Location + struct Location { + struct Archive { + String getName() const + { + return ""; + } + }; + // Use raw pointer to avoid shared_ptr dependency + Archive *archive; + }; + using LocationList = std::vector; + + static ResourceGroupManager &getSingleton() + { + static ResourceGroupManager instance; + return instance; + } + + const LocationList &getResourceLocationList(const String &) const + { + static LocationList empty; + return empty; + } }; } // namespace Ogre