direct save/load action database
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
#include "ActionDatabase.hpp"
|
||||
#ifndef OGRE_STUB_H
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#endif
|
||||
|
||||
#include <flecs.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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<int>();
|
||||
}
|
||||
bb.floatValues.clear();
|
||||
if (json.contains("floatValues") && json["floatValues"].is_object()) {
|
||||
for (auto &[key, val] : json["floatValues"].items())
|
||||
bb.floatValues[key] = val.get<float>();
|
||||
}
|
||||
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<float>(),
|
||||
val[1].get<float>(),
|
||||
val[2].get<float>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<int>(),
|
||||
entry["name"]
|
||||
.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
#include "GoapGoal.hpp"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Location>;
|
||||
|
||||
static ResourceGroupManager &getSingleton()
|
||||
{
|
||||
static ResourceGroupManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
const LocationList &getResourceLocationList(const String &) const
|
||||
{
|
||||
static LocationList empty;
|
||||
return empty;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Ogre
|
||||
|
||||
Reference in New Issue
Block a user