diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 0e4e5ce..30f23d7 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -39,6 +39,8 @@ set(EDITSCENE_SOURCES systems/ItemRegistry.cpp systems/ContainerStateRegistry.cpp systems/ItemStateRegistry.cpp + systems/SaveLoadSystem.cpp + systems/SaveLoadDialog.cpp systems/PlayerControllerSystem.cpp systems/CharacterSlotSystem.cpp systems/CharacterRegistry.cpp @@ -170,6 +172,7 @@ set(EDITSCENE_SOURCES lua/LuaGameModeApi.cpp lua/LuaCharacterClassApi.cpp lua/LuaCharacterApi.cpp + lua/LuaSaveLoadApi.cpp ) set(EDITSCENE_HEADERS @@ -197,6 +200,7 @@ set(EDITSCENE_HEADERS components/TriangleBuffer.hpp components/CharacterSlots.hpp components/CharacterIdentity.hpp + components/RuntimeMarker.hpp components/AnimationTree.hpp components/Character.hpp components/CellGrid.hpp @@ -264,6 +268,8 @@ set(EDITSCENE_HEADERS systems/ProceduralTextureSystem.hpp systems/StaticGeometrySystem.hpp systems/SceneSerializer.hpp + systems/SaveLoadSystem.hpp + systems/SaveLoadDialog.hpp systems/PhysicsSystem.hpp systems/BuoyancySystem.hpp systems/EditorSunSystem.hpp @@ -337,6 +343,7 @@ set(EDITSCENE_HEADERS lua/LuaGameModeApi.hpp lua/LuaCharacterClassApi.hpp lua/LuaCharacterApi.hpp + lua/LuaSaveLoadApi.hpp ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 6fc88fc..9a6da11 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -38,13 +38,17 @@ #include "systems/PregnancySystem.hpp" #include "components/CharacterClassDatabase.hpp" #include "lua/LuaCharacterApi.hpp" +#include "lua/LuaSaveLoadApi.hpp" #include "systems/PlayerControllerSystem.hpp" #include "systems/SceneSerializer.hpp" +#include "systems/SaveLoadSystem.hpp" +#include "systems/SaveLoadDialog.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" #include "components/Renderable.hpp" #include "components/EditorMarker.hpp" +#include "components/RuntimeMarker.hpp" #include "components/PhysicsCollider.hpp" #include "components/RigidBody.hpp" #include "components/GeneratedPhysicsTag.hpp" @@ -678,7 +682,9 @@ void EditorApp::clearScene() entitiesToDelete.push_back(e); }); for (auto &e : entitiesToDelete) { - if (e.is_alive()) { + if (e.is_alive() && m_uiSystem) { + m_uiSystem->deleteEntity(e); + } else if (e.is_alive()) { e.destruct(); } } @@ -695,6 +701,8 @@ void EditorApp::startNewGame(const Ogre::String &scenePath) PrefabSystem prefabSys(m_world, m_sceneMgr); prefabSys.resolveInstances(); setGamePlayState(GamePlayState::Playing); + m_currentBaseScene = scenePath; + m_playTime = 0.0f; Ogre::LogManager::getSingleton().logMessage( "Game started: loaded scene " + scenePath); @@ -804,6 +812,342 @@ void EditorApp::startNewGame(const Ogre::String &scenePath) } } +void EditorApp::saveGame(const std::string &slotPath, + const std::string &slotName) +{ + EventBus::getInstance().send("save_game_requested"); + + nlohmann::json saveData; + saveData["version"] = "2.0"; + saveData["saveGame"] = { + { "baseScene", m_currentBaseScene }, + { "timestamp", SaveLoadSystem::getCurrentTimestamp() }, + { "slotName", slotName }, + { "playTime", m_playTime } + }; + + /* Sync spawned character positions into the registry. + * Read from the live SceneNode because TransformComponent + * cache is not updated by physics/animation systems. */ + auto ®istry = CharacterRegistry::getSingleton(); + m_world.query().each( + [&](flecs::entity e, CharacterIdentityComponent &ci) { + auto *rec = registry.findCharacter(ci.registryId); + if (!rec || !e.has()) + return; + auto &t = e.get(); + if (t.node) { + rec->position = t.node->_getDerivedPosition(); + rec->rotation = t.node->_getDerivedOrientation(); + } else { + rec->position = t.position; + rec->rotation = t.rotation; + } + }); + + /* Identify the player character registry ID for load-time setup */ + uint64_t playerCharId = 0; + flecs::entity playerController; + m_world.query().each( + [&](flecs::entity e, EntityNameComponent &name) { + if (name.name == "player") + playerController = e; + }); + if (playerController.is_alive()) { + flecs::entity playerChar = playerController; + if (playerController.has()) { + auto &pc = playerController.get(); + if (!pc.targetCharacterName.empty()) { + m_world.query().each( + [&](flecs::entity e, + EntityNameComponent &en) { + if (en.name == pc.targetCharacterName) + playerChar = e; + }); + } + } + if (playerChar.has()) { + playerCharId = playerChar.get< + CharacterIdentityComponent>().registryId; + } + } + saveData["saveGame"]["playerCharacterId"] = playerCharId; + + saveData["characterRegistry"] = registry.serialize(); + saveData["containerState"] = + ContainerStateRegistry::getInstance().serialize(); + saveData["itemState"] = + ItemStateRegistry::getInstance().serialize(); + + /* Runtime entities — skip characters and player controller */ + saveData["runtimeEntities"] = nlohmann::json::array(); + SceneSerializer serializer(m_world, m_sceneMgr); + m_world.query().each( + [&](flecs::entity e, const RuntimeMarkerComponent &) { + if (e.has()) + return; + if (e.has()) + return; + saveData["runtimeEntities"].push_back( + serializer.serializeEntity(e)); + }); + + /* Character runtime component overrides (restored after registry spawn) */ + saveData["characterRuntimeData"] = nlohmann::json::object(); + static const std::vector s_runtimeKeys = { + "character", "goapBlackboard", "goapPlanner", + "goapRunner", "pathFollowing", "behaviorTree", + "inventory", "actionDebug", "animationTree", + "animationTreeTemplate" + }; + m_world.query().each( + [&](flecs::entity e, CharacterIdentityComponent &ci) { + nlohmann::json entityJson = serializer.serializeEntity(e); + nlohmann::json filtered; + for (const auto &key : s_runtimeKeys) { + if (entityJson.contains(key)) + filtered[key] = entityJson[key]; + } + saveData["characterRuntimeData"][ + std::to_string(ci.registryId)] = filtered; + }); + + /* Lua callback data */ + saveData["luaData"] = editScene::collectLuaSaveData(m_lua.getState()); + + if (SaveLoadSystem::writeSaveFile(slotPath, saveData)) { + Ogre::LogManager::getSingleton().logMessage( + "Game saved to: " + slotPath); + EventBus::getInstance().send("game_saved"); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Failed to save game to: " + slotPath); + } +} + +void EditorApp::loadGame(const std::string &slotPath) +{ + EventBus::getInstance().send("load_game_requested"); + + nlohmann::json saveData; + if (!SaveLoadSystem::readSaveFile(slotPath, saveData)) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to read save file: " + slotPath); + return; + } + + std::string baseScene = saveData["saveGame"].value("baseScene", ""); + if (baseScene.empty()) { + Ogre::LogManager::getSingleton().logMessage( + "Save file missing base scene reference"); + return; + } + + clearScene(); + + SceneSerializer serializer(m_world, m_sceneMgr); + if (!serializer.loadFromFile(baseScene, m_uiSystem.get())) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to load base scene: " + + serializer.getLastError()); + return; + } + PrefabSystem prefabSys(m_world, m_sceneMgr); + prefabSys.resolveInstances(); + + /* Restore registries */ + auto ®istry = CharacterRegistry::getSingleton(); + if (saveData.contains("characterRegistry")) + registry.deserialize(saveData["characterRegistry"]); + if (saveData.contains("containerState")) + ContainerStateRegistry::getInstance().deserialize( + saveData["containerState"]); + if (saveData.contains("itemState")) + ItemStateRegistry::getInstance().deserialize( + saveData["itemState"]); + + /* Destroy ALL characters (including editor-placed prefab + * instances that may lack CharacterIdentityComponent) so the + * registry spawn is the only source of characters. */ + std::vector charsToDestroy; + m_world.query().each( + [&](flecs::entity e, CharacterSlotsComponent &) { + charsToDestroy.push_back(e); + }); + for (auto e : charsToDestroy) { + if (!e.is_alive()) + continue; + if (m_uiSystem) + m_uiSystem->deleteEntity(e); + else + e.destruct(); + } + + /* Spawn persistent characters from registry. + * Only spawn characters that were actually present in the + * world at save time (listed in characterRuntimeData). + * This avoids respawning registry records that were not + * spawned (e.g. characters from other scenes) at origin. */ + const auto &runtimeData = + saveData.value("characterRuntimeData", + nlohmann::json::object()); + bool filterByRuntime = + saveData.contains("characterRuntimeData") && + runtimeData.is_object(); + for (const auto &pair : registry.getCharacters()) { + const auto &rec = pair.second; + if (!rec.persistent) + continue; + if (filterByRuntime && + !runtimeData.contains(std::to_string(rec.id))) + continue; + registry.spawnCharacter(rec.id); + } + + /* Restore character runtime component overrides */ + if (saveData.contains("characterRuntimeData") && + saveData["characterRuntimeData"].is_object()) { + for (auto &el : saveData["characterRuntimeData"].items()) { + uint64_t rid = std::stoull(el.key()); + flecs::entity spawned = registry.findSpawnedEntity(rid); + if (!spawned.is_alive()) + continue; + serializer.deserializeEntityComponents( + spawned, el.value(), + flecs::entity::null(), nullptr, + false, false, false); + } + } + + /* Restore other runtime entities (dropped items, etc.) */ + if (saveData.contains("runtimeEntities") && + saveData["runtimeEntities"].is_array()) { + for (const auto &entityJson : saveData["runtimeEntities"]) { + flecs::entity targetEntity = flecs::entity::null(); + + /* Try match by item instanceId */ + if (entityJson.contains("item") && + entityJson["item"].is_object() && + entityJson["item"].contains("instanceId")) { + std::string iid = entityJson["item"]["instanceId"]; + m_world.query() + .each([&](flecs::entity e, ItemComponent &item) { + if (item.instanceId == iid) + targetEntity = e; + }); + } + + /* Try match by name */ + if (!targetEntity.is_alive() && + entityJson.contains("name") && + entityJson["name"].is_object() && + entityJson["name"].contains("name")) { + std::string ename = entityJson["name"]["name"]; + m_world.query() + .each([&](flecs::entity e, + EntityNameComponent &nameComp) { + if (nameComp.name == ename) + targetEntity = e; + }); + } + + if (targetEntity.is_alive()) { + /* Apply overrides to existing entity. + * Handle transform specially to preserve SceneNode. */ + if (entityJson.contains("transform") && + entityJson["transform"].is_object()) { + auto &t = entityJson["transform"]; + if (targetEntity.has()) { + auto &trans = targetEntity.get_mut< + TransformComponent>(); + if (t.contains("position")) { + auto &p = t["position"]; + trans.position = Ogre::Vector3( + p.value("x", 0.0f), + p.value("y", 0.0f), + p.value("z", 0.0f)); + } + if (t.contains("rotation")) { + auto &r = t["rotation"]; + trans.rotation = Ogre::Quaternion( + r.value("w", 1.0f), + r.value("x", 0.0f), + r.value("y", 0.0f), + r.value("z", 0.0f)); + } + if (t.contains("scale")) { + auto &s = t["scale"]; + trans.scale = Ogre::Vector3( + s.value("x", 1.0f), + s.value("y", 1.0f), + s.value("z", 1.0f)); + } + trans.applyToNode(); + } + } + serializer.deserializeEntityComponents( + targetEntity, entityJson, + flecs::entity::null(), m_uiSystem.get(), + false); + } else { + /* Create new runtime entity */ + flecs::entity newEntity = m_world.entity(); + newEntity.add(); + serializer.deserializeEntityComponents( + newEntity, entityJson, + flecs::entity::null(), m_uiSystem.get(), + true); + } + } + } + + /* Setup player controller to point to spawned player character */ + uint64_t playerCharId = + saveData["saveGame"].value("playerCharacterId", 0ULL); + flecs::entity playerController; + m_world.query().each( + [&](flecs::entity e, EntityNameComponent &name) { + if (name.name == "player") + playerController = e; + }); + if (playerController.is_alive() && playerCharId != 0) { + flecs::entity playerChar; + m_world.query() + .each([&](flecs::entity e, + CharacterIdentityComponent &ci) { + if (ci.registryId == playerCharId) + playerChar = e; + }); + if (playerChar.is_alive() && + playerController.has()) { + auto &pc = playerController.get_mut< + PlayerControllerComponent>(); + if (playerChar.has()) { + pc.targetCharacterName = + playerChar.get().name; + } + if (!playerChar.has()) { + playerChar.set( + InventoryComponent()); + } + } + } + + /* Lua callback data */ + if (saveData.contains("luaData")) { + editScene::applyLuaLoadData(m_lua.getState(), + saveData["luaData"]); + } + + m_currentBaseScene = baseScene; + m_playTime = saveData["saveGame"].value("playTime", 0.0f); + setGamePlayState(GamePlayState::Playing); + + Ogre::LogManager::getSingleton().logMessage( + "Game loaded from: " + slotPath); + EventBus::getInstance().send("game_loaded"); +} + void EditorApp::setupECS() { // Register components @@ -1036,6 +1380,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) } } else if (m_gameMode == GameMode::Game) { if (m_gamePlayState == GamePlayState::Playing) { + m_playTime += evt.timeSinceLastFrame; if (m_playerControllerSystem) { m_playerControllerSystem->update( evt.timeSinceLastFrame); diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 33d2439..faecde4 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -164,6 +164,11 @@ public: void startNewGame(const Ogre::String &scenePath); void clearScene(); + // Save / Load + void saveGame(const std::string &slotPath, + const std::string &slotName); + void loadGame(const std::string &slotPath); + // Input access GameInputState &getGameInputState() { @@ -274,6 +279,8 @@ private: GameInputState m_gameInput; bool m_setupComplete = false; bool m_debugBuoyancy = false; + float m_playTime = 0.0f; + std::string m_currentBaseScene; // Lua scripting editScene::LuaState m_lua; diff --git a/src/features/editScene/components/RuntimeMarker.hpp b/src/features/editScene/components/RuntimeMarker.hpp new file mode 100644 index 0000000..d3b581b --- /dev/null +++ b/src/features/editScene/components/RuntimeMarker.hpp @@ -0,0 +1,14 @@ +#ifndef EDITSCENE_RUNTIMEMARKER_HPP +#define EDITSCENE_RUNTIMEMARKER_HPP +#pragma once + +/** + * Marker component for entities created at runtime (not in the editor). + * Used by the save/load system to distinguish runtime-spawned entities + * from editor-placed scene entities. + */ +struct RuntimeMarkerComponent { + // Empty marker component +}; + +#endif // EDITSCENE_RUNTIMEMARKER_HPP diff --git a/src/features/editScene/lua/LuaSaveLoadApi.cpp b/src/features/editScene/lua/LuaSaveLoadApi.cpp new file mode 100644 index 0000000..da45200 --- /dev/null +++ b/src/features/editScene/lua/LuaSaveLoadApi.cpp @@ -0,0 +1,220 @@ +#include "LuaSaveLoadApi.hpp" +#include +#include +#include + +namespace editScene +{ + +/* ===================================================================== */ +/* Internal state */ +/* ===================================================================== */ + +static std::unordered_map s_saveCallbacks; +static std::unordered_map s_loadCallbacks; + +/* ===================================================================== */ +/* Lua table <-> JSON conversion */ +/* ===================================================================== */ + +nlohmann::json luaTableToJson(lua_State *L, int index) +{ + nlohmann::json result; + int absIdx = lua_absindex(L, index); + + if (lua_istable(L, absIdx)) { + /* First, try to detect if it's an array */ + bool isArray = true; + lua_len(L, absIdx); + int len = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + + if (len > 0) { + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_isnil(L, -1)) { + isArray = false; + lua_pop(L, 1); + break; + } + lua_pop(L, 1); + } + } + + if (isArray && len > 0) { + result = nlohmann::json::array(); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, absIdx, i); + result.push_back(luaTableToJson(L, -1)); + lua_pop(L, 1); + } + } else { + result = nlohmann::json::object(); + lua_pushnil(L); + while (lua_next(L, absIdx) != 0) { + std::string key; + if (lua_type(L, -2) == LUA_TNUMBER) { + key = std::to_string( + (lua_Integer)lua_tonumber(L, -2)); + } else if (lua_type(L, -2) == LUA_TSTRING) { + key = lua_tostring(L, -2); + } else { + lua_pop(L, 1); + continue; + } + result[key] = luaTableToJson(L, -1); + lua_pop(L, 1); + } + } + } else if (lua_isboolean(L, absIdx)) { + result = lua_toboolean(L, absIdx) != 0; + } else if (lua_isinteger(L, absIdx)) { + result = (int64_t)lua_tointeger(L, absIdx); + } else if (lua_isnumber(L, absIdx)) { + result = lua_tonumber(L, absIdx); + } else if (lua_isstring(L, absIdx)) { + result = lua_tostring(L, absIdx); + } else { + result = nullptr; + } + return result; +} + +void jsonToLuaValue(lua_State *L, const nlohmann::json &j) +{ + if (j.is_null()) { + lua_pushnil(L); + } else if (j.is_boolean()) { + lua_pushboolean(L, j.get()); + } else if (j.is_number_integer()) { + lua_pushinteger(L, (lua_Integer)j.get()); + } else if (j.is_number_float()) { + lua_pushnumber(L, j.get()); + } else if (j.is_string()) { + lua_pushstring(L, j.get().c_str()); + } else if (j.is_array()) { + lua_newtable(L); + int i = 1; + for (const auto &elem : j) { + jsonToLuaValue(L, elem); + lua_rawseti(L, -2, i++); + } + } else if (j.is_object()) { + lua_newtable(L); + for (auto &[key, val] : j.items()) { + lua_pushstring(L, key.c_str()); + jsonToLuaValue(L, val); + lua_rawset(L, -3); + } + } else { + lua_pushnil(L); + } +} + +/* ===================================================================== */ +/* Save / Load data collection */ +/* ===================================================================== */ + +nlohmann::json collectLuaSaveData(lua_State *L) +{ + nlohmann::json data; + for (const auto &pair : s_saveCallbacks) { + const std::string &name = pair.first; + int ref = pair.second; + + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + if (lua_pcall(L, 0, 1, 0) == 0) { + if (lua_istable(L, -1)) { + data[name] = luaTableToJson(L, -1); + } + lua_pop(L, 1); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Lua save callback '" + name + + "' error: " + lua_tostring(L, -1)); + lua_pop(L, 1); + } + } + return data; +} + +void applyLuaLoadData(lua_State *L, const nlohmann::json &data) +{ + for (const auto &pair : s_loadCallbacks) { + const std::string &name = pair.first; + int ref = pair.second; + + if (!data.contains(name)) + continue; + + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + jsonToLuaValue(L, data[name]); + if (lua_pcall(L, 1, 0, 0) != 0) { + Ogre::LogManager::getSingleton().logMessage( + "Lua load callback '" + name + + "' error: " + lua_tostring(L, -1)); + lua_pop(L, 1); + } + } +} + +/* ===================================================================== */ +/* Lua C API functions */ +/* ===================================================================== */ + +static int luaRegisterSaveCallback(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + /* Unregister existing callback with same name */ + auto it = s_saveCallbacks.find(name); + if (it != s_saveCallbacks.end()) { + luaL_unref(L, LUA_REGISTRYINDEX, it->second); + } + + /* Store new callback in registry */ + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + s_saveCallbacks[name] = ref; + return 0; +} + +static int luaRegisterLoadCallback(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + auto it = s_loadCallbacks.find(name); + if (it != s_loadCallbacks.end()) { + luaL_unref(L, LUA_REGISTRYINDEX, it->second); + } + + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + s_loadCallbacks[name] = ref; + return 0; +} + +/* ===================================================================== */ +/* Registration */ +/* ===================================================================== */ + +void registerLuaSaveLoadApi(lua_State *L) +{ + lua_getglobal(L, "ecs"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + lua_pushcfunction(L, luaRegisterSaveCallback); + lua_setfield(L, -2, "register_save_callback"); + + lua_pushcfunction(L, luaRegisterLoadCallback); + lua_setfield(L, -2, "register_load_callback"); + + lua_pop(L, 1); +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaSaveLoadApi.hpp b/src/features/editScene/lua/LuaSaveLoadApi.hpp new file mode 100644 index 0000000..ef8aa19 --- /dev/null +++ b/src/features/editScene/lua/LuaSaveLoadApi.hpp @@ -0,0 +1,48 @@ +#ifndef EDITSCENE_LUA_SAVELOAD_API_HPP +#define EDITSCENE_LUA_SAVELOAD_API_HPP +#pragma once + +#include +#include + +namespace editScene +{ + +/** + * @brief Register save/load callback Lua API functions into the "ecs" table. + * + * Adds: + * ecs.register_save_callback(name, function() return table end) + * ecs.register_load_callback(name, function(table) end) + * + * @param L The Lua state. + */ +void registerLuaSaveLoadApi(lua_State *L); + +/** + * @brief Call all registered Lua save callbacks and collect their data. + * @param L Lua state. + * @return JSON object keyed by callback name. + */ +nlohmann::json collectLuaSaveData(lua_State *L); + +/** + * @brief Call all registered Lua load callbacks with saved data. + * @param L Lua state. + * @param data JSON object keyed by callback name. + */ +void applyLuaLoadData(lua_State *L, const nlohmann::json &data); + +/** + * @brief Convert a Lua table at the given stack index to JSON. + */ +nlohmann::json luaTableToJson(lua_State *L, int index); + +/** + * @brief Push a JSON value onto the Lua stack as a table/value. + */ +void jsonToLuaValue(lua_State *L, const nlohmann::json &j); + +} // namespace editScene + +#endif // EDITSCENE_LUA_SAVELOAD_API_HPP diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index f8b93ad..2489617 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -360,6 +360,8 @@ void ActuatorSystem::update(float deltaTime) // (those are handled above as actuators) if (e.has()) return; + if (item.disabled) + return; if (!trans.node) return; diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index ef0c4ec..2934b66 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -669,6 +669,8 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, if (!node.name.empty() && itemComp.itemId != node.name) return; + if (itemComp.disabled) + return; Ogre::Vector3 itemPos = Ogre::Vector3::ZERO; diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index daf0e62..bbd769c 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -7,6 +7,7 @@ #include "../components/Transform.hpp" #include "../components/EntityName.hpp" #include "../components/EditorMarker.hpp" +#include "../components/RuntimeMarker.hpp" #include #include #include @@ -522,7 +523,7 @@ flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c, TransformComponent transform; transform.node = node; transform.position = pos; - transform.rotation = Ogre::Quaternion::IDENTITY; + transform.rotation = c.rotation; transform.scale = Ogre::Vector3(1, 1, 1); transform.applyToNode(); inst.set(transform); @@ -707,20 +708,32 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) if (existing.is_alive()) existing.destruct(); - Ogre::Vector3 pos(0, 0, 0); + Ogre::Vector3 pos = c->position; flecs::entity inst; if (std::filesystem::exists(c->prefabPath)) { PrefabSystem prefabSys(*m_world, m_sceneMgr); inst = prefabSys.createInstance( c->prefabPath, flecs::entity::null(), pos, c->firstName + " " + c->lastName, m_uiSystem); + if (inst.is_alive() && inst.has()) { + auto &t = inst.get_mut(); + t.position = c->position; + t.rotation = c->rotation; + t.applyToNode(); + } } else { inst = spawnInlineCharacter(*c, pos); + if (inst.is_alive() && inst.has()) { + auto &t = inst.get_mut(); + t.rotation = c->rotation; + t.applyToNode(); + } } if (inst.is_alive()) { inst.set( CharacterIdentityComponent{id}); + inst.add(); m_uiSystem->addEntity(inst); } return inst; @@ -1198,6 +1211,25 @@ nlohmann::json CharacterRegistry::serialize() const rec["floatColumns"][kv.first] = kv.second; for (const auto &kv : c.stringColumns) rec["stringColumns"][kv.first] = kv.second; + rec["pregnantByFatherId"] = c.pregnantByFatherId; + rec["pregnancyProgress"] = c.pregnancyProgress; + rec["pregnancyMaxProgress"] = c.pregnancyMaxProgress; + rec["basePregnancyDuration"] = c.basePregnancyDuration; + { + nlohmann::json pos; + pos["x"] = c.position.x; + pos["y"] = c.position.y; + pos["z"] = c.position.z; + rec["position"] = pos; + } + { + nlohmann::json rot; + rot["w"] = c.rotation.w; + rot["x"] = c.rotation.x; + rot["y"] = c.rotation.y; + rot["z"] = c.rotation.z; + rec["rotation"] = rot; + } j["characters"].push_back(rec); } @@ -1311,6 +1343,19 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.inlineAge = rec.value("inlineAge", "adult"); c.inlineSex = rec.value("inlineSex", "male"); c.inlineOutfitLevel = rec.value("inlineOutfitLevel", 2); + if (rec.contains("position") && rec["position"].is_object()) { + auto &p = rec["position"]; + c.position = Ogre::Vector3(p.value("x", 0.0f), + p.value("y", 0.0f), + p.value("z", 0.0f)); + } + if (rec.contains("rotation") && rec["rotation"].is_object()) { + auto &r = rec["rotation"]; + c.rotation = Ogre::Quaternion(r.value("w", 1.0f), + r.value("x", 0.0f), + r.value("y", 0.0f), + r.value("z", 0.0f)); + } if (rec.contains("inlineSlotSelections")) { for (auto &[slot, s] : rec["inlineSlotSelections"].items()) { SlotSelection sel; diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index a904ca7..8ceccae 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -77,6 +77,10 @@ public: /* Runtime-only characters are NOT saved to character_registry.json */ bool persistent = true; + /* Spawn position/rotation (persisted in save games) */ + Ogre::Vector3 position = Ogre::Vector3::ZERO; + Ogre::Quaternion rotation = Ogre::Quaternion::IDENTITY; + /* Pregnancy state (progresses whether spawned or not) */ uint64_t pregnantByFatherId = 0; float pregnancyProgress = 0.0f; diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index 7aeeb8c..d202f0b 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -72,6 +72,11 @@ public: setSelectedEntity(flecs::entity::null()); } + /** + * Delete entity and all its children, cleaning up OGRE objects + */ + void deleteEntity(flecs::entity entity); + /** * Get the currently selected entity */ @@ -208,7 +213,6 @@ private: void renderEntityContextMenu(flecs::entity entity); void createNewEntity(); void createChildEntity(flecs::entity parent); - void deleteEntity(flecs::entity entity); void duplicateEntity(flecs::entity entity); // Component operations diff --git a/src/features/editScene/systems/ItemSystem.cpp b/src/features/editScene/systems/ItemSystem.cpp index 43d631c..1db7513 100644 --- a/src/features/editScene/systems/ItemSystem.cpp +++ b/src/features/editScene/systems/ItemSystem.cpp @@ -8,6 +8,7 @@ #include "../components/Transform.hpp" #include "../components/EntityName.hpp" #include "../components/ActionDatabase.hpp" +#include "../components/RuntimeMarker.hpp" #include #include #include @@ -282,6 +283,7 @@ bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex, } else { // Create a new world entity for the dropped item flecs::entity itemEntity = m_world.entity(); + itemEntity.add(); itemEntity.set(ItemComponent(slot.itemId, dropCount)); // Create a scene node for the dropped item diff --git a/src/features/editScene/systems/PauseMenuSystem.cpp b/src/features/editScene/systems/PauseMenuSystem.cpp index 3ed2699..65bd86c 100644 --- a/src/features/editScene/systems/PauseMenuSystem.cpp +++ b/src/features/editScene/systems/PauseMenuSystem.cpp @@ -1,6 +1,7 @@ #include "PauseMenuSystem.hpp" #include "../EditorApp.hpp" #include "../components/StartupMenu.hpp" +#include "SaveLoadDialog.hpp" #include #include #include @@ -138,8 +139,8 @@ void PauseMenuSystem::renderMenu() EditorApp::GamePlayState::Playing); } }); buttons.push_back({ "SAVE GAME", [&]() { - Ogre::LogManager::getSingleton().logMessage( - "Save game not implemented"); + SaveLoadDialog::show(m_editorApp, + SaveLoadDialog::Mode::Save); } }); buttons.push_back({ "OPTIONS", [&]() { Ogre::LogManager::getSingleton().logMessage( @@ -176,6 +177,8 @@ void PauseMenuSystem::renderMenu() if (m_menuFont) ImGui::PopFont(); + SaveLoadDialog::render(m_editorApp); + ImGui::End(); ImGui::PopStyleColor(); } diff --git a/src/features/editScene/systems/SaveLoadDialog.cpp b/src/features/editScene/systems/SaveLoadDialog.cpp new file mode 100644 index 0000000..6f3ae92 --- /dev/null +++ b/src/features/editScene/systems/SaveLoadDialog.cpp @@ -0,0 +1,211 @@ +#include "SaveLoadDialog.hpp" +#include "../EditorApp.hpp" +#include "SaveLoadSystem.hpp" +#include +#include +#include + +/* ===================================================================== */ +/* Static state */ +/* ===================================================================== */ + +bool SaveLoadDialog::s_open = false; +SaveLoadDialog::Mode SaveLoadDialog::s_mode = SaveLoadDialog::Mode::Save; +std::vector SaveLoadDialog::s_saves; +int SaveLoadDialog::s_selectedIndex = -1; +char SaveLoadDialog::s_slotNameInput[256] = {}; +bool SaveLoadDialog::s_needsRefresh = true; +bool SaveLoadDialog::s_showDeleteConfirm = false; +int SaveLoadDialog::s_deleteIndex = -1; + +/* ===================================================================== */ +/* Public API */ +/* ===================================================================== */ + +void SaveLoadDialog::show(EditorApp *editorApp, Mode mode) +{ + s_open = true; + s_mode = mode; + s_needsRefresh = true; + s_selectedIndex = -1; + s_showDeleteConfirm = false; + s_deleteIndex = -1; + memset(s_slotNameInput, 0, sizeof(s_slotNameInput)); + + if (mode == Mode::Save) { + /* Auto-suggest a slot name */ + auto saves = SaveLoadSystem::listSaves(); + int nextNum = (int)saves.size() + 1; + snprintf(s_slotNameInput, sizeof(s_slotNameInput), "Save %d", + nextNum); + } +} + +bool SaveLoadDialog::isOpen() +{ + return s_open; +} + +void SaveLoadDialog::close() +{ + s_open = false; +} + +/* ===================================================================== */ +/* Internals */ +/* ===================================================================== */ + +void SaveLoadDialog::refreshSaveList() +{ + s_saves = SaveLoadSystem::listSaves(); + s_needsRefresh = false; +} + +void SaveLoadDialog::doSave(EditorApp *editorApp) +{ + if (!editorApp) + return; + std::string slotName(s_slotNameInput); + /* Trim whitespace */ + slotName.erase(slotName.begin(), + std::find_if(slotName.begin(), slotName.end(), + [](unsigned char ch) { + return !std::isspace(ch); + })); + slotName.erase(std::find_if(slotName.rbegin(), slotName.rend(), + [](unsigned char ch) { + return !std::isspace(ch); + }).base(), + slotName.end()); + if (slotName.empty()) { + Ogre::LogManager::getSingleton().logMessage( + "SaveLoadDialog: cannot save with empty name"); + return; + } + + std::string path = SaveLoadSystem::generateSaveFilename(); + editorApp->saveGame(path, slotName); + s_open = false; +} + +void SaveLoadDialog::doLoad(EditorApp *editorApp, int index) +{ + if (!editorApp || index < 0 || index >= (int)s_saves.size()) + return; + editorApp->loadGame(s_saves[index].path); + s_open = false; +} + +void SaveLoadDialog::render(EditorApp *editorApp) +{ + if (!s_open) + return; + + if (s_needsRefresh) + refreshSaveList(); + + const char *title = (s_mode == Mode::Save) ? "Save Game" : "Load Game"; + ImGui::OpenPopup(title); + + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, + ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_Appearing); + + if (ImGui::BeginPopupModal(title, &s_open, + ImGuiWindowFlags_NoResize)) { + /* --- Slot name input (save mode only) --- */ + if (s_mode == Mode::Save) { + ImGui::Text("Save Name:"); + ImGui::SameLine(); + ImGui::InputText("##SlotName", s_slotNameInput, + sizeof(s_slotNameInput)); + ImGui::Separator(); + } + + /* --- Save list --- */ + ImGui::Text("Existing Saves:"); + if (ImGui::BeginChild("SaveList", ImVec2(0, -60), + ImGuiChildFlags_Borders)) { + for (int i = 0; i < (int)s_saves.size(); ++i) { + const auto &save = s_saves[i]; + bool isSelected = (s_selectedIndex == i); + + ImGui::PushID(i); + if (ImGui::Selectable(save.slotName.c_str(), + isSelected)) { + s_selectedIndex = i; + if (s_mode == Mode::Save) { + snprintf(s_slotNameInput, + sizeof(s_slotNameInput), "%s", + save.slotName.c_str()); + } + } + ImGui::PopID(); + + if (isSelected) + ImGui::SetItemDefaultFocus(); + + ImGui::SameLine(); + ImGui::TextDisabled("[%s] %s", save.timestamp.c_str(), + save.baseScene.c_str()); + } + if (s_saves.empty()) { + ImGui::TextDisabled("No saves found."); + } + } + ImGui::EndChild(); + + /* --- Delete confirmation --- */ + if (s_showDeleteConfirm && s_deleteIndex >= 0) { + ImGui::TextColored( + ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + "Are you sure you want to delete '%s'?", + s_saves[s_deleteIndex].slotName.c_str()); + if (ImGui::Button("Yes, Delete")) { + SaveLoadSystem::deleteSave( + s_saves[s_deleteIndex].path); + s_showDeleteConfirm = false; + s_deleteIndex = -1; + s_needsRefresh = true; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + s_showDeleteConfirm = false; + s_deleteIndex = -1; + } + ImGui::Separator(); + } + + /* --- Action buttons --- */ + if (s_mode == Mode::Save) { + if (ImGui::Button("Save", ImVec2(120, 0))) { + doSave(editorApp); + } + ImGui::SameLine(); + } else { + if (ImGui::Button("Load", ImVec2(120, 0)) && + s_selectedIndex >= 0) { + doLoad(editorApp, s_selectedIndex); + } + ImGui::SameLine(); + } + + if (s_selectedIndex >= 0 && !s_showDeleteConfirm) { + if (ImGui::Button("Delete", ImVec2(120, 0))) { + s_showDeleteConfirm = true; + s_deleteIndex = s_selectedIndex; + } + ImGui::SameLine(); + } + + ImGui::SetCursorPosX( + ImGui::GetWindowWidth() - 120 - + ImGui::GetStyle().WindowPadding.x); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + s_open = false; + } + + ImGui::EndPopup(); + } +} diff --git a/src/features/editScene/systems/SaveLoadDialog.hpp b/src/features/editScene/systems/SaveLoadDialog.hpp new file mode 100644 index 0000000..e4fe9cf --- /dev/null +++ b/src/features/editScene/systems/SaveLoadDialog.hpp @@ -0,0 +1,64 @@ +#ifndef EDITSCENE_SAVELOADDIALOG_HPP +#define EDITSCENE_SAVELOADDIALOG_HPP +#pragma once + +#include +#include +#include "SaveLoadSystem.hpp" + +class EditorApp; + +/** + * ImGui modal dialog for save/load slot selection. + * + * Used by both StartupMenuSystem (load mode) and PauseMenuSystem (save mode). + */ +class SaveLoadDialog { +public: + /** + * Mode of operation. + */ + enum class Mode { Save, Load }; + + /** + * Open the dialog. Call this when the user clicks Save/Load. + * + * @param editorApp The editor app for save/load callbacks. + * @param mode Save or Load mode. + */ + static void show(EditorApp *editorApp, Mode mode); + + /** + * Render the dialog. Call this every frame from the menu system's + * update/render loop. + * + * @param editorApp The editor app for save/load callbacks. + */ + static void render(EditorApp *editorApp); + + /** + * Check if the dialog is currently open. + */ + static bool isOpen(); + + /** + * Close the dialog. + */ + static void close(); + +private: + static bool s_open; + static Mode s_mode; + static std::vector s_saves; + static int s_selectedIndex; + static char s_slotNameInput[256]; + static bool s_needsRefresh; + static bool s_showDeleteConfirm; + static int s_deleteIndex; + + static void refreshSaveList(); + static void doSave(EditorApp *editorApp); + static void doLoad(EditorApp *editorApp, int index); +}; + +#endif // EDITSCENE_SAVELOADDIALOG_HPP diff --git a/src/features/editScene/systems/SaveLoadSystem.cpp b/src/features/editScene/systems/SaveLoadSystem.cpp new file mode 100644 index 0000000..3be1c96 --- /dev/null +++ b/src/features/editScene/systems/SaveLoadSystem.cpp @@ -0,0 +1,182 @@ +#include "SaveLoadSystem.hpp" +#include +#include +#include +#include +#include +#include + +/* ===================================================================== */ +/* OS-dependent save directory */ +/* ===================================================================== */ + +std::string SaveLoadSystem::getSaveDirectory() +{ + std::string dir; + +#ifdef _WIN32 + const char *appdata = getenv("APPDATA"); + if (appdata) + dir = std::string(appdata) + "/World2/saves/"; + else + dir = "./saves/"; +#elif defined(__APPLE__) + const char *home = getenv("HOME"); + if (home) + dir = std::string(home) + + "/Library/Application Support/World2/saves/"; + else + dir = "./saves/"; +#else /* Linux */ + const char *xdgDataHome = getenv("XDG_DATA_HOME"); + if (xdgDataHome) + dir = std::string(xdgDataHome) + "/World2/saves/"; + else { + const char *home = getenv("HOME"); + if (home) + dir = std::string(home) + "/.local/share/World2/saves/"; + else + dir = "./saves/"; + } +#endif + + std::filesystem::create_directories(dir); + return dir; +} + +/* ===================================================================== */ +/* Save slot management */ +/* ===================================================================== */ + +std::vector SaveLoadSystem::listSaves() +{ + std::vector result; + std::string dir = getSaveDirectory(); + + if (!std::filesystem::exists(dir)) + return result; + + for (const auto &entry : + std::filesystem::directory_iterator(dir)) { + if (!entry.is_regular_file()) + continue; + std::string ext = entry.path().extension().string(); + if (ext != ".json") + continue; + + nlohmann::json data; + std::ifstream file(entry.path()); + if (!file.is_open()) + continue; + try { + file >> data; + } catch (...) { + continue; + } + file.close(); + + SaveInfo info; + info.path = entry.path().string(); + info.filename = entry.path().filename().string(); + if (data.contains("saveGame") && data["saveGame"].is_object()) { + auto &sg = data["saveGame"]; + info.slotName = sg.value("slotName", info.filename); + info.timestamp = sg.value("timestamp", ""); + info.playTime = sg.value("playTime", 0.0f); + info.baseScene = sg.value("baseScene", ""); + } + result.push_back(info); + } + + /* Sort by filename (save index) so UI order is stable */ + std::sort(result.begin(), result.end(), + [](const SaveInfo &a, const SaveInfo &b) { + return a.filename < b.filename; + }); + return result; +} + +std::string SaveLoadSystem::generateSaveFilename() +{ + std::string dir = getSaveDirectory(); + int index = 1; + while (true) { + std::stringstream ss; + ss << dir << "save_" << std::setfill('0') << std::setw(3) + << index << ".json"; + std::string path = ss.str(); + if (!std::filesystem::exists(path)) + return path; + ++index; + } +} + +bool SaveLoadSystem::deleteSave(const std::string &path) +{ + try { + return std::filesystem::remove(path); + } catch (...) { + return false; + } +} + +/* ===================================================================== */ +/* File I/O */ +/* ===================================================================== */ + +bool SaveLoadSystem::writeSaveFile(const std::string &path, + const nlohmann::json &data) +{ + try { + std::ofstream file(path); + if (!file.is_open()) { + Ogre::LogManager::getSingleton().logMessage( + "SaveLoadSystem: failed to open file for writing: " + + path); + return false; + } + file << data.dump(4); + file.close(); + return true; + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "SaveLoadSystem: write error: " + + Ogre::String(e.what())); + return false; + } +} + +bool SaveLoadSystem::readSaveFile(const std::string &path, + nlohmann::json &outData) +{ + try { + std::ifstream file(path); + if (!file.is_open()) { + Ogre::LogManager::getSingleton().logMessage( + "SaveLoadSystem: failed to open file for reading: " + + path); + return false; + } + file >> outData; + file.close(); + return true; + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "SaveLoadSystem: read error: " + + Ogre::String(e.what())); + return false; + } +} + +/* ===================================================================== */ +/* Timestamp helper */ +/* ===================================================================== */ + +std::string SaveLoadSystem::getCurrentTimestamp() +{ + auto now = std::time(nullptr); + auto tm = *std::localtime(&now); + std::ostringstream oss; + oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); + return oss.str(); +} diff --git a/src/features/editScene/systems/SaveLoadSystem.hpp b/src/features/editScene/systems/SaveLoadSystem.hpp new file mode 100644 index 0000000..7848ac4 --- /dev/null +++ b/src/features/editScene/systems/SaveLoadSystem.hpp @@ -0,0 +1,75 @@ +#ifndef EDITSCENE_SAVELOADSYSTEM_HPP +#define EDITSCENE_SAVELOADSYSTEM_HPP +#pragma once + +#include +#include +#include + +/** + * Save/load system for game-mode saves. + * + * Manages save slots in an OS-dependent user data directory: + * Linux: ~/.local/share/World2/saves/ + * Windows: %APPDATA%/World2/saves/ + * macOS: ~/Library/Application Support/World2/saves/ + * + * Each slot is a single JSON file containing: + * - Save metadata (base scene, timestamp, play time, slot name) + * - CharacterRegistry state + * - ContainerStateRegistry state + * - ItemStateRegistry state + * - Runtime entity component overrides + * - Lua callback data + */ +class SaveLoadSystem { +public: + struct SaveInfo { + std::string path; + std::string filename; + std::string slotName; + std::string timestamp; + float playTime = 0.0f; + std::string baseScene; + }; + + /** + * Get the OS-dependent save directory. + * Creates the directory if it doesn't exist. + */ + static std::string getSaveDirectory(); + + /** + * List all existing save files in the save directory. + */ + static std::vector listSaves(); + + /** + * Generate a unique filename for a new save. + */ + static std::string generateSaveFilename(); + + /** + * Delete a save file. + */ + static bool deleteSave(const std::string &path); + + /** + * Write a save file. + */ + static bool writeSaveFile(const std::string &path, + const nlohmann::json &data); + + /** + * Read a save file. + */ + static bool readSaveFile(const std::string &path, + nlohmann::json &outData); + + /** + * Get current ISO-8601 timestamp string. + */ + static std::string getCurrentTimestamp(); +}; + +#endif // EDITSCENE_SAVELOADSYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 35e863b..7f16373 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -43,6 +43,7 @@ #include "../components/PathFollowing.hpp" #include "../components/BehaviorTree.hpp" #include "../components/GoapBlackboard.hpp" +#include "../components/GoapRunner.hpp" #include "../components/NavMesh.hpp" #include "../components/PrefabInstance.hpp" #include "EditorUISystem.hpp" @@ -51,6 +52,11 @@ #include #include +/* Forward declarations for static helpers used before their definition */ +static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb); +static void deserializeGoapBlackboard(GoapBlackboard &bb, + const nlohmann::json &json); + SceneSerializer::SceneSerializer(flecs::world &world, Ogre::SceneManager *sceneMgr) : m_world(world) @@ -330,10 +336,18 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["goapPlanner"] = serializeGoapPlanner(entity); } + if (entity.has()) { + json["goapRunner"] = serializeGoapRunner(entity); + } if (entity.has()) { json["behaviorTree"] = serializeBehaviorTree(entity); } + if (entity.has()) { + json["goapBlackboard"] = serializeGoapBlackboard( + entity.get()); + } + if (entity.has()) { json["item"] = serializeItem(entity); } @@ -552,10 +566,19 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } + if (json.contains("goapRunner")) { + deserializeGoapRunner(entity, json["goapRunner"]); + } if (json.contains("behaviorTree")) { deserializeBehaviorTree(entity, json["behaviorTree"]); } + if (json.contains("goapBlackboard")) { + GoapBlackboard bb; + deserializeGoapBlackboard(bb, json["goapBlackboard"]); + entity.set(bb); + } + if (json.contains("item")) { deserializeItem(entity, json["item"]); } @@ -846,6 +869,9 @@ void SceneSerializer::deserializeEntityComponents( if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } + if (json.contains("goapRunner")) { + deserializeGoapRunner(entity, json["goapRunner"]); + } if (json.contains("behaviorTree")) { deserializeBehaviorTree(entity, json["behaviorTree"]); } @@ -3348,6 +3374,11 @@ static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb) json["vec3Values"][pair.first] = v; } } + if (!bb.stringValues.empty()) { + json["stringValues"] = nlohmann::json::object(); + for (const auto &pair : bb.stringValues) + json["stringValues"][pair.first] = pair.second; + } return json; } @@ -3377,6 +3408,11 @@ static void deserializeGoapBlackboard(GoapBlackboard &bb, val[2].get()); } } + bb.stringValues.clear(); + if (json.contains("stringValues") && json["stringValues"].is_object()) { + for (auto &[key, val] : json["stringValues"].items()) + bb.stringValues[key] = val.get(); + } } static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node) @@ -3562,6 +3598,27 @@ nlohmann::json SceneSerializer::serializePathFollowing(flecs::entity entity) json["walkSpeed"] = pf.walkSpeed; json["runSpeed"] = pf.runSpeed; json["useRootMotion"] = pf.useRootMotion; + json["currentLocomotionState"] = pf.currentLocomotionState; + json["hasTarget"] = pf.hasTarget; + if (pf.hasTarget) { + nlohmann::json tp; + tp["x"] = pf.targetPosition.x; + tp["y"] = pf.targetPosition.y; + tp["z"] = pf.targetPosition.z; + json["targetPosition"] = tp; + } + if (!pf.path.empty()) { + json["path"] = nlohmann::json::array(); + for (const auto &p : pf.path) { + nlohmann::json pt; + pt["x"] = p.x; + pt["y"] = p.y; + pt["z"] = p.z; + json["path"].push_back(pt); + } + } + json["pathIndex"] = pf.pathIndex; + json["pathRecalcTimer"] = pf.pathRecalcTimer; return json; } @@ -3629,6 +3686,24 @@ void SceneSerializer::deserializePathFollowing(flecs::entity entity, pf.walkSpeed = json.value("walkSpeed", 2.5f); pf.runSpeed = json.value("runSpeed", 5.0f); pf.useRootMotion = json.value("useRootMotion", true); + pf.currentLocomotionState = json.value("currentLocomotionState", "idle"); + pf.hasTarget = json.value("hasTarget", false); + if (json.contains("targetPosition") && json["targetPosition"].is_object()) { + auto &tp = json["targetPosition"]; + pf.targetPosition = Ogre::Vector3(tp.value("x", 0.0f), + tp.value("y", 0.0f), + tp.value("z", 0.0f)); + } + if (json.contains("path") && json["path"].is_array()) { + pf.path.clear(); + for (const auto &pt : json["path"]) { + pf.path.push_back(Ogre::Vector3(pt.value("x", 0.0f), + pt.value("y", 0.0f), + pt.value("z", 0.0f))); + } + } + pf.pathIndex = json.value("pathIndex", 0); + pf.pathRecalcTimer = json.value("pathRecalcTimer", 0.0f); entity.set(pf); } @@ -3698,6 +3773,44 @@ void SceneSerializer::deserializeGoapPlanner(flecs::entity entity, entity.set(planner); } +nlohmann::json SceneSerializer::serializeGoapRunner(flecs::entity entity) +{ + const GoapRunnerComponent &runner = entity.get(); + nlohmann::json json; + json["state"] = static_cast(runner.state); + json["currentActionIndex"] = runner.currentActionIndex; + if (!runner.currentActionName.empty()) + json["currentActionName"] = runner.currentActionName; + json["actionTimer"] = runner.actionTimer; + if (runner.targetSmartObjectId != 0) + json["targetSmartObjectId"] = runner.targetSmartObjectId; + if (!runner.planActions.empty()) { + json["planActions"] = runner.planActions; + } + json["autoReplan"] = runner.autoReplan; + return json; +} + +void SceneSerializer::deserializeGoapRunner(flecs::entity entity, + const nlohmann::json &json) +{ + GoapRunnerComponent runner; + runner.state = static_cast( + json.value("state", 0)); + runner.currentActionIndex = json.value("currentActionIndex", 0); + runner.currentActionName = json.value("currentActionName", ""); + runner.actionTimer = json.value("actionTimer", 0.0f); + runner.targetSmartObjectId = json.value("targetSmartObjectId", 0ULL); + if (json.contains("planActions") && json["planActions"].is_array()) { + for (const auto &name : json["planActions"]) { + if (name.is_string()) + runner.planActions.push_back(name); + } + } + runner.autoReplan = json.value("autoReplan", true); + entity.set(runner); +} + nlohmann::json SceneSerializer::serializeBehaviorTree(flecs::entity entity) { const BehaviorTreeComponent &bt = entity.get(); @@ -3890,6 +4003,14 @@ void SceneSerializer::deserializeItem(flecs::entity entity, ItemComponent item; item.itemId = json.value("itemId", ""); item.stackSize = json.value("stackSize", 1); + item.instanceId = json.value("instanceId", ""); + item.disabled = json.value("disabled", false); + // If the item has an instanceId, check global state registry + // in case it was picked up in a previous session. + if (!item.instanceId.empty() && + ItemStateRegistry::getInstance().isDisabled(item.instanceId)) { + item.disabled = true; + } // Backward compatibility: old scenes had inline item data if (item.itemId.empty() && json.contains("itemName")) { diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index e92cca5..bfa307c 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -75,7 +75,6 @@ public: private: // Serialization helpers - nlohmann::json serializeEntity(flecs::entity entity); void deserializeEntity(const nlohmann::json &json, flecs::entity parent, EditorUISystem *uiSystem); @@ -233,6 +232,7 @@ private: nlohmann::json serializeActuator(flecs::entity entity); nlohmann::json serializeEventHandler(flecs::entity entity); nlohmann::json serializeGoapPlanner(flecs::entity entity); + nlohmann::json serializeGoapRunner(flecs::entity entity); nlohmann::json serializeBehaviorTree(flecs::entity entity); void deserializeActionDatabase(const nlohmann::json &json); void deserializeActionDebug(flecs::entity entity, @@ -247,6 +247,8 @@ private: const nlohmann::json &json); void deserializeGoapPlanner(flecs::entity entity, const nlohmann::json &json); + void deserializeGoapRunner(flecs::entity entity, + const nlohmann::json &json); void deserializeBehaviorTree(flecs::entity entity, const nlohmann::json &json); @@ -255,6 +257,8 @@ private: void deserializePrefabInstance(flecs::entity entity, const nlohmann::json &json); + /* --- Public helpers for save/load system --- */ + public: /** * Deserialize all components from JSON onto an existing entity. * Used by both scene load and prefab instantiation. @@ -277,6 +281,14 @@ private: bool processName = true, bool addEditorMarker = true); + /** + * Serialize a single entity (with all its components) to JSON. + * Public so SaveLoadSystem can serialize runtime entities. + */ + nlohmann::json serializeEntity(flecs::entity entity); + + private: + flecs::world &m_world; Ogre::SceneManager *m_sceneMgr; std::string m_lastError; diff --git a/src/features/editScene/systems/StartupMenuSystem.cpp b/src/features/editScene/systems/StartupMenuSystem.cpp index 3b27904..c977e37 100644 --- a/src/features/editScene/systems/StartupMenuSystem.cpp +++ b/src/features/editScene/systems/StartupMenuSystem.cpp @@ -2,6 +2,7 @@ #include "../EditorApp.hpp" #include "../components/StartupMenu.hpp" #include "EventBus.hpp" +#include "SaveLoadDialog.hpp" #include #include #include @@ -151,8 +152,8 @@ void StartupMenuSystem::renderMenu(StartupMenuComponent &sm) if (sm.showLoadGame) buttons.push_back( { "LOAD GAME", [&]() { - Ogre::LogManager::getSingleton().logMessage( - "Load game not implemented"); + SaveLoadDialog::show(m_editorApp, + SaveLoadDialog::Mode::Load); } }); if (sm.showOptions) @@ -196,6 +197,8 @@ void StartupMenuSystem::renderMenu(StartupMenuComponent &sm) if (m_menuFont) ImGui::PopFont(); + SaveLoadDialog::render(m_editorApp); + ImGui::End(); ImGui::PopStyleColor(); }