Update to use character registry; Character Slots fixes
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user