From b19033b557d565635a55a128c60a92d0546d51a2 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 21 May 2026 15:36:27 +0300 Subject: [PATCH] outfitLevel moved, character ID safeguard --- .../editScene/components/CharacterSlots.hpp | 3 - src/features/editScene/lua-scripts/data2.lua | 2 +- .../editScene/lua/LuaComponentApi.cpp | 46 ++++++++-- .../editScene/systems/CharacterRegistry.cpp | 85 ++++++++++++++++--- .../editScene/systems/CharacterRegistry.hpp | 7 ++ .../editScene/systems/CharacterSlotSystem.cpp | 27 +++++- .../editScene/systems/SceneSerializer.cpp | 38 +++++---- .../editScene/ui/CharacterSlotsEditor.cpp | 53 ++++++------ 8 files changed, 191 insertions(+), 70 deletions(-) diff --git a/src/features/editScene/components/CharacterSlots.hpp b/src/features/editScene/components/CharacterSlots.hpp index 0bfcc84..f212bca 100644 --- a/src/features/editScene/components/CharacterSlots.hpp +++ b/src/features/editScene/components/CharacterSlots.hpp @@ -26,9 +26,6 @@ struct SlotSelection { struct CharacterSlotsComponent { Ogre::String sex = "male"; - /* Global outfit level: 0=nude, 1=lingerie, 2=clothed */ - int outfitLevel = 2; - /* Backward-compat: old mesh-name map. Deserialized into slotSelections on load. */ std::unordered_map slots; diff --git a/src/features/editScene/lua-scripts/data2.lua b/src/features/editScene/lua-scripts/data2.lua index 0eb88cb..9d5125a 100644 --- a/src/features/editScene/lua-scripts/data2.lua +++ b/src/features/editScene/lua-scripts/data2.lua @@ -29,7 +29,7 @@ ecs.subscribe_event("game_start", 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") + -- 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 99bf3a5..7a5c04c 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -522,9 +522,19 @@ static void registerAllComponents() lua_setfield(L, -2, "explicitMesh"); lua_setfield(L, -2, kv.first.c_str()); } lua_setfield(L, -2, "slotSelections"); - lua_pushinteger(L, c.outfitLevel); - lua_setfield(L, -2, "outfitLevel"); pushVector3(L, c.frontAxis); - lua_setfield(L, -2, "frontAxis"); + { // 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)) { @@ -540,16 +550,38 @@ static void registerAllComponents() 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); - if (lua_getfield(L, idx, "outfitLevel"), lua_isnumber(L, -1)) - c.outfitLevel = lua_tointeger(L, -1); - lua_pop(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); diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index b58aaad..2e60250 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -297,7 +297,6 @@ readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs, if (j.contains("characterSlots")) { auto &s = j["characterSlots"]; cs.sex = s.value("sex", "male"); - cs.outfitLevel = s.value("outfitLevel", 2); if (s.contains("slotSelections")) { for (auto &[slot, selJson] : s["slotSelections"].items()) { @@ -429,7 +428,7 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) child->inlineShapeKeyWeights, prefabAge); child->age = prefabAge; child->inlineSex = tmpCs.sex; - child->inlineOutfitLevel = tmpCs.outfitLevel; + child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel; child->inlineSlotSelections = tmpCs.slotSelections; } else { child->age = sameSexParent->age; @@ -540,7 +539,6 @@ flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c, /* CharacterSlots */ CharacterSlotsComponent cs; cs.sex = c.inlineSex; - cs.outfitLevel = c.inlineOutfitLevel; cs.slotSelections = c.inlineSlotSelections; cs.dirty = true; inst.set(cs); @@ -695,6 +693,15 @@ flecs::entity CharacterRegistry::findSpawnedEntity(uint64_t id) const return result; } +void CharacterRegistry::markCharacterDirty(uint64_t id) +{ + flecs::entity e = findSpawnedEntity(id); + if (!e.is_alive()) + return; + if (e.has()) + e.get_mut().dirty = true; +} + bool CharacterRegistry::despawnCharacter(uint64_t id) { if (!m_world) @@ -829,6 +836,14 @@ uint64_t CharacterRegistry::createCharacter(const std::string &firstName, bool persistent) { uint64_t id = persistent ? m_nextId++ : m_nextRuntimeId++; + /* Safety: id must never be 0 */ + if (id == 0) { + id = persistent ? m_nextId++ : m_nextRuntimeId++; + Ogre::LogManager::getSingleton().logMessage( + "CharacterRegistry: id wrapped to 0, " + "incremented to " + + std::to_string(id)); + } CharacterRecord rec; rec.id = id; rec.firstName = firstName; @@ -1169,7 +1184,11 @@ nlohmann::json CharacterRegistry::serialize() const const CharacterRecord &c = pair.second; if (!c.persistent) continue; + /* Belt-and-suspenders: never serialize id == 0 */ + if (c.id == 0) + continue; nlohmann::json rec; + rec["id"] = c.id; rec["persistent"] = c.persistent; rec["firstName"] = c.firstName; @@ -1423,6 +1442,17 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.stringColumns[k] = v.get(); } + + /* ---- Safeguard: reject id == 0 ---- */ + if (c.id == 0) { + Ogre::LogManager::getSingleton().logMessage( + "CharacterRegistry: skipping corrupt " + "record with id=0 (firstName=" + + c.firstName + + " lastName=" + c.lastName + ")"); + continue; + } + m_characters[c.id] = c; } } @@ -1777,15 +1807,24 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::SameLine(); if (ImGui::SmallButton( "Promote to Roster")) { - c->persistent = true; - c->prefabPath = + /* Assign a proper persistent + * ID */ + uint64_t newId = m_nextId++; + CharacterRecord promoted = *c; + promoted.id = newId; + promoted.persistent = true; + promoted.prefabPath = generatePrefabPath( - c->id, + newId, c->firstName, c->lastName); + m_characters.erase(c->id); + m_characters[newId] = promoted; + m_selectedCharacterId = newId; autoSave(); } } + if (ImGui::InputText("First Name", fnBuf, sizeof(fnBuf))) { c->firstName = fnBuf; @@ -1848,13 +1887,20 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Age category from catalog */ CharacterSlotSystem::loadCatalog(); std::string currentAgeCat = c->age; - std::vector ageCats = CharacterSlotSystem::getAges(); - if (ImGui::BeginCombo("Age Category", currentAgeCat.c_str())) { + 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)) { + bool isSelected = + (currentAgeCat == ac); + if (ImGui::Selectable( + ac.c_str(), + isSelected)) { c->age = ac; autoSave(); + markCharacterDirty( + c->id); } if (isSelected) ImGui::SetItemDefaultFocus(); @@ -1862,6 +1908,25 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::EndCombo(); } + /* Outfit level */ + { + const char *outfitLabels[] = { + "Nude", "Lingerie", "Clothed" + }; + int outfit = c->inlineOutfitLevel; + if (outfit < 0) + outfit = 0; + if (outfit > 2) + outfit = 2; + if (ImGui::Combo("Outfit Level", + &outfit, outfitLabels, + 3)) { + c->inlineOutfitLevel = outfit; + autoSave(); + markCharacterDirty(c->id); + } + } + /* Family */ ImGui::Separator(); ImGui::Text("Family"); diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index dbc683c..159bfca 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -319,6 +319,13 @@ public: /* Spawn / Save */ /* ------------------------------------------------------------------ */ flecs::entity findSpawnedEntity(uint64_t id) const; + /** + * Mark the spawned entity for a character as dirty so its visual + * appearance (CharacterSlotsComponent) is rebuilt on the next + * update. Call this after changing outfitLevel, age, or any other + * registry field that affects the character's look. + */ + void markCharacterDirty(uint64_t id); bool despawnCharacter(uint64_t id); flecs::entity spawnCharacter(uint64_t id); flecs::entity spawnInlineCharacter(const CharacterRecord &c, diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index 629e62f..e1d4b2e 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -441,6 +441,23 @@ static Ogre::String getCharacterAge(flecs::entity e) return "adult"; } +/** + * 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) { @@ -473,12 +490,14 @@ 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"], - cs.outfitLevel); + outfitLevel); if (!mesh.empty()) masterSlot = "face"; } @@ -486,7 +505,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, for (const auto &pair : cs.slotSelections) { Ogre::String mesh = resolveMesh(age, cs.sex, pair.first, pair.second, - cs.outfitLevel); + outfitLevel); if (!mesh.empty()) { masterSlot = pair.first; break; @@ -499,7 +518,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, Ogre::String masterMesh = resolveMesh(age, cs.sex, masterSlot, cs.slotSelections[masterSlot], - cs.outfitLevel); + outfitLevel); if (masterMesh.empty()) return; @@ -539,7 +558,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, continue; Ogre::String mesh = - resolveMesh(age, cs.sex, slot, sel, cs.outfitLevel); + resolveMesh(age, cs.sex, slot, sel, outfitLevel); if (mesh.empty()) continue; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 566eeda..f3063e9 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -344,8 +344,8 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) } if (entity.has()) { - json["goapBlackboard"] = serializeGoapBlackboard( - entity.get()); + json["goapBlackboard"] = + serializeGoapBlackboard(entity.get()); } if (entity.has()) { @@ -2201,7 +2201,6 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) nlohmann::json json; json["sex"] = cs.sex; - json["outfitLevel"] = cs.outfitLevel; json["slots"] = nlohmann::json::object(); for (const auto &pair : cs.slots) json["slots"][pair.first] = pair.second; @@ -2224,26 +2223,28 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) } void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, - const nlohmann::json &json) + const nlohmann::json &json) { CharacterSlotsComponent cs; cs.sex = json.value("sex", "male"); - cs.outfitLevel = json.value("outfitLevel", 2); if (json.contains("slots") && json["slots"].is_object()) { for (auto &[slot, mesh] : json["slots"].items()) cs.slots[slot] = mesh.get(); } // Deserialize per-layer slot selections - if (json.contains("slotSelections") && json["slotSelections"].is_object()) { + if (json.contains("slotSelections") && + json["slotSelections"].is_object()) { for (auto &[slot, selJson] : json["slotSelections"].items()) { SlotSelection sel; if (selJson.contains("layer1Mesh")) - sel.layer1Mesh = selJson.value("layer1Mesh", ""); + sel.layer1Mesh = + selJson.value("layer1Mesh", ""); if (selJson.contains("layer2Mesh")) - sel.layer2Mesh = selJson.value("layer2Mesh", ""); + sel.layer2Mesh = + selJson.value("layer2Mesh", ""); // Backward compat: old format had layer/requiredTags/excludedTags - if (selJson.contains("layer") && sel.layer1Mesh.empty() && - sel.layer2Mesh.empty()) { + if (selJson.contains("layer") && + sel.layer1Mesh.empty() && sel.layer2Mesh.empty()) { int oldLayer = selJson.value("layer", 2); if (oldLayer == 1) sel.layer1Mesh = "auto"; @@ -3684,9 +3685,11 @@ void SceneSerializer::deserializePathFollowing(flecs::entity entity, pf.walkSpeed = json.value("walkSpeed", 2.5f); pf.runSpeed = json.value("runSpeed", 5.0f); pf.useRootMotion = json.value("useRootMotion", true); - pf.currentLocomotionState = json.value("currentLocomotionState", "idle"); + pf.currentLocomotionState = + json.value("currentLocomotionState", "idle"); pf.hasTarget = json.value("hasTarget", false); - if (json.contains("targetPosition") && json["targetPosition"].is_object()) { + if (json.contains("targetPosition") && + json["targetPosition"].is_object()) { auto &tp = json["targetPosition"]; pf.targetPosition = Ogre::Vector3(tp.value("x", 0.0f), tp.value("y", 0.0f), @@ -3696,8 +3699,8 @@ void SceneSerializer::deserializePathFollowing(flecs::entity entity, pf.path.clear(); for (const auto &pt : json["path"]) { pf.path.push_back(Ogre::Vector3(pt.value("x", 0.0f), - pt.value("y", 0.0f), - pt.value("z", 0.0f))); + pt.value("y", 0.0f), + pt.value("z", 0.0f))); } } pf.pathIndex = json.value("pathIndex", 0); @@ -3793,8 +3796,8 @@ void SceneSerializer::deserializeGoapRunner(flecs::entity entity, const nlohmann::json &json) { GoapRunnerComponent runner; - runner.state = static_cast( - json.value("state", 0)); + runner.state = + static_cast(json.value("state", 0)); runner.currentActionIndex = json.value("currentActionIndex", 0); runner.currentActionName = json.value("currentActionName", ""); runner.actionTimer = json.value("actionTimer", 0.0f); @@ -4038,7 +4041,8 @@ void SceneSerializer::deserializeInventory(flecs::entity entity, slot.stackSize = slotJson.value("stackSize", 0); // Backward compatibility: old slots had inline data - if (slot.itemId.empty() && slotJson.contains("itemName")) { + if (slot.itemId.empty() && + slotJson.contains("itemName")) { slot.itemId = slotJson.value("itemName", ""); } ensureItemInRegistry(slot.itemId, slotJson); diff --git a/src/features/editScene/ui/CharacterSlotsEditor.cpp b/src/features/editScene/ui/CharacterSlotsEditor.cpp index c478765..b6d3ecd 100644 --- a/src/features/editScene/ui/CharacterSlotsEditor.cpp +++ b/src/features/editScene/ui/CharacterSlotsEditor.cpp @@ -10,7 +10,6 @@ CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr) { } - bool CharacterSlotsEditor::renderComponent(flecs::entity entity, CharacterSlotsComponent &cs) { @@ -22,7 +21,6 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, CharacterSlotSystem::loadCatalog(); - /* Get current age from registry */ Ogre::String currentAge = "adult"; if (entity.has()) { @@ -55,21 +53,6 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, ImGui::Separator(); - /* Global outfit level */ - const char *outfitLabels[] = { "Nude", "Lingerie", "Clothed" }; - int outfit = cs.outfitLevel; - if (outfit < 0) - outfit = 0; - if (outfit > 2) - outfit = 2; - if (ImGui::Combo("Outfit", &outfit, outfitLabels, 3)) { - cs.outfitLevel = outfit; - modified = true; - cs.dirty = true; - } - ImGui::SameLine(); - ImGui::TextDisabled("(global layer switch)"); - ImGui::Separator(); /* Front-facing axis */ @@ -136,6 +119,17 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, 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); @@ -166,7 +160,7 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, Ogre::String resolved = CharacterSlotSystem::resolveMesh( currentAge, cs.sex, slot, sel, - cs.outfitLevel); + outfitLevel); ImGui::TextDisabled("Resolved: %s", resolved.empty() ? "(none)" : @@ -185,7 +179,7 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, currentAge, cs.sex, slot, sel, - cs.outfitLevel); + outfitLevel); } modified = true; cs.dirty = true; @@ -194,7 +188,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, if (useExplicit) { std::vector meshes = CharacterSlotSystem::getMeshes( - currentAge, cs.sex, slot); + currentAge, cs.sex, + slot); Ogre::String preview = sel.explicitMesh.empty() ? "(none)" : @@ -229,14 +224,15 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, std::vector layer1Meshes = CharacterSlotSystem:: getMeshesForLayer( - currentAge, cs.sex, - slot, 1); + currentAge, + cs.sex, slot, + 1); Ogre::String l1Preview = "none"; if (!sel.layer1Mesh.empty()) l1Preview = CharacterSlotSystem:: getMeshLabel( - currentAge, cs.sex, - slot, + currentAge, + cs.sex, slot, sel.layer1Mesh); if (ImGui::BeginCombo( "Lingerie (Layer 1)", @@ -277,14 +273,15 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, std::vector layer2Meshes = CharacterSlotSystem:: getMeshesForLayer( - currentAge, cs.sex, - slot, 2); + currentAge, + cs.sex, slot, + 2); Ogre::String l2Preview = "none"; if (!sel.layer2Mesh.empty()) l2Preview = CharacterSlotSystem:: getMeshLabel( - currentAge, cs.sex, - slot, + currentAge, + cs.sex, slot, sel.layer2Mesh); if (ImGui::BeginCombo( "Clothing (Layer 2)",