From 0ed5d9a9708dff7210fae3913ba83fe10ad54c73 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 19 Jun 2026 14:53:26 +0300 Subject: [PATCH] Improvements for character registry --- src/features/editScene/EditorApp.cpp | 12 +- src/features/editScene/Utils.hpp | 24 + .../editScene/components/CharacterSlots.hpp | 38 +- .../components/CharacterSlotsModule.cpp | 16 +- .../components/ProceduralMaterial.hpp | 6 + .../components/ProceduralTexture.hpp | 4 + .../editScene/lua/LuaComponentApi.cpp | 189 ++----- .../editScene/systems/AnimationTreeSystem.cpp | 10 +- .../editScene/systems/CharacterRegistry.cpp | 487 ++++++++++++++++-- .../editScene/systems/CharacterRegistry.hpp | 9 +- .../editScene/systems/CharacterSlotSystem.cpp | 158 +++--- .../editScene/systems/CharacterSlotSystem.hpp | 19 +- .../editScene/systems/CharacterSystem.cpp | 9 +- .../editScene/systems/EditorUISystem.cpp | 8 +- .../editScene/systems/EditorUISystem.hpp | 5 + .../editScene/systems/HairPhysicsSystem.cpp | 30 +- .../editScene/systems/PathFollowingSystem.cpp | 14 +- .../systems/ProceduralMaterialSystem.cpp | 8 +- .../systems/ProceduralMaterialSystem.hpp | 3 +- .../systems/ProceduralTextureSystem.cpp | 7 +- .../systems/ProceduralTextureSystem.hpp | 3 +- .../editScene/systems/SceneSerializer.cpp | 161 +++--- .../editScene/systems/SmartObjectSystem.cpp | 12 +- .../editScene/ui/CharacterSlotsEditor.cpp | 390 +------------- .../editScene/ui/CharacterSlotsEditor.hpp | 3 +- 25 files changed, 809 insertions(+), 816 deletions(-) create mode 100644 src/features/editScene/Utils.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index dd5f11a..b11e421 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -393,6 +393,9 @@ void EditorApp::setup() m_characterSlotSystem = std::make_unique( m_world, m_sceneMgr); m_characterSlotSystem->initialize(); + if (m_uiSystem) + m_uiSystem->getCharacterRegistry().setCharacterSlotSystem( + m_characterSlotSystem.get()); // Setup AnimationTree system m_animationTreeSystem = std::make_unique( @@ -989,12 +992,11 @@ void EditorApp::loadGame(const std::string &slotPath) ItemStateRegistry::getInstance().deserialize( saveData["itemState"]); - /* Destroy ALL characters (including editor-placed prefab - * instances that may lack CharacterIdentityComponent) so the - * registry spawn is the only source of characters. */ + /* Destroy ALL spawned characters so the registry spawn is the + * only source of characters. */ std::vector charsToDestroy; - m_world.query().each( - [&](flecs::entity e, CharacterSlotsComponent &) { + m_world.query().each( + [&](flecs::entity e, CharacterIdentityComponent &) { charsToDestroy.push_back(e); }); for (auto e : charsToDestroy) { diff --git a/src/features/editScene/Utils.hpp b/src/features/editScene/Utils.hpp new file mode 100644 index 0000000..b48f02f --- /dev/null +++ b/src/features/editScene/Utils.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +/** + * Generate a unique persistent identifier. + * The value is generated once and should be stored in a component field; + * it will survive scene save/load because it is serialized with the component. + */ +inline std::string generatePersistentId() +{ + static std::atomic s_counter{ 0 }; + + uint64_t counter = ++s_counter; + uint64_t timestamp = static_cast( + std::chrono::high_resolution_clock::now() + .time_since_epoch() + .count()); + + return "id_" + std::to_string(timestamp) + "_" + + std::to_string(counter); +} diff --git a/src/features/editScene/components/CharacterSlots.hpp b/src/features/editScene/components/CharacterSlots.hpp index 7b22dfb..e86f2c8 100644 --- a/src/features/editScene/components/CharacterSlots.hpp +++ b/src/features/editScene/components/CharacterSlots.hpp @@ -20,42 +20,24 @@ struct SlotSelection { }; /** - * Multi-slot mesh component for character parts sharing a skeleton. - * The "face" slot (or first available slot) serves as the master skeleton. + * Deprecated marker component for characters. * - * Age is now stored in CharacterRegistry::CharacterRecord::age - * and should be retrieved from there when needed. + * All persistent character appearance data (sex, slot selections, shape keys, + * front axis) now lives in CharacterRegistry::CharacterRecord. This component + * is kept only as a temporary reminder for scenes that still contain it; it + * is not serialized, cannot be added from the editor, and does not affect + * runtime behavior. */ struct CharacterSlotsComponent { - Ogre::String sex = "male"; - - /* Backward-compat: old mesh-name map. Deserialized into slotSelections on load. */ - std::unordered_map slots; - - /* Per-slot layer selections (runtime) */ - std::unordered_map slotSelections; - - bool dirty = true; - - /* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */ - Ogre::Entity *masterEntity = nullptr; - - /** - * Front-facing axis for this character model. - * Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z. - */ - Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; - - CharacterSlotsComponent() = default; }; /** - * Global shape key weights for a character. - * Same name applies to all slots uniformly. + * Deprecated marker component for character shape keys. + * + * Shape key weights are now stored in + * CharacterRegistry::CharacterRecord::inlineShapeKeyWeights. */ struct CharacterShapeKeysComponent { - std::unordered_map weights; - bool dirty = true; }; #endif // EDITSCENE_CHARACTERSLOTS_HPP diff --git a/src/features/editScene/components/CharacterSlotsModule.cpp b/src/features/editScene/components/CharacterSlotsModule.cpp index 999bc13..d63276e 100644 --- a/src/features/editScene/components/CharacterSlotsModule.cpp +++ b/src/features/editScene/components/CharacterSlotsModule.cpp @@ -13,19 +13,9 @@ REGISTER_COMPONENT_GROUP("Character Slots", "Rendering", "Character Slots", "Rendering", std::make_unique(sceneMgr), - /* Adder */ - [sceneMgr](flecs::entity e) { - if (!e.has()) { - TransformComponent transform; - transform.node = - sceneMgr->getRootSceneNode() - ->createChildSceneNode(); - e.set(transform); - } - CharacterSlotsComponent cs; - cs.dirty = true; - e.set(cs); - }, + /* Adder: disabled; this component is deprecated and cannot be + * created from the editor. */ + nullptr, /* Remover */ [sceneMgr](flecs::entity e) { (void)sceneMgr; diff --git a/src/features/editScene/components/ProceduralMaterial.hpp b/src/features/editScene/components/ProceduralMaterial.hpp index d0563e5..c73bb34 100644 --- a/src/features/editScene/components/ProceduralMaterial.hpp +++ b/src/features/editScene/components/ProceduralMaterial.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../Utils.hpp" #include #include #include @@ -18,6 +19,11 @@ struct ProceduralMaterialComponent { // Material name for Ogre resource std::string materialName; + ProceduralMaterialComponent() + { + materialId = generatePersistentId(); + } + // Persistent reference to texture by ID std::string diffuseTextureId; diff --git a/src/features/editScene/components/ProceduralTexture.hpp b/src/features/editScene/components/ProceduralTexture.hpp index 25e9a1e..eddccbc 100644 --- a/src/features/editScene/components/ProceduralTexture.hpp +++ b/src/features/editScene/components/ProceduralTexture.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../Utils.hpp" #include #include #include @@ -78,6 +79,9 @@ struct ProceduralTextureComponent { ProceduralTextureComponent() { + // Unique persistent ID for referencing this texture across saves. + textureId = generatePersistentId(); + // Initialize with default colors (checkerboard pattern) for (int i = 0; i < RECT_COUNT; ++i) { int row = i / GRID_SIZE; diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index aa999d9..4bd33e2 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -520,156 +520,53 @@ static void registerAllComponents() c.useGravity = lua_toboolean(L, -1) != 0; lua_pop(L, 1);); - // --- CharacterSlots --- - REGISTER_COMPONENT( - CharacterSlotsComponent, "CharacterSlots", - { // 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); - lua_pushstring(L, kv.second.layer1Mesh.c_str()); - lua_setfield(L, -2, "layer1Mesh"); - lua_pushstring(L, kv.second.layer2Mesh.c_str()); - lua_setfield(L, -2, "layer2Mesh"); - 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"); - { // Push: outfitLevel from registry - int outfitLevel = 2; - if (e.has()) { - auto &id = e.get(); - const CharacterRegistry::CharacterRecord *rec = - CharacterRegistry::getSingleton() - .findCharacter(id.registryId); - if (rec) - outfitLevel = rec->inlineOutfitLevel; - } - lua_pushinteger(L, outfitLevel); - } 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(); - CharacterRegistry::getSingleton() - .markCharacterDirty( - id.registryId); - } - } - } - } lua_pop(L, 1); - if (lua_getfield(L, idx, "sex"), lua_isstring(L, -1)) - c.sex = lua_tostring(L, -1); - lua_pop(L, 1); { // Read: outfitLevel into registry - if (lua_getfield(L, idx, "outfitLevel"), - lua_isnumber(L, -1)) { - int outfitLevel = (int)lua_tointeger(L, -1); - if (e.has()) { - auto &id = e.get< - CharacterIdentityComponent>(); - CharacterRegistry::CharacterRecord *rec = - CharacterRegistry::getSingleton() - .findCharacter( - id.registryId); - if (rec) { - rec->inlineOutfitLevel = - outfitLevel; - CharacterRegistry::getSingleton() - .autoSave(); - CharacterRegistry::getSingleton() - .markCharacterDirty( - id.registryId); - } - } - } - } lua_pop(L, 1); - if (lua_getfield(L, idx, "slots"), lua_istable(L, -1)) { - c.slots.clear(); - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - if (lua_isstring(L, -2) && lua_isstring(L, -1)) - c.slots[lua_tostring(L, -2)] = - lua_tostring(L, -1); - lua_pop(L, 1); - } - } lua_pop(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); - lua_pop(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); - lua_pop(L, 1); - c.slotSelections[slotName] = sel; - } - lua_pop(L, 1); - } - } lua_pop(L, 1); - if (lua_getfield(L, idx, "frontAxis"), lua_istable(L, -1)) - c.frontAxis = readVector3(L, lua_gettop(L)); - lua_pop(L, 1);); - // --- CharacterShapeKeys --- REGISTER_COMPONENT( CharacterShapeKeysComponent, "CharacterShapeKeys", - lua_newtable(L); - for (auto &kv : c.weights) { - lua_pushnumber(L, kv.second); - lua_setfield(L, -2, kv.first.c_str()); + { + const CharacterRegistry::CharacterRecord *rec = nullptr; + if (e.has()) { + auto &id = e.get(); + rec = CharacterRegistry::getSingleton() + .findCharacter(id.registryId); + } + lua_newtable(L); + if (rec) { + for (auto &kv : rec->inlineShapeKeyWeights) { + lua_pushnumber(L, kv.second); + lua_setfield(L, -2, kv.first.c_str()); + } + } }, - 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); - lua_pop(L, 1); + { + if (!e.has()) + return; + auto &id = e.get(); + CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton() + .findCharacter(id.registryId); + if (!rec) + return; + if (lua_istable(L, idx)) { + bool changed = false; + lua_pushnil(L); + while (lua_next(L, idx) != 0) { + if (lua_isstring(L, -2) && + lua_isnumber(L, -1)) { + rec->inlineShapeKeyWeights[ + lua_tostring(L, -2)] = + lua_tonumber(L, -1); + changed = true; + } + lua_pop(L, 1); + } + if (changed) { + CharacterRegistry::getSingleton() + .autoSave(); + CharacterRegistry::getSingleton() + .markCharacterDirty( + id.registryId); + } } }); diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index 0ab8f05..b0c0713 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -1,4 +1,5 @@ #include "AnimationTreeSystem.hpp" +#include "CharacterSlotSystem.hpp" #include "../components/Transform.hpp" #include "../components/Renderable.hpp" #include "../components/CharacterSlots.hpp" @@ -53,10 +54,11 @@ Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e) if (!e.is_alive()) return nullptr; - if (e.has()) { - auto &cs = e.get(); - if (cs.masterEntity && cs.masterEntity->hasSkeleton()) - return cs.masterEntity; + CharacterSlotSystem *slotSystem = CharacterSlotSystem::getSingletonPtr(); + if (slotSystem) { + Ogre::Entity *masterEnt = slotSystem->getMasterEntity(e); + if (masterEnt && masterEnt->hasSkeleton()) + return masterEnt; } if (e.has()) { diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index 6a3f68d..19b5e9e 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -284,9 +284,11 @@ std::vector CharacterRegistry::getChildren(uint64_t parentId) const /* ------------------------------------------------------------------ */ static bool -readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs, +readPrefabAppearance(const std::string &path, std::string &outSex, + std::unordered_map &outSelections, std::unordered_map &shapeKeys, - std::string &outAge) + std::string &outAge, + Ogre::Vector3 &outFrontAxis) { try { std::ifstream file(path); @@ -296,7 +298,17 @@ readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs, file >> j; if (j.contains("characterSlots")) { auto &s = j["characterSlots"]; - cs.sex = s.value("sex", "male"); + outSex = s.value("sex", "male"); + outAge = s.value("age", "adult"); + if (s.contains("frontAxis") && s["frontAxis"].is_array() && + s["frontAxis"].size() >= 3) { + outFrontAxis = Ogre::Vector3( + s["frontAxis"][0].get(), + s["frontAxis"][1].get(), + s["frontAxis"][2].get()); + if (outFrontAxis.squaredLength() > 0.0001f) + outFrontAxis.normalise(); + } if (s.contains("slotSelections")) { for (auto &[slot, selJson] : s["slotSelections"].items()) { @@ -309,7 +321,15 @@ readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs, selJson.value("layer2Mesh", ""); sel.explicitMesh = selJson.value( "explicitMesh", ""); - cs.slotSelections[slot] = sel; + outSelections[slot] = sel; + } + } + /* Backward compat: old flat slots map */ + if (s.contains("slots") && s["slots"].is_object()) { + for (auto &[slot, mesh] : s["slots"].items()) { + SlotSelection sel; + sel.explicitMesh = mesh.get(); + outSelections[slot] = sel; } } } @@ -424,14 +444,19 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) /* Copy appearance from same-sex parent */ if (sameSexParent->inlineSlotSelections.empty() && std::filesystem::exists(sameSexParent->prefabPath)) { - CharacterSlotsComponent tmpCs; + std::string prefabSex = "male"; std::string prefabAge; - readPrefabAppearance(sameSexParent->prefabPath, tmpCs, - child->inlineShapeKeyWeights, prefabAge); + std::unordered_map prefabSelections; + Ogre::Vector3 prefabFrontAxis = sameSexParent->frontAxis; + readPrefabAppearance(sameSexParent->prefabPath, prefabSex, + prefabSelections, + child->inlineShapeKeyWeights, prefabAge, + prefabFrontAxis); child->age = prefabAge; - child->inlineSex = tmpCs.sex; + child->inlineSex = prefabSex; child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel; - child->inlineSlotSelections = tmpCs.slotSelections; + child->inlineSlotSelections = prefabSelections; + child->frontAxis = sameSexParent->frontAxis; } else { child->age = sameSexParent->age; child->inlineSex = sameSexParent->inlineSex; @@ -440,6 +465,7 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) sameSexParent->inlineSlotSelections; child->inlineShapeKeyWeights = sameSexParent->inlineShapeKeyWeights; + child->frontAxis = sameSexParent->frontAxis; } /* Randomize configured birth shape keys */ @@ -538,20 +564,10 @@ flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c, transform.applyToNode(); inst.set(transform); - /* CharacterSlots */ - CharacterSlotsComponent cs; - cs.sex = c.inlineSex; - cs.slotSelections = c.inlineSlotSelections; - cs.dirty = true; - inst.set(cs); - - /* Shape keys */ - if (!c.inlineShapeKeyWeights.empty()) { - CharacterShapeKeysComponent skc; - skc.weights = c.inlineShapeKeyWeights; - skc.dirty = true; - inst.set(skc); - } + /* Character visual data is now read from the registry record at + * build time; no component storage is needed. */ + if (m_slotSystem) + m_slotSystem->markEntityDirty(inst.id()); return inst; } @@ -700,8 +716,8 @@ void CharacterRegistry::markCharacterDirty(uint64_t id) flecs::entity e = findSpawnedEntity(id); if (!e.is_alive()) return; - if (e.has()) - e.get_mut().dirty = true; + if (m_slotSystem) + m_slotSystem->markEntityDirty(e.id()); } bool CharacterRegistry::despawnCharacter(uint64_t id) @@ -720,7 +736,7 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) if (!m_world || !m_sceneMgr || !m_uiSystem) return flecs::entity::null(); - const CharacterRecord *c = findCharacter(id); + CharacterRecord *c = findCharacter(id); if (!c) return flecs::entity::null(); @@ -729,6 +745,20 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) if (existing.is_alive()) existing.destruct(); + /* Migrate prefab appearance data into the registry record if the + * record has no inline appearance data yet. */ + if (c->inlineSlotSelections.empty() && + std::filesystem::exists(c->prefabPath)) { + std::string prefabAge; + readPrefabAppearance(c->prefabPath, c->inlineSex, + c->inlineSlotSelections, + c->inlineShapeKeyWeights, prefabAge, + c->frontAxis); + if (!prefabAge.empty()) + c->age = prefabAge; + autoSave(); + } + Ogre::Vector3 pos = c->position; flecs::entity inst; if (std::filesystem::exists(c->prefabPath)) { @@ -762,32 +792,52 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) bool CharacterRegistry::savePrefabForCharacter(uint64_t id) { - if (!m_world || !m_sceneMgr || !m_uiSystem) - return false; - const CharacterRecord *c = findCharacter(id); if (!c) return false; - flecs::entity sel = flecs::entity::null(); - if (m_uiSystem) - sel = m_uiSystem->getSelectedEntity(); + try { + nlohmann::json j; + { + std::ifstream file(c->prefabPath); + if (file.is_open()) + file >> j; + } - if (!sel.is_alive()) { - m_lastError = "No entity selected"; - return false; - } - if (!sel.has()) { - m_lastError = "Selected entity has no CharacterSlots"; - return false; - } + j["characterSlots"]["age"] = c->age; + j["characterSlots"]["sex"] = c->inlineSex; + j["characterSlots"]["outfitLevel"] = c->inlineOutfitLevel; + nlohmann::json selections = nlohmann::json::object(); + for (const auto &kv : c->inlineSlotSelections) { + nlohmann::json s; + s["layer0Mesh"] = kv.second.layer0Mesh; + s["layer1Mesh"] = kv.second.layer1Mesh; + s["layer2Mesh"] = kv.second.layer2Mesh; + s["explicitMesh"] = kv.second.explicitMesh; + selections[kv.first] = s; + } + j["characterSlots"]["slotSelections"] = selections; - PrefabSystem prefabSys(*m_world, m_sceneMgr); - if (!prefabSys.savePrefab(sel, c->prefabPath)) { - m_lastError = prefabSys.getLastError(); + if (!c->inlineShapeKeyWeights.empty()) { + nlohmann::json weights = nlohmann::json::object(); + for (const auto &kv : c->inlineShapeKeyWeights) + weights[kv.first] = kv.second; + j["characterShapeKeys"]["weights"] = weights; + } + + std::filesystem::path p(c->prefabPath); + std::filesystem::create_directories(p.parent_path()); + std::ofstream out(c->prefabPath); + if (!out.is_open()) { + m_lastError = "Cannot open " + c->prefabPath; + return false; + } + out << j.dump(4); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Save prefab error: ") + e.what(); return false; } - return true; } /* ===================================================================== */ @@ -1205,6 +1255,13 @@ nlohmann::json CharacterRegistry::serialize() const rec["age"] = c.age; rec["inlineSex"] = c.inlineSex; rec["inlineOutfitLevel"] = c.inlineOutfitLevel; + { + nlohmann::json fa; + fa["x"] = c.frontAxis.x; + fa["y"] = c.frontAxis.y; + fa["z"] = c.frontAxis.z; + rec["frontAxis"] = fa; + } if (!c.inlineSlotSelections.empty()) { nlohmann::json selJson; for (const auto &kv : c.inlineSlotSelections) { @@ -1378,6 +1435,15 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.age = rec.value("age", "adult"); c.inlineSex = rec.value("inlineSex", "male"); c.inlineOutfitLevel = rec.value("inlineOutfitLevel", 2); + if (rec.contains("frontAxis") && + rec["frontAxis"].is_object()) { + auto &fa = rec["frontAxis"]; + c.frontAxis = Ogre::Vector3( + fa.value("x", 0.0f), fa.value("y", 0.0f), + fa.value("z", 0.0f)); + if (c.frontAxis.squaredLength() > 0.0001f) + c.frontAxis.normalise(); + } if (rec.contains("position") && rec["position"].is_object()) { auto &p = rec["position"]; @@ -1612,6 +1678,330 @@ static void drawColumnsEditor(const char *title, } } +/* ===================================================================== */ +/* Character appearance editor (moved from CharacterSlotsEditor) */ +/* ===================================================================== */ + +static void drawCharacterAppearanceEditor(CharacterRegistry ®, + CharacterRegistry::CharacterRecord *c) +{ + if (!c) + return; + + CharacterSlotSystem::loadCatalog(); + + const Ogre::String currentAge = c->age; + const int outfitLevel = c->inlineOutfitLevel; + + /* Sex selector */ + std::vector sexes = + CharacterSlotSystem::getSexes(currentAge); + Ogre::String currentSex = c->inlineSex; + if (ImGui::BeginCombo("Sex", currentSex.c_str())) { + for (const auto &sex : sexes) { + bool isSelected = (currentSex == sex); + if (ImGui::Selectable(sex.c_str(), isSelected)) { + c->inlineSex = sex; + reg.autoSave(); + reg.markCharacterDirty(c->id); + } + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + ImGui::Separator(); + + /* Front-facing axis */ + { + Ogre::Vector3 axis = c->frontAxis; + float axisVals[3] = { axis.x, axis.y, axis.z }; + ImGui::Text("Front Axis:"); + ImGui::SameLine(); + ImGui::TextDisabled( + "(direction character faces, used by path following)"); + if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f, -1.0f, + 1.0f)) { + c->frontAxis = Ogre::Vector3(axisVals[0], axisVals[1], + axisVals[2]); + if (c->frontAxis.squaredLength() > 0.0001f) + c->frontAxis.normalise(); + reg.autoSave(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("-Z")) { + c->frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; + reg.autoSave(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("+Z")) { + c->frontAxis = Ogre::Vector3::UNIT_Z; + reg.autoSave(); + } + } + + ImGui::Separator(); + + /* Shape Keys */ + if (ImGui::CollapsingHeader("Shape Keys")) { + auto shapeKeys = CharacterSlotSystem::getShapeKeyNames( + currentAge, c->inlineSex); + if (shapeKeys.empty()) { + ImGui::TextDisabled("No shape keys available."); + } else { + for (const auto &name : shapeKeys) { + float val = c->inlineShapeKeyWeights[name]; + if (ImGui::SliderFloat(name.c_str(), &val, 0.0f, + 1.0f)) { + c->inlineShapeKeyWeights[name] = val; + reg.autoSave(); + reg.markCharacterDirty(c->id); + } + } + } + } + + ImGui::Separator(); + + /* Slot selections */ + std::vector availableSlots = + CharacterSlotSystem::getSlots(currentAge, c->inlineSex); + for (const auto &pair : c->inlineSlotSelections) { + if (std::find(availableSlots.begin(), availableSlots.end(), + pair.first) == availableSlots.end()) + availableSlots.push_back(pair.first); + } + std::sort(availableSlots.begin(), availableSlots.end()); + + for (const auto &slot : availableSlots) { + if (c->inlineSlotSelections.find(slot) == + c->inlineSlotSelections.end()) { + c->inlineSlotSelections[slot] = SlotSelection(); + } + + SlotSelection &sel = c->inlineSlotSelections[slot]; + + if (ImGui::TreeNode(slot.c_str())) { + /* Resolved mesh preview */ + Ogre::String resolved = CharacterSlotSystem::resolveMesh( + currentAge, c->inlineSex, slot, sel, + outfitLevel); + ImGui::TextDisabled("Resolved: %s", + resolved.empty() ? + "(none)" : + resolved.c_str()); + + /* Explicit mesh override */ + bool useExplicit = !sel.explicitMesh.empty(); + if (ImGui::Checkbox("Lock explicit mesh", &useExplicit)) { + if (!useExplicit) { + sel.explicitMesh.clear(); + } else { + sel.explicitMesh = CharacterSlotSystem::resolveMesh( + currentAge, c->inlineSex, slot, sel, + outfitLevel); + } + reg.autoSave(); + reg.markCharacterDirty(c->id); + } + + if (useExplicit) { + std::vector meshes = + CharacterSlotSystem::getMeshes( + currentAge, c->inlineSex, slot); + Ogre::String preview = sel.explicitMesh.empty() ? + "(none)" : + sel.explicitMesh; + if (ImGui::BeginCombo("Mesh", preview.c_str())) { + if (ImGui::Selectable( + "(none)", + sel.explicitMesh.empty())) { + sel.explicitMesh.clear(); + reg.autoSave(); + reg.markCharacterDirty( + c->id); + } + for (const auto &m : meshes) { + bool isSelected = + (sel.explicitMesh == m); + if (ImGui::Selectable(m.c_str(), + isSelected)) { + sel.explicitMesh = m; + reg.autoSave(); + reg.markCharacterDirty( + c->id); + } + } + ImGui::EndCombo(); + } + } else { + /* Layer 0 combo (base meshes like hair) */ + if (slot != "hair") { + std::vector layer0Meshes = + CharacterSlotSystem:: + getMeshesForLayer( + currentAge, + c->inlineSex, slot, + 0); + if (!layer0Meshes.empty()) { + Ogre::String l0Preview = "auto"; + if (!sel.layer0Mesh.empty() && + sel.layer0Mesh != "none") + l0Preview = CharacterSlotSystem:: + getMeshLabel( + currentAge, + c->inlineSex, + slot, + sel.layer0Mesh); + if (ImGui::BeginCombo( + "Base (Layer 0)", + l0Preview.c_str())) { + if (ImGui::Selectable( + "auto", + sel.layer0Mesh.empty() || + sel.layer0Mesh == + "none")) { + sel.layer0Mesh = + "none"; + reg.autoSave(); + reg.markCharacterDirty( + c->id); + } + for (const auto &m : + layer0Meshes) { + Ogre::String label = + CharacterSlotSystem:: + getMeshLabel( + currentAge, + c->inlineSex, + slot, + m); + bool isSelected = + (sel.layer0Mesh == + m); + if (ImGui::Selectable( + label.c_str(), + isSelected)) { + sel.layer0Mesh = + m; + reg.autoSave(); + reg.markCharacterDirty( + c->id); + } + } + ImGui::EndCombo(); + } + } + } else { + ImGui::TextDisabled( + "Base: auto (stub hair)"); + } + /* Layer 1 combo */ + std::vector layer1Meshes = + CharacterSlotSystem:: + getMeshesForLayer( + currentAge, + c->inlineSex, slot, + 1); + Ogre::String l1Preview = "none"; + if (!sel.layer1Mesh.empty()) + l1Preview = CharacterSlotSystem:: + getMeshLabel( + currentAge, + c->inlineSex, 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"; + reg.autoSave(); + reg.markCharacterDirty(c->id); + } + for (const auto &m : layer1Meshes) { + Ogre::String label = + CharacterSlotSystem:: + getMeshLabel( + currentAge, + c->inlineSex, + slot, + m); + bool isSelected = + (sel.layer1Mesh == m); + if (ImGui::Selectable( + label.c_str(), + isSelected)) { + sel.layer1Mesh = m; + reg.autoSave(); + reg.markCharacterDirty( + c->id); + } + } + ImGui::EndCombo(); + } + + /* Layer 2 combo */ + std::vector layer2Meshes = + CharacterSlotSystem:: + getMeshesForLayer( + currentAge, + c->inlineSex, slot, + 2); + Ogre::String l2Preview = "none"; + if (!sel.layer2Mesh.empty()) + l2Preview = CharacterSlotSystem:: + getMeshLabel( + currentAge, + c->inlineSex, 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"; + reg.autoSave(); + reg.markCharacterDirty(c->id); + } + for (const auto &m : layer2Meshes) { + Ogre::String label = + CharacterSlotSystem:: + getMeshLabel( + currentAge, + c->inlineSex, + slot, + m); + bool isSelected = + (sel.layer2Mesh == m); + if (ImGui::Selectable( + label.c_str(), + isSelected)) { + sel.layer2Mesh = m; + reg.autoSave(); + reg.markCharacterDirty( + c->id); + } + } + ImGui::EndCombo(); + } + } + + ImGui::TreePop(); + } + } + + /* Rebuild button */ + if (ImGui::Button("Rebuild")) { + reg.markCharacterDirty(c->id); + } +} + /* ===================================================================== */ /* ImGui editor */ /* ===================================================================== */ @@ -1932,6 +2322,15 @@ void CharacterRegistry::drawEditor(bool *p_open) } } + /* Appearance (formerly CharacterSlots editor) */ + ImGui::Separator(); + if (ImGui::CollapsingHeader("Appearance", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + drawCharacterAppearanceEditor(*this, c); + ImGui::Unindent(); + } + /* Family */ ImGui::Separator(); ImGui::Text("Family"); diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index 159bfca..a96c6ef 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -15,6 +15,7 @@ #include "../components/CharacterSlots.hpp" class EditorUISystem; +class CharacterSlotSystem; /** * Global character and social-group registry. @@ -70,12 +71,13 @@ public: /* Age in years (runtime progression) */ int ageYears = 0; - /* Inline appearance (used when no prefab file exists) */ + /* Inline appearance (authoritative source for character visuals) */ std::string inlineSex = "male"; int inlineOutfitLevel = 2; std::unordered_map inlineSlotSelections; std::unordered_map inlineShapeKeyWeights; + Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; /* Runtime-only characters are NOT saved to character_registry.json */ bool persistent = true; @@ -145,6 +147,10 @@ public: { m_uiSystem = ui; } + void setCharacterSlotSystem(CharacterSlotSystem *slotSystem) + { + m_slotSystem = slotSystem; + } /** * Load registry from auto-save file. Call after setWorld/setSceneManager. @@ -399,6 +405,7 @@ private: flecs::world *m_world = nullptr; Ogre::SceneManager *m_sceneMgr = nullptr; EditorUISystem *m_uiSystem = nullptr; + CharacterSlotSystem *m_slotSystem = nullptr; void rebuildIndexes(); void addToIndex(size_t relIndex); diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index b700c6f..32d96ea 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -17,18 +17,30 @@ #include #include +CharacterSlotSystem *CharacterSlotSystem::ms_singleton = nullptr; bool CharacterSlotSystem::s_catalogLoaded = false; nlohmann::json CharacterSlotSystem::s_bodyParts = nlohmann::json::object(); std::set CharacterSlotSystem::s_meshNames; +CharacterSlotSystem *CharacterSlotSystem::getSingletonPtr() +{ + return ms_singleton; +} + CharacterSlotSystem::CharacterSlotSystem(flecs::world &world, Ogre::SceneManager *sceneMgr) : m_world(world) , m_sceneMgr(sceneMgr) { - m_world.observer("CharacterSlotsCleanup") + ms_singleton = this; + m_world.observer("CharacterSlotsBuild") + .event(flecs::OnAdd) + .each([this](flecs::entity e, CharacterIdentityComponent &) { + markEntityDirty(e.id()); + }); + m_world.observer("CharacterSlotsCleanup") .event(flecs::OnRemove) - .each([this](flecs::entity e, CharacterSlotsComponent &) { + .each([this](flecs::entity e, CharacterIdentityComponent &) { destroyCharacterParts(e); }); } @@ -40,6 +52,8 @@ CharacterSlotSystem::~CharacterSlotSystem() toRemove.push_back(pair.first); for (auto id : toRemove) destroyCharacterParts(m_world.entity(id)); + if (ms_singleton == this) + ms_singleton = nullptr; } void CharacterSlotSystem::initialize() @@ -402,13 +416,14 @@ void CharacterSlotSystem::update() if (!m_initialized) return; - m_world.query().each( - [&](flecs::entity e, CharacterSlotsComponent &cs) { - if (cs.dirty) { - buildCharacter(e, cs); - cs.dirty = false; - } - }); + std::vector dirtyList(m_dirtyEntities.begin(), + m_dirtyEntities.end()); + m_dirtyEntities.clear(); + for (flecs::entity_t id : dirtyList) { + flecs::entity e = m_world.entity(id); + if (e.is_alive() && e.has()) + buildCharacter(e); + } } static const nlohmann::json *findCatalogEntry(const Ogre::String &age, @@ -455,62 +470,38 @@ 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. + * Helper: retrieve the character's registry record. + * Returns nullptr if the entity is not a registered character. */ -static Ogre::String getCharacterAge(flecs::entity e) +static const CharacterRegistry::CharacterRecord * +getCharacterRecord(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"; + if (!e.has()) + return nullptr; + auto &id = e.get(); + return CharacterRegistry::getSingleton().findCharacter(id.registryId); } -/** - * Helper: retrieve the character's outfit level from the registry. - * Falls back to 2 (clothed) if no registry entry is found. - */ -static int getCharacterOutfitLevel(flecs::entity e) -{ - if (e.has()) { - auto &id = e.get(); - const CharacterRegistry::CharacterRecord *rec = - CharacterRegistry::getSingleton().findCharacter( - id.registryId); - if (rec) - return rec->inlineOutfitLevel; - } - return 2; -} - -void CharacterSlotSystem::buildCharacter(flecs::entity e, - CharacterSlotsComponent &cs) +void CharacterSlotSystem::buildCharacter(flecs::entity e) { destroyCharacterParts(e); - Ogre::String age = getCharacterAge(e); + const CharacterRegistry::CharacterRecord *rec = getCharacterRecord(e); + if (!rec) + return; - /* Migrate old slots map to slotSelections if needed */ - if (cs.slotSelections.empty() && !cs.slots.empty()) { - for (const auto &pair : cs.slots) { - SlotSelection sel; - sel.explicitMesh = pair.second; - cs.slotSelections[pair.first] = sel; - } - } + const Ogre::String &age = rec->age; + const Ogre::String &sex = rec->inlineSex; + const auto &slotSelections = rec->inlineSlotSelections; + const int outfitLevel = rec->inlineOutfitLevel; - /* Make sure every catalog slot exists in the selection map. This - * guarantees a face/body master is available so that hair with its - * own skeleton can attach to a real head bone. */ - auto slots = getSlots(age, cs.sex); + /* Make sure every catalog slot exists in the selection map. */ + auto slots = getSlots(age, sex); + std::unordered_map selections = + slotSelections; for (const auto &slot : slots) { - if (cs.slotSelections.find(slot) == cs.slotSelections.end()) - cs.slotSelections[slot] = SlotSelection(); + if (selections.find(slot) == selections.end()) + selections[slot] = SlotSelection(); } if (!e.has()) @@ -520,20 +511,18 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, if (!transform.node) return; - int outfitLevel = getCharacterOutfitLevel(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(age, cs.sex, "face", - cs.slotSelections["face"], + if (selections.find("face") != selections.end()) { + Ogre::String mesh = resolveMesh(age, sex, "face", + selections["face"], outfitLevel); if (!mesh.empty()) masterSlot = "face"; } if (masterSlot.empty()) { - for (const auto &pair : cs.slotSelections) { - Ogre::String mesh = resolveMesh(age, cs.sex, pair.first, + for (const auto &pair : selections) { + Ogre::String mesh = resolveMesh(age, sex, pair.first, pair.second, outfitLevel); if (!mesh.empty()) { @@ -546,8 +535,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, if (masterSlot.empty()) return; - Ogre::String masterMesh = resolveMesh(age, cs.sex, masterSlot, - cs.slotSelections[masterSlot], + Ogre::String masterMesh = resolveMesh(age, sex, masterSlot, + selections[masterSlot], outfitLevel); if (masterMesh.empty()) @@ -563,23 +552,16 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, masterEnt = m_sceneMgr->createEntity(meshPtr); transform.node->attachObject(masterEnt); m_entities[e.id()].parts[masterSlot] = masterEnt; - cs.masterEntity = masterEnt; + m_entities[e.id()].masterEntity = masterEnt; /* Setup pose animation for shape keys */ const nlohmann::json *entry = - findCatalogEntry(age, cs.sex, masterSlot, masterMesh); + findCatalogEntry(age, sex, masterSlot, masterMesh); applyShapeKeys(e, masterEnt, entry); - /* Re-prepare temp buffers after enabling vertex animation. - * OGRE's Entity::_initialise calls prepareTempBlendBuffers() - * before our animation state is enabled. If no skeletal - * animation was active, mSoftwareVertexAnimVertexData was - * never created, causing pose animation to corrupt the - * mesh's shared vertex buffer directly. - */ + /* Re-prepare temp buffers after enabling vertex animation. */ prepareEntityTempBlendBuffers(masterEnt); - /* Notify AnimationTreeSystem that entity changed */ if (e.has()) e.get_mut().dirty = true; @@ -590,7 +572,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, return; } - for (const auto &pair : cs.slotSelections) { + for (const auto &pair : selections) { const Ogre::String &slot = pair.first; const SlotSelection &sel = pair.second; @@ -598,7 +580,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, continue; Ogre::String mesh = - resolveMesh(age, cs.sex, slot, sel, outfitLevel); + resolveMesh(age, sex, slot, sel, outfitLevel); if (mesh.empty()) continue; @@ -615,7 +597,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, * armature). If so, attach to the master's * Head bone instead of sharing skeletons. */ const nlohmann::json *entry = - findCatalogEntry(age, cs.sex, slot, mesh); + findCatalogEntry(age, sex, slot, mesh); bool ownSkeleton = false; Ogre::String attachBone = "mixamorig:Head"; if (entry) { @@ -744,10 +726,10 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, for (size_t i = 0; i < shapeKeys.size(); ++i) nameToIndex[shapeKeys[i].get()] = i; - /* Apply weights from CharacterShapeKeysComponent */ - if (e.has()) { - auto &skc = e.get_mut(); - for (const auto &pair : skc.weights) { + /* Apply weights from the character registry record */ + const CharacterRegistry::CharacterRecord *rec = getCharacterRecord(e); + if (rec) { + for (const auto &pair : rec->inlineShapeKeyWeights) { auto it = nameToIndex.find(pair.first); if (it == nameToIndex.end()) continue; @@ -777,6 +759,11 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, as->setTimePosition(0.0f); } +void CharacterSlotSystem::markEntityDirty(flecs::entity_t id) +{ + m_dirtyEntities.insert(id); +} + void CharacterSlotSystem::destroyCharacterParts(flecs::entity e) { auto it = m_entities.find(e.id()); @@ -795,9 +782,18 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e) } it->second.parts.clear(); + it->second.masterEntity = nullptr; m_entities.erase(it); } +Ogre::Entity *CharacterSlotSystem::getMasterEntity(flecs::entity e) +{ + auto it = m_entities.find(e.id()); + if (it == m_entities.end()) + return nullptr; + return it->second.masterEntity; +} + Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e, const Ogre::String &slot) { diff --git a/src/features/editScene/systems/CharacterSlotSystem.hpp b/src/features/editScene/systems/CharacterSlotSystem.hpp index 4e1efc2..0820831 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.hpp +++ b/src/features/editScene/systems/CharacterSlotSystem.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "../components/CharacterSlots.hpp" @@ -26,6 +27,10 @@ struct BodyPartEntry { * System that manages multi-slot character meshes with shared skeleton. * Loads body part catalog from body_part_*.json files and creates/updates * Ogre entities for each slot, sharing the skeleton from the master slot. + * + * Character appearance is read from CharacterRegistry (via + * CharacterIdentityComponent); the deprecated CharacterSlotsComponent is no + * longer used as a data source. */ class CharacterSlotSystem { public: @@ -35,6 +40,9 @@ public: void initialize(); void update(); + /* Singleton access */ + static CharacterSlotSystem *getSingletonPtr(); + /* Static catalog access */ static void loadCatalog(); static bool isCatalogLoaded(); @@ -73,18 +81,23 @@ public: const SlotSelection &sel, int outfitLevel); - /* Slot visibility helpers */ + /* Mark an entity for rebuild on next update */ + void markEntityDirty(flecs::entity_t id); + + /* Runtime entity accessors */ + Ogre::Entity *getMasterEntity(flecs::entity e); Ogre::Entity *getSlotEntity(flecs::entity e, const Ogre::String &slot); void setSlotVisible(flecs::entity e, const Ogre::String &slot, bool visible); private: + static CharacterSlotSystem *ms_singleton; static bool s_catalogLoaded; static nlohmann::json s_bodyParts; static std::set s_meshNames; - void buildCharacter(flecs::entity e, CharacterSlotsComponent &cs); + void buildCharacter(flecs::entity e); void destroyCharacterParts(flecs::entity e); void applyShapeKeys(flecs::entity e, Ogre::Entity *ent, const nlohmann::json *entry); @@ -94,9 +107,11 @@ private: bool m_initialized = false; struct PartEntities { + Ogre::Entity *masterEntity = nullptr; std::unordered_map parts; }; std::unordered_map m_entities; + std::unordered_set m_dirtyEntities; }; #endif // EDITSCENE_CHARACTERSLOTSYSTEM_HPP diff --git a/src/features/editScene/systems/CharacterSystem.cpp b/src/features/editScene/systems/CharacterSystem.cpp index 9cedd49..e2767d4 100644 --- a/src/features/editScene/systems/CharacterSystem.cpp +++ b/src/features/editScene/systems/CharacterSystem.cpp @@ -1,4 +1,5 @@ #include "CharacterSystem.hpp" +#include "CharacterSlotSystem.hpp" #include "../components/CharacterSlots.hpp" #include #include @@ -11,10 +12,12 @@ static Ogre::Entity *getMasterEntity(flecs::entity e) { - if (!e.is_alive() || !e.has()) + if (!e.is_alive()) return nullptr; - const CharacterSlotsComponent &cs = e.get(); - return cs.masterEntity; + CharacterSlotSystem *slotSystem = CharacterSlotSystem::getSingletonPtr(); + if (!slotSystem) + return nullptr; + return slotSystem->getMasterEntity(e); } static bool getBoneWorldMatrix(Ogre::Entity *masterEnt, diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index f6b3b0c..338919c 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -1067,7 +1067,9 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render CharacterSlots if present if (entity.has()) { - auto &cs = entity.get_mut(); + /* CharacterSlotsComponent is now an empty tag; get_mut is illegal + * for zero-sized types in flecs. */ + CharacterSlotsComponent cs; m_componentRegistry.render(entity, cs); componentCount++; } @@ -1298,6 +1300,8 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity) [&](const std::type_index &type, const ComponentRegistry::ComponentInfo &info) { + if (!info.adder) + return; if (!m_componentRegistry .entityHasComponent( entity, type)) { @@ -1316,6 +1320,8 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity) [&](const std::type_index &type, const ComponentRegistry::ComponentInfo &info) { + if (!info.adder) + return; if (!m_componentRegistry .entityHasComponent( entity, diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index d202f0b..d9b678a 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -201,6 +201,11 @@ public: void renderDialogueSettingsWindow(); void renderInventoryConfigWindow(); + CharacterRegistry &getCharacterRegistry() + { + return m_characterRegistry; + } + private: // File menu void renderFileMenu(); diff --git a/src/features/editScene/systems/HairPhysicsSystem.cpp b/src/features/editScene/systems/HairPhysicsSystem.cpp index 39b9bc4..a692184 100644 --- a/src/features/editScene/systems/HairPhysicsSystem.cpp +++ b/src/features/editScene/systems/HairPhysicsSystem.cpp @@ -1,5 +1,7 @@ #include "HairPhysicsSystem.hpp" #include "CharacterSlotSystem.hpp" +#include "CharacterRegistry.hpp" +#include "../components/CharacterIdentity.hpp" #include "../components/CharacterSlots.hpp" #include "../components/Character.hpp" #include @@ -96,12 +98,22 @@ void HairPhysicsSystem::prePhysicsUpdate() if (!m_initialized || !m_physics) return; - m_world.query().each( - [this](flecs::entity e, CharacterSlotsComponent &cs) { - Ogre::Entity *masterEnt = cs.masterEntity; + m_world.query().each( + [this](flecs::entity e, CharacterIdentityComponent &ci) { + Ogre::Entity *masterEnt = + m_slotSystem->getMasterEntity(e); + if (!masterEnt) + return; - for (const auto &pair : cs.slotSelections) { - const Ogre::String &slot = pair.first; + const CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton().findCharacter( + ci.registryId); + if (!rec) + return; + + auto slots = CharacterSlotSystem::getSlots( + rec->age, rec->inlineSex); + for (const auto &slot : slots) { Ogre::Entity *partEnt = m_slotSystem->getSlotEntity(e, slot); if (!partEnt || !partEnt->hasSkeleton()) @@ -450,7 +462,7 @@ bool HairPhysicsSystem::isStateValid(const HairRagdollState &state) const return false; flecs::entity e = m_world.entity(state.entityId); - if (!e.is_alive() || !e.has()) + if (!e.is_alive() || !e.has()) return false; Ogre::Entity *current = m_slotSystem->getSlotEntity(e, state.slotName); @@ -463,8 +475,7 @@ void HairPhysicsSystem::syncRootToHead(HairRagdollState &state) return; flecs::entity e = m_world.entity(state.entityId); - const CharacterSlotsComponent &cs = e.get(); - Ogre::Entity *masterEnt = cs.masterEntity; + Ogre::Entity *masterEnt = m_slotSystem->getMasterEntity(e); if (!masterEnt) return; @@ -511,8 +522,7 @@ void HairPhysicsSystem::syncPhysicsToSkeleton(HairRagdollState &state) /* Get the attachment transform so we can compute hair-entity-local * transforms for the root bone. */ flecs::entity e = m_world.entity(state.entityId); - const CharacterSlotsComponent &cs = e.get(); - Ogre::Entity *masterEnt = cs.masterEntity; + Ogre::Entity *masterEnt = m_slotSystem->getMasterEntity(e); Ogre::Bone *attachBone = findAttachBone(state.hairEntity, masterEnt); Ogre::Matrix4 attachBoneInv = Ogre::Matrix4::IDENTITY; diff --git a/src/features/editScene/systems/PathFollowingSystem.cpp b/src/features/editScene/systems/PathFollowingSystem.cpp index 0e3e85d..70b0bae 100644 --- a/src/features/editScene/systems/PathFollowingSystem.cpp +++ b/src/features/editScene/systems/PathFollowingSystem.cpp @@ -1,8 +1,10 @@ #include "PathFollowingSystem.hpp" #include "AnimationTreeSystem.hpp" #include "NavMeshSystem.hpp" +#include "CharacterRegistry.hpp" #include "../components/PathFollowing.hpp" #include "../components/Character.hpp" +#include "../components/CharacterIdentity.hpp" #include "../components/CharacterSlots.hpp" #include "../components/Transform.hpp" #include "../components/NavMesh.hpp" @@ -47,11 +49,15 @@ void PathFollowingSystem::rotateTowards(flecs::entity e, return; flatDir.normalise(); - // Get the character's front-facing axis + // Get the character's front-facing axis from the registry Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; - if (e.has()) { - auto &slots = e.get(); - frontAxis = slots.frontAxis; + if (e.has()) { + auto &id = e.get(); + const CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton().findCharacter( + id.registryId); + if (rec) + frontAxis = rec->frontAxis; } // Use yaw rotation only (Y plane) diff --git a/src/features/editScene/systems/ProceduralMaterialSystem.cpp b/src/features/editScene/systems/ProceduralMaterialSystem.cpp index e375b37..b87dd94 100644 --- a/src/features/editScene/systems/ProceduralMaterialSystem.cpp +++ b/src/features/editScene/systems/ProceduralMaterialSystem.cpp @@ -60,11 +60,15 @@ void ProceduralMaterialSystem::createMaterial( flecs::entity entity, ProceduralMaterialComponent &component) { try { + // Ensure persistent ID is assigned. + if (component.materialId.empty()) { + component.materialId = generatePersistentId(); + } + // Generate unique material name if not set if (component.materialName.empty()) { component.materialName = - "ProceduralMat_" + std::to_string(entity.id()) + - "_" + std::to_string(m_createdCount++); + "ProceduralMat_" + component.materialId; } Ogre::MaterialManager &matMgr = diff --git a/src/features/editScene/systems/ProceduralMaterialSystem.hpp b/src/features/editScene/systems/ProceduralMaterialSystem.hpp index eca25f3..8ea034b 100644 --- a/src/features/editScene/systems/ProceduralMaterialSystem.hpp +++ b/src/features/editScene/systems/ProceduralMaterialSystem.hpp @@ -34,8 +34,7 @@ private: flecs::query m_query; bool m_initialized = false; - int m_createdCount = 0; - + // Create the Ogre material void createMaterial(flecs::entity entity, struct ProceduralMaterialComponent& component); diff --git a/src/features/editScene/systems/ProceduralTextureSystem.cpp b/src/features/editScene/systems/ProceduralTextureSystem.cpp index bc429af..416bd04 100644 --- a/src/features/editScene/systems/ProceduralTextureSystem.cpp +++ b/src/features/editScene/systems/ProceduralTextureSystem.cpp @@ -37,9 +37,14 @@ void ProceduralTextureSystem::update() void ProceduralTextureSystem::generateTexture(flecs::entity entity, ProceduralTextureComponent& component) { try { + // Ensure persistent ID is assigned. + if (component.textureId.empty()) { + component.textureId = generatePersistentId(); + } + // Generate unique texture name if not set if (component.textureName.empty()) { - component.textureName = "ProceduralTex_" + std::to_string(entity.id()) + "_" + std::to_string(m_generatedCount++); + component.textureName = "ProceduralTex_" + component.textureId; } const int gridSize = ProceduralTextureComponent::GRID_SIZE; diff --git a/src/features/editScene/systems/ProceduralTextureSystem.hpp b/src/features/editScene/systems/ProceduralTextureSystem.hpp index d1295c4..3e35fbe 100644 --- a/src/features/editScene/systems/ProceduralTextureSystem.hpp +++ b/src/features/editScene/systems/ProceduralTextureSystem.hpp @@ -31,8 +31,7 @@ private: flecs::query m_query; bool m_initialized = false; - int m_generatedCount = 0; - + // Generate the texture for a component void generateTexture(flecs::entity entity, struct ProceduralTextureComponent& component); }; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 9747941..2a2942b 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -21,6 +21,7 @@ #include "../components/Character.hpp" #include "../components/CharacterSlots.hpp" #include "../components/CharacterIdentity.hpp" +#include "CharacterRegistry.hpp" #include "../components/AnimationTree.hpp" #include "../components/AnimationTreeTemplate.hpp" #include "../components/StartupMenu.hpp" @@ -244,10 +245,6 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["character"] = serializeCharacter(entity); } - if (entity.has()) { - json["characterSlots"] = serializeCharacterSlots(entity); - } - if (entity.has()) { json["characterIdentity"] = serializeCharacterIdentity(entity); } @@ -470,14 +467,16 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, deserializeCharacter(entity, json["character"]); } - if (json.contains("characterSlots")) { - deserializeCharacterSlots(entity, json["characterSlots"]); - } - + /* Deserialize identity before slots so legacy slot data migrates into + * the correct registry record. */ if (json.contains("characterIdentity")) { deserializeCharacterIdentity(entity, json["characterIdentity"]); } + if (json.contains("characterSlots")) { + deserializeCharacterSlots(entity, json["characterSlots"]); + } + if (json.contains("animationTree")) { deserializeAnimationTree(entity, json["animationTree"]); } @@ -703,14 +702,16 @@ void SceneSerializer::deserializeEntityComponents( deserializeCharacter(entity, json["character"]); } - if (json.contains("characterSlots")) { - deserializeCharacterSlots(entity, json["characterSlots"]); - } - + /* Deserialize identity before slots so legacy slot data migrates into + * the correct registry record. */ if (json.contains("characterIdentity")) { deserializeCharacterIdentity(entity, json["characterIdentity"]); } + if (json.contains("characterSlots")) { + deserializeCharacterSlots(entity, json["characterSlots"]); + } + if (json.contains("animationTree")) { deserializeAnimationTree(entity, json["animationTree"]); } @@ -1891,7 +1892,7 @@ void SceneSerializer::deserializeProceduralTexture(flecs::entity entity, // Auto-generate texture ID if empty if (texture.textureId.empty()) { - texture.textureId = "texture_" + std::to_string(entity.id()); + texture.textureId = generatePersistentId(); } // Deserialize colors array @@ -1979,7 +1980,7 @@ void SceneSerializer::deserializeProceduralMaterial(flecs::entity entity, // Auto-generate material ID if empty if (material.materialId.empty()) { - material.materialId = "material_" + std::to_string(entity.id()); + material.materialId = generatePersistentId(); } // Resolve texture reference by ID (like LOD system does) @@ -2227,82 +2228,82 @@ static void deserializeAnimationTreeNode(AnimationTreeNode &node, nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) { - auto &cs = entity.get(); - nlohmann::json json; - - json["sex"] = cs.sex; - json["slots"] = nlohmann::json::object(); - for (const auto &pair : cs.slots) - json["slots"][pair.first] = pair.second; - - // Serialize per-layer slot selections - nlohmann::json selections = nlohmann::json::object(); - for (const auto &pair : cs.slotSelections) { - nlohmann::json sel; - sel["layer0Mesh"] = pair.second.layer0Mesh; - sel["layer1Mesh"] = pair.second.layer1Mesh; - sel["layer2Mesh"] = pair.second.layer2Mesh; - sel["explicitMesh"] = pair.second.explicitMesh; - selections[pair.first] = sel; - } - json["slotSelections"] = selections; - - - // Serialize front axis - json["frontAxis"] = { cs.frontAxis.x, cs.frontAxis.y, cs.frontAxis.z }; - - return json; + /* CharacterSlotsComponent is deprecated and no longer saved. */ + (void)entity; + return nlohmann::json::object(); } void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nlohmann::json &json) { - CharacterSlotsComponent cs; - cs.sex = json.value("sex", "male"); - if (json.contains("slots") && json["slots"].is_object()) { - for (auto &[slot, mesh] : json["slots"].items()) - cs.slots[slot] = mesh.get(); + /* Migrate legacy CharacterSlots data into the character registry. + * The empty component is added as a placeholder reminder. */ + uint64_t registryId = 0; + if (entity.has()) + registryId = entity.get().registryId; + + if (registryId == 0) { + registryId = CharacterRegistry::getSingleton().createCharacter( + "Migrated", "Character", "", true); + entity.set( + CharacterIdentityComponent{ registryId }); } - // Deserialize per-layer slot selections - if (json.contains("slotSelections") && - json["slotSelections"].is_object()) { - for (auto &[slot, selJson] : json["slotSelections"].items()) { - SlotSelection sel; - if (selJson.contains("layer0Mesh")) - sel.layer0Mesh = - selJson.value("layer0Mesh", ""); - if (selJson.contains("layer1Mesh")) - sel.layer1Mesh = - selJson.value("layer1Mesh", ""); - if (selJson.contains("layer2Mesh")) - sel.layer2Mesh = - selJson.value("layer2Mesh", ""); - // Backward compat: old format had layer/requiredTags/excludedTags - if (selJson.contains("layer") && - sel.layer1Mesh.empty() && sel.layer2Mesh.empty()) { - int oldLayer = selJson.value("layer", 2); - if (oldLayer == 1) - sel.layer1Mesh = "auto"; - else if (oldLayer >= 2) - sel.layer2Mesh = "auto"; + + CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton().findCharacter(registryId); + if (rec) { + rec->inlineSex = json.value("sex", rec->inlineSex); + if (json.contains("slots") && json["slots"].is_object()) { + for (auto &[slot, mesh] : json["slots"].items()) { + SlotSelection sel; + sel.explicitMesh = mesh.get(); + rec->inlineSlotSelections[slot] = sel; } - sel.explicitMesh = selJson.value("explicitMesh", ""); - cs.slotSelections[slot] = sel; } - } - // Deserialize front axis - if (json.contains("frontAxis") && json["frontAxis"].is_array() && - json["frontAxis"].size() >= 3) { - cs.frontAxis = Ogre::Vector3(json["frontAxis"][0].get(), - json["frontAxis"][1].get(), - json["frontAxis"][2].get()); - if (cs.frontAxis.squaredLength() > 0.0001f) - cs.frontAxis.normalise(); + if (json.contains("slotSelections") && + json["slotSelections"].is_object()) { + for (auto &[slot, selJson] : + json["slotSelections"].items()) { + SlotSelection sel; + if (selJson.contains("layer0Mesh")) + sel.layer0Mesh = + selJson.value("layer0Mesh", ""); + if (selJson.contains("layer1Mesh")) + sel.layer1Mesh = + selJson.value("layer1Mesh", ""); + if (selJson.contains("layer2Mesh")) + sel.layer2Mesh = + selJson.value("layer2Mesh", ""); + if (selJson.contains("layer") && + sel.layer1Mesh.empty() && + sel.layer2Mesh.empty()) { + int oldLayer = selJson.value("layer", 2); + if (oldLayer == 1) + sel.layer1Mesh = "auto"; + else if (oldLayer >= 2) + sel.layer2Mesh = "auto"; + } + sel.explicitMesh = + selJson.value("explicitMesh", ""); + rec->inlineSlotSelections[slot] = sel; + } + } + if (json.contains("frontAxis") && + json["frontAxis"].is_array() && + json["frontAxis"].size() >= 3) { + Ogre::Vector3 fa( + json["frontAxis"][0].get(), + json["frontAxis"][1].get(), + json["frontAxis"][2].get()); + if (fa.squaredLength() > 0.0001f) + fa.normalise(); + rec->frontAxis = fa; + } + CharacterRegistry::getSingleton().autoSave(); } - - cs.dirty = true; - entity.set(cs); + entity.add(); + CharacterRegistry::getSingleton().markCharacterDirty(registryId); } static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node) diff --git a/src/features/editScene/systems/SmartObjectSystem.cpp b/src/features/editScene/systems/SmartObjectSystem.cpp index ff2dcb6..f01bae8 100644 --- a/src/features/editScene/systems/SmartObjectSystem.cpp +++ b/src/features/editScene/systems/SmartObjectSystem.cpp @@ -3,7 +3,9 @@ #include "../components/Transform.hpp" #include "../components/Character.hpp" #include "../components/CharacterSlots.hpp" +#include "../components/CharacterIdentity.hpp" #include "../components/GoapBlackboard.hpp" +#include "CharacterRegistry.hpp" #include "../components/ActionDatabase.hpp" #include "../components/ActionDebug.hpp" #include "../components/PathFollowing.hpp" @@ -64,9 +66,13 @@ Ogre::Vector3 SmartObjectSystem::getEntityPosition(flecs::entity e) Ogre::Vector3 SmartObjectSystem::getFrontAxis(flecs::entity e) const { - if (e.has()) { - auto &slots = e.get(); - return slots.frontAxis; + if (e.has()) { + auto &id = e.get(); + const CharacterRegistry::CharacterRecord *rec = + CharacterRegistry::getSingleton().findCharacter( + id.registryId); + if (rec) + return rec->frontAxis; } return Ogre::Vector3::NEGATIVE_UNIT_Z; } diff --git a/src/features/editScene/ui/CharacterSlotsEditor.cpp b/src/features/editScene/ui/CharacterSlotsEditor.cpp index 89379a8..21f82f4 100644 --- a/src/features/editScene/ui/CharacterSlotsEditor.cpp +++ b/src/features/editScene/ui/CharacterSlotsEditor.cpp @@ -1,9 +1,5 @@ #include "CharacterSlotsEditor.hpp" -#include "../systems/CharacterSlotSystem.hpp" -#include "../systems/CharacterRegistry.hpp" -#include "../components/CharacterIdentity.hpp" #include -#include CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr) : m_sceneMgr(sceneMgr) @@ -13,384 +9,12 @@ CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr) bool CharacterSlotsEditor::renderComponent(flecs::entity entity, CharacterSlotsComponent &cs) { - bool modified = false; + (void)entity; + (void)cs; + (void)m_sceneMgr; - if (ImGui::CollapsingHeader("Character Slots", - ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Indent(); - - CharacterSlotSystem::loadCatalog(); - - /* 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(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)) { - cs.sex = sex; - modified = true; - cs.dirty = true; - } - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - - ImGui::Separator(); - - ImGui::Separator(); - - /* Front-facing axis */ - { - Ogre::Vector3 axis = cs.frontAxis; - float axisVals[3] = { axis.x, axis.y, axis.z }; - ImGui::Text("Front Axis:"); - ImGui::SameLine(); - ImGui::TextDisabled( - "(direction character faces, used by path following)"); - if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f, - -1.0f, 1.0f)) { - cs.frontAxis = Ogre::Vector3( - axisVals[0], axisVals[1], axisVals[2]); - if (cs.frontAxis.squaredLength() > 0.0001f) - cs.frontAxis.normalise(); - modified = true; - } - ImGui::SameLine(); - if (ImGui::SmallButton("-Z")) { - cs.frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; - modified = true; - } - ImGui::SameLine(); - if (ImGui::SmallButton("+Z")) { - cs.frontAxis = Ogre::Vector3::UNIT_Z; - modified = true; - } - } - - ImGui::Separator(); - - /* Shape Keys */ - if (ImGui::CollapsingHeader("Shape Keys")) { - auto shapeKeys = CharacterSlotSystem::getShapeKeyNames( - currentAge, cs.sex); - if (shapeKeys.empty()) { - ImGui::TextDisabled("No shape keys available."); - } else { - CharacterShapeKeysComponent *skc = nullptr; - if (entity.has()) - skc = &entity.get_mut< - CharacterShapeKeysComponent>(); - else { - 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, - 1.0f)) { - skc->weights[name] = val; - skc->dirty = true; - cs.dirty = true; - modified = true; - } - } - } - } - - ImGui::Separator(); - - /* Get outfit level from registry for resolveMesh calls */ - int outfitLevel = 2; - if (entity.has()) { - auto &id = entity.get(); - auto *rec = - CharacterRegistry::getSingleton().findCharacter( - id.registryId); - if (rec) - outfitLevel = rec->inlineOutfitLevel; - } - - /* Slot selections */ - std::vector availableSlots = - CharacterSlotSystem::getSlots(currentAge, cs.sex); - for (const auto &pair : cs.slotSelections) { - if (std::find(availableSlots.begin(), - availableSlots.end(), - pair.first) == availableSlots.end()) - availableSlots.push_back(pair.first); - } - std::sort(availableSlots.begin(), availableSlots.end()); - - for (const auto &slot : availableSlots) { - /* Ensure selection exists */ - if (cs.slotSelections.find(slot) == - cs.slotSelections.end()) { - SlotSelection sel; - /* Migrate old explicit mesh if present */ - auto oldIt = cs.slots.find(slot); - if (oldIt != cs.slots.end()) - sel.explicitMesh = oldIt->second; - cs.slotSelections[slot] = sel; - } - - SlotSelection &sel = cs.slotSelections[slot]; - - if (ImGui::TreeNode(slot.c_str())) { - /* Resolved mesh preview */ - Ogre::String resolved = - CharacterSlotSystem::resolveMesh( - currentAge, cs.sex, slot, sel, - outfitLevel); - ImGui::TextDisabled("Resolved: %s", - resolved.empty() ? - "(none)" : - resolved.c_str()); - - /* Explicit mesh override */ - bool useExplicit = !sel.explicitMesh.empty(); - if (ImGui::Checkbox("Lock explicit mesh", - &useExplicit)) { - if (!useExplicit) { - sel.explicitMesh.clear(); - } else { - /* Lock to currently resolved mesh */ - sel.explicitMesh = - CharacterSlotSystem::resolveMesh( - currentAge, - cs.sex, slot, - sel, - outfitLevel); - } - modified = true; - cs.dirty = true; - } - - if (useExplicit) { - std::vector meshes = - CharacterSlotSystem::getMeshes( - 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())) { - 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; - modified = true; - cs.dirty = true; - } - } - ImGui::EndCombo(); - } - } else { - /* Layer 0 combo (base meshes like hair) */ - /* Hair slot always uses auto stub; no base editing */ - if (slot != "hair") { - std::vector layer0Meshes = - CharacterSlotSystem:: - getMeshesForLayer( - currentAge, - cs.sex, slot, - 0); - if (!layer0Meshes.empty()) { - Ogre::String l0Preview = "auto"; - if (!sel.layer0Mesh.empty() && - sel.layer0Mesh != "none") - l0Preview = CharacterSlotSystem:: - getMeshLabel( - currentAge, - cs.sex, - slot, - sel.layer0Mesh); - if (ImGui::BeginCombo( - "Base (Layer 0)", - l0Preview.c_str())) { - if (ImGui::Selectable( - "auto", - sel.layer0Mesh.empty() || - sel.layer0Mesh == - "none")) { - sel.layer0Mesh = - "none"; - modified = true; - cs.dirty = true; - } - for (const auto &m : - layer0Meshes) { - Ogre::String label = CharacterSlotSystem:: - getMeshLabel( - currentAge, - cs.sex, - slot, - m); - bool isSelected = - (sel.layer0Mesh == - m); - if (ImGui::Selectable( - label.c_str(), - isSelected)) { - sel.layer0Mesh = - m; - modified = - true; - cs.dirty = - true; - } - } - ImGui::EndCombo(); - } - } - } else { - ImGui::TextDisabled("Base: auto (stub hair)"); - } - /* Layer 1 combo */ - std::vector layer1Meshes = - CharacterSlotSystem:: - getMeshesForLayer( - currentAge, - cs.sex, slot, - 1); - Ogre::String l1Preview = "none"; - if (!sel.layer1Mesh.empty()) - 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( - 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; - } - } - ImGui::EndCombo(); - } - - /* Layer 2 combo */ - std::vector layer2Meshes = - CharacterSlotSystem:: - getMeshesForLayer( - currentAge, - cs.sex, slot, - 2); - Ogre::String l2Preview = "none"; - if (!sel.layer2Mesh.empty()) - 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( - 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; - } - } - ImGui::EndCombo(); - } - } - - ImGui::TreePop(); - } - } - - /* Rebuild button */ - if (ImGui::Button("Rebuild")) { - cs.dirty = true; - modified = true; - } - - ImGui::Unindent(); - } - - return modified; + ImGui::Text("Character Slots"); + ImGui::TextDisabled( + "Appearance data has moved to Tools -> Character Registry."); + return false; } diff --git a/src/features/editScene/ui/CharacterSlotsEditor.hpp b/src/features/editScene/ui/CharacterSlotsEditor.hpp index 8f8d465..7327055 100644 --- a/src/features/editScene/ui/CharacterSlotsEditor.hpp +++ b/src/features/editScene/ui/CharacterSlotsEditor.hpp @@ -6,7 +6,8 @@ #include /** - * Editor for CharacterSlotsComponent + * Placeholder editor for the deprecated CharacterSlotsComponent. + * All appearance editing now happens in Tools -> Character Registry. */ class CharacterSlotsEditor : public ComponentEditor { public: