diff --git a/assets/blender/characters/clothes-male-top.blend b/assets/blender/characters/clothes-male-top.blend index c5c69ff..f98b54e 100644 --- a/assets/blender/characters/clothes-male-top.blend +++ b/assets/blender/characters/clothes-male-top.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10b18111add43e899ff9d6c37f59ebd099960cd92eee92b629541bd7d90abdb9 -size 5393044 +oid sha256:4ff80187a33acc2a4f6228dd6492b84aee342b22a18c68d2f4c9be9fb15ec9cf +size 2514944 diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 39c388c..846010e 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -37,6 +37,7 @@ set(EDITSCENE_SOURCES systems/StartupMenuSystem.cpp systems/PlayerControllerSystem.cpp systems/CharacterSlotSystem.cpp + systems/CharacterRegistry.cpp systems/AnimationTreeSystem.cpp systems/BehaviorTreeSystem.cpp systems/NavMeshSystem.cpp @@ -87,6 +88,7 @@ set(EDITSCENE_SOURCES ui/PrimitiveEditor.cpp ui/TriangleBufferEditor.cpp ui/CharacterSlotsEditor.cpp + ui/CharacterIdentityEditor.cpp ui/AnimationTreeEditor.cpp ui/AnimationTreeTemplateEditor.cpp ui/CharacterEditor.cpp @@ -191,6 +193,7 @@ set(EDITSCENE_HEADERS components/Primitive.hpp components/TriangleBuffer.hpp components/CharacterSlots.hpp + components/CharacterIdentity.hpp components/AnimationTree.hpp components/Character.hpp components/CellGrid.hpp @@ -215,6 +218,7 @@ set(EDITSCENE_HEADERS systems/ProceduralMaterialSystem.hpp systems/ProceduralMeshSystem.hpp systems/CharacterSlotSystem.hpp + systems/CharacterRegistry.hpp systems/AnimationTreeSystem.hpp systems/BehaviorTreeSystem.hpp systems/NavMeshSystem.hpp @@ -279,6 +283,7 @@ set(EDITSCENE_HEADERS ui/PrimitiveEditor.hpp ui/TriangleBufferEditor.hpp ui/CharacterSlotsEditor.hpp + ui/CharacterIdentityEditor.hpp ui/AnimationTreeEditor.hpp ui/AnimationTreeTemplateEditor.hpp ui/CharacterEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 7d0a5a9..d82f868 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -60,6 +60,7 @@ #include "components/Primitive.hpp" #include "components/TriangleBuffer.hpp" #include "components/CharacterSlots.hpp" +#include "components/CharacterIdentity.hpp" #include "components/AnimationTree.hpp" #include "components/AnimationTreeTemplate.hpp" #include "components/Character.hpp" @@ -707,6 +708,9 @@ void EditorApp::setupECS() // Register CharacterSlots component m_world.component(); + // Register CharacterIdentity component + m_world.component(); + // Register AnimationTree component m_world.component(); diff --git a/src/features/editScene/components/CharacterIdentity.hpp b/src/features/editScene/components/CharacterIdentity.hpp new file mode 100644 index 0000000..eac14ec --- /dev/null +++ b/src/features/editScene/components/CharacterIdentity.hpp @@ -0,0 +1,18 @@ +#ifndef EDITSCENE_CHARACTERIDENTITY_HPP +#define EDITSCENE_CHARACTERIDENTITY_HPP +#pragma once + +#include + +/** + * Links a spawned entity to a global character registry entry. + * + * When a character entity is created from a prefab or manually, + * this component stores the persistent registry ID so the entity + * knows who it is in the social graph. + */ +struct CharacterIdentityComponent { + uint64_t registryId = 0; +}; + +#endif // EDITSCENE_CHARACTERIDENTITY_HPP diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp new file mode 100644 index 0000000..cbc3e00 --- /dev/null +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -0,0 +1,1616 @@ +#include "CharacterRegistry.hpp" +#include "PrefabSystem.hpp" +#include "EditorUISystem.hpp" +#include "../components/CharacterIdentity.hpp" +#include "../components/CharacterSlots.hpp" +#include "../components/CharacterClassComponent.hpp" +#include "../components/CharacterClassDatabase.hpp" +#include +#include +#include +#include +#include + +/* ===================================================================== */ +/* Construction / init */ +/* ===================================================================== */ + +CharacterRegistry::CharacterRegistry() +{ + m_autoSavePath = "character_registry.json"; +} + +void CharacterRegistry::initialize() +{ + if (!std::filesystem::exists(m_autoSavePath)) + return; + if (!loadFromFile(m_autoSavePath)) { + Ogre::LogManager::getSingleton().logMessage( + "CharacterRegistry: auto-load failed: " + m_lastError); + } + scanTemplates(); +} + +void CharacterRegistry::autoSave() +{ + if (!m_autoSavePath.empty()) + saveToFile(m_autoSavePath); +} + +std::string CharacterRegistry::generatePrefabPath( + uint64_t id, const std::string &, + const std::string &) const +{ + return "prefabs/char_" + std::to_string(id) + ".json"; +} + +/* ===================================================================== */ +/* Prefab helpers */ +/* ===================================================================== */ + +bool CharacterRegistry::isValidCharacterPrefab(const std::string &filepath) +{ + try { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + nlohmann::json j; + file >> j; + return j.contains("characterSlots"); + } catch (...) { + return false; + } +} + +bool CharacterRegistry::readPrefabSlots(const std::string &filepath, + std::string &age, std::string &sex, + int &outfitLevel) +{ + try { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + nlohmann::json j; + file >> j; + if (!j.contains("characterSlots")) + return false; + auto &cs = j["characterSlots"]; + age = cs.value("age", "adult"); + sex = cs.value("sex", "male"); + outfitLevel = cs.value("outfitLevel", 2); + return true; + } catch (...) { + return false; + } +} + +bool CharacterRegistry::writePrefabSlots(const std::string &filepath, + const std::string &age, + const std::string &sex, + int outfitLevel) +{ + try { + nlohmann::json j; + { + std::ifstream file(filepath); + if (file.is_open()) + file >> j; + } + j["characterSlots"]["age"] = age; + j["characterSlots"]["sex"] = sex; + j["characterSlots"]["outfitLevel"] = outfitLevel; + std::ofstream out(filepath); + if (!out.is_open()) + return false; + out << j.dump(4); + return true; + } catch (...) { + return false; + } +} + +bool CharacterRegistry::copyPrefab(const std::string &src, + const std::string &dst) +{ + try { + std::filesystem::path dp(dst); + std::filesystem::create_directories(dp.parent_path()); + std::filesystem::copy_file( + src, dst, + std::filesystem::copy_options::overwrite_existing); + return true; + } catch (...) { + return false; + } +} + +bool CharacterRegistry::deletePrefabFile(const std::string &filepath) +{ + try { + if (std::filesystem::exists(filepath)) + return std::filesystem::remove(filepath); + return true; + } catch (...) { + return false; + } +} + +/* ===================================================================== */ +/* Templates */ +/* ===================================================================== */ + +void CharacterRegistry::scanTemplates() +{ + m_templates.clear(); + std::string dir = PrefabSystem::getPrefabsDirectory(); + if (!std::filesystem::exists(dir)) + return; + for (const auto &entry : std::filesystem::directory_iterator(dir)) { + if (entry.is_regular_file() && + entry.path().extension() == ".json") { + std::string p = entry.path().string(); + if (isValidCharacterPrefab(p)) + m_templates.push_back(p); + } + } +} + +/* ===================================================================== */ +/* Spawn / Save */ +/* ===================================================================== */ + +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) { + if (ci.registryId == id) + result = e; + }); + return result; +} + +bool CharacterRegistry::despawnCharacter(uint64_t id) +{ + if (!m_world) + return false; + flecs::entity e = findSpawnedEntity(id); + if (!e.is_alive()) + return false; + e.destruct(); + return true; +} + +flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) +{ + if (!m_world || !m_sceneMgr || !m_uiSystem) + return flecs::entity::null(); + + const CharacterRecord *c = findCharacter(id); + if (!c) + return flecs::entity::null(); + + /* Singleton: despawn any existing copy first */ + flecs::entity existing = findSpawnedEntity(id); + if (existing.is_alive()) + existing.destruct(); + + PrefabSystem prefabSys(*m_world, m_sceneMgr); + Ogre::Vector3 pos(0, 0, 0); + flecs::entity inst = prefabSys.createInstance( + c->prefabPath, flecs::entity::null(), pos, + c->firstName + " " + c->lastName, m_uiSystem); + + if (inst.is_alive()) { + inst.set( + CharacterIdentityComponent{id}); + + /* Apply RPG data from registry */ + if (!c->className.empty()) { + CharacterClassComponent cc; + cc.className = c->className; + cc.level = c->level; + cc.currentXP = c->currentXP; + cc.availablePoints = c->availablePoints; + cc.stats = c->stats; + cc.skills = c->skills; + cc.needs = c->needs; + inst.set(cc); + } + + m_uiSystem->addEntity(inst); + } + return inst; +} + +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(); + + if (!sel.is_alive()) { + m_lastError = "No entity selected"; + return false; + } + if (!sel.has()) { + m_lastError = "Selected entity has no CharacterSlots"; + return false; + } + + PrefabSystem prefabSys(*m_world, m_sceneMgr); + if (!prefabSys.savePrefab(sel, c->prefabPath)) { + m_lastError = prefabSys.getLastError(); + return false; + } + return true; +} + +/* ===================================================================== */ +/* Indexes */ +/* ===================================================================== */ + +void CharacterRegistry::rebuildIndexes() +{ + m_relBySource.clear(); + m_relByTarget.clear(); + for (size_t i = 0; i < m_relationships.size(); ++i) + addToIndex(i); +} + +void CharacterRegistry::addToIndex(size_t relIndex) +{ + const Relationship &r = m_relationships[relIndex]; + m_relBySource.insert({r.sourceId, relIndex}); + m_relByTarget.insert({r.targetId, relIndex}); +} + +void CharacterRegistry::removeFromIndex(uint64_t sourceId, uint64_t targetId, + size_t relIndex) +{ + auto srcRange = m_relBySource.equal_range(sourceId); + for (auto it = srcRange.first; it != srcRange.second; ++it) { + if (it->second == relIndex) { + m_relBySource.erase(it); + break; + } + } + auto tgtRange = m_relByTarget.equal_range(targetId); + for (auto it = tgtRange.first; it != tgtRange.second; ++it) { + if (it->second == relIndex) { + m_relByTarget.erase(it); + break; + } + } +} + +/* ===================================================================== */ +/* Characters */ +/* ===================================================================== */ + +uint64_t CharacterRegistry::createCharacter(const std::string &firstName, + const std::string &lastName, + const std::string &templatePath) +{ + uint64_t id = m_nextId++; + CharacterRecord rec; + rec.id = id; + rec.firstName = firstName; + rec.lastName = lastName; + rec.prefabPath = generatePrefabPath(id, firstName, lastName); + + if (!templatePath.empty() && + std::filesystem::exists(templatePath)) { + copyPrefab(templatePath, rec.prefabPath); + } + + m_characters[id] = rec; + autoSave(); + return id; +} + +void CharacterRegistry::deleteCharacter(uint64_t id) +{ + auto it = m_characters.find(id); + if (it != m_characters.end()) { + deletePrefabFile(it->second.prefabPath); + m_characters.erase(it); + } + purgeRelationships(id); + autoSave(); +} + +CharacterRegistry::CharacterRecord * +CharacterRegistry::findCharacter(uint64_t id) +{ + auto it = m_characters.find(id); + return it != m_characters.end() ? &it->second : nullptr; +} + +const CharacterRegistry::CharacterRecord * +CharacterRegistry::findCharacter(uint64_t id) const +{ + auto it = m_characters.find(id); + return it != m_characters.end() ? &it->second : nullptr; +} + +/* ===================================================================== */ +/* Groups */ +/* ===================================================================== */ + +uint64_t CharacterRegistry::createGroup(const std::string &name) +{ + uint64_t id = m_nextId++; + GroupRecord rec; + rec.id = id; + rec.name = name; + m_groups[id] = rec; + autoSave(); + return id; +} + +void CharacterRegistry::deleteGroup(uint64_t id) +{ + m_groups.erase(id); + purgeRelationships(id); + autoSave(); +} + +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()) { + g->memberIds.push_back(characterId); + autoSave(); + } +} + +void CharacterRegistry::removeFromGroup(uint64_t groupId, + uint64_t characterId) +{ + GroupRecord *g = findGroup(groupId); + if (!g) + return; + auto it = std::find(g->memberIds.begin(), g->memberIds.end(), + characterId); + if (it != g->memberIds.end()) { + g->memberIds.erase(it); + autoSave(); + } +} + +CharacterRegistry::GroupRecord *CharacterRegistry::findGroup(uint64_t id) +{ + auto it = m_groups.find(id); + return it != m_groups.end() ? &it->second : nullptr; +} + +const CharacterRegistry::GroupRecord * +CharacterRegistry::findGroup(uint64_t id) const +{ + auto it = m_groups.find(id); + return it != m_groups.end() ? &it->second : nullptr; +} + +/* ===================================================================== */ +/* Relationships */ +/* ===================================================================== */ + +CharacterRegistry::Relationship * +CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup) +{ + 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) + return &r; + } + return nullptr; +} + +const CharacterRegistry::Relationship * +CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup, + 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) + return &r; + } + return nullptr; +} + +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); + if (!r) { + Relationship nr; + nr.sourceId = sourceId; + nr.sourceIsGroup = sourceIsGroup; + nr.targetId = targetId; + nr.targetIsGroup = targetIsGroup; + m_relationships.push_back(nr); + addToIndex(m_relationships.size() - 1); + r = &m_relationships.back(); + } + r->stats[stat] = value; + autoSave(); +} + +float CharacterRegistry::getRelationshipStat(uint64_t sourceId, + bool sourceIsGroup, + uint64_t targetId, + bool targetIsGroup, + const std::string &stat) const +{ + const Relationship *r = findRelationship(sourceId, sourceIsGroup, + targetId, targetIsGroup); + if (!r) + return 0.0f; + auto it = r->stats.find(stat); + return it != r->stats.end() ? it->second : 0.0f; +} + +void CharacterRegistry::addRelationshipTag(uint64_t sourceId, + bool sourceIsGroup, + uint64_t targetId, + bool targetIsGroup, + const std::string &tag) +{ + Relationship *r = findRelationship(sourceId, sourceIsGroup, targetId, + targetIsGroup); + if (!r) { + Relationship nr; + nr.sourceId = sourceId; + nr.sourceIsGroup = sourceIsGroup; + nr.targetId = targetId; + nr.targetIsGroup = targetIsGroup; + m_relationships.push_back(nr); + addToIndex(m_relationships.size() - 1); + r = &m_relationships.back(); + } + r->tags.insert(tag); + autoSave(); +} + +void CharacterRegistry::removeRelationshipTag(uint64_t sourceId, + bool sourceIsGroup, + uint64_t targetId, + bool targetIsGroup, + const std::string &tag) +{ + Relationship *r = findRelationship(sourceId, sourceIsGroup, targetId, + targetIsGroup); + if (r) { + r->tags.erase(tag); + autoSave(); + } +} + +bool CharacterRegistry::hasRelationshipTag(uint64_t sourceId, + bool sourceIsGroup, + uint64_t targetId, + bool targetIsGroup, + const std::string &tag) const +{ + const Relationship *r = findRelationship(sourceId, sourceIsGroup, + targetId, targetIsGroup); + if (!r) + return false; + return r->tags.find(tag) != r->tags.end(); +} + +std::vector +CharacterRegistry::getRelationships(uint64_t id) const +{ + std::vector result; + auto srcRange = m_relBySource.equal_range(id); + for (auto it = srcRange.first; it != srcRange.second; ++it) + result.push_back(&m_relationships[it->second]); + auto tgtRange = m_relByTarget.equal_range(id); + for (auto it = tgtRange.first; it != tgtRange.second; ++it) + result.push_back(&m_relationships[it->second]); + return result; +} + +void CharacterRegistry::purgeRelationships(uint64_t id) +{ + std::vector toRemove; + auto srcRange = m_relBySource.equal_range(id); + for (auto it = srcRange.first; it != srcRange.second; ++it) + 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()) + toRemove.push_back(it->second); + + std::sort(toRemove.begin(), toRemove.end(), std::greater()); + for (size_t idx : toRemove) { + removeFromIndex(m_relationships[idx].sourceId, + m_relationships[idx].targetId, idx); + m_relationships.erase(m_relationships.begin() + idx); + } + rebuildIndexes(); +} + +/* ===================================================================== */ +/* Custom columns */ +/* ===================================================================== */ + +void CharacterRegistry::addCharacterColumn(const std::string &name, + ColumnDef::Type type) +{ + 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()); + autoSave(); +} + +void CharacterRegistry::addGroupColumn(const std::string &name, + ColumnDef::Type type) +{ + 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()); + autoSave(); +} + +/* ===================================================================== */ +/* Serialization */ +/* ===================================================================== */ + +nlohmann::json CharacterRegistry::serialize() const +{ + nlohmann::json j; + j["version"] = "1.0"; + j["nextId"] = m_nextId; + + 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"; + 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"; + j["groupColumns"].push_back(col); + } + + for (const auto &pair : m_characters) { + const CharacterRecord &c = pair.second; + nlohmann::json rec; + rec["id"] = c.id; + rec["firstName"] = c.firstName; + rec["lastName"] = c.lastName; + rec["prefabPath"] = c.prefabPath; + rec["className"] = c.className; + rec["level"] = c.level; + rec["currentXP"] = c.currentXP; + rec["availablePoints"] = c.availablePoints; + for (const auto &kv : c.stats) + rec["stats"][kv.first] = kv.second; + for (const auto &kv : c.skills) + rec["skills"][kv.first] = kv.second; + for (const auto &kv : c.needs) + rec["needs"][kv.first] = kv.second; + for (const auto &t : c.tags) + rec["tags"].push_back(t); + for (const auto &kv : c.intColumns) + rec["intColumns"][kv.first] = kv.second; + for (const auto &kv : c.floatColumns) + rec["floatColumns"][kv.first] = kv.second; + for (const auto &kv : c.stringColumns) + rec["stringColumns"][kv.first] = kv.second; + j["characters"].push_back(rec); + } + + for (const auto &pair : m_groups) { + const GroupRecord &g = pair.second; + nlohmann::json rec; + rec["id"] = g.id; + rec["name"] = g.name; + rec["memberIds"] = g.memberIds; + for (const auto &kv : g.intColumns) + rec["intColumns"][kv.first] = kv.second; + for (const auto &kv : g.floatColumns) + rec["floatColumns"][kv.first] = kv.second; + for (const auto &kv : g.stringColumns) + rec["stringColumns"][kv.first] = kv.second; + j["groups"].push_back(rec); + } + + for (const auto &r : m_relationships) { + nlohmann::json rec; + rec["sourceId"] = r.sourceId; + rec["sourceIsGroup"] = r.sourceIsGroup; + rec["targetId"] = r.targetId; + rec["targetIsGroup"] = r.targetIsGroup; + for (const auto &kv : r.stats) + rec["stats"][kv.first] = kv.second; + for (const auto &tag : r.tags) + rec["tags"].push_back(tag); + j["relationships"].push_back(rec); + } + + return j; +} + +void CharacterRegistry::deserialize(const nlohmann::json &j) +{ + m_characters.clear(); + m_groups.clear(); + m_relationships.clear(); + m_characterColumns.clear(); + m_groupColumns.clear(); + + m_nextId = j.value("nextId", 1); + + if (j.contains("characterColumns")) { + for (const auto &col : j["characterColumns"]) { + ColumnDef::Type t = ColumnDef::String; + std::string ts = col.value("type", "string"); + if (ts == "int") + t = ColumnDef::Int; + else if (ts == "float") + t = ColumnDef::Float; + m_characterColumns.push_back( + {t, col.value("name", "")}); + } + } + if (j.contains("groupColumns")) { + for (const auto &col : j["groupColumns"]) { + ColumnDef::Type t = ColumnDef::String; + std::string ts = col.value("type", "string"); + if (ts == "int") + t = ColumnDef::Int; + else if (ts == "float") + t = ColumnDef::Float; + m_groupColumns.push_back({t, col.value("name", "")}); + } + } + + if (j.contains("characters")) { + for (const auto &rec : j["characters"]) { + CharacterRecord c; + c.id = rec.value("id", 0); + c.firstName = rec.value("firstName", ""); + c.lastName = rec.value("lastName", ""); + c.prefabPath = rec.value("prefabPath", ""); + c.className = rec.value("className", ""); + c.level = rec.value("level", 1); + c.currentXP = rec.value("currentXP", 0); + c.availablePoints = rec.value("availablePoints", 0); + if (rec.contains("stats")) { + for (auto &[k, v] : rec["stats"].items()) + c.stats[k] = v.get(); + } + if (rec.contains("skills")) { + for (auto &[k, v] : rec["skills"].items()) + c.skills[k] = v.get(); + } + if (rec.contains("needs")) { + for (auto &[k, v] : rec["needs"].items()) + c.needs[k] = v.get(); + } + if (rec.contains("tags")) { + for (const auto &t : rec["tags"]) + c.tags.push_back(t.get()); + } + if (rec.contains("intColumns")) { + for (auto &[k, v] : rec["intColumns"].items()) + c.intColumns[k] = v.get(); + } + if (rec.contains("floatColumns")) { + for (auto &[k, v] : rec["floatColumns"].items()) + c.floatColumns[k] = v.get(); + } + if (rec.contains("stringColumns")) { + for (auto &[k, v] : rec["stringColumns"].items()) + c.stringColumns[k] = v.get(); + } + m_characters[c.id] = c; + } + } + + if (j.contains("groups")) { + for (const auto &rec : j["groups"]) { + GroupRecord g; + g.id = rec.value("id", 0); + g.name = rec.value("name", ""); + if (rec.contains("memberIds")) + for (const auto &m : rec["memberIds"]) + g.memberIds.push_back(m.get()); + if (rec.contains("intColumns")) { + for (auto &[k, v] : rec["intColumns"].items()) + g.intColumns[k] = v.get(); + } + if (rec.contains("floatColumns")) { + for (auto &[k, v] : rec["floatColumns"].items()) + g.floatColumns[k] = v.get(); + } + if (rec.contains("stringColumns")) { + for (auto &[k, v] : rec["stringColumns"].items()) + g.stringColumns[k] = v.get(); + } + m_groups[g.id] = g; + } + } + + if (j.contains("relationships")) { + for (const auto &rec : j["relationships"]) { + Relationship r; + r.sourceId = rec.value("sourceId", 0); + r.sourceIsGroup = rec.value("sourceIsGroup", false); + r.targetId = rec.value("targetId", 0); + r.targetIsGroup = rec.value("targetIsGroup", false); + if (rec.contains("stats")) { + for (auto &[k, v] : rec["stats"].items()) + r.stats[k] = v.get(); + } + if (rec.contains("tags")) { + for (const auto &tag : rec["tags"]) + r.tags.insert(tag.get()); + } + m_relationships.push_back(r); + } + } + + rebuildIndexes(); +} + +bool CharacterRegistry::saveToFile(const std::string &filepath) +{ + try { + std::ofstream file(filepath); + if (!file.is_open()) { + m_lastError = "Cannot open " + filepath; + return false; + } + file << serialize().dump(4); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Save error: ") + e.what(); + return false; + } +} + +bool CharacterRegistry::loadFromFile(const std::string &filepath) +{ + try { + std::ifstream file(filepath); + if (!file.is_open()) { + m_lastError = "Cannot open " + filepath; + return false; + } + nlohmann::json j; + file >> j; + deserialize(j); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Load error: ") + e.what(); + return false; + } +} +/* ===================================================================== */ +/* ImGui helpers */ +/* ===================================================================== */ + +static void labelBuf(char *buf, size_t bufSize, + const CharacterRegistry::CharacterRecord *c) +{ + if (!c) + snprintf(buf, bufSize, "(unknown)"); + else + snprintf(buf, bufSize, "%s %s (#%lu)", c->firstName.c_str(), + c->lastName.c_str(), (unsigned long)c->id); +} + +static void labelBuf(char *buf, size_t bufSize, + const CharacterRegistry::GroupRecord *g) +{ + if (!g) + snprintf(buf, bufSize, "(unknown)"); + else + snprintf(buf, bufSize, "%s (#%lu)", g->name.c_str(), + (unsigned long)g->id); +} + +static void drawColumnsEditor(const char *title, + std::vector &cols, + CharacterRegistry ®) +{ + if (ImGui::CollapsingHeader(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::SameLine(); + if (ImGui::SmallButton("Del")) { + cols.erase(cols.begin() + i--); + reg.autoSave(); + } + ImGui::PopID(); + } + + static char newColName[64] = ""; + static int newColType = 0; + ImGui::InputText("Name", newColName, sizeof(newColName)); + const char *types[] = {"int", "float", "string"}; + ImGui::Combo("Type", &newColType, types, 3); + if (ImGui::Button("Add Column") && strlen(newColName) > 0) { + CharacterRegistry::ColumnDef::Type t = + CharacterRegistry::ColumnDef::Int; + if (newColType == 1) + t = CharacterRegistry::ColumnDef::Float; + else if (newColType == 2) + t = CharacterRegistry::ColumnDef::String; + cols.push_back({t, newColName}); + newColName[0] = '\0'; + reg.autoSave(); + } + ImGui::Unindent(); + } +} + +/* ===================================================================== */ +/* ImGui editor */ +/* ===================================================================== */ + +void CharacterRegistry::drawEditor(bool *p_open) +{ + scanTemplates(); + + if (!ImGui::Begin("Character Registry", p_open, + ImGuiWindowFlags_MenuBar)) { + ImGui::End(); + return; + } + + /* Menu bar */ + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save Now")) { + autoSave(); + } + if (ImGui::MenuItem("Rescan Templates")) { + scanTemplates(); + } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + const char *tabs[] = {"Characters", "Groups", "Relationships", + "Columns"}; + if (ImGui::BeginTabBar("RegistryTabs")) { + for (int i = 0; i < 4; ++i) { + if (ImGui::BeginTabItem(tabs[i])) { + m_editorTab = i; + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } + + switch (m_editorTab) { + case 0: + /* ---------- Characters ---------- */ + if (ImGui::Button("Add Character")) { + uint64_t id = createCharacter("New", "Character"); + m_selectedCharacterId = id; + } + ImGui::SameLine(); + if (ImGui::Button("Delete") && m_selectedCharacterId != 0) { + deleteCharacter(m_selectedCharacterId); + m_selectedCharacterId = 0; + } + 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); + ImGui::TableSetupColumn("First Name"); + ImGui::TableSetupColumn("Last Name"); + ImGui::TableSetupColumn("Age", + ImGuiTableColumnFlags_WidthFixed, + 50); + ImGui::TableSetupColumn("Sex", + ImGuiTableColumnFlags_WidthFixed, + 50); + ImGui::TableSetupColumn("Class"); + ImGui::TableSetupColumn("Prefab"); + ImGui::TableSetupColumn("Actions", + ImGuiTableColumnFlags_WidthFixed, + 120); + for (const auto &c : m_characterColumns) + ImGui::TableSetupColumn(c.name.c_str()); + ImGui::TableHeadersRow(); + + for (auto &pair : m_characters) { + CharacterRecord &c = pair.second; + ImGui::TableNextRow(); + bool selected = (m_selectedCharacterId == c.id); + ImGui::TableSetColumnIndex(0); + if (ImGui::Selectable( + Ogre::StringConverter::toString(c.id).c_str(), + selected)) + m_selectedCharacterId = c.id; + + ImGui::TableSetColumnIndex(1); + ImGui::Text("%s", c.firstName.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%s", c.lastName.c_str()); + + std::string age = "?", sex = "?"; + int outfit = 2; + readPrefabSlots(c.prefabPath, age, sex, outfit); + ImGui::TableSetColumnIndex(3); + ImGui::Text("%s", age.c_str()); + ImGui::TableSetColumnIndex(4); + ImGui::Text("%s", sex.c_str()); + + ImGui::TableSetColumnIndex(5); + ImGui::Text("%s", c.className.c_str()); + + ImGui::TableSetColumnIndex(6); + ImGui::Text("%s", c.prefabPath.c_str()); + + ImGui::TableSetColumnIndex(7); + ImGui::PushID(static_cast(c.id * 10 + 7)); + bool spawned = findSpawnedEntity(c.id).is_alive(); + if (spawned) { + if (ImGui::SmallButton("Despawn")) + despawnCharacter(c.id); + } else { + if (ImGui::SmallButton("Spawn")) + spawnCharacter(c.id); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Save")) { + if (!savePrefabForCharacter(c.id)) + Ogre::LogManager::getSingleton().logMessage( + "CharacterRegistry: save prefab failed: " + + m_lastError); + } + ImGui::PopID(); + + int colIdx = 8; + for (const auto &col : m_characterColumns) { + ImGui::TableSetColumnIndex(colIdx++); + switch (col.type) { + case ColumnDef::Int: { + auto it = c.intColumns.find(col.name); + if (it != c.intColumns.end()) + ImGui::Text("%lld", + (long long)it->second); + break; + } + case ColumnDef::Float: { + auto it = c.floatColumns.find(col.name); + if (it != c.floatColumns.end()) + ImGui::Text("%.3f", it->second); + break; + } + case ColumnDef::String: { + auto it = c.stringColumns.find(col.name); + if (it != c.stringColumns.end()) + ImGui::Text("%s", + it->second.c_str()); + break; + } + } + } + } + ImGui::EndTable(); + } + + /* Selected character detail */ + if (m_selectedCharacterId != 0) { + CharacterRecord *c = findCharacter(m_selectedCharacterId); + if (c) { + static char fnBuf[128] = ""; + static char lnBuf[128] = ""; + static uint64_t lastId = 0; + if (lastId != c->id) { + snprintf(fnBuf, sizeof(fnBuf), "%s", + c->firstName.c_str()); + snprintf(lnBuf, sizeof(lnBuf), "%s", + c->lastName.c_str()); + lastId = c->id; + } + ImGui::Separator(); + ImGui::Text("Character #%lu", + (unsigned long)c->id); + if (ImGui::InputText("First Name", fnBuf, + sizeof(fnBuf))) { + c->firstName = fnBuf; + autoSave(); + } + if (ImGui::InputText("Last Name", lnBuf, + sizeof(lnBuf))) { + c->lastName = lnBuf; + autoSave(); + } + + ImGui::Text("Prefab: %s", c->prefabPath.c_str()); + bool exists = + std::filesystem::exists(c->prefabPath); + ImGui::Text("Status: %s", + 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())) + tmplIdx = 0; + char comboLabel[256]; + 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) { + 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); + } + ImGui::EndCombo(); + } + if (ImGui::Button("Create from Template")) { + copyPrefab(m_templates[tmplIdx], + c->prefabPath); + } + } + + /* ---- RPG Data ---- */ + ImGui::Separator(); + ImGui::Text("RPG Data"); + + /* Class selector */ + 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); + 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)) { + c->className.clear(); + autoSave(); + } + for (size_t i = 0; i < classNames.size(); ++i) { + if (ImGui::Selectable( + classNames[i].c_str(), + clsIdx == + static_cast(i))) { + c->className = classNames[i].c_str(); + autoSave(); + } + } + ImGui::EndCombo(); + } + } + + int lvl = c->level; + if (ImGui::InputInt("Level", &lvl)) { + c->level = lvl; + autoSave(); + } + long long xp = c->currentXP; + if (ImGui::InputScalar("Current XP", ImGuiDataType_S64, + &xp, nullptr, nullptr, + "%lld")) { + c->currentXP = xp; + autoSave(); + } + int pts = c->availablePoints; + if (ImGui::InputInt("Available Points", &pts)) { + c->availablePoints = pts; + autoSave(); + } + + /* Stats */ + if (ImGui::TreeNode("Stats")) { + for (const auto &name : db.getStatNames()) { + int val = 0; + 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; + autoSave(); + } + } + ImGui::TreePop(); + } + + /* Skills */ + if (ImGui::TreeNode("Skills")) { + for (const auto &name : db.getSkillNames()) { + int val = 0; + 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; + autoSave(); + } + } + ImGui::TreePop(); + } + + /* Needs */ + if (ImGui::TreeNode("Needs")) { + for (const auto &name : db.getNeedNames()) { + int val = 0; + 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; + autoSave(); + } + } + ImGui::TreePop(); + } + + /* Tags */ + ImGui::Separator(); + ImGui::Text("Tags"); + for (size_t i = 0; i < c->tags.size();) { + ImGui::PushID(static_cast(i)); + ImGui::Text("%s", c->tags[i].c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) { + c->tags.erase(c->tags.begin() + i); + autoSave(); + } else { + ++i; + } + ImGui::PopID(); + } + static char tagBuf[64] = ""; + ImGui::InputText("New tag", tagBuf, sizeof(tagBuf)); + ImGui::SameLine(); + if (ImGui::SmallButton("Add") && tagBuf[0] != '\0') { + c->tags.push_back(tagBuf); + tagBuf[0] = '\0'; + autoSave(); + } + } + } + break; + + case 1: + /* ---------- Groups ---------- */ + if (ImGui::Button("Add Group")) { + uint64_t id = createGroup("New Group"); + m_selectedGroupId = id; + } + ImGui::SameLine(); + if (ImGui::Button("Delete") && m_selectedGroupId != 0) { + deleteGroup(m_selectedGroupId); + m_selectedGroupId = 0; + } + ImGui::Separator(); + + if (ImGui::BeginTable("GroupsTable", 3 + m_groupColumns.size(), + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY, + ImVec2(0, 200))) { + ImGui::TableSetupColumn("ID", + ImGuiTableColumnFlags_WidthFixed, + 40); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Members"); + for (const auto &c : m_groupColumns) + ImGui::TableSetupColumn(c.name.c_str()); + ImGui::TableHeadersRow(); + + for (auto &pair : m_groups) { + GroupRecord &g = pair.second; + ImGui::TableNextRow(); + bool selected = (m_selectedGroupId == g.id); + ImGui::TableSetColumnIndex(0); + if (ImGui::Selectable( + Ogre::StringConverter::toString(g.id).c_str(), + selected, + ImGuiSelectableFlags_SpanAllColumns)) + m_selectedGroupId = g.id; + + ImGui::TableSetColumnIndex(1); + ImGui::Text("%s", g.name.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%zu", g.memberIds.size()); + + int colIdx = 3; + for (const auto &col : m_groupColumns) { + ImGui::TableSetColumnIndex(colIdx++); + switch (col.type) { + case ColumnDef::Int: { + auto it = g.intColumns.find(col.name); + if (it != g.intColumns.end()) + ImGui::Text("%lld", + (long long)it->second); + break; + } + case ColumnDef::Float: { + auto it = g.floatColumns.find(col.name); + if (it != g.floatColumns.end()) + ImGui::Text("%.3f", it->second); + break; + } + case ColumnDef::String: { + auto it = g.stringColumns.find(col.name); + if (it != g.stringColumns.end()) + ImGui::Text("%s", + it->second.c_str()); + break; + } + } + } + } + ImGui::EndTable(); + } + + if (m_selectedGroupId != 0) { + GroupRecord *g = findGroup(m_selectedGroupId); + if (g) { + static char nameBuf[128] = ""; + static uint64_t lastGId = 0; + if (lastGId != g->id) { + snprintf(nameBuf, sizeof(nameBuf), "%s", + g->name.c_str()); + lastGId = g->id; + } + ImGui::Separator(); + ImGui::Text("Group #%lu", + (unsigned long)g->id); + if (ImGui::InputText("Name", nameBuf, + sizeof(nameBuf))) { + g->name = nameBuf; + autoSave(); + } + + ImGui::Text("Members:"); + ImGui::Indent(); + for (size_t i = 0; i < g->memberIds.size(); ++i) { + uint64_t mid = g->memberIds[i]; + const CharacterRecord *mc = findCharacter(mid); + char lbl[256]; + labelBuf(lbl, sizeof(lbl), mc); + ImGui::PushID(static_cast(mid)); + ImGui::Text("%s", lbl); + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + removeFromGroup(g->id, mid); + } + ImGui::PopID(); + } + ImGui::Unindent(); + + if (!m_characters.empty()) { + 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(); + if (!inGroup) { + char lbl[256]; + labelBuf(lbl, sizeof(lbl), + &c); + if (ImGui::Selectable( + lbl)) + addToGroup(g->id, + c.id); + } + } + ImGui::EndCombo(); + } + } + } + } + break; + + case 2: + /* ---------- Relationships ---------- */ + ImGui::Text("Source:"); + ImGui::RadioButton("Character", &m_relSourceIsGroup, 0); + ImGui::SameLine(); + ImGui::RadioButton("Group", &m_relSourceIsGroup, 1); + + { + char srcLbl[256] = "(none)"; + if (m_relSourceId != 0) { + if (m_relSourceIsGroup) + labelBuf(srcLbl, sizeof(srcLbl), + findGroup(m_relSourceId)); + else + labelBuf(srcLbl, sizeof(srcLbl), + findCharacter(m_relSourceId)); + } + if (ImGui::BeginCombo("Select Source", srcLbl)) { + if (!m_relSourceIsGroup) { + for (const auto &pair : m_characters) { + char lbl[256]; + labelBuf(lbl, sizeof(lbl), + &pair.second); + if (ImGui::Selectable(lbl)) + m_relSourceId = pair.second.id; + } + } else { + for (const auto &pair : m_groups) { + char lbl[256]; + labelBuf(lbl, sizeof(lbl), + &pair.second); + if (ImGui::Selectable(lbl)) + m_relSourceId = pair.second.id; + } + } + ImGui::EndCombo(); + } + } + + if (m_relSourceId != 0) { + ImGui::Separator(); + ImGui::Text("Relationships:"); + + auto rels = getRelationships(m_relSourceId); + for (const Relationship *r : rels) { + bool isOutgoing = + (r->sourceId == m_relSourceId && + r->sourceIsGroup == + (m_relSourceIsGroup != 0)); + uint64_t otherId = isOutgoing ? r->targetId : + r->sourceId; + bool otherIsGroup = isOutgoing ? + r->targetIsGroup : + r->sourceIsGroup; + char otherLbl[256]; + if (otherIsGroup) + labelBuf(otherLbl, sizeof(otherLbl), + findGroup(otherId)); + else + labelBuf(otherLbl, sizeof(otherLbl), + findCharacter(otherId)); + + ImGui::PushID(static_cast(r->sourceId * + 1000000 + + r->targetId)); + ImGui::Text("%s %s %s", + isOutgoing ? "→" : "←", + otherLbl, + otherIsGroup ? "[group]" : + ""); + + for (auto &kv : r->stats) { + float v = kv.second; + if (ImGui::SliderFloat(kv.first.c_str(), + &v, -1.0f, + 1.0f)) { + setRelationshipStat( + r->sourceId, + r->sourceIsGroup, + r->targetId, + r->targetIsGroup, + kv.first, v); + } + } + + ImGui::Text("Tags:"); + ImGui::SameLine(); + for (const auto &tag : r->tags) { + ImGui::Text("%s", tag.c_str()); + ImGui::SameLine(); + } + ImGui::NewLine(); + + static char newTag[64] = ""; + ImGui::InputText("Tag", newTag, sizeof(newTag)); + ImGui::SameLine(); + if (ImGui::SmallButton("Add Tag") && + strlen(newTag) > 0) { + addRelationshipTag( + r->sourceId, r->sourceIsGroup, + r->targetId, r->targetIsGroup, + newTag); + if (strcmp(newTag, "wife") == 0 || + strcmp(newTag, "husband") == 0) { + addRelationshipTag( + r->targetId, + r->targetIsGroup, + r->sourceId, + r->sourceIsGroup, + strcmp(newTag, "wife") == 0 ? + "husband" : + "wife"); + } else if (strcmp(newTag, "sibling") == 0) { + addRelationshipTag( + r->targetId, + r->targetIsGroup, + r->sourceId, + r->sourceIsGroup, + "sibling"); + } else if (strcmp(newTag, "parent") == 0) { + addRelationshipTag( + r->targetId, + r->targetIsGroup, + r->sourceId, + r->sourceIsGroup, + "child"); + } else if (strcmp(newTag, "child") == 0) { + addRelationshipTag( + r->targetId, + r->targetIsGroup, + r->sourceId, + r->sourceIsGroup, + "parent"); + } + newTag[0] = '\0'; + } + + if (ImGui::SmallButton("Delete Relationship")) { + std::vector newRels; + 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); + } + m_relationships = std::move(newRels); + rebuildIndexes(); + } + ImGui::PopID(); + ImGui::Separator(); + } + + /* Add new relationship */ + ImGui::Text("Add new relationship:"); + static int newTargetIsGroup = 0; + static uint64_t newTargetId = 0; + ImGui::RadioButton("To Character", &newTargetIsGroup, 0); + ImGui::SameLine(); + ImGui::RadioButton("To Group", &newTargetIsGroup, 1); + + { + char tgtLbl[256] = "(select)"; + if (newTargetId != 0) { + if (newTargetIsGroup) + labelBuf(tgtLbl, sizeof(tgtLbl), + findGroup(newTargetId)); + else + labelBuf(tgtLbl, sizeof(tgtLbl), + findCharacter(newTargetId)); + } + if (ImGui::BeginCombo("Target", tgtLbl)) { + if (!newTargetIsGroup) { + for (const auto &pair : m_characters) { + if (pair.second.id == m_relSourceId) + continue; + char lbl[256]; + labelBuf(lbl, sizeof(lbl), + &pair.second); + if (ImGui::Selectable(lbl)) + newTargetId = pair.second.id; + } + } else { + for (const auto &pair : m_groups) { + char lbl[256]; + labelBuf(lbl, sizeof(lbl), + &pair.second); + if (ImGui::Selectable(lbl)) + newTargetId = pair.second.id; + } + } + ImGui::EndCombo(); + } + } + + if (newTargetId != 0 && + ImGui::Button("Create Relationship")) { + setRelationshipStat(m_relSourceId, + m_relSourceIsGroup, + newTargetId, + newTargetIsGroup, + "friendship", 0.0f); + newTargetId = 0; + } + } + break; + + case 3: + /* ---------- Columns ---------- */ + drawColumnsEditor("Character Columns", m_characterColumns, *this); + ImGui::Separator(); + drawColumnsEditor("Group Columns", m_groupColumns, *this); + break; + } + + ImGui::End(); +} diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp new file mode 100644 index 0000000..ddf791d --- /dev/null +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -0,0 +1,273 @@ +#ifndef EDITSCENE_CHARACTERREGISTRY_HPP +#define EDITSCENE_CHARACTERREGISTRY_HPP +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class EditorUISystem; + +/** + * Global character and social-group registry. + * + * Characters are referenced by a persistent uint64_t ID. Each character + * stores a path to its appearance prefab (CharacterSlotsComponent data). + * + * The registry auto-saves to a fixed file after every mutation and loads + * on initialization. Character prefabs are auto-named from the ID and + * name, created from templates, and deleted along with the character. + */ +class CharacterRegistry { +public: + /* ------------------------------------------------------------------ */ + /* Column schema */ + /* ------------------------------------------------------------------ */ + struct ColumnDef { + enum Type { Int, Float, String }; + Type type; + std::string name; + }; + + /* ------------------------------------------------------------------ */ + /* Records */ + /* ------------------------------------------------------------------ */ + struct CharacterRecord { + uint64_t id = 0; + std::string firstName; + std::string lastName; + std::string prefabPath; /* relative path to prefab JSON */ + + /* RPG data (supersedes per-entity CharacterClassComponent) */ + std::string className; + int level = 1; + int64_t currentXP = 0; + int availablePoints = 0; + std::unordered_map stats; + std::unordered_map skills; + std::unordered_map needs; + + /* Tags */ + std::vector tags; + + /* Extensible custom columns */ + std::unordered_map intColumns; + std::unordered_map floatColumns; + std::unordered_map stringColumns; + }; + + struct GroupRecord { + uint64_t id = 0; + std::string name; + std::vector memberIds; + + /* Extensible custom columns */ + std::unordered_map intColumns; + std::unordered_map floatColumns; + std::unordered_map stringColumns; + }; + + struct Relationship { + uint64_t sourceId = 0; + uint64_t targetId = 0; + bool sourceIsGroup = false; + bool targetIsGroup = false; + + /* Numeric stats: friendship, love, hate, loyalty, fear … */ + std::unordered_map stats; + + /* Tags: wife, sibling, parent, enemy, ally, mentor … */ + std::unordered_set tags; + }; + + /* ------------------------------------------------------------------ */ + /* Life-cycle */ + /* ------------------------------------------------------------------ */ + CharacterRegistry(); + ~CharacterRegistry() = default; + + /* non-copyable */ + CharacterRegistry(const CharacterRegistry &) = delete; + CharacterRegistry &operator=(const CharacterRegistry &) = delete; + + void setWorld(flecs::world *world) { m_world = world; } + void setSceneManager(Ogre::SceneManager *sceneMgr) + { + m_sceneMgr = sceneMgr; + } + void setEditorUISystem(EditorUISystem *ui) { m_uiSystem = ui; } + + /** + * Load registry from auto-save file. Call after setWorld/setSceneManager. + */ + void initialize(); + + /* ------------------------------------------------------------------ */ + /* Characters */ + /* ------------------------------------------------------------------ */ + uint64_t createCharacter(const std::string &firstName, + const std::string &lastName, + const std::string &templatePath = ""); + void deleteCharacter(uint64_t id); + CharacterRecord *findCharacter(uint64_t id); + const CharacterRecord *findCharacter(uint64_t id) const; + const std::unordered_map &getCharacters() const + { + return m_characters; + } + + /* ------------------------------------------------------------------ */ + /* Groups / Factions */ + /* ------------------------------------------------------------------ */ + uint64_t createGroup(const std::string &name); + void deleteGroup(uint64_t id); + void addToGroup(uint64_t groupId, uint64_t characterId); + void removeFromGroup(uint64_t groupId, uint64_t characterId); + GroupRecord *findGroup(uint64_t id); + const GroupRecord *findGroup(uint64_t id) const; + const std::unordered_map &getGroups() const + { + return m_groups; + } + + /* ------------------------------------------------------------------ */ + /* Relationships */ + /* ------------------------------------------------------------------ */ + Relationship *findRelationship(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup); + const Relationship *findRelationship(uint64_t sourceId, + bool sourceIsGroup, + uint64_t targetId, + bool targetIsGroup) const; + + void setRelationshipStat(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup, + const std::string &stat, float value); + float getRelationshipStat(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup, + const std::string &stat) const; + + void addRelationshipTag(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup, + const std::string &tag); + void removeRelationshipTag(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup, + const std::string &tag); + bool hasRelationshipTag(uint64_t sourceId, bool sourceIsGroup, + uint64_t targetId, bool targetIsGroup, + const std::string &tag) const; + + std::vector getRelationships(uint64_t id) const; + void purgeRelationships(uint64_t id); + + /* ------------------------------------------------------------------ */ + /* Custom columns */ + /* ------------------------------------------------------------------ */ + void addCharacterColumn(const std::string &name, ColumnDef::Type type); + void removeCharacterColumn(const std::string &name); + const std::vector &getCharacterColumns() const + { + return m_characterColumns; + } + + void addGroupColumn(const std::string &name, ColumnDef::Type type); + void removeGroupColumn(const std::string &name); + const std::vector &getGroupColumns() const + { + return m_groupColumns; + } + + /* ------------------------------------------------------------------ */ + /* Prefab helpers (static) */ + /* ------------------------------------------------------------------ */ + static bool isValidCharacterPrefab(const std::string &filepath); + static bool readPrefabSlots(const std::string &filepath, + std::string &age, std::string &sex, + 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); + static bool deletePrefabFile(const std::string &filepath); + + /* ------------------------------------------------------------------ */ + /* Templates */ + /* ------------------------------------------------------------------ */ + void scanTemplates(); + const std::vector &getTemplates() const + { + return m_templates; + } + + /* ------------------------------------------------------------------ */ + /* Spawn / Save */ + /* ------------------------------------------------------------------ */ + flecs::entity findSpawnedEntity(uint64_t id) const; + bool despawnCharacter(uint64_t id); + flecs::entity spawnCharacter(uint64_t id); + bool savePrefabForCharacter(uint64_t id); + + /* ------------------------------------------------------------------ */ + /* Persistence */ + /* ------------------------------------------------------------------ */ + nlohmann::json serialize() const; + void deserialize(const nlohmann::json &j); + + bool saveToFile(const std::string &filepath); + bool loadFromFile(const std::string &filepath); + const std::string &getLastError() const { return m_lastError; } + + /* ------------------------------------------------------------------ */ + /* ImGui editor */ + /* ------------------------------------------------------------------ */ + void drawEditor(bool *p_open = nullptr); + + /* Auto-save to fixed path (called after mutations) */ + void autoSave(); + +private: + uint64_t m_nextId = 1; + + std::unordered_map m_characters; + std::unordered_map m_groups; + std::vector m_relationships; + + std::vector m_characterColumns; + std::vector m_groupColumns; + + /* Fast relationship indexes (value = index into m_relationships) */ + std::unordered_multimap m_relBySource; + std::unordered_multimap m_relByTarget; + + mutable std::string m_lastError; + std::string m_autoSavePath; + std::vector m_templates; + + flecs::world *m_world = nullptr; + Ogre::SceneManager *m_sceneMgr = nullptr; + EditorUISystem *m_uiSystem = nullptr; + + void rebuildIndexes(); + void addToIndex(size_t relIndex); + void removeFromIndex(uint64_t sourceId, uint64_t targetId, + size_t relIndex); + + std::string generatePrefabPath(uint64_t id, const std::string &firstName, + const std::string &lastName) const; + + /* UI state */ + int m_editorTab = 0; + uint64_t m_selectedCharacterId = 0; + uint64_t m_selectedGroupId = 0; + uint64_t m_relSourceId = 0; + int m_relSourceIsGroup = 0; +}; + +#endif // EDITSCENE_CHARACTERREGISTRY_HPP diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index c8c3f8d..ca8779d 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -226,82 +226,137 @@ 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) +{ + if (!CharacterSlotSystem::isCatalogLoaded()) + return {}; + const nlohmann::json &cat = CharacterSlotSystem::getCatalog(); + if (!cat.contains(age) || !cat[age].contains(sex) || + !cat[age][sex].contains(slot)) + return {}; + for (const auto &entry : cat[age][sex][slot]) { + if (entry.value("mesh", "") == mesh) { + std::vector result; + for (const auto &g : + entry.value("garments", nlohmann::json::array())) + result.push_back(g.get()); + return result; + } + } + return {}; +} + 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()) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> explicit: " + sel.explicitMesh); + if (!sel.explicitMesh.empty()) return sel.explicitMesh; - } if (!s_catalogLoaded || !s_bodyParts.contains(age) || !s_bodyParts[age].contains(sex) || - !s_bodyParts[age][sex].contains(slot)) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> catalog miss"); + !s_bodyParts[age][sex].contains(slot)) return ""; - } const auto &slotEntries = s_bodyParts[age][sex][slot]; /* outfitLevel: 0=nude, 1=lingerie, 2=clothed */ if (outfitLevel >= 2 && sel.layer2Mesh != "none" && !sel.layer2Mesh.empty()) { - for (const auto &entry : slotEntries) { - if (entry.value("mesh", "") == sel.layer2Mesh) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> layer2: " + sel.layer2Mesh); - return sel.layer2Mesh; + /* 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); + std::set required; + for (const auto &g : l1g) + required.insert(g); + for (const auto &g : l2g) + required.insert(g); + if (!required.empty()) { + Ogre::String combinedMesh; + for (const auto &entry : slotEntries) { + auto entryGarments = + entry.value("garments", + nlohmann::json::array()); + std::set eg; + for (const auto &g : entryGarments) + eg.insert(g.get()); + bool containsAll = true; + for (const auto &g : required) { + if (eg.find(g) == eg.end()) { + containsAll = false; + break; + } + } + if (containsAll) { + Ogre::String m = + entry["mesh"].get(); + /* Prefer exact layer2 match if it already + * satisfies the requirement */ + if (m == sel.layer2Mesh) + return m; + if (combinedMesh.empty()) + combinedMesh = m; + } + } + if (!combinedMesh.empty()) + return combinedMesh; } } + for (const auto &entry : slotEntries) { + if (entry.value("mesh", "") == sel.layer2Mesh) + return sel.layer2Mesh; + } } if (outfitLevel >= 1 && sel.layer1Mesh != "none" && !sel.layer1Mesh.empty()) { for (const auto &entry : slotEntries) { - if (entry.value("mesh", "") == sel.layer1Mesh) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> layer1: " + sel.layer1Mesh); + if (entry.value("mesh", "") == sel.layer1Mesh) return sel.layer1Mesh; - } } } - /* Fallback to layer 0 (nude base) — prefer shortest name - * (base mesh name is shortest, combined meshes add suffixes) */ + /* Fallback to layer 0 (nude base) — prefer shortest name. + * Base mesh names are shortest; combined meshes add suffixes. + * We also penalise Blender duplicate names (.001, .002). */ Ogre::String bestLayer0; + size_t bestLen = 0; for (const auto &entry : slotEntries) { if (entry.value("layer", 0) == 0) { Ogre::String mesh = entry["mesh"].get(); - if (bestLayer0.empty() || mesh.length() < bestLayer0.length()) + size_t effectiveLen = mesh.length(); + 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) { + if (!isdigit(static_cast( + mesh[i]))) { + isDup = false; + break; + } + } + if (isDup && mesh[dotMesh - 4] == '.') + effectiveLen += 1000; + } + if (bestLayer0.empty() || + effectiveLen < bestLen) { bestLayer0 = mesh; + bestLen = effectiveLen; + } } } - if (!bestLayer0.empty()) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> layer0base: " + bestLayer0); + if (!bestLayer0.empty()) return bestLayer0; - } /* Last resort: first available entry */ - if (!slotEntries.empty()) { - Ogre::String mesh = slotEntries[0]["mesh"].get(); - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> lastResort: " + mesh); - return mesh; - } + if (!slotEntries.empty()) + return slotEntries[0]["mesh"].get(); - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] resolveMesh(" + age + "," + sex + - "," + slot + ") -> empty fallback"); return ""; } @@ -310,27 +365,13 @@ void CharacterSlotSystem::update() if (!m_initialized) return; - int total = 0; - int dirtyCount = 0; m_world.query().each( [&](flecs::entity e, CharacterSlotsComponent &cs) { - total++; if (cs.dirty) { - dirtyCount++; - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] update: entity " + - Ogre::StringConverter::toString(e.id()) + - " is dirty, rebuilding"); buildCharacter(e, cs); cs.dirty = false; } }); - if (dirtyCount > 0) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] update: total=" + - Ogre::StringConverter::toString(total) + " dirty=" + - Ogre::StringConverter::toString(dirtyCount)); - } } static const nlohmann::json *findCatalogEntry( @@ -377,13 +418,6 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName) void CharacterSlotSystem::buildCharacter(flecs::entity e, CharacterSlotsComponent &cs) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: age=" + cs.age + - " sex=" + cs.sex + " outfitLevel=" + - Ogre::StringConverter::toString(cs.outfitLevel) + - " slots=" + Ogre::StringConverter::toString( - (size_t)cs.slotSelections.size())); - destroyCharacterParts(e); /* Migrate old slots map to slotSelections if needed */ @@ -404,18 +438,12 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, } } - if (!e.has()) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: no TransformComponent"); + if (!e.has()) return; - } auto &transform = e.get_mut(); - if (!transform.node) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: no transform node"); + if (!transform.node) return; - } /* Determine master slot (face preferred, else first non-empty) */ Ogre::String masterSlot; @@ -438,27 +466,15 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, } } - if (masterSlot.empty()) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: masterSlot EMPTY — " - "no valid mesh for any slot"); + if (masterSlot.empty()) return; - } Ogre::String masterMesh = resolveMesh(cs.age, cs.sex, masterSlot, cs.slotSelections[masterSlot], cs.outfitLevel); - if (masterMesh.empty()) { - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: masterMesh empty for " + - masterSlot); + if (masterMesh.empty()) return; - } - - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: masterSlot=" + masterSlot + - " masterMesh=" + masterMesh); /* Pre-create pose animation on mesh so entity knows about it */ ensureMeshPoseAnimation(masterMesh); @@ -472,12 +488,6 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, m_entities[e.id()].parts[masterSlot] = masterEnt; cs.masterEntity = masterEnt; - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: created master entity " + - masterMesh + " (submeshes=" + - Ogre::StringConverter::toString(meshPtr->getNumSubMeshes()) + - ")"); - /* Setup pose animation for shape keys */ const nlohmann::json *entry = findCatalogEntry( cs.age, cs.sex, masterSlot, masterMesh); @@ -524,12 +534,6 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, "': " + ex.getDescription()); } } - - Ogre::LogManager::getSingleton().logMessage( - "[CharacterSlotSystem] buildCharacter: DONE for entity " + - Ogre::StringConverter::toString(e.id()) + - " parts=" + Ogre::StringConverter::toString( - (size_t)m_entities[e.id()].parts.size())); } void CharacterSlotSystem::applyShapeKeys(flecs::entity e, diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 80f2dea..4c40da6 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -25,6 +25,7 @@ #include "../components/TriangleBuffer.hpp" #include "../components/LodSettings.hpp" #include "../components/CharacterSlots.hpp" +#include "../components/CharacterIdentity.hpp" #include "../components/Character.hpp" #include "../components/AnimationTree.hpp" #include "../components/AnimationTreeTemplate.hpp" @@ -52,6 +53,7 @@ #include "../ui/PhysicsColliderEditor.hpp" #include "../ui/RigidBodyEditor.hpp" #include "../ui/ComponentRegistration.hpp" +#include "../ui/CharacterIdentityEditor.hpp" #include "../ui/PrefabInstanceEditor.hpp" #include "PhysicsSystem.hpp" #include "BuoyancySystem.hpp" @@ -77,6 +79,11 @@ EditorUISystem::EditorUISystem(flecs::world &world, m_gizmo = std::make_unique(m_sceneMgr); m_cursor3D = std::make_unique(m_sceneMgr); m_serializer = std::make_unique(m_world, m_sceneMgr); + + m_characterRegistry.setWorld(&m_world); + m_characterRegistry.setSceneManager(m_sceneMgr); + m_characterRegistry.setEditorUISystem(this); + m_characterRegistry.initialize(); } EditorUISystem::~EditorUISystem() = default; @@ -288,6 +295,20 @@ void EditorUISystem::registerComponentEditors() } }); + // Register CharacterIdentity component + auto characterIdentityEditor = std::make_unique(); + m_componentRegistry.registerComponent( + "Character Identity", "Character", std::move(characterIdentityEditor), + [](flecs::entity e) { + if (!e.has()) + e.set( + CharacterIdentityComponent{}); + }, + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); + // Register modular components (Light, Camera, etc.) registerModularComponents(); } @@ -347,6 +368,11 @@ void EditorUISystem::update(float deltaTime) &m_showCharacterClassDatabase); } + // Render Character registry window + if (m_showCharacterRegistry) { + m_characterRegistry.drawEditor(&m_showCharacterRegistry); + } + // Render FPS overlay renderFPSOverlay(deltaTime); } @@ -444,6 +470,10 @@ void EditorUISystem::renderHierarchyWindow() "Character Class Database")) { m_showCharacterClassDatabase = true; } + if (ImGui::MenuItem( + "Character Registry")) { + m_showCharacterRegistry = true; + } ImGui::EndMenu(); } @@ -964,6 +994,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } + // Render CharacterIdentity if present + if (entity.has()) { + auto &ci = entity.get_mut(); + m_componentRegistry.render(entity, ci); + componentCount++; + } + // Render AnimationTree if present if (entity.has()) { auto &at = entity.get_mut(); diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index a301fe4..6f9a719 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -13,6 +13,7 @@ #include "../gizmo/Gizmo.hpp" #include "../gizmo/Cursor3D.hpp" #include "SceneSerializer.hpp" +#include "CharacterRegistry.hpp" // Forward declarations class EditorPhysicsSystem; @@ -311,6 +312,10 @@ private: bool m_showCharacterClassDatabase = false; CharacterClassDatabaseEditor m_characterClassDatabaseEditor; + // Character registry + bool m_showCharacterRegistry = false; + CharacterRegistry m_characterRegistry; + // Queries flecs::query m_nameQuery; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 5d6cb31..b380047 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -17,6 +17,7 @@ #include "../components/TriangleBuffer.hpp" #include "../components/Character.hpp" #include "../components/CharacterSlots.hpp" +#include "../components/CharacterIdentity.hpp" #include "../components/AnimationTree.hpp" #include "../components/AnimationTreeTemplate.hpp" #include "../components/StartupMenu.hpp" @@ -238,6 +239,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["characterSlots"] = serializeCharacterSlots(entity); } + if (entity.has()) { + json["characterIdentity"] = serializeCharacterIdentity(entity); + } + if (entity.has()) { json["animationTree"] = serializeAnimationTree(entity); } @@ -452,6 +457,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, deserializeCharacterSlots(entity, json["characterSlots"]); } + if (json.contains("characterIdentity")) { + deserializeCharacterIdentity(entity, json["characterIdentity"]); + } + if (json.contains("animationTree")) { deserializeAnimationTree(entity, json["animationTree"]); } @@ -672,6 +681,10 @@ void SceneSerializer::deserializeEntityComponents( deserializeCharacterSlots(entity, json["characterSlots"]); } + if (json.contains("characterIdentity")) { + deserializeCharacterIdentity(entity, json["characterIdentity"]); + } + if (json.contains("animationTree")) { deserializeAnimationTree(entity, json["animationTree"]); } @@ -2137,6 +2150,22 @@ void SceneSerializer::deserializeCharacter(flecs::entity entity, entity.set(cc); } +nlohmann::json SceneSerializer::serializeCharacterIdentity(flecs::entity entity) +{ + auto &ci = entity.get(); + nlohmann::json json; + json["registryId"] = ci.registryId; + return json; +} + +void SceneSerializer::deserializeCharacterIdentity(flecs::entity entity, + const nlohmann::json &json) +{ + CharacterIdentityComponent ci; + ci.registryId = json.value("registryId", 0); + entity.set(ci); +} + nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) { auto &cs = entity.get(); diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 42c1895..e92cca5 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -105,6 +105,7 @@ private: nlohmann::json serializePrimitive(flecs::entity entity); nlohmann::json serializeTriangleBuffer(flecs::entity entity); nlohmann::json serializeCharacter(flecs::entity entity); + nlohmann::json serializeCharacterIdentity(flecs::entity entity); nlohmann::json serializeCharacterSlots(flecs::entity entity); nlohmann::json serializeCharacterShapeKeys(flecs::entity entity); nlohmann::json serializeAnimationTree(flecs::entity entity); @@ -156,6 +157,8 @@ private: const nlohmann::json &json); void deserializeCharacter(flecs::entity entity, const nlohmann::json &json); + void deserializeCharacterIdentity(flecs::entity entity, + const nlohmann::json &json); void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json &json); void deserializeCharacterShapeKeys(flecs::entity entity, diff --git a/src/features/editScene/ui/CharacterIdentityEditor.cpp b/src/features/editScene/ui/CharacterIdentityEditor.cpp new file mode 100644 index 0000000..3fe9b12 --- /dev/null +++ b/src/features/editScene/ui/CharacterIdentityEditor.cpp @@ -0,0 +1,47 @@ +#include "CharacterIdentityEditor.hpp" +#include "ComponentRegistration.hpp" +#include "../systems/CharacterRegistry.hpp" +#include + +bool CharacterIdentityEditor::renderComponent(flecs::entity entity, + CharacterIdentityComponent &identity) +{ + (void)entity; + bool modified = false; + ImGui::PushID("CharacterIdentity"); + + ImGui::Text("Registry ID: %llu", (unsigned long long)identity.registryId); + + static uint64_t selectedId = 0; + static bool showPicker = false; + + if (identity.registryId != 0) { + if (ImGui::Button("Clear")) { + identity.registryId = 0; + modified = true; + } + ImGui::SameLine(); + } + if (ImGui::Button("Select from Registry")) { + showPicker = !showPicker; + selectedId = identity.registryId; + } + + if (showPicker) { + ImGui::BeginChild("RegistryPicker", ImVec2(0, 150), true); + /* We don't have direct access to the registry here; the picker + * is a convenience that would need to be wired up if we want + * a live list. For now, allow typing the ID directly. */ + static char idBuf[32] = ""; + if (ImGui::InputText("Registry ID", idBuf, sizeof(idBuf), + ImGuiInputTextFlags_CharsDecimal)) { + identity.registryId = strtoull(idBuf, nullptr, 10); + modified = true; + } + ImGui::EndChild(); + } + + ImGui::PopID(); + return modified; +} + diff --git a/src/features/editScene/ui/CharacterIdentityEditor.hpp b/src/features/editScene/ui/CharacterIdentityEditor.hpp new file mode 100644 index 0000000..836c185 --- /dev/null +++ b/src/features/editScene/ui/CharacterIdentityEditor.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_CHARACTERIDENTITYEDITOR_HPP +#define EDITSCENE_CHARACTERIDENTITYEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/CharacterIdentity.hpp" + +class CharacterIdentityEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "Character Identity"; + } + +protected: + bool renderComponent(flecs::entity entity, + CharacterIdentityComponent &identity) override; +}; + +#endif // EDITSCENE_CHARACTERIDENTITYEDITOR_HPP