diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 829501b..071773a 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -365,6 +365,7 @@ target_link_libraries(editSceneEditor RecastNavigation::DetourTileCache RecastNavigation::DetourCrowd RecastNavigation::DebugUtils + PackageArchive lua ) @@ -628,6 +629,78 @@ target_include_directories(PackageTool PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ) +# --------------------------------------------------------------------------- +# lua-scripts.package - package all Lua scripts into a single archive +# --------------------------------------------------------------------------- +# Creates lua-scripts.package from the staged lua-scripts build directory. +# Files are stored with paths relative to lua-scripts/ (the "lua-scripts" +# directory prefix is stripped from archive paths). +# +# The staged directory is assembled from two sources: +# 1. Main lua-scripts (from ${CMAKE_BINARY_DIR}/lua-scripts, staged by +# the stage_lua_scripts target in the main build) +# 2. EditScene-specific lua-scripts (from +# ${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts, overlaid on top) +# The overlay ensures editScene files take precedence over main files +# with the same name. +set(LUA_SCRIPTS_PACKAGE "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts.package") +set(LUA_SCRIPTS_STAGED_DIR "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts") +set(LUA_SCRIPTS_EDITSCENE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts") + +# Collect all source files from the main lua-scripts source directory so +# that the package is rebuilt when any of them change. +# CONFIGURE_DEPENDS makes CMake re-evaluate the glob at build time, +# so newly added files are picked up without re-running cmake. +file(GLOB_RECURSE LUA_SCRIPTS_MAIN_FILES + CONFIGURE_DEPENDS + "${CMAKE_SOURCE_DIR}/lua-scripts/*.lua" + "${CMAKE_SOURCE_DIR}/lua-scripts/*.ink" +) + +# Collect all editScene-specific lua-scripts source files. +file(GLOB_RECURSE LUA_SCRIPTS_EDITSCENE_FILES + CONFIGURE_DEPENDS + "${LUA_SCRIPTS_EDITSCENE_SRC}/*.lua" + "${LUA_SCRIPTS_EDITSCENE_SRC}/*.ink" +) + +# Custom command to overlay editScene-specific lua-scripts on top of the +# staged main lua-scripts directory. This copies files from the editScene +# lua-scripts source directory into the staged directory, overlaying on +# top of any files already there from the main build. +set(LUA_SCRIPTS_OVERLAY_DONE "${CMAKE_CURRENT_BINARY_DIR}/.lua_scripts_overlay_done") + +add_custom_command( + OUTPUT "${LUA_SCRIPTS_OVERLAY_DONE}" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${LUA_SCRIPTS_EDITSCENE_SRC}" + "${LUA_SCRIPTS_STAGED_DIR}" + COMMAND ${CMAKE_COMMAND} -E touch "${LUA_SCRIPTS_OVERLAY_DONE}" + DEPENDS stage_lua_scripts + ${LUA_SCRIPTS_EDITSCENE_FILES} + COMMENT "Overlaying editScene lua-scripts on ${LUA_SCRIPTS_STAGED_DIR}" +) + +add_custom_command( + OUTPUT "${LUA_SCRIPTS_PACKAGE}" + COMMAND $ + create "${LUA_SCRIPTS_PACKAGE}" + --exclude "CMakeFiles" + --exclude "cmake_install.cmake" + --exclude "Makefile" + --exclude ".lua_scripts_overlay_done" + --exclude "*.lua-" + "${LUA_SCRIPTS_STAGED_DIR}" + DEPENDS PackageTool + "${LUA_SCRIPTS_OVERLAY_DONE}" + ${LUA_SCRIPTS_MAIN_FILES} + COMMENT "Creating lua-scripts.package from ${LUA_SCRIPTS_STAGED_DIR}" +) + +add_custom_target(lua_scripts_package ALL + DEPENDS "${LUA_SCRIPTS_PACKAGE}" +) + # --------------------------------------------------------------------------- # Package Archive Test # --------------------------------------------------------------------------- @@ -664,6 +737,10 @@ add_custom_command(TARGET editSceneEditor POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_BINARY_DIR}/lua-scripts" "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts" + # Overlay editScene-specific lua-scripts on top of main lua-scripts + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${LUA_SCRIPTS_EDITSCENE_SRC}" + "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg" "${CMAKE_CURRENT_BINARY_DIR}/resources.cfg" diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 9a6da11..302e791 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -99,6 +99,8 @@ #include "components/Inventory.hpp" #include +#include +#include "package/OgrePackageArchive.h" #include #include "lua/LuaEntityApi.hpp" #include "lua/LuaComponentApi.hpp" @@ -268,6 +270,13 @@ EditorApp::~EditorApp() void EditorApp::setup() { try { + // Register the Package archive factory before any resource + // locations are resolved (locateResources is called inside + // ApplicationContext::setup()). + static Ogre::PackageArchiveFactory s_packageFactory; + Ogre::ArchiveManager::getSingleton().addArchiveFactory( + &s_packageFactory); + // Base setup OgreBites::ApplicationContext::setup(); @@ -719,12 +728,14 @@ void EditorApp::startNewGame(const Ogre::String &scenePath) flecs::entity playerCharacter = playerController; if (playerController.is_alive() && playerController.has()) { - auto &pc = playerController.get(); + auto &pc = playerController + .get(); if (!pc.targetCharacterName.empty()) { m_world.query().each( [&](flecs::entity e, EntityNameComponent &en) { - if (en.name == pc.targetCharacterName) + if (en.name == + pc.targetCharacterName) playerCharacter = e; }); } @@ -819,12 +830,11 @@ void EditorApp::saveGame(const std::string &slotPath, nlohmann::json saveData; saveData["version"] = "2.0"; - saveData["saveGame"] = { - { "baseScene", m_currentBaseScene }, - { "timestamp", SaveLoadSystem::getCurrentTimestamp() }, - { "slotName", slotName }, - { "playTime", m_playTime } - }; + 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 @@ -838,7 +848,8 @@ void EditorApp::saveGame(const std::string &slotPath, auto &t = e.get(); if (t.node) { rec->position = t.node->_getDerivedPosition(); - rec->rotation = t.node->_getDerivedOrientation(); + rec->rotation = + t.node->_getDerivedOrientation(); } else { rec->position = t.position; rec->rotation = t.rotation; @@ -856,19 +867,22 @@ void EditorApp::saveGame(const std::string &slotPath, if (playerController.is_alive()) { flecs::entity playerChar = playerController; if (playerController.has()) { - auto &pc = playerController.get(); + auto &pc = playerController + .get(); if (!pc.targetCharacterName.empty()) { m_world.query().each( [&](flecs::entity e, EntityNameComponent &en) { - if (en.name == pc.targetCharacterName) + if (en.name == + pc.targetCharacterName) playerChar = e; }); } } if (playerChar.has()) { - playerCharId = playerChar.get< - CharacterIdentityComponent>().registryId; + playerCharId = + playerChar.get() + .registryId; } } saveData["saveGame"]["playerCharacterId"] = playerCharId; @@ -876,8 +890,7 @@ void EditorApp::saveGame(const std::string &slotPath, saveData["characterRegistry"] = registry.serialize(); saveData["containerState"] = ContainerStateRegistry::getInstance().serialize(); - saveData["itemState"] = - ItemStateRegistry::getInstance().serialize(); + saveData["itemState"] = ItemStateRegistry::getInstance().serialize(); /* Runtime entities — skip characters and player controller */ saveData["runtimeEntities"] = nlohmann::json::array(); @@ -895,29 +908,31 @@ void EditorApp::saveGame(const std::string &slotPath, /* 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" + "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 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; + 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); + Ogre::LogManager::getSingleton().logMessage("Game saved to: " + + slotPath); EventBus::getInstance().send("game_saved"); } else { Ogre::LogManager::getSingleton().logMessage( @@ -988,12 +1003,10 @@ void EditorApp::loadGame(const std::string &slotPath) * 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(); + 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) @@ -1013,9 +1026,8 @@ void EditorApp::loadGame(const std::string &slotPath) if (!spawned.is_alive()) continue; serializer.deserializeEntityComponents( - spawned, el.value(), - flecs::entity::null(), nullptr, - false, false, false); + spawned, el.value(), flecs::entity::null(), + nullptr, false, false, false); } } @@ -1029,9 +1041,11 @@ void EditorApp::loadGame(const std::string &slotPath) 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) { + std::string iid = + entityJson["item"]["instanceId"]; + m_world.query().each( + [&](flecs::entity e, + ItemComponent &item) { if (item.instanceId == iid) targetEntity = e; }); @@ -1043,9 +1057,9 @@ void EditorApp::loadGame(const std::string &slotPath) entityJson["name"].is_object() && entityJson["name"].contains("name")) { std::string ename = entityJson["name"]["name"]; - m_world.query() - .each([&](flecs::entity e, - EntityNameComponent &nameComp) { + m_world.query().each( + [&](flecs::entity e, + EntityNameComponent &nameComp) { if (nameComp.name == ename) targetEntity = e; }); @@ -1057,30 +1071,41 @@ void EditorApp::loadGame(const std::string &slotPath) if (entityJson.contains("transform") && entityJson["transform"].is_object()) { auto &t = entityJson["transform"]; - if (targetEntity.has()) { + 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)); + 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)); + 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)); + s.value("x", + 1.0f), + s.value("y", + 1.0f), + s.value("z", + 1.0f)); } trans.applyToNode(); } @@ -1112,19 +1137,20 @@ void EditorApp::loadGame(const std::string &slotPath) }); if (playerController.is_alive() && playerCharId != 0) { flecs::entity playerChar; - m_world.query() - .each([&](flecs::entity e, - CharacterIdentityComponent &ci) { - if (ci.registryId == playerCharId) - playerChar = e; - }); + 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>(); + auto &pc = + playerController + .get_mut(); if (playerChar.has()) { pc.targetCharacterName = - playerChar.get().name; + playerChar.get() + .name; } if (!playerChar.has()) { playerChar.set( @@ -1143,8 +1169,8 @@ void EditorApp::loadGame(const std::string &slotPath) m_playTime = saveData["saveGame"].value("playTime", 0.0f); setGamePlayState(GamePlayState::Playing); - Ogre::LogManager::getSingleton().logMessage( - "Game loaded from: " + slotPath); + Ogre::LogManager::getSingleton().logMessage("Game loaded from: " + + slotPath); EventBus::getInstance().send("game_loaded"); } @@ -1549,17 +1575,21 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) flecs::entity playerCharacter = playerController; if (playerController.is_alive() && playerController.has()) { - auto &pc = playerController.get(); + auto &pc = playerController.get< + PlayerControllerComponent>(); if (!pc.targetCharacterName.empty()) { m_world.query().each( [&](flecs::entity e, EntityNameComponent &en) { - if (en.name == pc.targetCharacterName) - playerCharacter = e; + if (en.name == + pc.targetCharacterName) + playerCharacter = + e; }); } } - if (playerCharacter.is_valid() && playerCharacter.is_alive()) { + if (playerCharacter.is_valid() && + playerCharacter.is_alive()) { m_characterClassSystem->toggleCharacterSheet( playerCharacter); setGamePlayState(GamePlayState::Paused); @@ -1814,7 +1844,7 @@ void EditorApp::locateResources() Ogre::ResourceGroupManager::getSingleton().createResourceGroup( "LuaScripts", false); Ogre::ResourceGroupManager::getSingleton().addResourceLocation( - "./lua-scripts", "FileSystem", "LuaScripts", true, true); + "./lua-scripts.package", "Package", "LuaScripts", true, true); /* Try multiple relative paths for characters to handle different * working directories (build root vs binary subdirectory) */ diff --git a/src/features/editScene/components/CharacterSlots.hpp b/src/features/editScene/components/CharacterSlots.hpp index 8f4a66c..0bfcc84 100644 --- a/src/features/editScene/components/CharacterSlots.hpp +++ b/src/features/editScene/components/CharacterSlots.hpp @@ -11,17 +11,19 @@ * Layer 1 and 2 are selected via combo boxes. */ struct SlotSelection { - Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1 - Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2 + Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1 + Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2 Ogre::String explicitMesh; // backward-compat override }; /** * Multi-slot mesh component for character parts sharing a skeleton. * The "face" slot (or first available slot) serves as the master skeleton. + * + * Age is now stored in CharacterRegistry::CharacterRecord::age + * and should be retrieved from there when needed. */ struct CharacterSlotsComponent { - Ogre::String age = "adult"; Ogre::String sex = "male"; /* Global outfit level: 0=nude, 1=lingerie, 2=clothed */ diff --git a/src/features/editScene/lua-scripts/data2.lua b/src/features/editScene/lua-scripts/data2.lua new file mode 100644 index 0000000..0eb88cb --- /dev/null +++ b/src/features/editScene/lua-scripts/data2.lua @@ -0,0 +1,36 @@ +print("hello!") + +ecs.behavior_tree.register_node("luaHello", function(entity_id, params) + local message = params.message or "Hello!" + print("Entity " .. entity_id .. " says: " .. message) + return "success" +end) + +ecs.action_db.add_action("lua_hello_action", 1, + {}, -- no preconditions + {}, -- no effects + { -- behavior tree + type = "sequence", + children = { + ecs.behavior_tree.create_node("luaHello", "message=Welcome to the game!"), + ecs.behavior_tree.create_node("setAnimationState", "main/action"), + ecs.behavior_tree.create_node("setAnimationState", "action/sitting-ground"), + ecs.behavior_tree.create_node("delay", "dly", "9.0"), + ecs.behavior_tree.create_node("setAnimationState", "main/locomotion"), + ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"), + } + } +) + +ecs.subscribe_event("game_start", function(event, params) + print("Event: " .. event) + -- ecs.debug_crash("game_start triggered") + ecs.subscribe_event("new_game", function(event, params) + -- ecs.debug_crash("new_game triggered") + local tsub = ecs.subscribe_event("scene_ready", function(event, params) + -- ecs.unsubscribe_event(tsub) + ecs.debug_crash("scene_ready triggered") + end) + end) +end) + diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index 70bb1fe..99bf3a5 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -33,6 +33,8 @@ #include "components/Primitive.hpp" #include "components/TriangleBuffer.hpp" #include "components/CharacterSlots.hpp" +#include "components/CharacterIdentity.hpp" +#include "systems/CharacterRegistry.hpp" #include "components/AnimationTree.hpp" #include "components/AnimationTreeTemplate.hpp" #include "components/Character.hpp" @@ -491,17 +493,26 @@ static void registerAllComponents() // --- CharacterSlots --- REGISTER_COMPONENT( CharacterSlotsComponent, "CharacterSlots", - lua_pushstring(L, c.age.c_str()); - lua_setfield(L, -2, "age"); lua_pushstring(L, c.sex.c_str()); - lua_setfield(L, -2, "sex"); + { // Push: age from registry + Ogre::String age = "adult"; + if (e.has()) { + auto &id = e.get(); + const CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton() + .findCharacter(id.registryId); + if (rec && !rec->age.empty()) + age = rec->age; + } + lua_pushstring(L, age.c_str()); + } lua_setfield(L, -2, "age"); + lua_pushstring(L, c.sex.c_str()); lua_setfield(L, -2, "sex"); // slots map: push as table lua_newtable(L); for (auto &kv : c.slots) { lua_pushstring(L, kv.second.c_str()); lua_setfield(L, -2, kv.first.c_str()); } lua_setfield(L, -2, "slots"); // slotSelections map: push as nested table - lua_newtable(L); - for (auto &kv : c.slotSelections) { + lua_newtable(L); for (auto &kv : c.slotSelections) { lua_newtable(L); lua_pushstring(L, kv.second.layer1Mesh.c_str()); lua_setfield(L, -2, "layer1Mesh"); @@ -510,14 +521,29 @@ static void registerAllComponents() lua_pushstring(L, kv.second.explicitMesh.c_str()); lua_setfield(L, -2, "explicitMesh"); lua_setfield(L, -2, kv.first.c_str()); - } - lua_setfield(L, -2, "slotSelections"); + } lua_setfield(L, -2, "slotSelections"); lua_pushinteger(L, c.outfitLevel); - lua_setfield(L, -2, "outfitLevel"); - pushVector3(L, c.frontAxis); lua_setfield(L, -2, "frontAxis"); - , if (lua_getfield(L, idx, "age"), lua_isstring(L, -1)) - c.age = lua_tostring(L, -1); - lua_pop(L, 1); + lua_setfield(L, -2, "outfitLevel"); pushVector3(L, c.frontAxis); + lua_setfield(L, -2, "frontAxis"); + , + { // Read: age into registry + if (lua_getfield(L, idx, "age"), lua_isstring(L, -1)) { + Ogre::String age = lua_tostring(L, -1); + if (e.has()) { + auto &id = e.get< + CharacterIdentityComponent>(); + CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton() + .findCharacter( + id.registryId); + if (rec) { + rec->age = age; + CharacterRegistry::getSingleton() + .autoSave(); + } + } + } + } lua_pop(L, 1); if (lua_getfield(L, idx, "sex"), lua_isstring(L, -1)) c.sex = lua_tostring(L, -1); lua_pop(L, 1); @@ -534,21 +560,29 @@ static void registerAllComponents() lua_pop(L, 1); } } lua_pop(L, 1); - if (lua_getfield(L, idx, "slotSelections"), lua_istable(L, -1)) { + if (lua_getfield(L, idx, "slotSelections"), + lua_istable(L, -1)) { c.slotSelections.clear(); lua_pushnil(L); while (lua_next(L, -2) != 0) { if (lua_isstring(L, -2) && lua_istable(L, -1)) { SlotSelection sel; - Ogre::String slotName = lua_tostring(L, -2); - if (lua_getfield(L, -1, "layer1Mesh"), lua_isstring(L, -1)) - sel.layer1Mesh = lua_tostring(L, -1); + Ogre::String slotName = + lua_tostring(L, -2); + if (lua_getfield(L, -1, "layer1Mesh"), + lua_isstring(L, -1)) + sel.layer1Mesh = + lua_tostring(L, -1); lua_pop(L, 1); - if (lua_getfield(L, -1, "layer2Mesh"), lua_isstring(L, -1)) - sel.layer2Mesh = lua_tostring(L, -1); + if (lua_getfield(L, -1, "layer2Mesh"), + lua_isstring(L, -1)) + sel.layer2Mesh = + lua_tostring(L, -1); lua_pop(L, 1); - if (lua_getfield(L, -1, "explicitMesh"), lua_isstring(L, -1)) - sel.explicitMesh = lua_tostring(L, -1); + if (lua_getfield(L, -1, "explicitMesh"), + lua_isstring(L, -1)) + sel.explicitMesh = + lua_tostring(L, -1); lua_pop(L, 1); c.slotSelections[slotName] = sel; } @@ -566,12 +600,13 @@ static void registerAllComponents() for (auto &kv : c.weights) { lua_pushnumber(L, kv.second); lua_setfield(L, -2, kv.first.c_str()); - } - , if (lua_istable(L, idx)) { + }, + if (lua_istable(L, idx)) { lua_pushnil(L); while (lua_next(L, idx) != 0) { if (lua_isstring(L, -2) && lua_isnumber(L, -1)) - c.weights[lua_tostring(L, -2)] = lua_tonumber(L, -1); + c.weights[lua_tostring(L, -2)] = + lua_tonumber(L, -1); lua_pop(L, 1); } }); @@ -830,8 +865,7 @@ static void registerAllComponents() // --- Item --- REGISTER_COMPONENT( ItemComponent, "Item", lua_pushstring(L, c.itemId.c_str()); - lua_setfield(L, -2, "itemId"); - lua_pushinteger(L, c.stackSize); + lua_setfield(L, -2, "itemId"); lua_pushinteger(L, c.stackSize); lua_setfield(L, -2, "stackSize"); lua_pushstring(L, c.action.c_str()); lua_setfield(L, -2, "action"); @@ -1293,7 +1327,6 @@ static void registerAllComponents() c.showQuit = lua_toboolean(L, -1) != 0; lua_pop(L, 1);); - // --- PlayerController --- REGISTER_COMPONENT( PlayerControllerComponent, "PlayerController", diff --git a/src/features/editScene/package/PackageTool.cpp b/src/features/editScene/package/PackageTool.cpp index 4198f6c..ef815fe 100644 --- a/src/features/editScene/package/PackageTool.cpp +++ b/src/features/editScene/package/PackageTool.cpp @@ -46,6 +46,70 @@ static void printUsage(const char *prog) << "added from subdirectories." << std::endl; } +/** + * Check if a filename matches a simple glob pattern. + * Supports '*' (matches any sequence of characters except '/'). + */ +static bool matchesGlob(const std::string &filename, const std::string &pattern) +{ + size_t pi = 0, fi = 0; + while (pi < pattern.size()) { + if (pattern[pi] == '*') { + // '*' matches any sequence of non-'/' characters + pi++; + if (pi == pattern.size()) + return true; // trailing '*' matches everything + // Find the next character in filename that matches + // pattern[pi] + while (fi < filename.size() && filename[fi] != '/') { + if (filename[fi] == pattern[pi]) + break; + fi++; + } + if (fi >= filename.size() || filename[fi] == '/') + return false; + } else { + if (fi >= filename.size() || + filename[fi] != pattern[pi]) + return false; + fi++; + pi++; + } + } + return fi == filename.size(); +} + +/** + * Check if a relative path matches any of the exclude patterns. + * Patterns can be: + * - Prefix match: "CMakeFiles" matches "CMakeFiles/foo" or "CMakeFiles" + * - Glob match: "*.lua-" matches "data.lua-" or "foo/bar.lua-" + * - Exact match: "Makefile" matches "Makefile" + */ +static bool isExcluded(const std::string &path, + const std::vector &excludes) +{ + // Extract just the filename part for glob matching + std::string filename = path; + auto slashPos = path.rfind('/'); + if (slashPos != std::string::npos) + filename = path.substr(slashPos + 1); + + for (const auto &pattern : excludes) { + // If pattern contains a glob character, use glob matching + if (pattern.find('*') != std::string::npos) { + if (matchesGlob(filename, pattern)) + return true; + } else { + // Otherwise use prefix matching (existing behavior) + if (path == pattern || path.find(pattern + "/") == 0) { + return true; + } + } + } + return false; +} + /** * Add files from a source path to the archive. * If the source is a directory, it is added recursively. @@ -54,7 +118,8 @@ static void printUsage(const char *prog) */ static void addPathToArchive(Ogre::PackageArchive &archive, const fs::path &sourcePath, - const std::string &destPrefix = "") + const std::string &destPrefix = "", + const std::vector &excludes = {}) { std::error_code ec; @@ -79,6 +144,14 @@ static void addPathToArchive(Ogre::PackageArchive &archive, c = '/'; } + // Skip excluded paths + if (isExcluded(destName, excludes)) { + std::cout << "Skipping (excluded): " + << entry.path().string() + << std::endl; + continue; + } + std::cout << "Adding: " << entry.path().string() << " -> " << destName << std::endl; archive.addFileFromDisk(destName, @@ -100,23 +173,46 @@ static void addPathToArchive(Ogre::PackageArchive &archive, } } +/** + * Parse --exclude options from the argument list. + * Returns the remaining non-option arguments. + */ +static std::vector +parseExcludes(const std::vector &args, + std::vector &excludes) +{ + std::vector remaining; + for (size_t i = 0; i < args.size(); i++) { + if (args[i] == "--exclude" && i + 1 < args.size()) { + excludes.push_back(args[i + 1]); + i++; + } else { + remaining.push_back(args[i]); + } + } + return remaining; +} + static int cmdCreate(const std::vector &args) { - if (args.size() < 2) { + std::vector excludes; + std::vector remaining = parseExcludes(args, excludes); + + if (remaining.size() < 2) { std::cerr << "Error: create requires a package path and at " "least one source file" << std::endl; return 1; } - const std::string &packagePath = args[0]; + const std::string &packagePath = remaining[0]; Ogre::PackageArchive archive(packagePath, "Package"); // Load will create the file if it doesn't exist archive.load(); - for (size_t i = 1; i < args.size(); i++) { - addPathToArchive(archive, args[i]); + for (size_t i = 1; i < remaining.size(); i++) { + addPathToArchive(archive, remaining[i], "", excludes); } std::cout << "Created package: " << packagePath << " with " diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index bbd769c..b58aaad 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -1,3 +1,4 @@ +#include "CharacterSlotSystem.hpp" #include "CharacterRegistry.hpp" #include "PrefabSystem.hpp" #include "EditorUISystem.hpp" @@ -60,9 +61,9 @@ void CharacterRegistry::autoSave() saveToFile(m_autoSavePath); } -std::string CharacterRegistry::generatePrefabPath( - uint64_t id, const std::string &, - const std::string &) const +std::string CharacterRegistry::generatePrefabPath(uint64_t id, + const std::string &, + const std::string &) const { return "prefabs/char_" + std::to_string(id) + ".json"; } @@ -182,13 +183,13 @@ void CharacterRegistry::scanTemplates() /* Name Generation */ /* ===================================================================== */ -static std::string getCharacterSexForLearning(const CharacterRegistry::CharacterRecord &c) +static std::string +getCharacterSexForLearning(const CharacterRegistry::CharacterRecord &c) { if (c.inlineSex == "female") return "female"; /* If inlineSex is default male, try prefab for accuracy */ - if (c.inlineSex == "male" && - std::filesystem::exists(c.prefabPath)) { + if (c.inlineSex == "male" && std::filesystem::exists(c.prefabPath)) { std::string age, sex; int outfit; if (CharacterRegistry::readPrefabSlots(c.prefabPath, age, sex, @@ -282,9 +283,10 @@ std::vector CharacterRegistry::getChildren(uint64_t parentId) const /* Prefab appearance reader */ /* ------------------------------------------------------------------ */ -static bool readPrefabAppearance(const std::string &path, - CharacterSlotsComponent &cs, - std::unordered_map &shapeKeys) +static bool +readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs, + std::unordered_map &shapeKeys, + std::string &outAge) { try { std::ifstream file(path); @@ -294,15 +296,18 @@ static bool readPrefabAppearance(const std::string &path, file >> j; if (j.contains("characterSlots")) { auto &s = j["characterSlots"]; - cs.age = s.value("age", "adult"); cs.sex = s.value("sex", "male"); cs.outfitLevel = s.value("outfitLevel", 2); if (s.contains("slotSelections")) { - for (auto &[slot, selJson] : s["slotSelections"].items()) { + for (auto &[slot, selJson] : + s["slotSelections"].items()) { SlotSelection sel; - sel.layer1Mesh = selJson.value("layer1Mesh", ""); - sel.layer2Mesh = selJson.value("layer2Mesh", ""); - sel.explicitMesh = selJson.value("explicitMesh", ""); + sel.layer1Mesh = + selJson.value("layer1Mesh", ""); + sel.layer2Mesh = + selJson.value("layer2Mesh", ""); + sel.explicitMesh = selJson.value( + "explicitMesh", ""); cs.slotSelections[slot] = sel; } } @@ -360,10 +365,10 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) child->ageYears = 0; child->inlineSex = childSex; - const CharacterRecord *sameSexParent = - (child->inlineSex == sexA) ? pA : pB; + const CharacterRecord *sameSexParent = (child->inlineSex == sexA) ? pA : + pB; - child->inlineAge = "adult"; /* default; can be changed */ + child->age = "adult"; /* default; can be changed */ child->inlineOutfitLevel = 2; /* Inherit class from same-sex parent, fallback to other */ @@ -397,7 +402,8 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) const auto *cls = db.findClass(child->className); if (cls) { auto itBase = cls->baseStats.find(name.c_str()); - if (itBase != cls->baseStats.end() && result < itBase->second) + if (itBase != cls->baseStats.end() && + result < itBase->second) result = itBase->second; } const auto *statDef = db.findStat(name); @@ -418,18 +424,21 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) if (sameSexParent->inlineSlotSelections.empty() && std::filesystem::exists(sameSexParent->prefabPath)) { CharacterSlotsComponent tmpCs; + std::string prefabAge; readPrefabAppearance(sameSexParent->prefabPath, tmpCs, - child->inlineShapeKeyWeights); - child->inlineAge = tmpCs.age; + child->inlineShapeKeyWeights, prefabAge); + child->age = prefabAge; child->inlineSex = tmpCs.sex; child->inlineOutfitLevel = tmpCs.outfitLevel; child->inlineSlotSelections = tmpCs.slotSelections; } else { - child->inlineAge = sameSexParent->inlineAge; + child->age = sameSexParent->age; child->inlineSex = sameSexParent->inlineSex; child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel; - child->inlineSlotSelections = sameSexParent->inlineSlotSelections; - child->inlineShapeKeyWeights = sameSexParent->inlineShapeKeyWeights; + child->inlineSlotSelections = + sameSexParent->inlineSlotSelections; + child->inlineShapeKeyWeights = + sameSexParent->inlineShapeKeyWeights; } /* Randomize configured birth shape keys */ @@ -506,15 +515,15 @@ bool CharacterRegistry::isPregnant(uint64_t motherId) const /* ------------------------------------------------------------------ */ flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c, - const Ogre::Vector3 &pos) + const Ogre::Vector3 &pos) { if (!m_world || !m_sceneMgr) return flecs::entity::null(); flecs::entity inst = m_world->entity(); inst.add(); - inst.set(EntityNameComponent( - c.firstName + " " + c.lastName)); + inst.set( + EntityNameComponent(c.firstName + " " + c.lastName)); /* Transform */ Ogre::SceneNode *node = @@ -530,7 +539,6 @@ flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c, /* CharacterSlots */ CharacterSlotsComponent cs; - cs.age = c.inlineAge; cs.sex = c.inlineSex; cs.outfitLevel = c.inlineOutfitLevel; cs.slotSelections = c.inlineSlotSelections; @@ -558,8 +566,8 @@ void CharacterRegistry::initializeFromClass(uint64_t id) if (!c || c->className.empty()) return; - const auto *cls = CharacterClassDatabase::getSingleton().findClass( - c->className); + const auto *cls = + CharacterClassDatabase::getSingleton().findClass(c->className); if (!cls) return; @@ -593,15 +601,19 @@ void CharacterRegistry::initializeFromClass(uint64_t id) for (int lvl = 2; lvl <= targetLevel; ++lvl) { c->level = lvl; for (const auto &pair : cls->statGrowth) { - int growth = db.computeStatGrowth(pair.first, c->level, *cls); + int growth = db.computeStatGrowth(pair.first, c->level, + *cls); c->stats[pair.first.c_str()] += growth; } for (const auto &pair : cls->skillGrowth) { - int growth = db.computeSkillGrowth(pair.first, c->level, *cls); + int growth = db.computeSkillGrowth(pair.first, c->level, + *cls); c->skills[pair.first.c_str()] += growth; const auto *skillDef = db.findSkill(pair.first); - if (skillDef && c->skills[pair.first.c_str()] > skillDef->maxValue) - c->skills[pair.first.c_str()] = skillDef->maxValue; + if (skillDef && + c->skills[pair.first.c_str()] > skillDef->maxValue) + c->skills[pair.first.c_str()] = + skillDef->maxValue; } c->availablePoints += db.computePointsForLevel(c->level, *cls); } @@ -610,8 +622,7 @@ void CharacterRegistry::initializeFromClass(uint64_t id) int points = c->availablePoints; int idx = 0; int safety = 0; - while (points > 0 && !cls->primaryStats.empty() && - safety < 1000) { + while (points > 0 && !cls->primaryStats.empty() && safety < 1000) { safety++; const auto &statName = cls->primaryStats[idx % cls->primaryStats.size()]; @@ -656,7 +667,8 @@ void CharacterRegistry::initializeFromClass(uint64_t id) /* Initialize resource pools to full */ for (const auto &name : db.getStatNames()) { const auto *def = db.findStat(name); - if (def && def->kind == + if (def && + def->kind == CharacterClassDatabase::StatKind::ResourcePool) { int maxVal = c->stats.count(name.c_str()) ? c->stats[name.c_str()] : @@ -675,8 +687,8 @@ flecs::entity CharacterRegistry::findSpawnedEntity(uint64_t id) const if (!m_world) return flecs::entity::null(); flecs::entity result = flecs::entity::null(); - m_world->query() - .each([&](flecs::entity e, CharacterIdentityComponent &ci) { + m_world->query().each( + [&](flecs::entity e, CharacterIdentityComponent &ci) { if (ci.registryId == id) result = e; }); @@ -732,7 +744,7 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) if (inst.is_alive()) { inst.set( - CharacterIdentityComponent{id}); + CharacterIdentityComponent{ id }); inst.add(); m_uiSystem->addEntity(inst); } @@ -784,8 +796,8 @@ void CharacterRegistry::rebuildIndexes() void CharacterRegistry::addToIndex(size_t relIndex) { const Relationship &r = m_relationships[relIndex]; - m_relBySource.insert({r.sourceId, relIndex}); - m_relByTarget.insert({r.targetId, relIndex}); + m_relBySource.insert({ r.sourceId, relIndex }); + m_relByTarget.insert({ r.targetId, relIndex }); } void CharacterRegistry::removeFromIndex(uint64_t sourceId, uint64_t targetId, @@ -812,9 +824,9 @@ void CharacterRegistry::removeFromIndex(uint64_t sourceId, uint64_t targetId, /* ===================================================================== */ uint64_t CharacterRegistry::createCharacter(const std::string &firstName, - const std::string &lastName, - const std::string &templatePath, - bool persistent) + const std::string &lastName, + const std::string &templatePath, + bool persistent) { uint64_t id = persistent ? m_nextId++ : m_nextRuntimeId++; CharacterRecord rec; @@ -889,15 +901,14 @@ void CharacterRegistry::addToGroup(uint64_t groupId, uint64_t characterId) GroupRecord *g = findGroup(groupId); if (!g) return; - if (std::find(g->memberIds.begin(), g->memberIds.end(), - characterId) == g->memberIds.end()) { + if (std::find(g->memberIds.begin(), g->memberIds.end(), characterId) == + g->memberIds.end()) { g->memberIds.push_back(characterId); autoSave(); } } -void CharacterRegistry::removeFromGroup(uint64_t groupId, - uint64_t characterId) +void CharacterRegistry::removeFromGroup(uint64_t groupId, uint64_t characterId) { GroupRecord *g = findGroup(groupId); if (!g) @@ -934,8 +945,8 @@ CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup, auto srcRange = m_relBySource.equal_range(sourceId); for (auto it = srcRange.first; it != srcRange.second; ++it) { Relationship &r = m_relationships[it->second]; - if (r.sourceIsGroup == sourceIsGroup && r.targetId == targetId && - r.targetIsGroup == targetIsGroup) + if (r.sourceIsGroup == sourceIsGroup && + r.targetId == targetId && r.targetIsGroup == targetIsGroup) return &r; } return nullptr; @@ -943,25 +954,21 @@ CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup, const CharacterRegistry::Relationship * CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup, - uint64_t targetId, - bool targetIsGroup) const + uint64_t targetId, bool targetIsGroup) const { auto srcRange = m_relBySource.equal_range(sourceId); for (auto it = srcRange.first; it != srcRange.second; ++it) { const Relationship &r = m_relationships[it->second]; - if (r.sourceIsGroup == sourceIsGroup && r.targetId == targetId && - r.targetIsGroup == targetIsGroup) + if (r.sourceIsGroup == sourceIsGroup && + r.targetId == targetId && r.targetIsGroup == targetIsGroup) return &r; } return nullptr; } -void CharacterRegistry::setRelationshipStat(uint64_t sourceId, - bool sourceIsGroup, - uint64_t targetId, - bool targetIsGroup, - const std::string &stat, - float value) +void CharacterRegistry::setRelationshipStat( + uint64_t sourceId, bool sourceIsGroup, uint64_t targetId, + bool targetIsGroup, const std::string &stat, float value) { Relationship *r = findRelationship(sourceId, sourceIsGroup, targetId, targetIsGroup); @@ -1063,8 +1070,8 @@ void CharacterRegistry::purgeRelationships(uint64_t id) toRemove.push_back(it->second); auto tgtRange = m_relByTarget.equal_range(id); for (auto it = tgtRange.first; it != tgtRange.second; ++it) - if (std::find(toRemove.begin(), toRemove.end(), - it->second) == toRemove.end()) + if (std::find(toRemove.begin(), toRemove.end(), it->second) == + toRemove.end()) toRemove.push_back(it->second); std::sort(toRemove.begin(), toRemove.end(), std::greater()); @@ -1083,37 +1090,36 @@ void CharacterRegistry::purgeRelationships(uint64_t id) void CharacterRegistry::addCharacterColumn(const std::string &name, ColumnDef::Type type) { - m_characterColumns.push_back({type, name}); + m_characterColumns.push_back({ type, name }); autoSave(); } void CharacterRegistry::removeCharacterColumn(const std::string &name) { - m_characterColumns.erase( - std::remove_if(m_characterColumns.begin(), - m_characterColumns.end(), - [&](const ColumnDef &c) { - return c.name == name; - }), - m_characterColumns.end()); + m_characterColumns.erase(std::remove_if(m_characterColumns.begin(), + m_characterColumns.end(), + [&](const ColumnDef &c) { + return c.name == name; + }), + m_characterColumns.end()); autoSave(); } void CharacterRegistry::addGroupColumn(const std::string &name, ColumnDef::Type type) { - m_groupColumns.push_back({type, name}); + m_groupColumns.push_back({ type, name }); autoSave(); } void CharacterRegistry::removeGroupColumn(const std::string &name) { - m_groupColumns.erase( - std::remove_if(m_groupColumns.begin(), m_groupColumns.end(), - [&](const ColumnDef &c) { - return c.name == name; - }), - m_groupColumns.end()); + m_groupColumns.erase(std::remove_if(m_groupColumns.begin(), + m_groupColumns.end(), + [&](const ColumnDef &c) { + return c.name == name; + }), + m_groupColumns.end()); autoSave(); } @@ -1145,17 +1151,17 @@ nlohmann::json CharacterRegistry::serialize() const for (const auto &c : m_characterColumns) { nlohmann::json col; col["name"] = c.name; - col["type"] = (c.type == ColumnDef::Int) ? "int" : - (c.type == ColumnDef::Float) ? "float" : - "string"; + col["type"] = (c.type == ColumnDef::Int) ? "int" : + (c.type == ColumnDef::Float) ? "float" : + "string"; j["characterColumns"].push_back(col); } for (const auto &c : m_groupColumns) { nlohmann::json col; col["name"] = c.name; - col["type"] = (c.type == ColumnDef::Int) ? "int" : - (c.type == ColumnDef::Float) ? "float" : - "string"; + col["type"] = (c.type == ColumnDef::Int) ? "int" : + (c.type == ColumnDef::Float) ? "float" : + "string"; j["groupColumns"].push_back(col); } @@ -1175,7 +1181,7 @@ nlohmann::json CharacterRegistry::serialize() const rec["availablePoints"] = c.availablePoints; rec["levelUpPending"] = c.levelUpPending; rec["ageYears"] = c.ageYears; - rec["inlineAge"] = c.inlineAge; + rec["age"] = c.age; rec["inlineSex"] = c.inlineSex; rec["inlineOutfitLevel"] = c.inlineOutfitLevel; if (!c.inlineSlotSelections.empty()) { @@ -1280,7 +1286,8 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) } if (j.contains("femaleFirstNameSeeds")) { for (const auto &name : j["femaleFirstNameSeeds"]) - m_femaleFirstNameSeeds.push_back(name.get()); + m_femaleFirstNameSeeds.push_back( + name.get()); } if (j.contains("lastNameSeeds")) { for (const auto &name : j["lastNameSeeds"]) @@ -1288,11 +1295,13 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) } if (j.contains("birthRandomizableShapeKeys")) { for (const auto &key : j["birthRandomizableShapeKeys"]) - m_birthRandomizableShapeKeys.push_back(key.get()); + m_birthRandomizableShapeKeys.push_back( + key.get()); } if (j.contains("birthExcludedShapeKeys")) { for (const auto &key : j["birthExcludedShapeKeys"]) - m_birthExcludedShapeKeys.push_back(key.get()); + m_birthExcludedShapeKeys.push_back( + key.get()); } m_basePregnancyDuration = j.value("basePregnancyDuration", 300.0f); @@ -1307,7 +1316,7 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) else if (ts == "float") t = ColumnDef::Float; m_characterColumns.push_back( - {t, col.value("name", "")}); + { t, col.value("name", "") }); } } if (j.contains("groupColumns")) { @@ -1318,7 +1327,7 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) t = ColumnDef::Int; else if (ts == "float") t = ColumnDef::Float; - m_groupColumns.push_back({t, col.value("name", "")}); + m_groupColumns.push_back({ t, col.value("name", "") }); } } @@ -1329,10 +1338,14 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.persistent = rec.value("persistent", true); c.firstName = rec.value("firstName", ""); c.lastName = rec.value("lastName", ""); - c.pregnantByFatherId = rec.value("pregnantByFatherId", 0); - c.pregnancyProgress = rec.value("pregnancyProgress", 0.0f); - c.pregnancyMaxProgress = rec.value("pregnancyMaxProgress", 0.0f); - c.basePregnancyDuration = rec.value("basePregnancyDuration", 0.0f); + c.pregnantByFatherId = + rec.value("pregnantByFatherId", 0); + c.pregnancyProgress = + rec.value("pregnancyProgress", 0.0f); + c.pregnancyMaxProgress = + rec.value("pregnancyMaxProgress", 0.0f); + c.basePregnancyDuration = + rec.value("basePregnancyDuration", 0.0f); c.prefabPath = rec.value("prefabPath", ""); c.className = rec.value("className", ""); c.level = rec.value("level", 1); @@ -1340,34 +1353,41 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.availablePoints = rec.value("availablePoints", 0); c.levelUpPending = rec.value("levelUpPending", false); c.ageYears = rec.value("ageYears", 0); - c.inlineAge = rec.value("inlineAge", "adult"); + c.age = rec.value("age", "adult"); c.inlineSex = rec.value("inlineSex", "male"); c.inlineOutfitLevel = rec.value("inlineOutfitLevel", 2); - if (rec.contains("position") && rec["position"].is_object()) { + 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()) { + 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)); + 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()) { + for (auto &[slot, s] : + rec["inlineSlotSelections"].items()) { SlotSelection sel; - sel.layer1Mesh = s.value("layer1Mesh", ""); - sel.layer2Mesh = s.value("layer2Mesh", ""); - sel.explicitMesh = s.value("explicitMesh", ""); + sel.layer1Mesh = + s.value("layer1Mesh", ""); + sel.layer2Mesh = + s.value("layer2Mesh", ""); + sel.explicitMesh = + s.value("explicitMesh", ""); c.inlineSlotSelections[slot] = sel; } } if (rec.contains("inlineShapeKeys")) { - for (auto &[k, v] : rec["inlineShapeKeys"].items()) - c.inlineShapeKeyWeights[k] = v.get(); + for (auto &[k, v] : + rec["inlineShapeKeys"].items()) + c.inlineShapeKeyWeights[k] = + v.get(); } if (rec.contains("stats")) { for (auto &[k, v] : rec["stats"].items()) @@ -1398,8 +1418,10 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.floatColumns[k] = v.get(); } if (rec.contains("stringColumns")) { - for (auto &[k, v] : rec["stringColumns"].items()) - c.stringColumns[k] = v.get(); + for (auto &[k, v] : + rec["stringColumns"].items()) + c.stringColumns[k] = + v.get(); } m_characters[c.id] = c; } @@ -1412,7 +1434,8 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) g.name = rec.value("name", ""); if (rec.contains("memberIds")) for (const auto &m : rec["memberIds"]) - g.memberIds.push_back(m.get()); + g.memberIds.push_back( + m.get()); if (rec.contains("intColumns")) { for (auto &[k, v] : rec["intColumns"].items()) g.intColumns[k] = v.get(); @@ -1422,8 +1445,10 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) g.floatColumns[k] = v.get(); } if (rec.contains("stringColumns")) { - for (auto &[k, v] : rec["stringColumns"].items()) - g.stringColumns[k] = v.get(); + for (auto &[k, v] : + rec["stringColumns"].items()) + g.stringColumns[k] = + v.get(); } m_groups[g.id] = g; } @@ -1516,12 +1541,14 @@ static void drawColumnsEditor(const char *title, ImGui::Indent(); for (size_t i = 0; i < cols.size(); ++i) { ImGui::PushID(static_cast(i)); - ImGui::Text("%s (%s)", cols[i].name.c_str(), - cols[i].type == CharacterRegistry::ColumnDef::Int - ? "int" : - cols[i].type == CharacterRegistry::ColumnDef::Float - ? "float" : - "string"); + ImGui::Text( + "%s (%s)", cols[i].name.c_str(), + cols[i].type == CharacterRegistry::ColumnDef::Int ? + "int" : + cols[i].type == CharacterRegistry::ColumnDef:: + Float ? + "float" : + "string"); ImGui::SameLine(); if (ImGui::SmallButton("Del")) { cols.erase(cols.begin() + i--); @@ -1533,7 +1560,7 @@ static void drawColumnsEditor(const char *title, static char newColName[64] = ""; static int newColType = 0; ImGui::InputText("Name", newColName, sizeof(newColName)); - const char *types[] = {"int", "float", "string"}; + const char *types[] = { "int", "float", "string" }; ImGui::Combo("Type", &newColType, types, 3); if (ImGui::Button("Add Column") && strlen(newColName) > 0) { CharacterRegistry::ColumnDef::Type t = @@ -1542,7 +1569,7 @@ static void drawColumnsEditor(const char *title, t = CharacterRegistry::ColumnDef::Float; else if (newColType == 2) t = CharacterRegistry::ColumnDef::String; - cols.push_back({t, newColName}); + cols.push_back({ t, newColName }); newColName[0] = '\0'; reg.autoSave(); } @@ -1578,8 +1605,8 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::EndMenuBar(); } - const char *tabs[] = {"Characters", "Groups", "Relationships", - "Names", "Columns"}; + const char *tabs[] = { "Characters", "Groups", "Relationships", "Names", + "Columns" }; if (ImGui::BeginTabBar("RegistryTabs")) { for (int i = 0; i < 5; ++i) { if (ImGui::BeginTabItem(tabs[i])) { @@ -1596,7 +1623,7 @@ void CharacterRegistry::drawEditor(bool *p_open) static bool addRuntime = false; if (ImGui::Button("Add Character")) { uint64_t id = createCharacter("New", "Character", "", - addRuntime); + addRuntime); m_selectedCharacterId = id; } ImGui::SameLine(); @@ -1609,28 +1636,25 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Separator(); /* Table */ - if (ImGui::BeginTable("CharactersTable", - 8 + m_characterColumns.size(), - ImGuiTableFlags_Borders | - ImGuiTableFlags_RowBg | - ImGuiTableFlags_ScrollY, - ImVec2(0, 200))) { - ImGui::TableSetupColumn("ID", - ImGuiTableColumnFlags_WidthFixed, - 40); + if (ImGui::BeginTable( + "CharactersTable", 8 + m_characterColumns.size(), + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY, + ImVec2(0, 200))) { + ImGui::TableSetupColumn( + "ID", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("First Name"); ImGui::TableSetupColumn("Last Name"); - ImGui::TableSetupColumn("Age(Yrs)", - ImGuiTableColumnFlags_WidthFixed, - 50); - ImGui::TableSetupColumn("Sex", - ImGuiTableColumnFlags_WidthFixed, - 50); + ImGui::TableSetupColumn( + "Age(Yrs)", ImGuiTableColumnFlags_WidthFixed, + 50); + ImGui::TableSetupColumn( + "Sex", ImGuiTableColumnFlags_WidthFixed, 50); ImGui::TableSetupColumn("Class"); ImGui::TableSetupColumn("Prefab"); - ImGui::TableSetupColumn("Actions", - ImGuiTableColumnFlags_WidthFixed, - 120); + ImGui::TableSetupColumn( + "Actions", ImGuiTableColumnFlags_WidthFixed, + 120); for (const auto &c : m_characterColumns) ImGui::TableSetupColumn(c.name.c_str()); ImGui::TableHeadersRow(); @@ -1641,21 +1665,22 @@ void CharacterRegistry::drawEditor(bool *p_open) bool selected = (m_selectedCharacterId == c.id); ImGui::TableSetColumnIndex(0); if (ImGui::Selectable( - Ogre::StringConverter::toString(c.id).c_str(), + Ogre::StringConverter::toString(c.id) + .c_str(), selected)) - m_selectedCharacterId = c.id; + m_selectedCharacterId = c.id; ImGui::TableSetColumnIndex(1); - ImGui::Text("%s%s", - c.persistent ? "" : "[R] ", + ImGui::Text("%s%s", c.persistent ? "" : "[R] ", c.firstName.c_str()); ImGui::TableSetColumnIndex(2); ImGui::Text("%s", c.lastName.c_str()); std::string age = "?", sex = "?"; int outfit = 2; - if (!readPrefabSlots(c.prefabPath, age, sex, outfit)) { - age = c.inlineAge; + if (!readPrefabSlots(c.prefabPath, age, sex, + outfit)) { + age = c.age; sex = c.inlineSex; outfit = c.inlineOutfitLevel; } @@ -1672,7 +1697,8 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::TableSetColumnIndex(7); ImGui::PushID(static_cast(c.id * 10 + 7)); - bool spawned = findSpawnedEntity(c.id).is_alive(); + bool spawned = + findSpawnedEntity(c.id).is_alive(); if (spawned) { if (ImGui::SmallButton("Despawn")) despawnCharacter(c.id); @@ -1682,11 +1708,12 @@ void CharacterRegistry::drawEditor(bool *p_open) } ImGui::SameLine(); if (ImGui::SmallButton("Save")) { - if (!savePrefabForCharacter(c.id)) - Ogre::LogManager::getSingleton().logMessage( + if (!savePrefabForCharacter(c.id)) + Ogre::LogManager::getSingleton() + .logMessage( "CharacterRegistry: save prefab failed: " + m_lastError); - } + } ImGui::PopID(); int colIdx = 8; @@ -1694,23 +1721,31 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::TableSetColumnIndex(colIdx++); switch (col.type) { case ColumnDef::Int: { - auto it = c.intColumns.find(col.name); + auto it = c.intColumns.find( + col.name); if (it != c.intColumns.end()) - ImGui::Text("%lld", - (long long)it->second); + ImGui::Text( + "%lld", + (long long)it + ->second); break; } case ColumnDef::Float: { - auto it = c.floatColumns.find(col.name); + auto it = c.floatColumns.find( + col.name); if (it != c.floatColumns.end()) - ImGui::Text("%.3f", it->second); + ImGui::Text("%.3f", + it->second); break; } case ColumnDef::String: { - auto it = c.stringColumns.find(col.name); + auto it = c.stringColumns.find( + col.name); if (it != c.stringColumns.end()) - ImGui::Text("%s", - it->second.c_str()); + ImGui::Text( + "%s", + it->second + .c_str()); break; } } @@ -1721,7 +1756,8 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Selected character detail */ if (m_selectedCharacterId != 0) { - CharacterRecord *c = findCharacter(m_selectedCharacterId); + CharacterRecord *c = + findCharacter(m_selectedCharacterId); if (c) { static char fnBuf[128] = ""; static char lnBuf[128] = ""; @@ -1739,16 +1775,19 @@ void CharacterRegistry::drawEditor(bool *p_open) c->persistent ? "" : "[RUNTIME]"); if (!c->persistent) { ImGui::SameLine(); - if (ImGui::SmallButton("Promote to Roster")) { + if (ImGui::SmallButton( + "Promote to Roster")) { c->persistent = true; - c->prefabPath = generatePrefabPath( - c->id, c->firstName, - c->lastName); + c->prefabPath = + generatePrefabPath( + c->id, + c->firstName, + c->lastName); autoSave(); } } if (ImGui::InputText("First Name", fnBuf, - sizeof(fnBuf))) { + sizeof(fnBuf))) { c->firstName = fnBuf; autoSave(); } @@ -1761,21 +1800,24 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Name generation */ ImGui::Separator(); if (ImGui::Button("Generate Male First Name")) { - std::string g = generateFirstName("male"); + std::string g = + generateFirstName("male"); if (!g.empty()) { - snprintf(fnBuf, sizeof(fnBuf), "%s", - g.c_str()); + snprintf(fnBuf, sizeof(fnBuf), + "%s", g.c_str()); c->firstName = fnBuf; autoSave(); learnNamesFromRegistry(); } } ImGui::SameLine(); - if (ImGui::Button("Generate Female First Name")) { - std::string g = generateFirstName("female"); + if (ImGui::Button( + "Generate Female First Name")) { + std::string g = + generateFirstName("female"); if (!g.empty()) { - snprintf(fnBuf, sizeof(fnBuf), "%s", - g.c_str()); + snprintf(fnBuf, sizeof(fnBuf), + "%s", g.c_str()); c->firstName = fnBuf; autoSave(); learnNamesFromRegistry(); @@ -1785,8 +1827,8 @@ void CharacterRegistry::drawEditor(bool *p_open) if (ImGui::Button("Generate Last Name")) { std::string g = generateLastName(); if (!g.empty()) { - snprintf(lnBuf, sizeof(lnBuf), "%s", - g.c_str()); + snprintf(lnBuf, sizeof(lnBuf), + "%s", g.c_str()); c->lastName = lnBuf; autoSave(); learnNamesFromRegistry(); @@ -1803,6 +1845,23 @@ void CharacterRegistry::drawEditor(bool *p_open) autoSave(); } + /* Age category from catalog */ + CharacterSlotSystem::loadCatalog(); + std::string currentAgeCat = c->age; + std::vector ageCats = CharacterSlotSystem::getAges(); + if (ImGui::BeginCombo("Age Category", currentAgeCat.c_str())) { + for (const auto &ac : ageCats) { + bool isSelected = (currentAgeCat == ac); + if (ImGui::Selectable(ac.c_str(), isSelected)) { + c->age = ac; + autoSave(); + } + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + /* Family */ ImGui::Separator(); ImGui::Text("Family"); @@ -1810,16 +1869,19 @@ void CharacterRegistry::drawEditor(bool *p_open) if (!parents.empty()) { ImGui::Text("Parents:"); for (uint64_t pid : parents) { - const CharacterRecord *p = findCharacter(pid); + const CharacterRecord *p = + findCharacter(pid); if (!p) continue; char lbl[256]; - snprintf(lbl, sizeof(lbl), "#%lu %s %s", - (unsigned long)pid, - p->firstName.c_str(), - p->lastName.c_str()); + snprintf(lbl, sizeof(lbl), + "#%lu %s %s", + (unsigned long)pid, + p->firstName.c_str(), + p->lastName.c_str()); if (ImGui::SmallButton(lbl)) - m_selectedCharacterId = pid; + m_selectedCharacterId = + pid; ImGui::SameLine(); } ImGui::NewLine(); @@ -1828,16 +1890,19 @@ void CharacterRegistry::drawEditor(bool *p_open) if (!children.empty()) { ImGui::Text("Children:"); for (uint64_t cid : children) { - const CharacterRecord *ch = findCharacter(cid); + const CharacterRecord *ch = + findCharacter(cid); if (!ch) continue; char lbl[256]; - snprintf(lbl, sizeof(lbl), "#%lu %s %s", - (unsigned long)cid, - ch->firstName.c_str(), - ch->lastName.c_str()); + snprintf(lbl, sizeof(lbl), + "#%lu %s %s", + (unsigned long)cid, + ch->firstName.c_str(), + ch->lastName.c_str()); if (ImGui::SmallButton(lbl)) - m_selectedCharacterId = cid; + m_selectedCharacterId = + cid; ImGui::SameLine(); } ImGui::NewLine(); @@ -1846,9 +1911,12 @@ void CharacterRegistry::drawEditor(bool *p_open) static uint64_t partnerId = 0; char partnerLbl[256] = "(select partner)"; if (partnerId != 0) { - const CharacterRecord *pc = findCharacter(partnerId); + const CharacterRecord *pc = + findCharacter(partnerId); if (pc) - labelBuf(partnerLbl, sizeof(partnerLbl), pc); + labelBuf(partnerLbl, + sizeof(partnerLbl), + pc); else partnerId = 0; } @@ -1857,16 +1925,22 @@ void CharacterRegistry::drawEditor(bool *p_open) if (pair.second.id == c->id) continue; char lbl[256]; - labelBuf(lbl, sizeof(lbl), &pair.second); - if (ImGui::Selectable(lbl, - partnerId == pair.second.id)) - partnerId = pair.second.id; + labelBuf(lbl, sizeof(lbl), + &pair.second); + if (ImGui::Selectable( + lbl, + partnerId == + pair.second + .id)) + partnerId = + pair.second.id; } ImGui::EndCombo(); } if (partnerId != 0 && ImGui::Button("Create Child")) { - uint64_t childId = createChild(c->id, partnerId); + uint64_t childId = + createChild(c->id, partnerId); if (childId != 0) m_selectedCharacterId = childId; } @@ -1875,12 +1949,16 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Separator(); ImGui::Text("Pregnancy"); if (c->pregnantByFatherId != 0) { - const CharacterRecord *father = findCharacter( - c->pregnantByFatherId); + const CharacterRecord *father = + findCharacter( + c->pregnantByFatherId); ImGui::Text("Pregnant by: %s", - father ? (father->firstName + " " + - father->lastName).c_str() : - "unknown"); + father ? + (father->firstName + + " " + + father->lastName) + .c_str() : + "unknown"); float pct = 0.0f; if (c->pregnancyMaxProgress > 0.0f) pct = c->pregnancyProgress / @@ -1888,7 +1966,7 @@ void CharacterRegistry::drawEditor(bool *p_open) if (pct > 1.0f) pct = 1.0f; ImGui::ProgressBar(pct, - ImVec2(0.0f, 0.0f)); + ImVec2(0.0f, 0.0f)); ImGui::Text("%.1f / %.1f s", c->pregnancyProgress, c->pregnancyMaxProgress); @@ -1896,9 +1974,9 @@ void CharacterRegistry::drawEditor(bool *p_open) abortPregnancy(c->id); } float prog = c->pregnancyProgress; - if (ImGui::SliderFloat("Progress", &prog, - 0.0f, - c->pregnancyMaxProgress)) { + if (ImGui::SliderFloat( + "Progress", &prog, 0.0f, + c->pregnancyMaxProgress)) { c->pregnancyProgress = prog; autoSave(); } @@ -1906,24 +1984,35 @@ void CharacterRegistry::drawEditor(bool *p_open) static uint64_t fatherId = 0; char fatherLbl[256] = "(select father)"; if (fatherId != 0) { - const CharacterRecord *fc = findCharacter(fatherId); + const CharacterRecord *fc = + findCharacter(fatherId); if (fc) - labelBuf(fatherLbl, - sizeof(fatherLbl), fc); + labelBuf( + fatherLbl, + sizeof(fatherLbl), + fc); else fatherId = 0; } - if (ImGui::BeginCombo("Father", fatherLbl)) { - for (const auto &pair : m_characters) { - if (pair.second.id == c->id) + if (ImGui::BeginCombo("Father", + fatherLbl)) { + for (const auto &pair : + m_characters) { + if (pair.second.id == + c->id) continue; char lbl[256]; - labelBuf(lbl, sizeof(lbl), + labelBuf(lbl, + sizeof(lbl), &pair.second); if (ImGui::Selectable( lbl, - fatherId == pair.second.id)) - fatherId = pair.second.id; + fatherId == + pair.second + .id)) + fatherId = + pair.second + .id; } ImGui::EndCombo(); } @@ -1934,39 +2023,55 @@ void CharacterRegistry::drawEditor(bool *p_open) } } - ImGui::Text("Prefab: %s", c->prefabPath.c_str()); + ImGui::Text("Prefab: %s", + c->prefabPath.c_str()); bool exists = std::filesystem::exists(c->prefabPath); ImGui::Text("Status: %s", - exists ? "exists" : - "missing"); + exists ? "exists" : "missing"); /* If prefab missing, offer template creation */ if (!exists && !m_templates.empty()) { static int tmplIdx = 0; - if (tmplIdx >= static_cast(m_templates.size())) + if (tmplIdx >= + static_cast( + m_templates.size())) tmplIdx = 0; char comboLabel[256]; - snprintf(comboLabel, sizeof(comboLabel), "%s", - std::filesystem::path(m_templates[tmplIdx]) + snprintf(comboLabel, sizeof(comboLabel), + "%s", + std::filesystem::path( + m_templates[tmplIdx]) .filename() .c_str()); - if (ImGui::BeginCombo("Template", comboLabel)) { - for (size_t i = 0; i < m_templates.size(); ++i) { + if (ImGui::BeginCombo("Template", + comboLabel)) { + for (size_t i = 0; + i < m_templates.size(); + ++i) { char lbl[256]; - snprintf(lbl, sizeof(lbl), "%s", - std::filesystem::path( - m_templates[i]) - .filename() - .c_str()); - if (ImGui::Selectable(lbl, - tmplIdx == - static_cast(i))) - tmplIdx = static_cast(i); + snprintf( + lbl, + sizeof(lbl), + "%s", + std::filesystem::path( + m_templates + [i]) + .filename() + .c_str()); + if (ImGui::Selectable( + lbl, + tmplIdx == + static_cast< + int>( + i))) + tmplIdx = static_cast< + int>(i); } ImGui::EndCombo(); } - if (ImGui::Button("Create from Template")) { + if (ImGui::Button( + "Create from Template")) { copyPrefab(m_templates[tmplIdx], c->prefabPath); } @@ -1977,34 +2082,55 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Text("RPG Data"); /* Class selector */ - auto &db = CharacterClassDatabase::getSingleton(); + auto &db = + CharacterClassDatabase::getSingleton(); const auto &classNames = db.getClassNames(); if (!classNames.empty()) { int clsIdx = -1; - for (size_t i = 0; i < classNames.size(); ++i) { - if (classNames[i] == c->className) { - clsIdx = static_cast(i); + for (size_t i = 0; + i < classNames.size(); ++i) { + if (classNames[i] == + c->className) { + clsIdx = + static_cast( + i); break; } } char clsLbl[256]; snprintf(clsLbl, sizeof(clsLbl), "%s", - clsIdx >= 0 ? classNames[clsIdx].c_str() : - "(none)"); - if (ImGui::BeginCombo("Class", clsLbl)) { - if (ImGui::Selectable("(none)", clsIdx < 0)) { + clsIdx >= 0 ? + classNames[clsIdx] + .c_str() : + "(none)"); + if (ImGui::BeginCombo("Class", + clsLbl)) { + if (ImGui::Selectable( + "(none)", + clsIdx < 0)) { c->className.clear(); autoSave(); } - for (size_t i = 0; i < classNames.size(); ++i) { + for (size_t i = 0; + i < classNames.size(); + ++i) { if (ImGui::Selectable( - classNames[i].c_str(), + classNames[i] + .c_str(), clsIdx == - static_cast(i))) { - bool hadClass = !c->className.empty(); - c->className = classNames[i].c_str(); - if (!hadClass || c->stats.empty()) - initializeFromClass(c->id); + static_cast< + int>( + i))) { + bool hadClass = + !c->className + .empty(); + c->className = + classNames[i] + .c_str(); + if (!hadClass || + c->stats.empty()) + initializeFromClass( + c->id); autoSave(); } } @@ -2018,9 +2144,9 @@ void CharacterRegistry::drawEditor(bool *p_open) autoSave(); } long long xp = c->currentXP; - if (ImGui::InputScalar("Current XP", ImGuiDataType_S64, - &xp, nullptr, nullptr, - "%lld")) { + if (ImGui::InputScalar( + "Current XP", ImGuiDataType_S64, + &xp, nullptr, nullptr, "%lld")) { c->currentXP = xp; autoSave(); } @@ -2032,13 +2158,17 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Stats */ if (ImGui::TreeNode("Stats")) { - for (const auto &name : db.getStatNames()) { + for (const auto &name : + db.getStatNames()) { int val = 0; - auto it = c->stats.find(name.c_str()); + auto it = c->stats.find( + name.c_str()); if (it != c->stats.end()) val = it->second; - if (ImGui::InputInt(name.c_str(), &val)) { - c->stats[name.c_str()] = val; + if (ImGui::InputInt(name.c_str(), + &val)) { + c->stats[name.c_str()] = + val; autoSave(); } } @@ -2047,13 +2177,17 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Skills */ if (ImGui::TreeNode("Skills")) { - for (const auto &name : db.getSkillNames()) { + for (const auto &name : + db.getSkillNames()) { int val = 0; - auto it = c->skills.find(name.c_str()); + auto it = c->skills.find( + name.c_str()); if (it != c->skills.end()) val = it->second; - if (ImGui::InputInt(name.c_str(), &val)) { - c->skills[name.c_str()] = val; + if (ImGui::InputInt(name.c_str(), + &val)) { + c->skills[name.c_str()] = + val; autoSave(); } } @@ -2062,13 +2196,17 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Needs */ if (ImGui::TreeNode("Needs")) { - for (const auto &name : db.getNeedNames()) { + for (const auto &name : + db.getNeedNames()) { int val = 0; - auto it = c->needs.find(name.c_str()); + auto it = c->needs.find( + name.c_str()); if (it != c->needs.end()) val = it->second; - if (ImGui::InputInt(name.c_str(), &val)) { - c->needs[name.c_str()] = val; + if (ImGui::InputInt(name.c_str(), + &val)) { + c->needs[name.c_str()] = + val; autoSave(); } } @@ -2077,18 +2215,25 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Current Pools */ if (ImGui::TreeNode("Current Pools")) { - for (const auto &name : db.getStatNames()) { - const auto *def = db.findStat(name); - if (!def || def->kind != - CharacterClassDatabase::StatKind:: - ResourcePool) + for (const auto &name : + db.getStatNames()) { + const auto *def = + db.findStat(name); + if (!def || + def->kind != + CharacterClassDatabase:: + StatKind::ResourcePool) continue; int val = 0; - auto it = c->currentPools.find(name.c_str()); + auto it = c->currentPools.find( + name.c_str()); if (it != c->currentPools.end()) val = it->second; - if (ImGui::InputInt(name.c_str(), &val)) { - c->currentPools[name.c_str()] = val; + if (ImGui::InputInt(name.c_str(), + &val)) { + c->currentPools + [name.c_str()] = + val; autoSave(); } } @@ -2103,7 +2248,8 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Text("%s", c->tags[i].c_str()); ImGui::SameLine(); if (ImGui::SmallButton("x")) { - c->tags.erase(c->tags.begin() + i); + c->tags.erase(c->tags.begin() + + i); autoSave(); } else { ++i; @@ -2111,14 +2257,15 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::PopID(); } static char tagBuf[64] = ""; - ImGui::InputText("New tag", tagBuf, sizeof(tagBuf)); + ImGui::InputText("New tag", tagBuf, + sizeof(tagBuf)); ImGui::SameLine(); - if (ImGui::SmallButton("Add") && tagBuf[0] != '\0') { + if (ImGui::SmallButton("Add") && + tagBuf[0] != '\0') { c->tags.push_back(tagBuf); tagBuf[0] = '\0'; autoSave(); } - } } break; @@ -2141,9 +2288,8 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, 200))) { - ImGui::TableSetupColumn("ID", - ImGuiTableColumnFlags_WidthFixed, - 40); + ImGui::TableSetupColumn( + "ID", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Name"); ImGui::TableSetupColumn("Members"); for (const auto &c : m_groupColumns) @@ -2156,7 +2302,8 @@ void CharacterRegistry::drawEditor(bool *p_open) bool selected = (m_selectedGroupId == g.id); ImGui::TableSetColumnIndex(0); if (ImGui::Selectable( - Ogre::StringConverter::toString(g.id).c_str(), + Ogre::StringConverter::toString(g.id) + .c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) m_selectedGroupId = g.id; @@ -2171,23 +2318,31 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::TableSetColumnIndex(colIdx++); switch (col.type) { case ColumnDef::Int: { - auto it = g.intColumns.find(col.name); + auto it = g.intColumns.find( + col.name); if (it != g.intColumns.end()) - ImGui::Text("%lld", - (long long)it->second); + ImGui::Text( + "%lld", + (long long)it + ->second); break; } case ColumnDef::Float: { - auto it = g.floatColumns.find(col.name); + auto it = g.floatColumns.find( + col.name); if (it != g.floatColumns.end()) - ImGui::Text("%.3f", it->second); + ImGui::Text("%.3f", + it->second); break; } case ColumnDef::String: { - auto it = g.stringColumns.find(col.name); + auto it = g.stringColumns.find( + col.name); if (it != g.stringColumns.end()) - ImGui::Text("%s", - it->second.c_str()); + ImGui::Text( + "%s", + it->second + .c_str()); break; } } @@ -2207,8 +2362,7 @@ void CharacterRegistry::drawEditor(bool *p_open) lastGId = g->id; } ImGui::Separator(); - ImGui::Text("Group #%lu", - (unsigned long)g->id); + ImGui::Text("Group #%lu", (unsigned long)g->id); if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) { g->name = nameBuf; @@ -2217,9 +2371,11 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Text("Members:"); ImGui::Indent(); - for (size_t i = 0; i < g->memberIds.size(); ++i) { + for (size_t i = 0; i < g->memberIds.size(); + ++i) { uint64_t mid = g->memberIds[i]; - const CharacterRecord *mc = findCharacter(mid); + const CharacterRecord *mc = + findCharacter(mid); char lbl[256]; labelBuf(lbl, sizeof(lbl), mc); ImGui::PushID(static_cast(mid)); @@ -2233,23 +2389,32 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Unindent(); if (!m_characters.empty()) { - if (ImGui::BeginCombo("Add Member", "")) { - for (const auto &pair : m_characters) { - const CharacterRecord &c = - pair.second; + if (ImGui::BeginCombo("Add Member", + "")) { + for (const auto &pair : + m_characters) { + const CharacterRecord & + c = pair.second; bool inGroup = - std::find(g->memberIds.begin(), - g->memberIds.end(), - c.id) != - g->memberIds.end(); + std::find( + g->memberIds + .begin(), + g->memberIds + .end(), + c.id) != + g->memberIds + .end(); if (!inGroup) { char lbl[256]; - labelBuf(lbl, sizeof(lbl), - &c); + labelBuf( + lbl, + sizeof(lbl), + &c); if (ImGui::Selectable( lbl)) - addToGroup(g->id, - c.id); + addToGroup( + g->id, + c.id); } } ImGui::EndCombo(); @@ -2283,7 +2448,8 @@ void CharacterRegistry::drawEditor(bool *p_open) labelBuf(lbl, sizeof(lbl), &pair.second); if (ImGui::Selectable(lbl)) - m_relSourceId = pair.second.id; + m_relSourceId = + pair.second.id; } } else { for (const auto &pair : m_groups) { @@ -2291,7 +2457,8 @@ void CharacterRegistry::drawEditor(bool *p_open) labelBuf(lbl, sizeof(lbl), &pair.second); if (ImGui::Selectable(lbl)) - m_relSourceId = pair.second.id; + m_relSourceId = + pair.second.id; } } ImGui::EndCombo(); @@ -2309,7 +2476,7 @@ void CharacterRegistry::drawEditor(bool *p_open) r->sourceIsGroup == (m_relSourceIsGroup != 0)); uint64_t otherId = isOutgoing ? r->targetId : - r->sourceId; + r->sourceId; bool otherIsGroup = isOutgoing ? r->targetIsGroup : r->sourceIsGroup; @@ -2321,14 +2488,11 @@ void CharacterRegistry::drawEditor(bool *p_open) labelBuf(otherLbl, sizeof(otherLbl), findCharacter(otherId)); - ImGui::PushID(static_cast(r->sourceId * - 1000000 + - r->targetId)); - ImGui::Text("%s %s %s", - isOutgoing ? "→" : "←", + ImGui::PushID(static_cast( + r->sourceId * 1000000 + r->targetId)); + ImGui::Text("%s %s %s", isOutgoing ? "→" : "←", otherLbl, - otherIsGroup ? "[group]" : - ""); + otherIsGroup ? "[group]" : ""); for (auto &kv : r->stats) { float v = kv.second; @@ -2357,10 +2521,11 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::SameLine(); if (ImGui::SmallButton("Add Tag") && strlen(newTag) > 0) { - addRelationshipTag( - r->sourceId, r->sourceIsGroup, - r->targetId, r->targetIsGroup, - newTag); + addRelationshipTag(r->sourceId, + r->sourceIsGroup, + r->targetId, + r->targetIsGroup, + newTag); if (strcmp(newTag, "wife") == 0 || strcmp(newTag, "husband") == 0) { addRelationshipTag( @@ -2368,24 +2533,28 @@ void CharacterRegistry::drawEditor(bool *p_open) r->targetIsGroup, r->sourceId, r->sourceIsGroup, - strcmp(newTag, "wife") == 0 ? + strcmp(newTag, + "wife") == 0 ? "husband" : "wife"); - } else if (strcmp(newTag, "sibling") == 0) { + } else if (strcmp(newTag, "sibling") == + 0) { addRelationshipTag( r->targetId, r->targetIsGroup, r->sourceId, r->sourceIsGroup, "sibling"); - } else if (strcmp(newTag, "parent") == 0) { + } else if (strcmp(newTag, "parent") == + 0) { addRelationshipTag( r->targetId, r->targetIsGroup, r->sourceId, r->sourceIsGroup, "child"); - } else if (strcmp(newTag, "child") == 0) { + } else if (strcmp(newTag, "child") == + 0) { addRelationshipTag( r->targetId, r->targetIsGroup, @@ -2398,15 +2567,18 @@ void CharacterRegistry::drawEditor(bool *p_open) if (ImGui::SmallButton("Delete Relationship")) { std::vector newRels; - for (const auto &existing : m_relationships) { - if (!(existing.sourceId == r->sourceId && + for (const auto &existing : + m_relationships) { + if (!(existing.sourceId == + r->sourceId && existing.sourceIsGroup == r->sourceIsGroup && existing.targetId == r->targetId && existing.targetIsGroup == r->targetIsGroup)) - newRels.push_back(existing); + newRels.push_back( + existing); } m_relationships = std::move(newRels); rebuildIndexes(); @@ -2419,7 +2591,8 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Text("Add new relationship:"); static int newTargetIsGroup = 0; static uint64_t newTargetId = 0; - ImGui::RadioButton("To Character", &newTargetIsGroup, 0); + ImGui::RadioButton("To Character", &newTargetIsGroup, + 0); ImGui::SameLine(); ImGui::RadioButton("To Group", &newTargetIsGroup, 1); @@ -2427,30 +2600,43 @@ void CharacterRegistry::drawEditor(bool *p_open) char tgtLbl[256] = "(select)"; if (newTargetId != 0) { if (newTargetIsGroup) - labelBuf(tgtLbl, sizeof(tgtLbl), - findGroup(newTargetId)); + labelBuf( + tgtLbl, sizeof(tgtLbl), + findGroup(newTargetId)); else labelBuf(tgtLbl, sizeof(tgtLbl), - findCharacter(newTargetId)); + findCharacter( + newTargetId)); } if (ImGui::BeginCombo("Target", tgtLbl)) { if (!newTargetIsGroup) { - for (const auto &pair : m_characters) { - if (pair.second.id == m_relSourceId) + for (const auto &pair : + m_characters) { + if (pair.second.id == + m_relSourceId) continue; char lbl[256]; - labelBuf(lbl, sizeof(lbl), + labelBuf(lbl, + sizeof(lbl), &pair.second); - if (ImGui::Selectable(lbl)) - newTargetId = pair.second.id; + if (ImGui::Selectable( + lbl)) + newTargetId = + pair.second + .id; } } else { - for (const auto &pair : m_groups) { + for (const auto &pair : + m_groups) { char lbl[256]; - labelBuf(lbl, sizeof(lbl), + labelBuf(lbl, + sizeof(lbl), &pair.second); - if (ImGui::Selectable(lbl)) - newTargetId = pair.second.id; + if (ImGui::Selectable( + lbl)) + newTargetId = + pair.second + .id; } } ImGui::EndCombo(); @@ -2485,43 +2671,48 @@ void CharacterRegistry::drawEditor(bool *p_open) lastSampleTime = ImGui::GetTime(); maleSamples = m_maleFirstNameGen.generateMany( 5, 3, 12, nullptr); - femaleSamples = m_femaleFirstNameGen.generateMany( - 5, 3, 12, nullptr); + femaleSamples = + m_femaleFirstNameGen.generateMany( + 5, 3, 12, nullptr); lastSamples = m_lastNameGen.generateMany( 5, 3, 12, nullptr); } if (!m_maleFirstNameGen.empty()) { ImGui::TextDisabled("Male first name samples:"); - for (size_t i = 0; i < maleSamples.size(); ++i) { + for (size_t i = 0; i < maleSamples.size(); + ++i) { if (i > 0) ImGui::SameLine(); - ImGui::TextDisabled("%s", - maleSamples[i].c_str()); + ImGui::TextDisabled( + "%s", maleSamples[i].c_str()); } } if (!m_femaleFirstNameGen.empty()) { - ImGui::TextDisabled("Female first name samples:"); - for (size_t i = 0; i < femaleSamples.size(); ++i) { + ImGui::TextDisabled( + "Female first name samples:"); + for (size_t i = 0; i < femaleSamples.size(); + ++i) { if (i > 0) ImGui::SameLine(); - ImGui::TextDisabled("%s", - femaleSamples[i].c_str()); + ImGui::TextDisabled( + "%s", femaleSamples[i].c_str()); } } if (!m_lastNameGen.empty()) { ImGui::TextDisabled("Last name samples:"); - for (size_t i = 0; i < lastSamples.size(); ++i) { + for (size_t i = 0; i < lastSamples.size(); + ++i) { if (i > 0) ImGui::SameLine(); - ImGui::TextDisabled("%s", - lastSamples[i].c_str()); + ImGui::TextDisabled( + "%s", lastSamples[i].c_str()); } } } /* Seed name lists with stored seeds */ auto drawSeedList = [&](const char *label, - std::vector &seeds) { + std::vector &seeds) { ImGui::Text("%s (%zu):", label, seeds.size()); for (size_t i = 0; i < seeds.size();) { ImGui::PushID(static_cast(i + 5000)); @@ -2537,28 +2728,20 @@ void CharacterRegistry::drawEditor(bool *p_open) } }; - drawSeedList("Male first name seeds", - m_maleFirstNameSeeds); - drawSeedList("Female first name seeds", - m_femaleFirstNameSeeds); + drawSeedList("Male first name seeds", m_maleFirstNameSeeds); + drawSeedList("Female first name seeds", m_femaleFirstNameSeeds); drawSeedList("Last name seeds", m_lastNameSeeds); /* Bulk add seeds */ static char maleSeedBuf[4096] = ""; static char femaleSeedBuf[4096] = ""; static char lastSeedBuf[4096] = ""; - ImGui::InputTextMultiline("Male first names", - maleSeedBuf, - sizeof(maleSeedBuf), - ImVec2(0, 60)); - ImGui::InputTextMultiline("Female first names", - femaleSeedBuf, - sizeof(femaleSeedBuf), - ImVec2(0, 60)); - ImGui::InputTextMultiline("Last names", - lastSeedBuf, - sizeof(lastSeedBuf), - ImVec2(0, 60)); + ImGui::InputTextMultiline("Male first names", maleSeedBuf, + sizeof(maleSeedBuf), ImVec2(0, 60)); + ImGui::InputTextMultiline("Female first names", femaleSeedBuf, + sizeof(femaleSeedBuf), ImVec2(0, 60)); + ImGui::InputTextMultiline("Last names", lastSeedBuf, + sizeof(lastSeedBuf), ImVec2(0, 60)); if (ImGui::Button("Add Seeds")) { auto parseLines = [](const char *buf) { std::vector lines; @@ -2569,11 +2752,15 @@ void CharacterRegistry::drawEditor(bool *p_open) *end != '\r') ++end; std::string line(p, end - p); - size_t start = line.find_first_not_of(" \t"); + size_t start = + line.find_first_not_of(" \t"); if (start != std::string::npos) { - size_t last = line.find_last_not_of(" \t"); - lines.push_back(line.substr(start, - last - start + 1)); + size_t last = + line.find_last_not_of( + " \t"); + lines.push_back(line.substr( + start, + last - start + 1)); } p = end; while (*p == '\n' || *p == '\r') @@ -2612,15 +2799,15 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::Separator(); ImGui::Text("Birth Config"); ImGui::TextDisabled("Shape keys randomized at birth:"); - for (size_t i = 0; - i < m_birthRandomizableShapeKeys.size();) { + for (size_t i = 0; i < m_birthRandomizableShapeKeys.size();) { ImGui::PushID(static_cast(i)); ImGui::Text("%s", m_birthRandomizableShapeKeys[i].c_str()); ImGui::SameLine(); if (ImGui::SmallButton("x")) { m_birthRandomizableShapeKeys.erase( - m_birthRandomizableShapeKeys.begin() + i); + m_birthRandomizableShapeKeys.begin() + + i); autoSave(); } else { ++i; @@ -2638,11 +2825,9 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::TextDisabled( "Excluded keys (never copied or randomized):"); - for (size_t i = 0; - i < m_birthExcludedShapeKeys.size();) { + for (size_t i = 0; i < m_birthExcludedShapeKeys.size();) { ImGui::PushID(static_cast(i + 1000)); - ImGui::Text("%s", - m_birthExcludedShapeKeys[i].c_str()); + ImGui::Text("%s", m_birthExcludedShapeKeys[i].c_str()); ImGui::SameLine(); if (ImGui::SmallButton("x")) { m_birthExcludedShapeKeys.erase( @@ -2666,7 +2851,8 @@ void CharacterRegistry::drawEditor(bool *p_open) case 4: /* ---------- Columns ---------- */ - drawColumnsEditor("Character Columns", m_characterColumns, *this); + drawColumnsEditor("Character Columns", m_characterColumns, + *this); ImGui::Separator(); drawColumnsEditor("Group Columns", m_groupColumns, *this); break; diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index 8ceccae..dbc683c 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -64,14 +64,17 @@ public: std::unordered_map currentPools; bool levelUpPending = false; + /* Age category string (e.g. "adult", "child", "elder") */ + std::string age = "adult"; + /* Age in years (runtime progression) */ int ageYears = 0; /* Inline appearance (used when no prefab file exists) */ - std::string inlineAge = "adult"; std::string inlineSex = "male"; int inlineOutfitLevel = 2; - std::unordered_map inlineSlotSelections; + std::unordered_map + inlineSlotSelections; std::unordered_map inlineShapeKeyWeights; /* Runtime-only characters are NOT saved to character_registry.json */ @@ -130,12 +133,18 @@ public: CharacterRegistry(const CharacterRegistry &) = delete; CharacterRegistry &operator=(const CharacterRegistry &) = delete; - void setWorld(flecs::world *world) { m_world = world; } + void setWorld(flecs::world *world) + { + m_world = world; + } void setSceneManager(Ogre::SceneManager *sceneMgr) { m_sceneMgr = sceneMgr; } - void setEditorUISystem(EditorUISystem *ui) { m_uiSystem = ui; } + void setEditorUISystem(EditorUISystem *ui) + { + m_uiSystem = ui; + } /** * Load registry from auto-save file. Call after setWorld/setSceneManager. @@ -152,7 +161,8 @@ public: void deleteCharacter(uint64_t id); CharacterRecord *findCharacter(uint64_t id); const CharacterRecord *findCharacter(uint64_t id) const; - const std::unordered_map &getCharacters() const + const std::unordered_map & + getCharacters() const { return m_characters; } @@ -227,10 +237,8 @@ public: int &outfitLevel); static bool writePrefabSlots(const std::string &filepath, const std::string &age, - const std::string &sex, - int outfitLevel); - static bool copyPrefab(const std::string &src, - const std::string &dst); + const std::string &sex, int outfitLevel); + static bool copyPrefab(const std::string &src, const std::string &dst); static bool deletePrefabFile(const std::string &filepath); /* ------------------------------------------------------------------ */ @@ -332,7 +340,10 @@ public: bool saveToFile(const std::string &filepath); bool loadFromFile(const std::string &filepath); - const std::string &getLastError() const { return m_lastError; } + const std::string &getLastError() const + { + return m_lastError; + } /* ------------------------------------------------------------------ */ /* ImGui editor */ @@ -387,7 +398,8 @@ private: void removeFromIndex(uint64_t sourceId, uint64_t targetId, size_t relIndex); - std::string generatePrefabPath(uint64_t id, const std::string &firstName, + std::string generatePrefabPath(uint64_t id, + const std::string &firstName, const std::string &lastName) const; /* UI state */ diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index ca8779d..629e62f 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -1,6 +1,9 @@ #include "CharacterSlotSystem.hpp" +#include "CharacterRegistry.hpp" #include "../components/Transform.hpp" #include "../components/AnimationTree.hpp" +#include "../components/CharacterIdentity.hpp" +#include "../components/CharacterSlots.hpp" #include #include #include @@ -18,7 +21,7 @@ nlohmann::json CharacterSlotSystem::s_bodyParts = nlohmann::json::object(); std::set CharacterSlotSystem::s_meshNames; CharacterSlotSystem::CharacterSlotSystem(flecs::world &world, - Ogre::SceneManager *sceneMgr) + Ogre::SceneManager *sceneMgr) : m_world(world) , m_sceneMgr(sceneMgr) { @@ -67,7 +70,6 @@ void CharacterSlotSystem::loadCatalog() names->end()); } - for (const auto &name : partNames) { Ogre::String group = rgm.findGroupContainingResource(name); Ogre::DataStreamPtr stream = rgm.openResource(name, group); @@ -86,7 +88,8 @@ void CharacterSlotSystem::loadCatalog() if (!s_bodyParts.contains(age)) s_bodyParts[age] = nlohmann::json::object(); if (!s_bodyParts[age].contains(sex)) - s_bodyParts[age][sex] = nlohmann::json::object(); + s_bodyParts[age][sex] = + nlohmann::json::object(); if (!s_bodyParts[age][sex].contains(slot)) s_bodyParts[age][sex][slot] = nlohmann::json::array(); @@ -96,8 +99,8 @@ void CharacterSlotSystem::loadCatalog() /* Preload mesh into Characters group */ try { - Ogre::MeshManager::getSingleton().load(mesh, - "Characters"); + Ogre::MeshManager::getSingleton().load( + mesh, "Characters"); } catch (...) { } } catch (...) { @@ -105,7 +108,6 @@ void CharacterSlotSystem::loadCatalog() } } - s_catalogLoaded = true; } @@ -124,8 +126,7 @@ std::vector CharacterSlotSystem::getAges() return ages; } -std::vector CharacterSlotSystem::getSexes( - const Ogre::String &age) +std::vector CharacterSlotSystem::getSexes(const Ogre::String &age) { std::vector sexes; if (!s_catalogLoaded || !s_bodyParts.contains(age)) @@ -135,8 +136,8 @@ std::vector CharacterSlotSystem::getSexes( return sexes; } -std::vector CharacterSlotSystem::getSlots( - const Ogre::String &age, const Ogre::String &sex) +std::vector CharacterSlotSystem::getSlots(const Ogre::String &age, + const Ogre::String &sex) { std::vector slots; if (!s_catalogLoaded || !s_bodyParts.contains(age) || @@ -147,9 +148,10 @@ std::vector CharacterSlotSystem::getSlots( return slots; } -std::vector CharacterSlotSystem::getMeshesForLayer( - const Ogre::String &age, const Ogre::String &sex, - const Ogre::String &slot, int layer) +std::vector +CharacterSlotSystem::getMeshesForLayer(const Ogre::String &age, + const Ogre::String &sex, + const Ogre::String &slot, int layer) { std::vector meshes; if (!s_catalogLoaded || !s_bodyParts.contains(age) || @@ -165,9 +167,10 @@ std::vector CharacterSlotSystem::getMeshesForLayer( return meshes; } -Ogre::String CharacterSlotSystem::getMeshLabel( - const Ogre::String &age, const Ogre::String &sex, - const Ogre::String &slot, const Ogre::String &mesh) +Ogre::String CharacterSlotSystem::getMeshLabel(const Ogre::String &age, + const Ogre::String &sex, + const Ogre::String &slot, + const Ogre::String &mesh) { if (!s_catalogLoaded || !s_bodyParts.contains(age) || !s_bodyParts[age].contains(sex) || @@ -176,8 +179,8 @@ Ogre::String CharacterSlotSystem::getMeshLabel( for (const auto &entry : s_bodyParts[age][sex][slot]) { if (entry.value("mesh", "") == mesh) { - const auto &garments = - entry.value("garments", nlohmann::json::array()); + const auto &garments = entry.value( + "garments", nlohmann::json::array()); if (garments.empty()) return "nude"; Ogre::String label; @@ -192,9 +195,9 @@ Ogre::String CharacterSlotSystem::getMeshLabel( return mesh; } -std::vector CharacterSlotSystem::getMeshes( - const Ogre::String &age, const Ogre::String &sex, - const Ogre::String &slot) +std::vector +CharacterSlotSystem::getMeshes(const Ogre::String &age, const Ogre::String &sex, + const Ogre::String &slot) { std::vector meshes; if (!s_catalogLoaded || !s_bodyParts.contains(age) || @@ -206,8 +209,9 @@ std::vector CharacterSlotSystem::getMeshes( return meshes; } -std::vector CharacterSlotSystem::getShapeKeyNames( - const Ogre::String &age, const Ogre::String &sex) +std::vector +CharacterSlotSystem::getShapeKeyNames(const Ogre::String &age, + const Ogre::String &sex) { std::set keySet; if (!s_catalogLoaded || !s_bodyParts.contains(age) || @@ -216,8 +220,8 @@ std::vector CharacterSlotSystem::getShapeKeyNames( for (auto &slotEl : s_bodyParts[age][sex].items()) { for (const auto &entry : slotEl.value()) { - const auto &keys = - entry.value("shape_keys", nlohmann::json::array()); + const auto &keys = entry.value("shape_keys", + nlohmann::json::array()); for (const auto &k : keys) keySet.insert(k.get()); } @@ -226,9 +230,10 @@ std::vector CharacterSlotSystem::getShapeKeyNames( return std::vector(keySet.begin(), keySet.end()); } -static std::vector garmentsForMesh( - const Ogre::String &age, const Ogre::String &sex, - const Ogre::String &slot, const Ogre::String &mesh) +static std::vector garmentsForMesh(const Ogre::String &age, + const Ogre::String &sex, + const Ogre::String &slot, + const Ogre::String &mesh) { if (!CharacterSlotSystem::isCatalogLoaded()) return {}; @@ -248,10 +253,11 @@ static std::vector garmentsForMesh( return {}; } -Ogre::String CharacterSlotSystem::resolveMesh( - const Ogre::String &age, const Ogre::String &sex, - const Ogre::String &slot, const SlotSelection &sel, - int outfitLevel) +Ogre::String CharacterSlotSystem::resolveMesh(const Ogre::String &age, + const Ogre::String &sex, + const Ogre::String &slot, + const SlotSelection &sel, + int outfitLevel) { if (!sel.explicitMesh.empty()) return sel.explicitMesh; @@ -269,8 +275,10 @@ Ogre::String CharacterSlotSystem::resolveMesh( /* If layer 1 is also selected, try to find a combined mesh * whose garments array contains both selections */ if (sel.layer1Mesh != "none" && !sel.layer1Mesh.empty()) { - auto l1g = garmentsForMesh(age, sex, slot, sel.layer1Mesh); - auto l2g = garmentsForMesh(age, sex, slot, sel.layer2Mesh); + auto l1g = + garmentsForMesh(age, sex, slot, sel.layer1Mesh); + auto l2g = + garmentsForMesh(age, sex, slot, sel.layer2Mesh); std::set required; for (const auto &g : l1g) required.insert(g); @@ -279,9 +287,9 @@ Ogre::String CharacterSlotSystem::resolveMesh( if (!required.empty()) { Ogre::String combinedMesh; for (const auto &entry : slotEntries) { - auto entryGarments = - entry.value("garments", - nlohmann::json::array()); + auto entryGarments = entry.value( + "garments", + nlohmann::json::array()); std::set eg; for (const auto &g : entryGarments) eg.insert(g.get()); @@ -294,7 +302,8 @@ Ogre::String CharacterSlotSystem::resolveMesh( } if (containsAll) { Ogre::String m = - entry["mesh"].get(); + entry["mesh"] + .get(); /* Prefer exact layer2 match if it already * satisfies the requirement */ if (m == sel.layer2Mesh) @@ -332,8 +341,7 @@ Ogre::String CharacterSlotSystem::resolveMesh( size_t dotMesh = mesh.rfind(".mesh"); if (dotMesh != Ogre::String::npos && dotMesh >= 4) { bool isDup = true; - for (size_t i = dotMesh - 3; - i < dotMesh; ++i) { + for (size_t i = dotMesh - 3; i < dotMesh; ++i) { if (!isdigit(static_cast( mesh[i]))) { isDup = false; @@ -343,8 +351,7 @@ Ogre::String CharacterSlotSystem::resolveMesh( if (isDup && mesh[dotMesh - 4] == '.') effectiveLen += 1000; } - if (bestLayer0.empty() || - effectiveLen < bestLen) { + if (bestLayer0.empty() || effectiveLen < bestLen) { bestLayer0 = mesh; bestLen = effectiveLen; } @@ -374,9 +381,10 @@ void CharacterSlotSystem::update() }); } -static const nlohmann::json *findCatalogEntry( - const Ogre::String &age, const Ogre::String &sex, - const Ogre::String &slot, const Ogre::String &mesh) +static const nlohmann::json *findCatalogEntry(const Ogre::String &age, + const Ogre::String &sex, + const Ogre::String &slot, + const Ogre::String &mesh) { if (!CharacterSlotSystem::isCatalogLoaded()) return nullptr; @@ -396,7 +404,7 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName) Ogre::MeshPtr mesh; try { mesh = Ogre::MeshManager::getSingleton().load(meshName, - "Characters"); + "Characters"); } catch (...) { return; } @@ -405,9 +413,10 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName) try { mesh->getAnimation("ShapeKeys"); } catch (...) { - Ogre::Animation *anim = mesh->createAnimation("ShapeKeys", 1.0f); - Ogre::VertexAnimationTrack *track = anim->createVertexTrack( - 0, Ogre::VAT_POSE); + Ogre::Animation *anim = + mesh->createAnimation("ShapeKeys", 1.0f); + Ogre::VertexAnimationTrack *track = + anim->createVertexTrack(0, Ogre::VAT_POSE); Ogre::VertexPoseKeyFrame *kf = track->createVertexPoseKeyFrame(0.0f); for (size_t i = 0; i < mesh->getPoseCount(); ++i) @@ -415,11 +424,30 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName) } } +/** + * Helper: retrieve the character's age string from the registry. + * Falls back to "adult" if no registry entry is found. + */ +static Ogre::String getCharacterAge(flecs::entity e) +{ + if (e.has()) { + auto &id = e.get(); + const CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton().findCharacter( + id.registryId); + if (rec && !rec->age.empty()) + return rec->age; + } + return "adult"; +} + void CharacterSlotSystem::buildCharacter(flecs::entity e, - CharacterSlotsComponent &cs) + CharacterSlotsComponent &cs) { destroyCharacterParts(e); + Ogre::String age = getCharacterAge(e); + /* Migrate old slots map to slotSelections if needed */ if (cs.slotSelections.empty() && !cs.slots.empty()) { for (const auto &pair : cs.slots) { @@ -431,7 +459,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, /* Populate default slots from catalog if still empty */ if (cs.slotSelections.empty()) { - auto slots = getSlots(cs.age, cs.sex); + auto slots = getSlots(age, cs.sex); for (const auto &slot : slots) { SlotSelection sel; cs.slotSelections[slot] = sel; @@ -448,7 +476,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, /* Determine master slot (face preferred, else first non-empty) */ Ogre::String masterSlot; if (cs.slotSelections.find("face") != cs.slotSelections.end()) { - Ogre::String mesh = resolveMesh(cs.age, cs.sex, "face", + Ogre::String mesh = resolveMesh(age, cs.sex, "face", cs.slotSelections["face"], cs.outfitLevel); if (!mesh.empty()) @@ -456,7 +484,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, } if (masterSlot.empty()) { for (const auto &pair : cs.slotSelections) { - Ogre::String mesh = resolveMesh(cs.age, cs.sex, pair.first, + Ogre::String mesh = resolveMesh(age, cs.sex, pair.first, pair.second, cs.outfitLevel); if (!mesh.empty()) { @@ -469,7 +497,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, if (masterSlot.empty()) return; - Ogre::String masterMesh = resolveMesh(cs.age, cs.sex, masterSlot, + Ogre::String masterMesh = resolveMesh(age, cs.sex, masterSlot, cs.slotSelections[masterSlot], cs.outfitLevel); @@ -489,8 +517,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, cs.masterEntity = masterEnt; /* Setup pose animation for shape keys */ - const nlohmann::json *entry = findCatalogEntry( - cs.age, cs.sex, masterSlot, masterMesh); + const nlohmann::json *entry = + findCatalogEntry(age, cs.sex, masterSlot, masterMesh); applyShapeKeys(e, masterEnt, entry); /* Notify AnimationTreeSystem that entity changed */ @@ -510,8 +538,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, if (slot == masterSlot) continue; - Ogre::String mesh = resolveMesh(cs.age, cs.sex, slot, sel, - cs.outfitLevel); + Ogre::String mesh = + resolveMesh(age, cs.sex, slot, sel, cs.outfitLevel); if (mesh.empty()) continue; @@ -520,12 +548,13 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, Ogre::MeshPtr partMesh = Ogre::MeshManager::getSingleton().load( mesh, "Characters"); - Ogre::Entity *partEnt = m_sceneMgr->createEntity(partMesh); + Ogre::Entity *partEnt = + m_sceneMgr->createEntity(partMesh); partEnt->shareSkeletonInstanceWith(masterEnt); transform.node->attachObject(partEnt); m_entities[e.id()].parts[slot] = partEnt; - const nlohmann::json *entry = findCatalogEntry( - cs.age, cs.sex, slot, mesh); + const nlohmann::json *entry = + findCatalogEntry(age, cs.sex, slot, mesh); applyShapeKeys(e, partEnt, entry); } catch (const Ogre::Exception &ex) { Ogre::LogManager::getSingleton().logMessage( @@ -536,9 +565,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, } } -void CharacterSlotSystem::applyShapeKeys(flecs::entity e, - Ogre::Entity *ent, - const nlohmann::json *entry) +void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, + const nlohmann::json *entry) { if (!ent || !entry) return; @@ -553,8 +581,8 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, anim = mesh->getAnimation("ShapeKeys"); } catch (...) { anim = mesh->createAnimation("ShapeKeys", 1.0f); - Ogre::VertexAnimationTrack *track = anim->createVertexTrack( - 0, Ogre::VAT_POSE); + Ogre::VertexAnimationTrack *track = + anim->createVertexTrack(0, Ogre::VAT_POSE); Ogre::VertexPoseKeyFrame *kf = track->createVertexPoseKeyFrame(0.0f); for (size_t i = 0; i < mesh->getPoseCount(); ++i) @@ -623,7 +651,7 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e) } Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e, - const Ogre::String &slot) + const Ogre::String &slot) { auto it = m_entities.find(e.id()); if (it == m_entities.end()) @@ -635,8 +663,7 @@ Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e, } void CharacterSlotSystem::setSlotVisible(flecs::entity e, - const Ogre::String &slot, - bool visible) + const Ogre::String &slot, bool visible) { Ogre::Entity *ent = getSlotEntity(e, slot); if (ent) { diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 7f16373..566eeda 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -2200,7 +2200,6 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) auto &cs = entity.get(); nlohmann::json json; - json["age"] = cs.age; json["sex"] = cs.sex; json["outfitLevel"] = cs.outfitLevel; json["slots"] = nlohmann::json::object(); @@ -2228,7 +2227,6 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nlohmann::json &json) { CharacterSlotsComponent cs; - cs.age = json.value("age", "adult"); cs.sex = json.value("sex", "male"); cs.outfitLevel = json.value("outfitLevel", 2); if (json.contains("slots") && json["slots"].is_object()) { diff --git a/src/features/editScene/ui/CharacterSlotsEditor.cpp b/src/features/editScene/ui/CharacterSlotsEditor.cpp index ef90d4a..c478765 100644 --- a/src/features/editScene/ui/CharacterSlotsEditor.cpp +++ b/src/features/editScene/ui/CharacterSlotsEditor.cpp @@ -1,5 +1,7 @@ #include "CharacterSlotsEditor.hpp" #include "../systems/CharacterSlotSystem.hpp" +#include "../systems/CharacterRegistry.hpp" +#include "../components/CharacterIdentity.hpp" #include #include @@ -8,8 +10,9 @@ CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr) { } + bool CharacterSlotsEditor::renderComponent(flecs::entity entity, - CharacterSlotsComponent &cs) + CharacterSlotsComponent &cs) { bool modified = false; @@ -19,31 +22,27 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, CharacterSlotSystem::loadCatalog(); - /* Age selector */ - std::vector ages = CharacterSlotSystem::getAges(); - Ogre::String currentAge = cs.age; - if (ImGui::BeginCombo("Age", currentAge.c_str())) { - for (const auto &age : ages) { - bool isSelected = (currentAge == age); - if (ImGui::Selectable(age.c_str(), isSelected)) { - cs.age = age; - modified = true; - cs.dirty = true; - } - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); + + /* Get current age from registry */ + Ogre::String currentAge = "adult"; + if (entity.has()) { + auto &id = entity.get(); + const CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton().findCharacter( + id.registryId); + if (rec && !rec->age.empty()) + currentAge = rec->age; } /* Sex selector */ std::vector sexes = - CharacterSlotSystem::getSexes(cs.age); + CharacterSlotSystem::getSexes(currentAge); Ogre::String currentSex = cs.sex; if (ImGui::BeginCombo("Sex", currentSex.c_str())) { for (const auto &sex : sexes) { bool isSelected = (currentSex == sex); - if (ImGui::Selectable(sex.c_str(), isSelected)) { + if (ImGui::Selectable(sex.c_str(), + isSelected)) { cs.sex = sex; modified = true; cs.dirty = true; @@ -106,21 +105,25 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, /* Shape Keys */ if (ImGui::CollapsingHeader("Shape Keys")) { auto shapeKeys = CharacterSlotSystem::getShapeKeyNames( - cs.age, cs.sex); + currentAge, cs.sex); if (shapeKeys.empty()) { ImGui::TextDisabled("No shape keys available."); } else { CharacterShapeKeysComponent *skc = nullptr; if (entity.has()) - skc = &entity.get_mut(); + skc = &entity.get_mut< + CharacterShapeKeysComponent>(); else { - entity.set({}); - skc = &entity.get_mut(); + entity.set( + {}); + skc = &entity.get_mut< + CharacterShapeKeysComponent>(); } for (const auto &name : shapeKeys) { float val = skc->weights[name]; - if (ImGui::SliderFloat(name.c_str(), &val, 0.0f, + if (ImGui::SliderFloat(name.c_str(), + &val, 0.0f, 1.0f)) { skc->weights[name] = val; skc->dirty = true; @@ -135,9 +138,10 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, /* Slot selections */ std::vector availableSlots = - CharacterSlotSystem::getSlots(cs.age, cs.sex); + CharacterSlotSystem::getSlots(currentAge, cs.sex); for (const auto &pair : cs.slotSelections) { - if (std::find(availableSlots.begin(), availableSlots.end(), + if (std::find(availableSlots.begin(), + availableSlots.end(), pair.first) == availableSlots.end()) availableSlots.push_back(pair.first); } @@ -145,7 +149,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, for (const auto &slot : availableSlots) { /* Ensure selection exists */ - if (cs.slotSelections.find(slot) == cs.slotSelections.end()) { + if (cs.slotSelections.find(slot) == + cs.slotSelections.end()) { SlotSelection sel; /* Migrate old explicit mesh if present */ auto oldIt = cs.slots.find(slot); @@ -159,22 +164,28 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, if (ImGui::TreeNode(slot.c_str())) { /* Resolved mesh preview */ Ogre::String resolved = - CharacterSlotSystem::resolveMesh(cs.age, cs.sex, - slot, sel, - cs.outfitLevel); + CharacterSlotSystem::resolveMesh( + currentAge, cs.sex, slot, sel, + cs.outfitLevel); ImGui::TextDisabled("Resolved: %s", - resolved.empty() ? "(none)" : - resolved.c_str()); + resolved.empty() ? + "(none)" : + resolved.c_str()); /* Explicit mesh override */ bool useExplicit = !sel.explicitMesh.empty(); - if (ImGui::Checkbox("Lock explicit mesh", &useExplicit)) { + if (ImGui::Checkbox("Lock explicit mesh", + &useExplicit)) { if (!useExplicit) { sel.explicitMesh.clear(); } else { /* Lock to currently resolved mesh */ - sel.explicitMesh = CharacterSlotSystem::resolveMesh( - cs.age, cs.sex, slot, sel, cs.outfitLevel); + sel.explicitMesh = + CharacterSlotSystem::resolveMesh( + currentAge, + cs.sex, slot, + sel, + cs.outfitLevel); } modified = true; cs.dirty = true; @@ -183,22 +194,30 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, if (useExplicit) { std::vector meshes = CharacterSlotSystem::getMeshes( - cs.age, cs.sex, slot); + currentAge, cs.sex, slot); Ogre::String preview = sel.explicitMesh.empty() ? "(none)" : sel.explicitMesh; - if (ImGui::BeginCombo("Mesh", preview.c_str())) { - if (ImGui::Selectable("(none)", - sel.explicitMesh.empty())) { + if (ImGui::BeginCombo( + "Mesh", preview.c_str())) { + if (ImGui::Selectable( + "(none)", + sel.explicitMesh + .empty())) { sel.explicitMesh.clear(); modified = true; cs.dirty = true; } for (const auto &m : meshes) { - bool isSelected = (sel.explicitMesh == m); - if (ImGui::Selectable(m.c_str(), isSelected)) { - sel.explicitMesh = m; + bool isSelected = + (sel.explicitMesh == + m); + if (ImGui::Selectable( + m.c_str(), + isSelected)) { + sel.explicitMesh = + m; modified = true; cs.dirty = true; } @@ -208,29 +227,45 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, } else { /* Layer 1 combo */ std::vector layer1Meshes = - CharacterSlotSystem::getMeshesForLayer( - cs.age, cs.sex, slot, 1); + CharacterSlotSystem:: + getMeshesForLayer( + currentAge, cs.sex, + slot, 1); Ogre::String l1Preview = "none"; if (!sel.layer1Mesh.empty()) - l1Preview = CharacterSlotSystem::getMeshLabel( - cs.age, cs.sex, slot, sel.layer1Mesh); - if (ImGui::BeginCombo("Lingerie (Layer 1)", - l1Preview.c_str())) { - if (ImGui::Selectable("none", - sel.layer1Mesh.empty() || - sel.layer1Mesh == "none")) { + l1Preview = CharacterSlotSystem:: + getMeshLabel( + currentAge, cs.sex, + slot, + sel.layer1Mesh); + if (ImGui::BeginCombo( + "Lingerie (Layer 1)", + l1Preview.c_str())) { + if (ImGui::Selectable( + "none", + sel.layer1Mesh.empty() || + sel.layer1Mesh == + "none")) { sel.layer1Mesh = "none"; modified = true; cs.dirty = true; } - for (const auto &m : layer1Meshes) { - Ogre::String label = - CharacterSlotSystem::getMeshLabel( - cs.age, cs.sex, slot, m); - bool isSelected = (sel.layer1Mesh == m); - if (ImGui::Selectable(label.c_str(), - isSelected)) { - sel.layer1Mesh = m; + for (const auto &m : + layer1Meshes) { + Ogre::String label = CharacterSlotSystem:: + getMeshLabel( + currentAge, + cs.sex, + slot, + m); + bool isSelected = + (sel.layer1Mesh == + m); + if (ImGui::Selectable( + label.c_str(), + isSelected)) { + sel.layer1Mesh = + m; modified = true; cs.dirty = true; } @@ -240,29 +275,45 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, /* Layer 2 combo */ std::vector layer2Meshes = - CharacterSlotSystem::getMeshesForLayer( - cs.age, cs.sex, slot, 2); + CharacterSlotSystem:: + getMeshesForLayer( + currentAge, cs.sex, + slot, 2); Ogre::String l2Preview = "none"; if (!sel.layer2Mesh.empty()) - l2Preview = CharacterSlotSystem::getMeshLabel( - cs.age, cs.sex, slot, sel.layer2Mesh); - if (ImGui::BeginCombo("Clothing (Layer 2)", - l2Preview.c_str())) { - if (ImGui::Selectable("none", - sel.layer2Mesh.empty() || - sel.layer2Mesh == "none")) { + l2Preview = CharacterSlotSystem:: + getMeshLabel( + currentAge, cs.sex, + slot, + sel.layer2Mesh); + if (ImGui::BeginCombo( + "Clothing (Layer 2)", + l2Preview.c_str())) { + if (ImGui::Selectable( + "none", + sel.layer2Mesh.empty() || + sel.layer2Mesh == + "none")) { sel.layer2Mesh = "none"; modified = true; cs.dirty = true; } - for (const auto &m : layer2Meshes) { - Ogre::String label = - CharacterSlotSystem::getMeshLabel( - cs.age, cs.sex, slot, m); - bool isSelected = (sel.layer2Mesh == m); - if (ImGui::Selectable(label.c_str(), - isSelected)) { - sel.layer2Mesh = m; + for (const auto &m : + layer2Meshes) { + Ogre::String label = CharacterSlotSystem:: + getMeshLabel( + currentAge, + cs.sex, + slot, + m); + bool isSelected = + (sel.layer2Mesh == + m); + if (ImGui::Selectable( + label.c_str(), + isSelected)) { + sel.layer2Mesh = + m; modified = true; cs.dirty = true; }