Lua scripts package

This commit is contained in:
2026-05-21 12:36:08 +03:00
parent aae7620512
commit 9968cb8c75
11 changed files with 1178 additions and 630 deletions
+77
View File
@@ -365,6 +365,7 @@ target_link_libraries(editSceneEditor
RecastNavigation::DetourTileCache
RecastNavigation::DetourCrowd
RecastNavigation::DebugUtils
PackageArchive
lua
)
@@ -628,6 +629,78 @@ target_include_directories(PackageTool PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
# ---------------------------------------------------------------------------
# lua-scripts.package - package all Lua scripts into a single archive
# ---------------------------------------------------------------------------
# Creates lua-scripts.package from the staged lua-scripts build directory.
# Files are stored with paths relative to lua-scripts/ (the "lua-scripts"
# directory prefix is stripped from archive paths).
#
# The staged directory is assembled from two sources:
# 1. Main lua-scripts (from ${CMAKE_BINARY_DIR}/lua-scripts, staged by
# the stage_lua_scripts target in the main build)
# 2. EditScene-specific lua-scripts (from
# ${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts, overlaid on top)
# The overlay ensures editScene files take precedence over main files
# with the same name.
set(LUA_SCRIPTS_PACKAGE "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts.package")
set(LUA_SCRIPTS_STAGED_DIR "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts")
set(LUA_SCRIPTS_EDITSCENE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts")
# Collect all source files from the main lua-scripts source directory so
# that the package is rebuilt when any of them change.
# CONFIGURE_DEPENDS makes CMake re-evaluate the glob at build time,
# so newly added files are picked up without re-running cmake.
file(GLOB_RECURSE LUA_SCRIPTS_MAIN_FILES
CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/lua-scripts/*.lua"
"${CMAKE_SOURCE_DIR}/lua-scripts/*.ink"
)
# Collect all editScene-specific lua-scripts source files.
file(GLOB_RECURSE LUA_SCRIPTS_EDITSCENE_FILES
CONFIGURE_DEPENDS
"${LUA_SCRIPTS_EDITSCENE_SRC}/*.lua"
"${LUA_SCRIPTS_EDITSCENE_SRC}/*.ink"
)
# Custom command to overlay editScene-specific lua-scripts on top of the
# staged main lua-scripts directory. This copies files from the editScene
# lua-scripts source directory into the staged directory, overlaying on
# top of any files already there from the main build.
set(LUA_SCRIPTS_OVERLAY_DONE "${CMAKE_CURRENT_BINARY_DIR}/.lua_scripts_overlay_done")
add_custom_command(
OUTPUT "${LUA_SCRIPTS_OVERLAY_DONE}"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${LUA_SCRIPTS_EDITSCENE_SRC}"
"${LUA_SCRIPTS_STAGED_DIR}"
COMMAND ${CMAKE_COMMAND} -E touch "${LUA_SCRIPTS_OVERLAY_DONE}"
DEPENDS stage_lua_scripts
${LUA_SCRIPTS_EDITSCENE_FILES}
COMMENT "Overlaying editScene lua-scripts on ${LUA_SCRIPTS_STAGED_DIR}"
)
add_custom_command(
OUTPUT "${LUA_SCRIPTS_PACKAGE}"
COMMAND $<TARGET_FILE:PackageTool>
create "${LUA_SCRIPTS_PACKAGE}"
--exclude "CMakeFiles"
--exclude "cmake_install.cmake"
--exclude "Makefile"
--exclude ".lua_scripts_overlay_done"
--exclude "*.lua-"
"${LUA_SCRIPTS_STAGED_DIR}"
DEPENDS PackageTool
"${LUA_SCRIPTS_OVERLAY_DONE}"
${LUA_SCRIPTS_MAIN_FILES}
COMMENT "Creating lua-scripts.package from ${LUA_SCRIPTS_STAGED_DIR}"
)
add_custom_target(lua_scripts_package ALL
DEPENDS "${LUA_SCRIPTS_PACKAGE}"
)
# ---------------------------------------------------------------------------
# Package Archive Test
# ---------------------------------------------------------------------------
@@ -664,6 +737,10 @@ add_custom_command(TARGET editSceneEditor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_BINARY_DIR}/lua-scripts"
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
# Overlay editScene-specific lua-scripts on top of main lua-scripts
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${LUA_SCRIPTS_EDITSCENE_SRC}"
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg"
"${CMAKE_CURRENT_BINARY_DIR}/resources.cfg"
+96 -66
View File
@@ -99,6 +99,8 @@
#include "components/Inventory.hpp"
#include <OgreRTShaderSystem.h>
#include <OgreArchiveManager.h>
#include "package/OgrePackageArchive.h"
#include <imgui.h>
#include "lua/LuaEntityApi.hpp"
#include "lua/LuaComponentApi.hpp"
@@ -268,6 +270,13 @@ EditorApp::~EditorApp()
void EditorApp::setup()
{
try {
// Register the Package archive factory before any resource
// locations are resolved (locateResources is called inside
// ApplicationContext::setup()).
static Ogre::PackageArchiveFactory s_packageFactory;
Ogre::ArchiveManager::getSingleton().addArchiveFactory(
&s_packageFactory);
// Base setup
OgreBites::ApplicationContext::setup();
@@ -719,12 +728,14 @@ void EditorApp::startNewGame(const Ogre::String &scenePath)
flecs::entity playerCharacter = playerController;
if (playerController.is_alive() &&
playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController.get<PlayerControllerComponent>();
auto &pc = playerController
.get<PlayerControllerComponent>();
if (!pc.targetCharacterName.empty()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &en) {
if (en.name == pc.targetCharacterName)
if (en.name ==
pc.targetCharacterName)
playerCharacter = e;
});
}
@@ -819,12 +830,11 @@ void EditorApp::saveGame(const std::string &slotPath,
nlohmann::json saveData;
saveData["version"] = "2.0";
saveData["saveGame"] = {
{ "baseScene", m_currentBaseScene },
{ "timestamp", SaveLoadSystem::getCurrentTimestamp() },
{ "slotName", slotName },
{ "playTime", m_playTime }
};
saveData["saveGame"] = { { "baseScene", m_currentBaseScene },
{ "timestamp",
SaveLoadSystem::getCurrentTimestamp() },
{ "slotName", slotName },
{ "playTime", m_playTime } };
/* Sync spawned character positions into the registry.
* Read from the live SceneNode because TransformComponent
@@ -838,7 +848,8 @@ void EditorApp::saveGame(const std::string &slotPath,
auto &t = e.get<TransformComponent>();
if (t.node) {
rec->position = t.node->_getDerivedPosition();
rec->rotation = t.node->_getDerivedOrientation();
rec->rotation =
t.node->_getDerivedOrientation();
} else {
rec->position = t.position;
rec->rotation = t.rotation;
@@ -856,19 +867,22 @@ void EditorApp::saveGame(const std::string &slotPath,
if (playerController.is_alive()) {
flecs::entity playerChar = playerController;
if (playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController.get<PlayerControllerComponent>();
auto &pc = playerController
.get<PlayerControllerComponent>();
if (!pc.targetCharacterName.empty()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &en) {
if (en.name == pc.targetCharacterName)
if (en.name ==
pc.targetCharacterName)
playerChar = e;
});
}
}
if (playerChar.has<CharacterIdentityComponent>()) {
playerCharId = playerChar.get<
CharacterIdentityComponent>().registryId;
playerCharId =
playerChar.get<CharacterIdentityComponent>()
.registryId;
}
}
saveData["saveGame"]["playerCharacterId"] = playerCharId;
@@ -876,8 +890,7 @@ void EditorApp::saveGame(const std::string &slotPath,
saveData["characterRegistry"] = registry.serialize();
saveData["containerState"] =
ContainerStateRegistry::getInstance().serialize();
saveData["itemState"] =
ItemStateRegistry::getInstance().serialize();
saveData["itemState"] = ItemStateRegistry::getInstance().serialize();
/* Runtime entities — skip characters and player controller */
saveData["runtimeEntities"] = nlohmann::json::array();
@@ -895,29 +908,31 @@ void EditorApp::saveGame(const std::string &slotPath,
/* Character runtime component overrides (restored after registry spawn) */
saveData["characterRuntimeData"] = nlohmann::json::object();
static const std::vector<std::string> s_runtimeKeys = {
"character", "goapBlackboard", "goapPlanner",
"goapRunner", "pathFollowing", "behaviorTree",
"inventory", "actionDebug", "animationTree",
"animationTreeTemplate"
"character", "goapBlackboard",
"goapPlanner", "goapRunner",
"pathFollowing", "behaviorTree",
"inventory", "actionDebug",
"animationTree", "animationTreeTemplate"
};
m_world.query<CharacterIdentityComponent>().each(
[&](flecs::entity e, CharacterIdentityComponent &ci) {
nlohmann::json entityJson = serializer.serializeEntity(e);
nlohmann::json entityJson =
serializer.serializeEntity(e);
nlohmann::json filtered;
for (const auto &key : s_runtimeKeys) {
if (entityJson.contains(key))
filtered[key] = entityJson[key];
}
saveData["characterRuntimeData"][
std::to_string(ci.registryId)] = filtered;
saveData["characterRuntimeData"]
[std::to_string(ci.registryId)] = filtered;
});
/* Lua callback data */
saveData["luaData"] = editScene::collectLuaSaveData(m_lua.getState());
if (SaveLoadSystem::writeSaveFile(slotPath, saveData)) {
Ogre::LogManager::getSingleton().logMessage(
"Game saved to: " + slotPath);
Ogre::LogManager::getSingleton().logMessage("Game saved to: " +
slotPath);
EventBus::getInstance().send("game_saved");
} else {
Ogre::LogManager::getSingleton().logMessage(
@@ -988,12 +1003,10 @@ void EditorApp::loadGame(const std::string &slotPath)
* world at save time (listed in characterRuntimeData).
* This avoids respawning registry records that were not
* spawned (e.g. characters from other scenes) at origin. */
const auto &runtimeData =
saveData.value("characterRuntimeData",
nlohmann::json::object());
bool filterByRuntime =
saveData.contains("characterRuntimeData") &&
runtimeData.is_object();
const auto &runtimeData = saveData.value("characterRuntimeData",
nlohmann::json::object());
bool filterByRuntime = saveData.contains("characterRuntimeData") &&
runtimeData.is_object();
for (const auto &pair : registry.getCharacters()) {
const auto &rec = pair.second;
if (!rec.persistent)
@@ -1013,9 +1026,8 @@ void EditorApp::loadGame(const std::string &slotPath)
if (!spawned.is_alive())
continue;
serializer.deserializeEntityComponents(
spawned, el.value(),
flecs::entity::null(), nullptr,
false, false, false);
spawned, el.value(), flecs::entity::null(),
nullptr, false, false, false);
}
}
@@ -1029,9 +1041,11 @@ void EditorApp::loadGame(const std::string &slotPath)
if (entityJson.contains("item") &&
entityJson["item"].is_object() &&
entityJson["item"].contains("instanceId")) {
std::string iid = entityJson["item"]["instanceId"];
m_world.query<ItemComponent>()
.each([&](flecs::entity e, ItemComponent &item) {
std::string iid =
entityJson["item"]["instanceId"];
m_world.query<ItemComponent>().each(
[&](flecs::entity e,
ItemComponent &item) {
if (item.instanceId == iid)
targetEntity = e;
});
@@ -1043,9 +1057,9 @@ void EditorApp::loadGame(const std::string &slotPath)
entityJson["name"].is_object() &&
entityJson["name"].contains("name")) {
std::string ename = entityJson["name"]["name"];
m_world.query<EntityNameComponent>()
.each([&](flecs::entity e,
EntityNameComponent &nameComp) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &nameComp) {
if (nameComp.name == ename)
targetEntity = e;
});
@@ -1057,30 +1071,41 @@ void EditorApp::loadGame(const std::string &slotPath)
if (entityJson.contains("transform") &&
entityJson["transform"].is_object()) {
auto &t = entityJson["transform"];
if (targetEntity.has<TransformComponent>()) {
if (targetEntity
.has<TransformComponent>()) {
auto &trans = targetEntity.get_mut<
TransformComponent>();
if (t.contains("position")) {
auto &p = t["position"];
trans.position = Ogre::Vector3(
p.value("x", 0.0f),
p.value("y", 0.0f),
p.value("z", 0.0f));
p.value("x",
0.0f),
p.value("y",
0.0f),
p.value("z",
0.0f));
}
if (t.contains("rotation")) {
auto &r = t["rotation"];
trans.rotation = Ogre::Quaternion(
r.value("w", 1.0f),
r.value("x", 0.0f),
r.value("y", 0.0f),
r.value("z", 0.0f));
r.value("w",
1.0f),
r.value("x",
0.0f),
r.value("y",
0.0f),
r.value("z",
0.0f));
}
if (t.contains("scale")) {
auto &s = t["scale"];
trans.scale = Ogre::Vector3(
s.value("x", 1.0f),
s.value("y", 1.0f),
s.value("z", 1.0f));
s.value("x",
1.0f),
s.value("y",
1.0f),
s.value("z",
1.0f));
}
trans.applyToNode();
}
@@ -1112,19 +1137,20 @@ void EditorApp::loadGame(const std::string &slotPath)
});
if (playerController.is_alive() && playerCharId != 0) {
flecs::entity playerChar;
m_world.query<CharacterIdentityComponent>()
.each([&](flecs::entity e,
CharacterIdentityComponent &ci) {
if (ci.registryId == playerCharId)
playerChar = e;
});
m_world.query<CharacterIdentityComponent>().each(
[&](flecs::entity e, CharacterIdentityComponent &ci) {
if (ci.registryId == playerCharId)
playerChar = e;
});
if (playerChar.is_alive() &&
playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController.get_mut<
PlayerControllerComponent>();
auto &pc =
playerController
.get_mut<PlayerControllerComponent>();
if (playerChar.has<EntityNameComponent>()) {
pc.targetCharacterName =
playerChar.get<EntityNameComponent>().name;
playerChar.get<EntityNameComponent>()
.name;
}
if (!playerChar.has<InventoryComponent>()) {
playerChar.set<InventoryComponent>(
@@ -1143,8 +1169,8 @@ void EditorApp::loadGame(const std::string &slotPath)
m_playTime = saveData["saveGame"].value("playTime", 0.0f);
setGamePlayState(GamePlayState::Playing);
Ogre::LogManager::getSingleton().logMessage(
"Game loaded from: " + slotPath);
Ogre::LogManager::getSingleton().logMessage("Game loaded from: " +
slotPath);
EventBus::getInstance().send("game_loaded");
}
@@ -1549,17 +1575,21 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
flecs::entity playerCharacter = playerController;
if (playerController.is_alive() &&
playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController.get<PlayerControllerComponent>();
auto &pc = playerController.get<
PlayerControllerComponent>();
if (!pc.targetCharacterName.empty()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &en) {
if (en.name == pc.targetCharacterName)
playerCharacter = e;
if (en.name ==
pc.targetCharacterName)
playerCharacter =
e;
});
}
}
if (playerCharacter.is_valid() && playerCharacter.is_alive()) {
if (playerCharacter.is_valid() &&
playerCharacter.is_alive()) {
m_characterClassSystem->toggleCharacterSheet(
playerCharacter);
setGamePlayState(GamePlayState::Paused);
@@ -1814,7 +1844,7 @@ void EditorApp::locateResources()
Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
"LuaScripts", false);
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
"./lua-scripts", "FileSystem", "LuaScripts", true, true);
"./lua-scripts.package", "Package", "LuaScripts", true, true);
/* Try multiple relative paths for characters to handle different
* working directories (build root vs binary subdirectory) */
@@ -11,17 +11,19 @@
* Layer 1 and 2 are selected via combo boxes.
*/
struct SlotSelection {
Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1
Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2
Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1
Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2
Ogre::String explicitMesh; // backward-compat override
};
/**
* Multi-slot mesh component for character parts sharing a skeleton.
* The "face" slot (or first available slot) serves as the master skeleton.
*
* Age is now stored in CharacterRegistry::CharacterRecord::age
* and should be retrieved from there when needed.
*/
struct CharacterSlotsComponent {
Ogre::String age = "adult";
Ogre::String sex = "male";
/* Global outfit level: 0=nude, 1=lingerie, 2=clothed */
@@ -0,0 +1,36 @@
print("hello!")
ecs.behavior_tree.register_node("luaHello", function(entity_id, params)
local message = params.message or "Hello!"
print("Entity " .. entity_id .. " says: " .. message)
return "success"
end)
ecs.action_db.add_action("lua_hello_action", 1,
{}, -- no preconditions
{}, -- no effects
{ -- behavior tree
type = "sequence",
children = {
ecs.behavior_tree.create_node("luaHello", "message=Welcome to the game!"),
ecs.behavior_tree.create_node("setAnimationState", "main/action"),
ecs.behavior_tree.create_node("setAnimationState", "action/sitting-ground"),
ecs.behavior_tree.create_node("delay", "dly", "9.0"),
ecs.behavior_tree.create_node("setAnimationState", "main/locomotion"),
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
}
}
)
ecs.subscribe_event("game_start", function(event, params)
print("Event: " .. event)
-- ecs.debug_crash("game_start triggered")
ecs.subscribe_event("new_game", function(event, params)
-- ecs.debug_crash("new_game triggered")
local tsub = ecs.subscribe_event("scene_ready", function(event, params)
-- ecs.unsubscribe_event(tsub)
ecs.debug_crash("scene_ready triggered")
end)
end)
end)
+59 -26
View File
@@ -33,6 +33,8 @@
#include "components/Primitive.hpp"
#include "components/TriangleBuffer.hpp"
#include "components/CharacterSlots.hpp"
#include "components/CharacterIdentity.hpp"
#include "systems/CharacterRegistry.hpp"
#include "components/AnimationTree.hpp"
#include "components/AnimationTreeTemplate.hpp"
#include "components/Character.hpp"
@@ -491,17 +493,26 @@ static void registerAllComponents()
// --- CharacterSlots ---
REGISTER_COMPONENT(
CharacterSlotsComponent, "CharacterSlots",
lua_pushstring(L, c.age.c_str());
lua_setfield(L, -2, "age"); lua_pushstring(L, c.sex.c_str());
lua_setfield(L, -2, "sex");
{ // Push: age from registry
Ogre::String age = "adult";
if (e.has<CharacterIdentityComponent>()) {
auto &id = e.get<CharacterIdentityComponent>();
const CharacterRegistry::CharacterRecord *rec =
CharacterRegistry::getSingleton()
.findCharacter(id.registryId);
if (rec && !rec->age.empty())
age = rec->age;
}
lua_pushstring(L, age.c_str());
} lua_setfield(L, -2, "age");
lua_pushstring(L, c.sex.c_str()); lua_setfield(L, -2, "sex");
// slots map: push as table
lua_newtable(L); for (auto &kv : c.slots) {
lua_pushstring(L, kv.second.c_str());
lua_setfield(L, -2, kv.first.c_str());
} lua_setfield(L, -2, "slots");
// slotSelections map: push as nested table
lua_newtable(L);
for (auto &kv : c.slotSelections) {
lua_newtable(L); for (auto &kv : c.slotSelections) {
lua_newtable(L);
lua_pushstring(L, kv.second.layer1Mesh.c_str());
lua_setfield(L, -2, "layer1Mesh");
@@ -510,14 +521,29 @@ static void registerAllComponents()
lua_pushstring(L, kv.second.explicitMesh.c_str());
lua_setfield(L, -2, "explicitMesh");
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "slotSelections");
} lua_setfield(L, -2, "slotSelections");
lua_pushinteger(L, c.outfitLevel);
lua_setfield(L, -2, "outfitLevel");
pushVector3(L, c.frontAxis); lua_setfield(L, -2, "frontAxis");
, if (lua_getfield(L, idx, "age"), lua_isstring(L, -1))
c.age = lua_tostring(L, -1);
lua_pop(L, 1);
lua_setfield(L, -2, "outfitLevel"); pushVector3(L, c.frontAxis);
lua_setfield(L, -2, "frontAxis");
,
{ // Read: age into registry
if (lua_getfield(L, idx, "age"), lua_isstring(L, -1)) {
Ogre::String age = lua_tostring(L, -1);
if (e.has<CharacterIdentityComponent>()) {
auto &id = e.get<
CharacterIdentityComponent>();
CharacterRegistry::CharacterRecord *rec =
CharacterRegistry::getSingleton()
.findCharacter(
id.registryId);
if (rec) {
rec->age = age;
CharacterRegistry::getSingleton()
.autoSave();
}
}
}
} lua_pop(L, 1);
if (lua_getfield(L, idx, "sex"), lua_isstring(L, -1))
c.sex = lua_tostring(L, -1);
lua_pop(L, 1);
@@ -534,21 +560,29 @@ static void registerAllComponents()
lua_pop(L, 1);
}
} lua_pop(L, 1);
if (lua_getfield(L, idx, "slotSelections"), lua_istable(L, -1)) {
if (lua_getfield(L, idx, "slotSelections"),
lua_istable(L, -1)) {
c.slotSelections.clear();
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
SlotSelection sel;
Ogre::String slotName = lua_tostring(L, -2);
if (lua_getfield(L, -1, "layer1Mesh"), lua_isstring(L, -1))
sel.layer1Mesh = lua_tostring(L, -1);
Ogre::String slotName =
lua_tostring(L, -2);
if (lua_getfield(L, -1, "layer1Mesh"),
lua_isstring(L, -1))
sel.layer1Mesh =
lua_tostring(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, -1, "layer2Mesh"), lua_isstring(L, -1))
sel.layer2Mesh = lua_tostring(L, -1);
if (lua_getfield(L, -1, "layer2Mesh"),
lua_isstring(L, -1))
sel.layer2Mesh =
lua_tostring(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, -1, "explicitMesh"), lua_isstring(L, -1))
sel.explicitMesh = lua_tostring(L, -1);
if (lua_getfield(L, -1, "explicitMesh"),
lua_isstring(L, -1))
sel.explicitMesh =
lua_tostring(L, -1);
lua_pop(L, 1);
c.slotSelections[slotName] = sel;
}
@@ -566,12 +600,13 @@ static void registerAllComponents()
for (auto &kv : c.weights) {
lua_pushnumber(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
, if (lua_istable(L, idx)) {
},
if (lua_istable(L, idx)) {
lua_pushnil(L);
while (lua_next(L, idx) != 0) {
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
c.weights[lua_tostring(L, -2)] = lua_tonumber(L, -1);
c.weights[lua_tostring(L, -2)] =
lua_tonumber(L, -1);
lua_pop(L, 1);
}
});
@@ -830,8 +865,7 @@ static void registerAllComponents()
// --- Item ---
REGISTER_COMPONENT(
ItemComponent, "Item", lua_pushstring(L, c.itemId.c_str());
lua_setfield(L, -2, "itemId");
lua_pushinteger(L, c.stackSize);
lua_setfield(L, -2, "itemId"); lua_pushinteger(L, c.stackSize);
lua_setfield(L, -2, "stackSize");
lua_pushstring(L, c.action.c_str());
lua_setfield(L, -2, "action");
@@ -1293,7 +1327,6 @@ static void registerAllComponents()
c.showQuit = lua_toboolean(L, -1) != 0;
lua_pop(L, 1););
// --- PlayerController ---
REGISTER_COMPONENT(
PlayerControllerComponent, "PlayerController",
+101 -5
View File
@@ -46,6 +46,70 @@ static void printUsage(const char *prog)
<< "added from subdirectories." << std::endl;
}
/**
* Check if a filename matches a simple glob pattern.
* Supports '*' (matches any sequence of characters except '/').
*/
static bool matchesGlob(const std::string &filename, const std::string &pattern)
{
size_t pi = 0, fi = 0;
while (pi < pattern.size()) {
if (pattern[pi] == '*') {
// '*' matches any sequence of non-'/' characters
pi++;
if (pi == pattern.size())
return true; // trailing '*' matches everything
// Find the next character in filename that matches
// pattern[pi]
while (fi < filename.size() && filename[fi] != '/') {
if (filename[fi] == pattern[pi])
break;
fi++;
}
if (fi >= filename.size() || filename[fi] == '/')
return false;
} else {
if (fi >= filename.size() ||
filename[fi] != pattern[pi])
return false;
fi++;
pi++;
}
}
return fi == filename.size();
}
/**
* Check if a relative path matches any of the exclude patterns.
* Patterns can be:
* - Prefix match: "CMakeFiles" matches "CMakeFiles/foo" or "CMakeFiles"
* - Glob match: "*.lua-" matches "data.lua-" or "foo/bar.lua-"
* - Exact match: "Makefile" matches "Makefile"
*/
static bool isExcluded(const std::string &path,
const std::vector<std::string> &excludes)
{
// Extract just the filename part for glob matching
std::string filename = path;
auto slashPos = path.rfind('/');
if (slashPos != std::string::npos)
filename = path.substr(slashPos + 1);
for (const auto &pattern : excludes) {
// If pattern contains a glob character, use glob matching
if (pattern.find('*') != std::string::npos) {
if (matchesGlob(filename, pattern))
return true;
} else {
// Otherwise use prefix matching (existing behavior)
if (path == pattern || path.find(pattern + "/") == 0) {
return true;
}
}
}
return false;
}
/**
* Add files from a source path to the archive.
* If the source is a directory, it is added recursively.
@@ -54,7 +118,8 @@ static void printUsage(const char *prog)
*/
static void addPathToArchive(Ogre::PackageArchive &archive,
const fs::path &sourcePath,
const std::string &destPrefix = "")
const std::string &destPrefix = "",
const std::vector<std::string> &excludes = {})
{
std::error_code ec;
@@ -79,6 +144,14 @@ static void addPathToArchive(Ogre::PackageArchive &archive,
c = '/';
}
// Skip excluded paths
if (isExcluded(destName, excludes)) {
std::cout << "Skipping (excluded): "
<< entry.path().string()
<< std::endl;
continue;
}
std::cout << "Adding: " << entry.path().string()
<< " -> " << destName << std::endl;
archive.addFileFromDisk(destName,
@@ -100,23 +173,46 @@ static void addPathToArchive(Ogre::PackageArchive &archive,
}
}
/**
* Parse --exclude options from the argument list.
* Returns the remaining non-option arguments.
*/
static std::vector<std::string>
parseExcludes(const std::vector<std::string> &args,
std::vector<std::string> &excludes)
{
std::vector<std::string> remaining;
for (size_t i = 0; i < args.size(); i++) {
if (args[i] == "--exclude" && i + 1 < args.size()) {
excludes.push_back(args[i + 1]);
i++;
} else {
remaining.push_back(args[i]);
}
}
return remaining;
}
static int cmdCreate(const std::vector<std::string> &args)
{
if (args.size() < 2) {
std::vector<std::string> excludes;
std::vector<std::string> remaining = parseExcludes(args, excludes);
if (remaining.size() < 2) {
std::cerr << "Error: create requires a package path and at "
"least one source file"
<< std::endl;
return 1;
}
const std::string &packagePath = args[0];
const std::string &packagePath = remaining[0];
Ogre::PackageArchive archive(packagePath, "Package");
// Load will create the file if it doesn't exist
archive.load();
for (size_t i = 1; i < args.size(); i++) {
addPathToArchive(archive, args[i]);
for (size_t i = 1; i < remaining.size(); i++) {
addPathToArchive(archive, remaining[i], "", excludes);
}
std::cout << "Created package: " << packagePath << " with "
File diff suppressed because it is too large Load Diff
@@ -64,14 +64,17 @@ public:
std::unordered_map<std::string, int> currentPools;
bool levelUpPending = false;
/* Age category string (e.g. "adult", "child", "elder") */
std::string age = "adult";
/* Age in years (runtime progression) */
int ageYears = 0;
/* Inline appearance (used when no prefab file exists) */
std::string inlineAge = "adult";
std::string inlineSex = "male";
int inlineOutfitLevel = 2;
std::unordered_map<std::string, SlotSelection> inlineSlotSelections;
std::unordered_map<std::string, SlotSelection>
inlineSlotSelections;
std::unordered_map<std::string, float> inlineShapeKeyWeights;
/* Runtime-only characters are NOT saved to character_registry.json */
@@ -130,12 +133,18 @@ public:
CharacterRegistry(const CharacterRegistry &) = delete;
CharacterRegistry &operator=(const CharacterRegistry &) = delete;
void setWorld(flecs::world *world) { m_world = world; }
void setWorld(flecs::world *world)
{
m_world = world;
}
void setSceneManager(Ogre::SceneManager *sceneMgr)
{
m_sceneMgr = sceneMgr;
}
void setEditorUISystem(EditorUISystem *ui) { m_uiSystem = ui; }
void setEditorUISystem(EditorUISystem *ui)
{
m_uiSystem = ui;
}
/**
* Load registry from auto-save file. Call after setWorld/setSceneManager.
@@ -152,7 +161,8 @@ public:
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
const std::unordered_map<uint64_t, CharacterRecord> &
getCharacters() const
{
return m_characters;
}
@@ -227,10 +237,8 @@ public:
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);
const std::string &sex, int outfitLevel);
static bool copyPrefab(const std::string &src, const std::string &dst);
static bool deletePrefabFile(const std::string &filepath);
/* ------------------------------------------------------------------ */
@@ -332,7 +340,10 @@ public:
bool saveToFile(const std::string &filepath);
bool loadFromFile(const std::string &filepath);
const std::string &getLastError() const { return m_lastError; }
const std::string &getLastError() const
{
return m_lastError;
}
/* ------------------------------------------------------------------ */
/* ImGui editor */
@@ -387,7 +398,8 @@ private:
void removeFromIndex(uint64_t sourceId, uint64_t targetId,
size_t relIndex);
std::string generatePrefabPath(uint64_t id, const std::string &firstName,
std::string generatePrefabPath(uint64_t id,
const std::string &firstName,
const std::string &lastName) const;
/* UI state */
@@ -1,6 +1,9 @@
#include "CharacterSlotSystem.hpp"
#include "CharacterRegistry.hpp"
#include "../components/Transform.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/CharacterIdentity.hpp"
#include "../components/CharacterSlots.hpp"
#include <OgreAnimation.h>
#include <OgreAnimationState.h>
#include <OgreAnimationTrack.h>
@@ -18,7 +21,7 @@ nlohmann::json CharacterSlotSystem::s_bodyParts = nlohmann::json::object();
std::set<Ogre::String> CharacterSlotSystem::s_meshNames;
CharacterSlotSystem::CharacterSlotSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr)
Ogre::SceneManager *sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
{
@@ -67,7 +70,6 @@ void CharacterSlotSystem::loadCatalog()
names->end());
}
for (const auto &name : partNames) {
Ogre::String group = rgm.findGroupContainingResource(name);
Ogre::DataStreamPtr stream = rgm.openResource(name, group);
@@ -86,7 +88,8 @@ void CharacterSlotSystem::loadCatalog()
if (!s_bodyParts.contains(age))
s_bodyParts[age] = nlohmann::json::object();
if (!s_bodyParts[age].contains(sex))
s_bodyParts[age][sex] = nlohmann::json::object();
s_bodyParts[age][sex] =
nlohmann::json::object();
if (!s_bodyParts[age][sex].contains(slot))
s_bodyParts[age][sex][slot] =
nlohmann::json::array();
@@ -96,8 +99,8 @@ void CharacterSlotSystem::loadCatalog()
/* Preload mesh into Characters group */
try {
Ogre::MeshManager::getSingleton().load(mesh,
"Characters");
Ogre::MeshManager::getSingleton().load(
mesh, "Characters");
} catch (...) {
}
} catch (...) {
@@ -105,7 +108,6 @@ void CharacterSlotSystem::loadCatalog()
}
}
s_catalogLoaded = true;
}
@@ -124,8 +126,7 @@ std::vector<Ogre::String> CharacterSlotSystem::getAges()
return ages;
}
std::vector<Ogre::String> CharacterSlotSystem::getSexes(
const Ogre::String &age)
std::vector<Ogre::String> CharacterSlotSystem::getSexes(const Ogre::String &age)
{
std::vector<Ogre::String> sexes;
if (!s_catalogLoaded || !s_bodyParts.contains(age))
@@ -135,8 +136,8 @@ std::vector<Ogre::String> CharacterSlotSystem::getSexes(
return sexes;
}
std::vector<Ogre::String> CharacterSlotSystem::getSlots(
const Ogre::String &age, const Ogre::String &sex)
std::vector<Ogre::String> CharacterSlotSystem::getSlots(const Ogre::String &age,
const Ogre::String &sex)
{
std::vector<Ogre::String> slots;
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
@@ -147,9 +148,10 @@ std::vector<Ogre::String> CharacterSlotSystem::getSlots(
return slots;
}
std::vector<Ogre::String> CharacterSlotSystem::getMeshesForLayer(
const Ogre::String &age, const Ogre::String &sex,
const Ogre::String &slot, int layer)
std::vector<Ogre::String>
CharacterSlotSystem::getMeshesForLayer(const Ogre::String &age,
const Ogre::String &sex,
const Ogre::String &slot, int layer)
{
std::vector<Ogre::String> meshes;
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
@@ -165,9 +167,10 @@ std::vector<Ogre::String> CharacterSlotSystem::getMeshesForLayer(
return meshes;
}
Ogre::String CharacterSlotSystem::getMeshLabel(
const Ogre::String &age, const Ogre::String &sex,
const Ogre::String &slot, const Ogre::String &mesh)
Ogre::String CharacterSlotSystem::getMeshLabel(const Ogre::String &age,
const Ogre::String &sex,
const Ogre::String &slot,
const Ogre::String &mesh)
{
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
!s_bodyParts[age].contains(sex) ||
@@ -176,8 +179,8 @@ Ogre::String CharacterSlotSystem::getMeshLabel(
for (const auto &entry : s_bodyParts[age][sex][slot]) {
if (entry.value("mesh", "") == mesh) {
const auto &garments =
entry.value("garments", nlohmann::json::array());
const auto &garments = entry.value(
"garments", nlohmann::json::array());
if (garments.empty())
return "nude";
Ogre::String label;
@@ -192,9 +195,9 @@ Ogre::String CharacterSlotSystem::getMeshLabel(
return mesh;
}
std::vector<Ogre::String> CharacterSlotSystem::getMeshes(
const Ogre::String &age, const Ogre::String &sex,
const Ogre::String &slot)
std::vector<Ogre::String>
CharacterSlotSystem::getMeshes(const Ogre::String &age, const Ogre::String &sex,
const Ogre::String &slot)
{
std::vector<Ogre::String> meshes;
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
@@ -206,8 +209,9 @@ std::vector<Ogre::String> CharacterSlotSystem::getMeshes(
return meshes;
}
std::vector<Ogre::String> CharacterSlotSystem::getShapeKeyNames(
const Ogre::String &age, const Ogre::String &sex)
std::vector<Ogre::String>
CharacterSlotSystem::getShapeKeyNames(const Ogre::String &age,
const Ogre::String &sex)
{
std::set<Ogre::String> keySet;
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
@@ -216,8 +220,8 @@ std::vector<Ogre::String> CharacterSlotSystem::getShapeKeyNames(
for (auto &slotEl : s_bodyParts[age][sex].items()) {
for (const auto &entry : slotEl.value()) {
const auto &keys =
entry.value("shape_keys", nlohmann::json::array());
const auto &keys = entry.value("shape_keys",
nlohmann::json::array());
for (const auto &k : keys)
keySet.insert(k.get<Ogre::String>());
}
@@ -226,9 +230,10 @@ 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)
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 {};
@@ -248,10 +253,11 @@ static std::vector<Ogre::String> garmentsForMesh(
return {};
}
Ogre::String CharacterSlotSystem::resolveMesh(
const Ogre::String &age, const Ogre::String &sex,
const Ogre::String &slot, const SlotSelection &sel,
int outfitLevel)
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())
return sel.explicitMesh;
@@ -269,8 +275,10 @@ Ogre::String CharacterSlotSystem::resolveMesh(
/* 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);
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);
@@ -279,9 +287,9 @@ Ogre::String CharacterSlotSystem::resolveMesh(
if (!required.empty()) {
Ogre::String combinedMesh;
for (const auto &entry : slotEntries) {
auto entryGarments =
entry.value("garments",
nlohmann::json::array());
auto entryGarments = entry.value(
"garments",
nlohmann::json::array());
std::set<Ogre::String> eg;
for (const auto &g : entryGarments)
eg.insert(g.get<std::string>());
@@ -294,7 +302,8 @@ Ogre::String CharacterSlotSystem::resolveMesh(
}
if (containsAll) {
Ogre::String m =
entry["mesh"].get<Ogre::String>();
entry["mesh"]
.get<Ogre::String>();
/* Prefer exact layer2 match if it already
* satisfies the requirement */
if (m == sel.layer2Mesh)
@@ -332,8 +341,7 @@ Ogre::String CharacterSlotSystem::resolveMesh(
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) {
for (size_t i = dotMesh - 3; i < dotMesh; ++i) {
if (!isdigit(static_cast<unsigned char>(
mesh[i]))) {
isDup = false;
@@ -343,8 +351,7 @@ Ogre::String CharacterSlotSystem::resolveMesh(
if (isDup && mesh[dotMesh - 4] == '.')
effectiveLen += 1000;
}
if (bestLayer0.empty() ||
effectiveLen < bestLen) {
if (bestLayer0.empty() || effectiveLen < bestLen) {
bestLayer0 = mesh;
bestLen = effectiveLen;
}
@@ -374,9 +381,10 @@ void CharacterSlotSystem::update()
});
}
static const nlohmann::json *findCatalogEntry(
const Ogre::String &age, const Ogre::String &sex,
const Ogre::String &slot, const Ogre::String &mesh)
static const nlohmann::json *findCatalogEntry(const Ogre::String &age,
const Ogre::String &sex,
const Ogre::String &slot,
const Ogre::String &mesh)
{
if (!CharacterSlotSystem::isCatalogLoaded())
return nullptr;
@@ -396,7 +404,7 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName)
Ogre::MeshPtr mesh;
try {
mesh = Ogre::MeshManager::getSingleton().load(meshName,
"Characters");
"Characters");
} catch (...) {
return;
}
@@ -405,9 +413,10 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName)
try {
mesh->getAnimation("ShapeKeys");
} catch (...) {
Ogre::Animation *anim = mesh->createAnimation("ShapeKeys", 1.0f);
Ogre::VertexAnimationTrack *track = anim->createVertexTrack(
0, Ogre::VAT_POSE);
Ogre::Animation *anim =
mesh->createAnimation("ShapeKeys", 1.0f);
Ogre::VertexAnimationTrack *track =
anim->createVertexTrack(0, Ogre::VAT_POSE);
Ogre::VertexPoseKeyFrame *kf =
track->createVertexPoseKeyFrame(0.0f);
for (size_t i = 0; i < mesh->getPoseCount(); ++i)
@@ -415,11 +424,30 @@ static void ensureMeshPoseAnimation(const Ogre::String &meshName)
}
}
/**
* Helper: retrieve the character's age string from the registry.
* Falls back to "adult" if no registry entry is found.
*/
static Ogre::String getCharacterAge(flecs::entity e)
{
if (e.has<CharacterIdentityComponent>()) {
auto &id = e.get<CharacterIdentityComponent>();
const CharacterRegistry::CharacterRecord *rec =
CharacterRegistry::getSingleton().findCharacter(
id.registryId);
if (rec && !rec->age.empty())
return rec->age;
}
return "adult";
}
void CharacterSlotSystem::buildCharacter(flecs::entity e,
CharacterSlotsComponent &cs)
CharacterSlotsComponent &cs)
{
destroyCharacterParts(e);
Ogre::String age = getCharacterAge(e);
/* Migrate old slots map to slotSelections if needed */
if (cs.slotSelections.empty() && !cs.slots.empty()) {
for (const auto &pair : cs.slots) {
@@ -431,7 +459,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
/* Populate default slots from catalog if still empty */
if (cs.slotSelections.empty()) {
auto slots = getSlots(cs.age, cs.sex);
auto slots = getSlots(age, cs.sex);
for (const auto &slot : slots) {
SlotSelection sel;
cs.slotSelections[slot] = sel;
@@ -448,7 +476,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
/* Determine master slot (face preferred, else first non-empty) */
Ogre::String masterSlot;
if (cs.slotSelections.find("face") != cs.slotSelections.end()) {
Ogre::String mesh = resolveMesh(cs.age, cs.sex, "face",
Ogre::String mesh = resolveMesh(age, cs.sex, "face",
cs.slotSelections["face"],
cs.outfitLevel);
if (!mesh.empty())
@@ -456,7 +484,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
}
if (masterSlot.empty()) {
for (const auto &pair : cs.slotSelections) {
Ogre::String mesh = resolveMesh(cs.age, cs.sex, pair.first,
Ogre::String mesh = resolveMesh(age, cs.sex, pair.first,
pair.second,
cs.outfitLevel);
if (!mesh.empty()) {
@@ -469,7 +497,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
if (masterSlot.empty())
return;
Ogre::String masterMesh = resolveMesh(cs.age, cs.sex, masterSlot,
Ogre::String masterMesh = resolveMesh(age, cs.sex, masterSlot,
cs.slotSelections[masterSlot],
cs.outfitLevel);
@@ -489,8 +517,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
cs.masterEntity = masterEnt;
/* Setup pose animation for shape keys */
const nlohmann::json *entry = findCatalogEntry(
cs.age, cs.sex, masterSlot, masterMesh);
const nlohmann::json *entry =
findCatalogEntry(age, cs.sex, masterSlot, masterMesh);
applyShapeKeys(e, masterEnt, entry);
/* Notify AnimationTreeSystem that entity changed */
@@ -510,8 +538,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
if (slot == masterSlot)
continue;
Ogre::String mesh = resolveMesh(cs.age, cs.sex, slot, sel,
cs.outfitLevel);
Ogre::String mesh =
resolveMesh(age, cs.sex, slot, sel, cs.outfitLevel);
if (mesh.empty())
continue;
@@ -520,12 +548,13 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
Ogre::MeshPtr partMesh =
Ogre::MeshManager::getSingleton().load(
mesh, "Characters");
Ogre::Entity *partEnt = m_sceneMgr->createEntity(partMesh);
Ogre::Entity *partEnt =
m_sceneMgr->createEntity(partMesh);
partEnt->shareSkeletonInstanceWith(masterEnt);
transform.node->attachObject(partEnt);
m_entities[e.id()].parts[slot] = partEnt;
const nlohmann::json *entry = findCatalogEntry(
cs.age, cs.sex, slot, mesh);
const nlohmann::json *entry =
findCatalogEntry(age, cs.sex, slot, mesh);
applyShapeKeys(e, partEnt, entry);
} catch (const Ogre::Exception &ex) {
Ogre::LogManager::getSingleton().logMessage(
@@ -536,9 +565,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
}
}
void CharacterSlotSystem::applyShapeKeys(flecs::entity e,
Ogre::Entity *ent,
const nlohmann::json *entry)
void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
const nlohmann::json *entry)
{
if (!ent || !entry)
return;
@@ -553,8 +581,8 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e,
anim = mesh->getAnimation("ShapeKeys");
} catch (...) {
anim = mesh->createAnimation("ShapeKeys", 1.0f);
Ogre::VertexAnimationTrack *track = anim->createVertexTrack(
0, Ogre::VAT_POSE);
Ogre::VertexAnimationTrack *track =
anim->createVertexTrack(0, Ogre::VAT_POSE);
Ogre::VertexPoseKeyFrame *kf =
track->createVertexPoseKeyFrame(0.0f);
for (size_t i = 0; i < mesh->getPoseCount(); ++i)
@@ -623,7 +651,7 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e)
}
Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e,
const Ogre::String &slot)
const Ogre::String &slot)
{
auto it = m_entities.find(e.id());
if (it == m_entities.end())
@@ -635,8 +663,7 @@ Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e,
}
void CharacterSlotSystem::setSlotVisible(flecs::entity e,
const Ogre::String &slot,
bool visible)
const Ogre::String &slot, bool visible)
{
Ogre::Entity *ent = getSlotEntity(e, slot);
if (ent) {
@@ -2200,7 +2200,6 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
auto &cs = entity.get<CharacterSlotsComponent>();
nlohmann::json json;
json["age"] = cs.age;
json["sex"] = cs.sex;
json["outfitLevel"] = cs.outfitLevel;
json["slots"] = nlohmann::json::object();
@@ -2228,7 +2227,6 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
const nlohmann::json &json)
{
CharacterSlotsComponent cs;
cs.age = json.value("age", "adult");
cs.sex = json.value("sex", "male");
cs.outfitLevel = json.value("outfitLevel", 2);
if (json.contains("slots") && json["slots"].is_object()) {
@@ -1,5 +1,7 @@
#include "CharacterSlotsEditor.hpp"
#include "../systems/CharacterSlotSystem.hpp"
#include "../systems/CharacterRegistry.hpp"
#include "../components/CharacterIdentity.hpp"
#include <imgui.h>
#include <algorithm>
@@ -8,8 +10,9 @@ CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr)
{
}
bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
CharacterSlotsComponent &cs)
CharacterSlotsComponent &cs)
{
bool modified = false;
@@ -19,31 +22,27 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
CharacterSlotSystem::loadCatalog();
/* Age selector */
std::vector<Ogre::String> ages = CharacterSlotSystem::getAges();
Ogre::String currentAge = cs.age;
if (ImGui::BeginCombo("Age", currentAge.c_str())) {
for (const auto &age : ages) {
bool isSelected = (currentAge == age);
if (ImGui::Selectable(age.c_str(), isSelected)) {
cs.age = age;
modified = true;
cs.dirty = true;
}
if (isSelected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
/* Get current age from registry */
Ogre::String currentAge = "adult";
if (entity.has<CharacterIdentityComponent>()) {
auto &id = entity.get<CharacterIdentityComponent>();
const CharacterRegistry::CharacterRecord *rec =
CharacterRegistry::getSingleton().findCharacter(
id.registryId);
if (rec && !rec->age.empty())
currentAge = rec->age;
}
/* Sex selector */
std::vector<Ogre::String> sexes =
CharacterSlotSystem::getSexes(cs.age);
CharacterSlotSystem::getSexes(currentAge);
Ogre::String currentSex = cs.sex;
if (ImGui::BeginCombo("Sex", currentSex.c_str())) {
for (const auto &sex : sexes) {
bool isSelected = (currentSex == sex);
if (ImGui::Selectable(sex.c_str(), isSelected)) {
if (ImGui::Selectable(sex.c_str(),
isSelected)) {
cs.sex = sex;
modified = true;
cs.dirty = true;
@@ -106,21 +105,25 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
/* Shape Keys */
if (ImGui::CollapsingHeader("Shape Keys")) {
auto shapeKeys = CharacterSlotSystem::getShapeKeyNames(
cs.age, cs.sex);
currentAge, cs.sex);
if (shapeKeys.empty()) {
ImGui::TextDisabled("No shape keys available.");
} else {
CharacterShapeKeysComponent *skc = nullptr;
if (entity.has<CharacterShapeKeysComponent>())
skc = &entity.get_mut<CharacterShapeKeysComponent>();
skc = &entity.get_mut<
CharacterShapeKeysComponent>();
else {
entity.set<CharacterShapeKeysComponent>({});
skc = &entity.get_mut<CharacterShapeKeysComponent>();
entity.set<CharacterShapeKeysComponent>(
{});
skc = &entity.get_mut<
CharacterShapeKeysComponent>();
}
for (const auto &name : shapeKeys) {
float val = skc->weights[name];
if (ImGui::SliderFloat(name.c_str(), &val, 0.0f,
if (ImGui::SliderFloat(name.c_str(),
&val, 0.0f,
1.0f)) {
skc->weights[name] = val;
skc->dirty = true;
@@ -135,9 +138,10 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
/* Slot selections */
std::vector<Ogre::String> availableSlots =
CharacterSlotSystem::getSlots(cs.age, cs.sex);
CharacterSlotSystem::getSlots(currentAge, cs.sex);
for (const auto &pair : cs.slotSelections) {
if (std::find(availableSlots.begin(), availableSlots.end(),
if (std::find(availableSlots.begin(),
availableSlots.end(),
pair.first) == availableSlots.end())
availableSlots.push_back(pair.first);
}
@@ -145,7 +149,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
for (const auto &slot : availableSlots) {
/* Ensure selection exists */
if (cs.slotSelections.find(slot) == cs.slotSelections.end()) {
if (cs.slotSelections.find(slot) ==
cs.slotSelections.end()) {
SlotSelection sel;
/* Migrate old explicit mesh if present */
auto oldIt = cs.slots.find(slot);
@@ -159,22 +164,28 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
if (ImGui::TreeNode(slot.c_str())) {
/* Resolved mesh preview */
Ogre::String resolved =
CharacterSlotSystem::resolveMesh(cs.age, cs.sex,
slot, sel,
cs.outfitLevel);
CharacterSlotSystem::resolveMesh(
currentAge, cs.sex, slot, sel,
cs.outfitLevel);
ImGui::TextDisabled("Resolved: %s",
resolved.empty() ? "(none)" :
resolved.c_str());
resolved.empty() ?
"(none)" :
resolved.c_str());
/* Explicit mesh override */
bool useExplicit = !sel.explicitMesh.empty();
if (ImGui::Checkbox("Lock explicit mesh", &useExplicit)) {
if (ImGui::Checkbox("Lock explicit mesh",
&useExplicit)) {
if (!useExplicit) {
sel.explicitMesh.clear();
} else {
/* Lock to currently resolved mesh */
sel.explicitMesh = CharacterSlotSystem::resolveMesh(
cs.age, cs.sex, slot, sel, cs.outfitLevel);
sel.explicitMesh =
CharacterSlotSystem::resolveMesh(
currentAge,
cs.sex, slot,
sel,
cs.outfitLevel);
}
modified = true;
cs.dirty = true;
@@ -183,22 +194,30 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
if (useExplicit) {
std::vector<Ogre::String> meshes =
CharacterSlotSystem::getMeshes(
cs.age, cs.sex, slot);
currentAge, cs.sex, slot);
Ogre::String preview =
sel.explicitMesh.empty() ?
"(none)" :
sel.explicitMesh;
if (ImGui::BeginCombo("Mesh", preview.c_str())) {
if (ImGui::Selectable("(none)",
sel.explicitMesh.empty())) {
if (ImGui::BeginCombo(
"Mesh", preview.c_str())) {
if (ImGui::Selectable(
"(none)",
sel.explicitMesh
.empty())) {
sel.explicitMesh.clear();
modified = true;
cs.dirty = true;
}
for (const auto &m : meshes) {
bool isSelected = (sel.explicitMesh == m);
if (ImGui::Selectable(m.c_str(), isSelected)) {
sel.explicitMesh = m;
bool isSelected =
(sel.explicitMesh ==
m);
if (ImGui::Selectable(
m.c_str(),
isSelected)) {
sel.explicitMesh =
m;
modified = true;
cs.dirty = true;
}
@@ -208,29 +227,45 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
} else {
/* Layer 1 combo */
std::vector<Ogre::String> layer1Meshes =
CharacterSlotSystem::getMeshesForLayer(
cs.age, cs.sex, slot, 1);
CharacterSlotSystem::
getMeshesForLayer(
currentAge, cs.sex,
slot, 1);
Ogre::String l1Preview = "none";
if (!sel.layer1Mesh.empty())
l1Preview = CharacterSlotSystem::getMeshLabel(
cs.age, cs.sex, slot, sel.layer1Mesh);
if (ImGui::BeginCombo("Lingerie (Layer 1)",
l1Preview.c_str())) {
if (ImGui::Selectable("none",
sel.layer1Mesh.empty() ||
sel.layer1Mesh == "none")) {
l1Preview = CharacterSlotSystem::
getMeshLabel(
currentAge, cs.sex,
slot,
sel.layer1Mesh);
if (ImGui::BeginCombo(
"Lingerie (Layer 1)",
l1Preview.c_str())) {
if (ImGui::Selectable(
"none",
sel.layer1Mesh.empty() ||
sel.layer1Mesh ==
"none")) {
sel.layer1Mesh = "none";
modified = true;
cs.dirty = true;
}
for (const auto &m : layer1Meshes) {
Ogre::String label =
CharacterSlotSystem::getMeshLabel(
cs.age, cs.sex, slot, m);
bool isSelected = (sel.layer1Mesh == m);
if (ImGui::Selectable(label.c_str(),
isSelected)) {
sel.layer1Mesh = m;
for (const auto &m :
layer1Meshes) {
Ogre::String label = CharacterSlotSystem::
getMeshLabel(
currentAge,
cs.sex,
slot,
m);
bool isSelected =
(sel.layer1Mesh ==
m);
if (ImGui::Selectable(
label.c_str(),
isSelected)) {
sel.layer1Mesh =
m;
modified = true;
cs.dirty = true;
}
@@ -240,29 +275,45 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
/* Layer 2 combo */
std::vector<Ogre::String> layer2Meshes =
CharacterSlotSystem::getMeshesForLayer(
cs.age, cs.sex, slot, 2);
CharacterSlotSystem::
getMeshesForLayer(
currentAge, cs.sex,
slot, 2);
Ogre::String l2Preview = "none";
if (!sel.layer2Mesh.empty())
l2Preview = CharacterSlotSystem::getMeshLabel(
cs.age, cs.sex, slot, sel.layer2Mesh);
if (ImGui::BeginCombo("Clothing (Layer 2)",
l2Preview.c_str())) {
if (ImGui::Selectable("none",
sel.layer2Mesh.empty() ||
sel.layer2Mesh == "none")) {
l2Preview = CharacterSlotSystem::
getMeshLabel(
currentAge, cs.sex,
slot,
sel.layer2Mesh);
if (ImGui::BeginCombo(
"Clothing (Layer 2)",
l2Preview.c_str())) {
if (ImGui::Selectable(
"none",
sel.layer2Mesh.empty() ||
sel.layer2Mesh ==
"none")) {
sel.layer2Mesh = "none";
modified = true;
cs.dirty = true;
}
for (const auto &m : layer2Meshes) {
Ogre::String label =
CharacterSlotSystem::getMeshLabel(
cs.age, cs.sex, slot, m);
bool isSelected = (sel.layer2Mesh == m);
if (ImGui::Selectable(label.c_str(),
isSelected)) {
sel.layer2Mesh = m;
for (const auto &m :
layer2Meshes) {
Ogre::String label = CharacterSlotSystem::
getMeshLabel(
currentAge,
cs.sex,
slot,
m);
bool isSelected =
(sel.layer2Mesh ==
m);
if (ImGui::Selectable(
label.c_str(),
isSelected)) {
sel.layer2Mesh =
m;
modified = true;
cs.dirty = true;
}