Save/Load system works well
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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<CharacterIdentityComponent>().each(
|
||||
[&](flecs::entity e, CharacterIdentityComponent &ci) {
|
||||
auto *rec = registry.findCharacter(ci.registryId);
|
||||
if (!rec || !e.has<TransformComponent>())
|
||||
return;
|
||||
auto &t = e.get<TransformComponent>();
|
||||
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<EntityNameComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name) {
|
||||
if (name.name == "player")
|
||||
playerController = e;
|
||||
});
|
||||
if (playerController.is_alive()) {
|
||||
flecs::entity playerChar = playerController;
|
||||
if (playerController.has<PlayerControllerComponent>()) {
|
||||
auto &pc = playerController.get<PlayerControllerComponent>();
|
||||
if (!pc.targetCharacterName.empty()) {
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e,
|
||||
EntityNameComponent &en) {
|
||||
if (en.name == pc.targetCharacterName)
|
||||
playerChar = e;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (playerChar.has<CharacterIdentityComponent>()) {
|
||||
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<RuntimeMarkerComponent>().each(
|
||||
[&](flecs::entity e, const RuntimeMarkerComponent &) {
|
||||
if (e.has<CharacterIdentityComponent>())
|
||||
return;
|
||||
if (e.has<PlayerControllerComponent>())
|
||||
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<std::string> s_runtimeKeys = {
|
||||
"character", "goapBlackboard", "goapPlanner",
|
||||
"goapRunner", "pathFollowing", "behaviorTree",
|
||||
"inventory", "actionDebug", "animationTree",
|
||||
"animationTreeTemplate"
|
||||
};
|
||||
m_world.query<CharacterIdentityComponent>().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<flecs::entity> charsToDestroy;
|
||||
m_world.query<CharacterSlotsComponent>().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<ItemComponent>()
|
||||
.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<EntityNameComponent>()
|
||||
.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<TransformComponent>()) {
|
||||
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<RuntimeMarkerComponent>();
|
||||
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<EntityNameComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name) {
|
||||
if (name.name == "player")
|
||||
playerController = e;
|
||||
});
|
||||
if (playerController.is_alive() && playerCharId != 0) {
|
||||
flecs::entity playerChar;
|
||||
m_world.query<CharacterIdentityComponent>()
|
||||
.each([&](flecs::entity e,
|
||||
CharacterIdentityComponent &ci) {
|
||||
if (ci.registryId == playerCharId)
|
||||
playerChar = e;
|
||||
});
|
||||
if (playerChar.is_alive() &&
|
||||
playerController.has<PlayerControllerComponent>()) {
|
||||
auto &pc = playerController.get_mut<
|
||||
PlayerControllerComponent>();
|
||||
if (playerChar.has<EntityNameComponent>()) {
|
||||
pc.targetCharacterName =
|
||||
playerChar.get<EntityNameComponent>().name;
|
||||
}
|
||||
if (!playerChar.has<InventoryComponent>()) {
|
||||
playerChar.set<InventoryComponent>(
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "LuaSaveLoadApi.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Internal state */
|
||||
/* ===================================================================== */
|
||||
|
||||
static std::unordered_map<std::string, int> s_saveCallbacks;
|
||||
static std::unordered_map<std::string, int> 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<bool>());
|
||||
} else if (j.is_number_integer()) {
|
||||
lua_pushinteger(L, (lua_Integer)j.get<int64_t>());
|
||||
} else if (j.is_number_float()) {
|
||||
lua_pushnumber(L, j.get<double>());
|
||||
} else if (j.is_string()) {
|
||||
lua_pushstring(L, j.get<std::string>().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
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef EDITSCENE_LUA_SAVELOAD_API_HPP
|
||||
#define EDITSCENE_LUA_SAVELOAD_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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
|
||||
@@ -360,6 +360,8 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// (those are handled above as actuators)
|
||||
if (e.has<ActuatorComponent>())
|
||||
return;
|
||||
if (item.disabled)
|
||||
return;
|
||||
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/EditorMarker.hpp"
|
||||
#include "../components/RuntimeMarker.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreSceneManager.h>
|
||||
#include <fstream>
|
||||
@@ -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<TransformComponent>(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<TransformComponent>()) {
|
||||
auto &t = inst.get_mut<TransformComponent>();
|
||||
t.position = c->position;
|
||||
t.rotation = c->rotation;
|
||||
t.applyToNode();
|
||||
}
|
||||
} else {
|
||||
inst = spawnInlineCharacter(*c, pos);
|
||||
if (inst.is_alive() && inst.has<TransformComponent>()) {
|
||||
auto &t = inst.get_mut<TransformComponent>();
|
||||
t.rotation = c->rotation;
|
||||
t.applyToNode();
|
||||
}
|
||||
}
|
||||
|
||||
if (inst.is_alive()) {
|
||||
inst.set<CharacterIdentityComponent>(
|
||||
CharacterIdentityComponent{id});
|
||||
inst.add<RuntimeMarkerComponent>();
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/RuntimeMarker.hpp"
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
@@ -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<RuntimeMarkerComponent>();
|
||||
itemEntity.set<ItemComponent>(ItemComponent(slot.itemId, dropCount));
|
||||
|
||||
// Create a scene node for the dropped item
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "PauseMenuSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/StartupMenu.hpp"
|
||||
#include "SaveLoadDialog.hpp"
|
||||
#include <flecs.h>
|
||||
#include <functional>
|
||||
#include <imgui.h>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
#include "SaveLoadDialog.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "SaveLoadSystem.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Static state */
|
||||
/* ===================================================================== */
|
||||
|
||||
bool SaveLoadDialog::s_open = false;
|
||||
SaveLoadDialog::Mode SaveLoadDialog::s_mode = SaveLoadDialog::Mode::Save;
|
||||
std::vector<SaveLoadSystem::SaveInfo> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef EDITSCENE_SAVELOADDIALOG_HPP
|
||||
#define EDITSCENE_SAVELOADDIALOG_HPP
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#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<SaveLoadSystem::SaveInfo> 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
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "SaveLoadSystem.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
/* ===================================================================== */
|
||||
/* 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::SaveInfo> SaveLoadSystem::listSaves()
|
||||
{
|
||||
std::vector<SaveInfo> 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();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef EDITSCENE_SAVELOADSYSTEM_HPP
|
||||
#define EDITSCENE_SAVELOADSYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* 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<SaveInfo> 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
|
||||
@@ -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 <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
/* 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<GoapPlannerComponent>()) {
|
||||
json["goapPlanner"] = serializeGoapPlanner(entity);
|
||||
}
|
||||
if (entity.has<GoapRunnerComponent>()) {
|
||||
json["goapRunner"] = serializeGoapRunner(entity);
|
||||
}
|
||||
if (entity.has<BehaviorTreeComponent>()) {
|
||||
json["behaviorTree"] = serializeBehaviorTree(entity);
|
||||
}
|
||||
|
||||
if (entity.has<GoapBlackboard>()) {
|
||||
json["goapBlackboard"] = serializeGoapBlackboard(
|
||||
entity.get<GoapBlackboard>());
|
||||
}
|
||||
|
||||
if (entity.has<ItemComponent>()) {
|
||||
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<GoapBlackboard>(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<float>());
|
||||
}
|
||||
}
|
||||
bb.stringValues.clear();
|
||||
if (json.contains("stringValues") && json["stringValues"].is_object()) {
|
||||
for (auto &[key, val] : json["stringValues"].items())
|
||||
bb.stringValues[key] = val.get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
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<PathFollowingComponent>(pf);
|
||||
}
|
||||
|
||||
@@ -3698,6 +3773,44 @@ void SceneSerializer::deserializeGoapPlanner(flecs::entity entity,
|
||||
entity.set<GoapPlannerComponent>(planner);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeGoapRunner(flecs::entity entity)
|
||||
{
|
||||
const GoapRunnerComponent &runner = entity.get<GoapRunnerComponent>();
|
||||
nlohmann::json json;
|
||||
json["state"] = static_cast<int>(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<GoapRunnerComponent::State>(
|
||||
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<GoapRunnerComponent>(runner);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeBehaviorTree(flecs::entity entity)
|
||||
{
|
||||
const BehaviorTreeComponent &bt = entity.get<BehaviorTreeComponent>();
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/StartupMenu.hpp"
|
||||
#include "EventBus.hpp"
|
||||
#include "SaveLoadDialog.hpp"
|
||||
#include <imgui.h>
|
||||
#include <OgreFontManager.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user