Update to use character registry; Character Slots fixes

This commit is contained in:
2026-05-11 13:12:18 +03:00
parent 42f6a218fb
commit f9e61dcb05
13 changed files with 2157 additions and 96 deletions
Binary file not shown.
+5
View File
@@ -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
+4
View File
@@ -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<CharacterSlotsComponent>();
// Register CharacterIdentity component
m_world.component<CharacterIdentityComponent>();
// Register AnimationTree component
m_world.component<AnimationTreeComponent>();
@@ -0,0 +1,18 @@
#ifndef EDITSCENE_CHARACTERIDENTITY_HPP
#define EDITSCENE_CHARACTERIDENTITY_HPP
#pragma once
#include <cstdint>
/**
* 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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,273 @@
#ifndef EDITSCENE_CHARACTERREGISTRY_HPP
#define EDITSCENE_CHARACTERREGISTRY_HPP
#pragma once
#include <Ogre.h>
#include <flecs.h>
#include <nlohmann/json.hpp>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<std::string, int> stats;
std::unordered_map<std::string, int> skills;
std::unordered_map<std::string, int> needs;
/* Tags */
std::vector<std::string> tags;
/* Extensible custom columns */
std::unordered_map<std::string, int64_t> intColumns;
std::unordered_map<std::string, double> floatColumns;
std::unordered_map<std::string, std::string> stringColumns;
};
struct GroupRecord {
uint64_t id = 0;
std::string name;
std::vector<uint64_t> memberIds;
/* Extensible custom columns */
std::unordered_map<std::string, int64_t> intColumns;
std::unordered_map<std::string, double> floatColumns;
std::unordered_map<std::string, std::string> 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<std::string, float> stats;
/* Tags: wife, sibling, parent, enemy, ally, mentor … */
std::unordered_set<std::string> 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<uint64_t, CharacterRecord> &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<uint64_t, GroupRecord> &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<const Relationship *> 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<ColumnDef> &getCharacterColumns() const
{
return m_characterColumns;
}
void addGroupColumn(const std::string &name, ColumnDef::Type type);
void removeGroupColumn(const std::string &name);
const std::vector<ColumnDef> &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<std::string> &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<uint64_t, CharacterRecord> m_characters;
std::unordered_map<uint64_t, GroupRecord> m_groups;
std::vector<Relationship> m_relationships;
std::vector<ColumnDef> m_characterColumns;
std::vector<ColumnDef> m_groupColumns;
/* Fast relationship indexes (value = index into m_relationships) */
std::unordered_multimap<uint64_t, size_t> m_relBySource;
std::unordered_multimap<uint64_t, size_t> m_relByTarget;
mutable std::string m_lastError;
std::string m_autoSavePath;
std::vector<std::string> 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
@@ -226,82 +226,137 @@ std::vector<Ogre::String> CharacterSlotSystem::getShapeKeyNames(
return std::vector<Ogre::String>(keySet.begin(), keySet.end());
}
static std::vector<Ogre::String> 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<Ogre::String> result;
for (const auto &g :
entry.value("garments", nlohmann::json::array()))
result.push_back(g.get<std::string>());
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<Ogre::String> 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<Ogre::String> eg;
for (const auto &g : entryGarments)
eg.insert(g.get<std::string>());
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<Ogre::String>();
/* 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<Ogre::String>();
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<unsigned char>(
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::String>();
Ogre::LogManager::getSingleton().logMessage(
"[CharacterSlotSystem] resolveMesh(" + age + "," + sex +
"," + slot + ") -> lastResort: " + mesh);
return mesh;
}
if (!slotEntries.empty())
return slotEntries[0]["mesh"].get<Ogre::String>();
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<CharacterSlotsComponent>().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<TransformComponent>()) {
Ogre::LogManager::getSingleton().logMessage(
"[CharacterSlotSystem] buildCharacter: no TransformComponent");
if (!e.has<TransformComponent>())
return;
}
auto &transform = e.get_mut<TransformComponent>();
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,
@@ -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<Gizmo>(m_sceneMgr);
m_cursor3D = std::make_unique<Cursor3D>(m_sceneMgr);
m_serializer = std::make_unique<SceneSerializer>(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<CharacterIdentityEditor>();
m_componentRegistry.registerComponent<CharacterIdentityComponent>(
"Character Identity", "Character", std::move(characterIdentityEditor),
[](flecs::entity e) {
if (!e.has<CharacterIdentityComponent>())
e.set<CharacterIdentityComponent>(
CharacterIdentityComponent{});
},
[](flecs::entity e) {
if (e.has<CharacterIdentityComponent>())
e.remove<CharacterIdentityComponent>();
});
// 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<CharacterIdentityComponent>()) {
auto &ci = entity.get_mut<CharacterIdentityComponent>();
m_componentRegistry.render<CharacterIdentityComponent>(entity, ci);
componentCount++;
}
// Render AnimationTree if present
if (entity.has<AnimationTreeComponent>()) {
auto &at = entity.get_mut<AnimationTreeComponent>();
@@ -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<EntityNameComponent> m_nameQuery;
@@ -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<CharacterIdentityComponent>()) {
json["characterIdentity"] = serializeCharacterIdentity(entity);
}
if (entity.has<AnimationTreeComponent>()) {
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<CharacterComponent>(cc);
}
nlohmann::json SceneSerializer::serializeCharacterIdentity(flecs::entity entity)
{
auto &ci = entity.get<CharacterIdentityComponent>();
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<CharacterIdentityComponent>(ci);
}
nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
{
auto &cs = entity.get<CharacterSlotsComponent>();
@@ -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,
@@ -0,0 +1,47 @@
#include "CharacterIdentityEditor.hpp"
#include "ComponentRegistration.hpp"
#include "../systems/CharacterRegistry.hpp"
#include <imgui.h>
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;
}
@@ -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<CharacterIdentityComponent> {
public:
const char *getName() const override
{
return "Character Identity";
}
protected:
bool renderComponent(flecs::entity entity,
CharacterIdentityComponent &identity) override;
};
#endif // EDITSCENE_CHARACTERIDENTITYEDITOR_HPP