Compare commits
3 Commits
0a5edacf8a
...
d4974f5d90
| Author | SHA1 | Date | |
|---|---|---|---|
| d4974f5d90 | |||
| 68ca1f750d | |||
| 0ed5d9a970 |
@@ -0,0 +1,370 @@
|
||||
# World2 Game Project - Agent Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
World2 is a 3D life simulation game built using the OGRE3D rendering engine. The project features an Entity-Component-System (ECS) architecture powered by Flecs, with Jolt Physics for simulation, Lua scripting for story content, and ImGui for user interfaces.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Dependencies
|
||||
- **Rendering**: OGRE3D 14.x+ (Object-Oriented Graphics Rendering Engine)
|
||||
- **ECS Framework**: Flecs (Fast Lightweight Entity Component System)
|
||||
- **Physics**: Jolt Physics with double precision support (JPH_DOUBLE_PRECISION)
|
||||
- **Scripting**: Lua 5.4 with LPEG library
|
||||
- **Audio**: miniaudio (single-header audio library)
|
||||
- **UI**: ImGui (integrated via Ogre::ImGuiOverlay)
|
||||
- **Model Loading**: Assimp with pugixml
|
||||
- **Navigation**: Recast/Detour for crowd simulation
|
||||
- **Procedural Generation**: OgreProcedural
|
||||
- **Profiling**: Tracy frame profiler
|
||||
|
||||
### Build Requirements
|
||||
- CMake 3.13+
|
||||
- C++17 compatible compiler
|
||||
- Blender (for asset processing, path configurable via `BLENDER` CMake variable)
|
||||
- OGRE SDK with components: Bites, Paging, Terrain, MeshLodGenerator
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── CMakeLists.txt # Main build configuration
|
||||
├── resources.cfg # OGRE resource locations
|
||||
├── .clang-format # Linux kernel code style config
|
||||
│
|
||||
├── Game.cpp # Main game executable entry point (legacy)
|
||||
├── Editor.cpp # World editor executable (legacy)
|
||||
├── Procedural.cpp # Procedural generation test / character controller demo (legacy)
|
||||
├── Bootstrap.cpp # Legacy character controller demo (legacy)
|
||||
├── terrain.cpp # Terrain system test executable (legacy)
|
||||
│
|
||||
├── src/ # Source code
|
||||
│ ├── gamedata/ # Core ECS modules and game logic (legacy)
|
||||
│ │ ├── GameData.cpp/h # ECS world setup and management (legacy)
|
||||
│ │ ├── Components.h # ECS component definitions (legacy)
|
||||
│ │ ├── *Module.cpp/h # Individual ECS modules (legacy)
|
||||
│ │ └── items/ # Game item implementations (harbour, temple, town, etc.) (legacy)
|
||||
│ ├── lua/ # Lua integration
|
||||
│ │ ├── lua-5.4.8/ # Lua source code
|
||||
│ │ └── lpeg-1.1.0/ # LPEG parsing library
|
||||
│ ├── physics/ # Jolt Physics wrapper (physics.h/cpp) (legacy - for buildiung legacy code only)
|
||||
│ ├── sound/ # Audio system (miniaudio)
|
||||
│ ├── crowd/ # Recast/Detour navigation
|
||||
│ │ ├── include/ # Crowd headers (OgreRecast, OgreDetourCrowd, etc.)
|
||||
│ │ └── src/ # Crowd implementations
|
||||
│ ├── editor/ # Editor-specific code (legacy)
|
||||
│ │ ├── EditorGizmoModule.h/cpp
|
||||
│ │ ├── EditorInputModule.h/cpp
|
||||
│ │ └── main.cpp
|
||||
│ ├── features/ # Feature modules
|
||||
│ │ ├── characters/ # Character rendering test
|
||||
│ │ ├── editScene/ # Scene editor with ECS, it has game mode and all features, current work is here
|
||||
│ │ └── sceneEditor/ # some other editor code
|
||||
│ ├── world/ # World generation and GOAP AI (legacy)
|
||||
│ ├── terrain/ # Terrain system (legacy)
|
||||
│ ├── tests/ # Test utilities
|
||||
│ ├── text_editor/ # ImGui text editor component
|
||||
│ ├── miniaudio/ # Audio library
|
||||
│ ├── sceneloader/ # Scene loading utilitie (legacy)
|
||||
│ ├── aitoolkit/ # AI utilities (GOAP, FSM, Behavior Trees) (legacy)
|
||||
│ └── vehicles/ # Vehicle definitions (legacy)
|
||||
│
|
||||
├── assets/ # Source assets (Blender files)
|
||||
│ ├── blender/ # Blender source files
|
||||
│ │ ├── buildings/ # Building models (.blend)
|
||||
│ │ ├── characters/ # Character models and clothes (characters asset pipeline)
|
||||
│ │ ├── vehicles/ # Vehicle models
|
||||
│ │ ├── scripts/ # Blender Python scripts for export
|
||||
│ │ └── world/ # World terrain models
|
||||
│ ├── textures/ # Source textures
|
||||
│ └── fonts/ # Game fonts
|
||||
│
|
||||
├── lua-scripts/ # Lua game scripts
|
||||
│ ├── narrator/ # Ink narrative parser
|
||||
│ ├── stories/ # Story scripts (.ink format, compiled to .lua)
|
||||
│ └── json/ # JSON library for Lua
|
||||
│
|
||||
├── water/ # Water rendering system (legacy)
|
||||
│ ├── water.h/cpp # Water class
|
||||
│ ├── *.frag, *.vert # GLSL shaders
|
||||
│ └── water.compositor # OGRE compositor script
|
||||
│
|
||||
├── skybox/ # Sky rendering resources
|
||||
├── resources/ # Runtime resources (build output)
|
||||
├── audio/ # Audio resources
|
||||
├── morph/ # Character morphing data
|
||||
└── build/ # build directory
|
||||
└── build-vscode/ # VSCode build directory
|
||||
```
|
||||
## Critical Constraints & Rules
|
||||
- The source code is the only source of truth. Please check code as documentation might be out of sync.
|
||||
- Do not modify legacy code. Current code is at src/features/editScene and its dependencies.
|
||||
- Please ask questions if anything is unclear.
|
||||
- Please update tests, documentation and examples every time API or modules and systems change.
|
||||
|
||||
|
||||
## Build Instructions
|
||||
|
||||
### Prerequisites
|
||||
1. Install OGRE3D SDK with Bites, Paging, Terrain, MeshLodGenerator components
|
||||
2. Install dependencies: Jolt Physics, Flecs, Assimp, pugixml, OgreProcedural, Tracy, SDL2, ZLIB
|
||||
3. Set `CMAKE_PREFIX_PATH` to OGRE installation
|
||||
4. Configure Blender path in CMake (default: `${CMAKE_SOURCE_DIR}/../../blender-bin/bin/blender`)
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Configure (adjust paths as needed)
|
||||
cmake -B build -S . -DCMAKE_PREFIX_PATH=/path/to/ogre-sdk
|
||||
|
||||
# Build all targets
|
||||
cmake --build build
|
||||
|
||||
# Build specific targets
|
||||
cmake --build build --target Game # Main game executable (legacy)
|
||||
cmake --build build --target Editor # World editor (legacy)
|
||||
cmake --build build --target Procedural # Procedural test (legacy)
|
||||
cmake --build build --target TerrainTest # Terrain test (legacy)
|
||||
cmake --build build --target TerrainTest # Terrain test (legacy)
|
||||
cmake --build build --target editSceneEditor # Current executable for scene editor and game
|
||||
```
|
||||
|
||||
### Build Outputs
|
||||
- `build/Game` - Main game executable (legacy)
|
||||
- `build/Editor` - World editor executable (legacy)
|
||||
- `build/Procedural` - Procedural generation test (legacy)
|
||||
- `build/TerrainTest` - Terrain system test (legacy)
|
||||
- `build//src/features/editScene/editSceneEditor` - current executable
|
||||
- `build/resources/` - Staged game resources
|
||||
- `build/characters/` - Processed character models
|
||||
- `build/water/` - Water assets (legacy)
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
This project follows the **Linux Kernel Coding Style** for C++:
|
||||
|
||||
### Formatting Rules
|
||||
- **Indentation**: 8-character tabs (UseTab: Always)
|
||||
- **Line Length**: 80 characters maximum
|
||||
- **Braces**:
|
||||
- Functions: Opening brace on new line
|
||||
- Control structures: Opening brace on same line
|
||||
- **Pointer Alignment**: Right (`int *ptr` not `int* ptr`)
|
||||
- **Namespaces**: No indentation inside namespaces
|
||||
|
||||
### Example
|
||||
```cpp
|
||||
void myFunction(int arg)
|
||||
{
|
||||
if (arg > 0) {
|
||||
doSomething();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < arg; i++) {
|
||||
process(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running clang-format
|
||||
```bash
|
||||
clang-format -i src/gamedata/*.cpp src/gamedata/*.h
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### ECS (Entity Component System)
|
||||
|
||||
The game uses Flecs for ECS architecture. Core systems:
|
||||
|
||||
#### Components (src/gamedata/Components.h) (legacy)
|
||||
- `GameData` - Global game state
|
||||
- `EngineData` - Engine configuration (SceneManager, delta time, debug draw)
|
||||
- `Input` - Input state (keyboard, mouse, controls)
|
||||
- `Camera` - Camera node and configuration
|
||||
- `RenderWindow` - Window and DPI information
|
||||
- `CollisionShape` - Physics shape references
|
||||
- `EditorSceneSwitch` - Editor scene switching
|
||||
- `GameState` - Game running state
|
||||
|
||||
#### Modules (src/gamedata/*Module.h) (legacy)
|
||||
Modules are ECS systems organized by functionality:
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| AppModule | Application context management |
|
||||
| GUIModule | ImGui interface management |
|
||||
| EditorGUIModule | Editor UI |
|
||||
| PhysicsModule | Jolt physics integration |
|
||||
| CharacterModule | Character spawning and management |
|
||||
| CharacterManagerModule | Player/NPC character management |
|
||||
| CharacterAnimationModule | Animation state machines |
|
||||
| CharacterAIModule | AI behavior (GOAP-based) |
|
||||
| TerrainModule | Terrain generation and rendering |
|
||||
| WaterModule | Water surface and physics |
|
||||
| SunModule | Day/night cycle and lighting |
|
||||
| WorldMapModule | World map overlay |
|
||||
| QuestModule | Quest system |
|
||||
| EventModule | Game event handling |
|
||||
| EventTriggerModule | Event triggers |
|
||||
| SlotsModule | Inventory slot management |
|
||||
| BoatModule | Boat physics and control |
|
||||
| VehicleManagerModule | Vehicle management |
|
||||
| PlayerActionModule | Player action handling |
|
||||
| LuaModule | Lua scripting integration |
|
||||
| StaticGeometryModule | Static geometry management |
|
||||
|
||||
### EditScene Modules (`src/features/editScene`)
|
||||
The editor/game mode executable adds its own ECS components.
|
||||
|
||||
The editor/game mode executable adds its own ECS modules:
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| CharacterSlotSystem | Catalog-driven multi-slot character mesh builder |
|
||||
| CharacterSpawnerSystem | Distance-based spawn/despawn of registry characters |
|
||||
| PlayerControllerSystem | Player input, camera, locomotion and animation state |
|
||||
| ActuatorSystem | Player interaction prompts and smart-object/item actions |
|
||||
| EditorUISystem | ImGui property panels and scene management |
|
||||
| ProceduralTextureSystem | Runtime procedural texture generation |
|
||||
| ProceduralMaterialSystem | Runtime procedural material creation |
|
||||
|
||||
#### PlayerControllerSystem
|
||||
|
||||
`PlayerControllerSystem` reads the shared `GameInputState`, drives the TPS/FPS
|
||||
camera and sets the locomotion animation state (`idle`/`walking`/`running` and
|
||||
the swim equivalents) on the controlled entity. In game mode it adds the
|
||||
`PlayerControlledComponent` tag to the target so AI systems know to leave it
|
||||
alone.
|
||||
|
||||
When a controller's `targetCharacterName` points at an entity with
|
||||
`CharacterSpawnerComponent`, the system locks the spawner and forces an
|
||||
immediate spawn; the actual controlled entity is the spawned character
|
||||
instance, not the spawner.
|
||||
|
||||
#### Player Character Resolution
|
||||
|
||||
Systems that need the live player character (inventory UI, character sheet,
|
||||
save/load, actuators) must not resolve the controller's
|
||||
`targetCharacterName` against `EntityNameComponent` directly – that returns the
|
||||
spawner entity when the controller targets a spawner. Instead use:
|
||||
|
||||
```cpp
|
||||
flecs::entity player = editorApp->getPlayerCharacterEntity();
|
||||
```
|
||||
|
||||
`EditorApp::getPlayerCharacterEntity()` returns the controller's current
|
||||
target, spawning the character through `CharacterSpawnerSystem` if necessary.
|
||||
`CharacterSpawnerSystem::getSpawnerForCharacter()` can be used in save/load to
|
||||
keep the controller pointed at the spawner rather than the transient instance.
|
||||
|
||||
#### Game Mode Input
|
||||
|
||||
Game mode input is kept in sync with the physical keyboard each frame by
|
||||
calling `SDL_PumpEvents()` followed by `SDL_GetKeyboardState()`. This prevents
|
||||
missed `keyReleased` events from leaving movement keys stuck (the original
|
||||
cause of the "infinite slow walk" after releasing Shift+W). Event-driven
|
||||
`keyPressed`/`keyReleased` handlers are still used for one-shot actions such
|
||||
as `ePressed`, `fPressed` and `iPressed`.
|
||||
|
||||
#### CharacterSpawnerComponent
|
||||
|
||||
- `registryId` - Character registry ID to spawn.
|
||||
- `spawnDistanceSq` - Squared distance to camera at which the character spawns (default 100²).
|
||||
- `despawnDistanceSq` - Squared distance to camera at which the character despawns (default 200²).
|
||||
|
||||
The spawner uses the entity's `TransformComponent` for spawn position/rotation. When the spawner itself moves/rotates/scales in the editor, the spawned character is updated to match; when the spawner is stationary, the character is left alone so it can move on its own. Spawned characters are created without `EditorMarkerComponent` and removed from the editor UI cache so they remain visible but are not selectable/editable in the editor. Registry changes that bump the character version trigger an immediate respawn. Scene serialization stores plain `spawnDistance`/`despawnDistance` values for readability while the component keeps squared values for comparisons.
|
||||
|
||||
### Physics System
|
||||
|
||||
Uses Jolt Physics with custom wrapper (legacy - `src/physics/physics.h`, current - `src/features/editScene/physics/physics.h`):
|
||||
|
||||
```cpp
|
||||
// Example: Creating a physics body
|
||||
JoltPhysicsWrapper *physics = new JoltPhysicsWrapper(scnMgr, cameraNode);
|
||||
JPH::ShapeRefC shape = physics->createBoxShape(Ogre::Vector3(1, 1, 1));
|
||||
JPH::BodyID body = physics->createBody(shape, 1.0f, position, rotation,
|
||||
JPH::EMotionType::Dynamic, Layers::MOVING);
|
||||
```
|
||||
|
||||
### Rendering Pipeline
|
||||
|
||||
1. **Main Render**: Standard OGRE render with RTSS (Real Time Shader System)
|
||||
2. **Water**: Reflection/refraction with custom shaders
|
||||
3. **Sky**: Dynamic skybox with day/night cycle
|
||||
4. **Shadows**: Configurable shadow techniques
|
||||
|
||||
### Asset Pipeline
|
||||
|
||||
1. **Source**: Blender `.blend` files in `assets/`
|
||||
2. **Export**: Python scripts in `assets/blender/scripts/`
|
||||
- `export_buildings.py` - Building export to glTF
|
||||
- `export_characters_ogre.py` - Character export to OGRE format
|
||||
- `export_vehicles.py` - Vehicle export
|
||||
- `import_vrm.py` - VRM character import
|
||||
3. **Build**: CMake processes assets during build
|
||||
4. **Runtime**: Assets loaded from `resources.cfg` paths
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Module Registration
|
||||
Modules register themselves with Flecs using the `FLECS_CPP_NO_AUTO_REGISTRATION` macro:
|
||||
|
||||
```cpp
|
||||
// In module header
|
||||
struct MyModule {
|
||||
MyModule(flecs::world &ecs);
|
||||
};
|
||||
|
||||
// In module implementation
|
||||
MyModule::MyModule(flecs::world &ecs) {
|
||||
// Register systems, components, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Setup
|
||||
Scenes are configured through ECS setup functions:
|
||||
|
||||
```cpp
|
||||
// Setup exterior game scene
|
||||
ECS::setupExteriorScene(scnMgr, cameraNode, camera, renderWindow);
|
||||
|
||||
// Setup interior scene
|
||||
ECS::setupInteriorScene(scnMgr, cameraNode, camera, renderWindow);
|
||||
|
||||
// Setup editor
|
||||
ECS::setupEditor(scnMgr, cameraNode, camera, renderWindow);
|
||||
```
|
||||
|
||||
### Lua Scripting
|
||||
Story content is written in Ink format and parsed to Lua:
|
||||
- Source: `lua-scripts/stories/*.ink`
|
||||
- Compiled: `lua-scripts/stories/*.lua`
|
||||
- Runtime: Narrator parser loads and executes stories
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Adding Assets
|
||||
1. Create/modify `.blend` file in `assets/blender/`
|
||||
2. Add export logic to relevant Python script if needed
|
||||
3. Add CMake custom command in `CMakeLists.txt` if new asset type
|
||||
4. Rebuild to process assets
|
||||
|
||||
### Debugging
|
||||
- Use `ECS::EngineData.enableDbgDraw` for physics debug visualization
|
||||
- Tracy profiler integration for performance analysis
|
||||
- FPS and draw call stats printed to console
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Lua scripts run in sandboxed environment
|
||||
- File system access restricted to resource directories
|
||||
- No network functionality currently implemented
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **OGRE3D Docs**: https://ogrecave.github.io/ogre/api/latest/
|
||||
- **Flecs Manual**: https://www.flecs.dev/flecs/
|
||||
- **Jolt Physics**: https://jrouwe.github.io/JoltPhysics/
|
||||
- **Ink Narrative**: https://www.inklestudios.com/ink/
|
||||
@@ -0,0 +1,222 @@
|
||||
# EditScene Feature - Agent Guide
|
||||
|
||||
This guide covers the `src/features/editScene` sub-project: the OGRE3D/Flecs scene
|
||||
editor and the `--game` playable mode built on top of it.
|
||||
|
||||
## Overview
|
||||
|
||||
`editScene` is a self-contained executable (`editSceneEditor`) that can run in two
|
||||
modes:
|
||||
|
||||
- **Editor mode** (default): load, edit and save JSON scenes through ImGui panels.
|
||||
- **Game mode** (`--game`): present a startup menu, load a base scene, and let the
|
||||
player control a character with save/load support.
|
||||
|
||||
The feature is built from `src/features/editScene/CMakeLists.txt` and links against
|
||||
OGRE (Bites, Overlay, MeshLodGenerator), Flecs, nlohmann/json, SDL2, Jolt and
|
||||
OgreProcedural. Recast/Detour is built from the copy in
|
||||
`src/features/editScene/recastnavigation`.
|
||||
|
||||
## Build & Run
|
||||
|
||||
The CMake target is `editSceneEditor`. The working directory for the runtime must
|
||||
be the executable's own directory because resources are copied next to it at build
|
||||
time.
|
||||
|
||||
```bash
|
||||
# From the project root
|
||||
cmake -B build-vscode -S . -DCMAKE_PREFIX_PATH=/path/to/ogre-sdk
|
||||
cmake --build build-vscode --target editSceneEditor -j4
|
||||
|
||||
# Run editor
|
||||
cd build-vscode/src/features/editScene
|
||||
./editSceneEditor
|
||||
|
||||
# Run game mode (loads the configured base scene through the startup menu)
|
||||
./editSceneEditor --game
|
||||
|
||||
# Auto-load a scene in editor mode
|
||||
./editSceneEditor /path/to/scene.json
|
||||
```
|
||||
|
||||
The main test target is `component_lua_test`:
|
||||
|
||||
```bash
|
||||
cmake --build build-vscode --target component_lua_test -j4
|
||||
./build-vscode/src/features/editScene/component_lua_test
|
||||
```
|
||||
|
||||
## Entry Point & Modes
|
||||
|
||||
- `main.cpp` parses `--game` and `--debug-buoyancy`, sets the mode on
|
||||
`EditorApp`, calls `initApp()` and enters the OGRE render loop.
|
||||
- `EditorApp` owns all systems and implements `frameRenderingQueued()`, which is
|
||||
the per-frame update function.
|
||||
- Modes/states are also exposed globally through `GameMode.hpp` so systems can
|
||||
query `editScene::isGameMode()` / `editScene::isGamePlaying()` without a pointer
|
||||
to `EditorApp`.
|
||||
|
||||
| Mode | Purpose |
|
||||
|------|---------|
|
||||
| `EditorApp::GameMode::Editor` | Free-camera scene editing, full UI panels |
|
||||
| `EditorApp::GameMode::Game` | Playable mode with menu / playing / paused states |
|
||||
|
||||
| Game play state | Purpose |
|
||||
|-----------------|---------|
|
||||
| `Menu` | Startup menu, game not running yet |
|
||||
| `Playing` | Player controller and gameplay systems active |
|
||||
| `Paused` | Pause menu / character sheet open, gameplay systems frozen |
|
||||
|
||||
## Frame Update Order
|
||||
|
||||
`EditorApp::frameRenderingQueued()` updates systems in this order when not paused:
|
||||
|
||||
1. `PlayerControllerSystem` (only in `Playing`)
|
||||
2. `CharacterSpawnerSystem`
|
||||
3. `CharacterSlotSystem`
|
||||
4. `AnimationTreeSystem` / `BehaviorTreeSystem`
|
||||
5. `PathFollowingSystem`
|
||||
6. `ProceduralMeshSystem`
|
||||
7. `RoomLayoutSystem`
|
||||
8. `CellGridSystem`
|
||||
9. `NormalDebugSystem`
|
||||
10. `NavMeshSystem`
|
||||
11. `SmartObjectSystem`
|
||||
12. `GoapPlannerSystem`
|
||||
13. `GoapRunnerSystem`
|
||||
14. `ActuatorSystem`
|
||||
15. `EventHandlerSystem`
|
||||
16. `CharacterSystem`
|
||||
17. `BuoyancySystem`
|
||||
18. `PhysicsSystem`
|
||||
19. `HairPhysicsSystem` pose read-back
|
||||
20. Rendering support systems (sun, skybox, water, light, LOD, etc.)
|
||||
|
||||
Systems that write animation state or velocity should respect systems that run
|
||||
before them. In particular, `PlayerControllerSystem` runs first and is the only
|
||||
system that should drive player locomotion animation.
|
||||
|
||||
## Important Subsystems
|
||||
|
||||
### PlayerControllerSystem
|
||||
|
||||
- Reads `GameInputState` and drives TPS/FPS camera and locomotion animation.
|
||||
- Sets animation states on the `locomotion` state machine:
|
||||
`idle` / `walking` / `running` and the swim equivalents.
|
||||
- In game mode it adds `PlayerControlledComponent` to the target entity so AI
|
||||
systems skip it.
|
||||
- When `targetCharacterName` resolves to a `CharacterSpawnerComponent`, it locks
|
||||
the spawner and forces a spawn; the controlled entity is the spawned instance,
|
||||
not the spawner.
|
||||
|
||||
### CharacterSpawnerSystem
|
||||
|
||||
Distance-based spawn/despawn of registry characters. Special API for player
|
||||
controller integration:
|
||||
|
||||
```cpp
|
||||
void spawn(flecs::entity spawner);
|
||||
void lockSpawner(flecs::entity spawner); // keep alive even out of range
|
||||
void unlockSpawner(flecs::entity spawner);
|
||||
flecs::entity getSpawnedCharacter(flecs::entity spawner) const;
|
||||
flecs::entity getSpawnerForCharacter(flecs::entity character) const;
|
||||
bool isSpawnerLocked(flecs::entity spawner) const;
|
||||
```
|
||||
|
||||
Spawned characters have `EditorMarkerComponent` removed and are removed from
|
||||
`EditorUISystem` caches so they don't appear in editor lists.
|
||||
|
||||
### Player Character Resolution
|
||||
|
||||
Never resolve the player by matching `PlayerControllerComponent::targetCharacterName`
|
||||
against `EntityNameComponent` – when the controller targets a spawner that returns
|
||||
the spawner entity, not the character. Use the central helper:
|
||||
|
||||
```cpp
|
||||
flecs::entity player = editorApp->getPlayerCharacterEntity();
|
||||
```
|
||||
|
||||
This returns the live target (spawning through the spawner if necessary). It is
|
||||
used by inventory/character-sheet UI, save/load and `ActuatorSystem`.
|
||||
|
||||
### Game Mode Input
|
||||
|
||||
Movement keys are polled each frame with `SDL_PumpEvents()` + `SDL_GetKeyboardState()`
|
||||
to avoid stuck keys caused by missed `keyReleased` events. One-shot action flags
|
||||
(`ePressed`, `fPressed`, `iPressed`) are still set from event handlers.
|
||||
|
||||
### AnimationTreeSystem & Root Motion
|
||||
|
||||
- Selects a root bone (`Root`, `mixamorig:Hips`, then `Spineroot`), freezes it,
|
||||
and excludes it from animation via a blend mask.
|
||||
- Computes `CharacterComponent::linearVelocity` from root-bone displacement when
|
||||
`AnimationTreeComponent::useRootMotion` is true.
|
||||
- `CharacterSystem` then applies that velocity to the Jolt physics character.
|
||||
|
||||
### ActuatorSystem
|
||||
|
||||
Handles player interaction prompts and executes smart-object/item actions. It
|
||||
uses `EditorApp::getPlayerCharacterEntity()` to know which character performs the
|
||||
action. Actions run through `BehaviorTreeSystem` and lock player input while
|
||||
running (`PlayerControllerComponent::inputLocked`).
|
||||
|
||||
### Save/Load
|
||||
|
||||
Game-mode saves are JSON files in the OS user-data directory (see
|
||||
`docs/SaveLoadSystem.md`). Key fields:
|
||||
|
||||
- `baseScene` – the scene file loaded as the foundation
|
||||
- `playerCharacterId` – registry ID of the player character
|
||||
- `characterRegistry` – full character registry state
|
||||
- `runtimeEntities` – runtime-spawned entities (dropped items, etc.)
|
||||
- `characterRuntimeData` – per-character component overrides
|
||||
- `luaData` – data from Lua save callbacks
|
||||
|
||||
On load, if the saved character is owned by a spawner, the controller target is
|
||||
restored to the spawner name so the character respawns correctly.
|
||||
|
||||
## Adding a New Component
|
||||
|
||||
1. Define the component struct in `components/MyComponent.hpp`.
|
||||
2. Register it for UI editing in `components/MyComponentModule.cpp` using
|
||||
`REGISTER_COMPONENT_GROUP(...)` and `registry.registerComponent<T>(...)`.
|
||||
3. Implement an editor in `ui/MyComponentEditor.hpp` / `.cpp` deriving from
|
||||
`ComponentEditor`.
|
||||
4. If the component should be saved/loaded, add serialization support in
|
||||
`systems/SceneSerializer.cpp`.
|
||||
5. Add a Lua binding in `lua/LuaComponentApi.cpp` if needed.
|
||||
6. Add a component-module test case in `tests/component_lua_test.cpp`.
|
||||
|
||||
## Adding a New System
|
||||
|
||||
1. Create `systems/MySystem.hpp` and `systems/MySystem.cpp`.
|
||||
2. Add the source files to `CMakeLists.txt`.
|
||||
3. Instantiate the system in `EditorApp::setup()` and wire any dependencies.
|
||||
4. Call `update()` from `EditorApp::frameRenderingQueued()` in the appropriate
|
||||
order.
|
||||
5. Skip `PlayerControlledComponent` entities if the system would override player
|
||||
animation or movement.
|
||||
|
||||
## Critical Constraints & Rules
|
||||
- The source code is the only source of truth. Please check code as documentation might be out of sync.
|
||||
- Please ask questions if anything is unclear.
|
||||
- Please update tests, documentation and examples every time API or modules and systems change.
|
||||
Make sure documentation, tests and examples are always in sync.
|
||||
|
||||
## Conventions & Caveats
|
||||
|
||||
- Use `editScene::isGameMode()` / `editScene::isGamePlaying()` for mode checks
|
||||
when you don't have an `EditorApp` pointer.
|
||||
- Systems that set animation states must not affect player-controlled entities;
|
||||
check `e.has<PlayerControlledComponent>()` early.
|
||||
- Spawner-targeted characters are transient instances; persistent player data
|
||||
(inventory, identity) must live on the spawned instance, not the spawner.
|
||||
- The editor uses `EditorMarkerComponent` to know which entities belong to the
|
||||
scene file. Spawned/runtime characters usually don't have this tag.
|
||||
- Keep changes in `EditorApp::frameRenderingQueued()` order-aware; systems later
|
||||
in the list should not stomp state produced by earlier systems.
|
||||
|
||||
## Additional Docs
|
||||
|
||||
- `docs/SaveLoadSystem.md` – save file format and API details
|
||||
- Root `AGENTS.md` – project-wide build, style and architecture notes
|
||||
@@ -393,6 +393,9 @@ void EditorApp::setup()
|
||||
m_characterSlotSystem = std::make_unique<CharacterSlotSystem>(
|
||||
m_world, m_sceneMgr);
|
||||
m_characterSlotSystem->initialize();
|
||||
if (m_uiSystem)
|
||||
m_uiSystem->getCharacterRegistry().setCharacterSlotSystem(
|
||||
m_characterSlotSystem.get());
|
||||
|
||||
// Setup AnimationTree system
|
||||
m_animationTreeSystem = std::make_unique<AnimationTreeSystem>(
|
||||
@@ -989,12 +992,11 @@ void EditorApp::loadGame(const std::string &slotPath)
|
||||
ItemStateRegistry::getInstance().deserialize(
|
||||
saveData["itemState"]);
|
||||
|
||||
/* Destroy ALL characters (including editor-placed prefab
|
||||
* instances that may lack CharacterIdentityComponent) so the
|
||||
* registry spawn is the only source of characters. */
|
||||
/* Destroy ALL spawned characters so the registry spawn is the
|
||||
* only source of characters. */
|
||||
std::vector<flecs::entity> charsToDestroy;
|
||||
m_world.query<CharacterSlotsComponent>().each(
|
||||
[&](flecs::entity e, CharacterSlotsComponent &) {
|
||||
m_world.query<CharacterIdentityComponent>().each(
|
||||
[&](flecs::entity e, CharacterIdentityComponent &) {
|
||||
charsToDestroy.push_back(e);
|
||||
});
|
||||
for (auto e : charsToDestroy) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Generate a unique persistent identifier.
|
||||
* The value is generated once and should be stored in a component field;
|
||||
* it will survive scene save/load because it is serialized with the component.
|
||||
*/
|
||||
inline std::string generatePersistentId()
|
||||
{
|
||||
static std::atomic<uint64_t> s_counter{ 0 };
|
||||
|
||||
uint64_t counter = ++s_counter;
|
||||
uint64_t timestamp = static_cast<uint64_t>(
|
||||
std::chrono::high_resolution_clock::now()
|
||||
.time_since_epoch()
|
||||
.count());
|
||||
|
||||
return "id_" + std::to_string(timestamp) + "_" +
|
||||
std::to_string(counter);
|
||||
}
|
||||
@@ -20,42 +20,24 @@ struct SlotSelection {
|
||||
};
|
||||
|
||||
/**
|
||||
* Multi-slot mesh component for character parts sharing a skeleton.
|
||||
* The "face" slot (or first available slot) serves as the master skeleton.
|
||||
* Deprecated marker component for characters.
|
||||
*
|
||||
* Age is now stored in CharacterRegistry::CharacterRecord::age
|
||||
* and should be retrieved from there when needed.
|
||||
* All persistent character appearance data (sex, slot selections, shape keys,
|
||||
* front axis) now lives in CharacterRegistry::CharacterRecord. This component
|
||||
* is kept only as a temporary reminder for scenes that still contain it; it
|
||||
* is not serialized, cannot be added from the editor, and does not affect
|
||||
* runtime behavior.
|
||||
*/
|
||||
struct CharacterSlotsComponent {
|
||||
Ogre::String sex = "male";
|
||||
|
||||
/* Backward-compat: old mesh-name map. Deserialized into slotSelections on load. */
|
||||
std::unordered_map<Ogre::String, Ogre::String> slots;
|
||||
|
||||
/* Per-slot layer selections (runtime) */
|
||||
std::unordered_map<Ogre::String, SlotSelection> slotSelections;
|
||||
|
||||
bool dirty = true;
|
||||
|
||||
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
|
||||
Ogre::Entity *masterEntity = nullptr;
|
||||
|
||||
/**
|
||||
* Front-facing axis for this character model.
|
||||
* Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z.
|
||||
*/
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
|
||||
CharacterSlotsComponent() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global shape key weights for a character.
|
||||
* Same name applies to all slots uniformly.
|
||||
* Deprecated marker component for character shape keys.
|
||||
*
|
||||
* Shape key weights are now stored in
|
||||
* CharacterRegistry::CharacterRecord::inlineShapeKeyWeights.
|
||||
*/
|
||||
struct CharacterShapeKeysComponent {
|
||||
std::unordered_map<Ogre::String, float> weights;
|
||||
bool dirty = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTERSLOTS_HPP
|
||||
|
||||
@@ -13,19 +13,9 @@ REGISTER_COMPONENT_GROUP("Character Slots", "Rendering",
|
||||
"Character Slots",
|
||||
"Rendering",
|
||||
std::make_unique<CharacterSlotsEditor>(sceneMgr),
|
||||
/* Adder */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
CharacterSlotsComponent cs;
|
||||
cs.dirty = true;
|
||||
e.set<CharacterSlotsComponent>(cs);
|
||||
},
|
||||
/* Adder: disabled; this component is deprecated and cannot be
|
||||
* created from the editor. */
|
||||
nullptr,
|
||||
/* Remover */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
(void)sceneMgr;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Utils.hpp"
|
||||
#include <string>
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
@@ -18,6 +19,11 @@ struct ProceduralMaterialComponent {
|
||||
// Material name for Ogre resource
|
||||
std::string materialName;
|
||||
|
||||
ProceduralMaterialComponent()
|
||||
{
|
||||
materialId = generatePersistentId();
|
||||
}
|
||||
|
||||
// Persistent reference to texture by ID
|
||||
std::string diffuseTextureId;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Utils.hpp"
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
@@ -78,6 +79,9 @@ struct ProceduralTextureComponent {
|
||||
|
||||
ProceduralTextureComponent()
|
||||
{
|
||||
// Unique persistent ID for referencing this texture across saves.
|
||||
textureId = generatePersistentId();
|
||||
|
||||
// Initialize with default colors (checkerboard pattern)
|
||||
for (int i = 0; i < RECT_COUNT; ++i) {
|
||||
int row = i / GRID_SIZE;
|
||||
|
||||
@@ -520,156 +520,53 @@ static void registerAllComponents()
|
||||
c.useGravity = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- CharacterSlots ---
|
||||
REGISTER_COMPONENT(
|
||||
CharacterSlotsComponent, "CharacterSlots",
|
||||
{ // 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);
|
||||
lua_pushstring(L, kv.second.layer1Mesh.c_str());
|
||||
lua_setfield(L, -2, "layer1Mesh");
|
||||
lua_pushstring(L, kv.second.layer2Mesh.c_str());
|
||||
lua_setfield(L, -2, "layer2Mesh");
|
||||
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");
|
||||
{ // Push: outfitLevel from registry
|
||||
int outfitLevel = 2;
|
||||
if (e.has<CharacterIdentityComponent>()) {
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
const CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton()
|
||||
.findCharacter(id.registryId);
|
||||
if (rec)
|
||||
outfitLevel = rec->inlineOutfitLevel;
|
||||
}
|
||||
lua_pushinteger(L, outfitLevel);
|
||||
} lua_setfield(L, -2, "outfitLevel");
|
||||
pushVector3(L, c.frontAxis); lua_setfield(L, -2, "frontAxis");
|
||||
,
|
||||
{ // Read: age into registry
|
||||
if (lua_getfield(L, idx, "age"), lua_isstring(L, -1)) {
|
||||
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();
|
||||
CharacterRegistry::getSingleton()
|
||||
.markCharacterDirty(
|
||||
id.registryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "sex"), lua_isstring(L, -1))
|
||||
c.sex = lua_tostring(L, -1);
|
||||
lua_pop(L, 1); { // Read: outfitLevel into registry
|
||||
if (lua_getfield(L, idx, "outfitLevel"),
|
||||
lua_isnumber(L, -1)) {
|
||||
int outfitLevel = (int)lua_tointeger(L, -1);
|
||||
if (e.has<CharacterIdentityComponent>()) {
|
||||
auto &id = e.get<
|
||||
CharacterIdentityComponent>();
|
||||
CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton()
|
||||
.findCharacter(
|
||||
id.registryId);
|
||||
if (rec) {
|
||||
rec->inlineOutfitLevel =
|
||||
outfitLevel;
|
||||
CharacterRegistry::getSingleton()
|
||||
.autoSave();
|
||||
CharacterRegistry::getSingleton()
|
||||
.markCharacterDirty(
|
||||
id.registryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "slots"), lua_istable(L, -1)) {
|
||||
c.slots.clear();
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1))
|
||||
c.slots[lua_tostring(L, -2)] =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} lua_pop(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);
|
||||
lua_pop(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);
|
||||
lua_pop(L, 1);
|
||||
c.slotSelections[slotName] = sel;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "frontAxis"), lua_istable(L, -1))
|
||||
c.frontAxis = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- CharacterShapeKeys ---
|
||||
REGISTER_COMPONENT(
|
||||
CharacterShapeKeysComponent, "CharacterShapeKeys",
|
||||
lua_newtable(L);
|
||||
for (auto &kv : c.weights) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
{
|
||||
const CharacterRegistry::CharacterRecord *rec = nullptr;
|
||||
if (e.has<CharacterIdentityComponent>()) {
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
rec = CharacterRegistry::getSingleton()
|
||||
.findCharacter(id.registryId);
|
||||
}
|
||||
lua_newtable(L);
|
||||
if (rec) {
|
||||
for (auto &kv : rec->inlineShapeKeyWeights) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
lua_pop(L, 1);
|
||||
{
|
||||
if (!e.has<CharacterIdentityComponent>())
|
||||
return;
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton()
|
||||
.findCharacter(id.registryId);
|
||||
if (!rec)
|
||||
return;
|
||||
if (lua_istable(L, idx)) {
|
||||
bool changed = false;
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, idx) != 0) {
|
||||
if (lua_isstring(L, -2) &&
|
||||
lua_isnumber(L, -1)) {
|
||||
rec->inlineShapeKeyWeights[
|
||||
lua_tostring(L, -2)] =
|
||||
lua_tonumber(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
if (changed) {
|
||||
CharacterRegistry::getSingleton()
|
||||
.autoSave();
|
||||
CharacterRegistry::getSingleton()
|
||||
.markCharacterDirty(
|
||||
id.registryId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "CharacterSlotSystem.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
@@ -53,10 +54,11 @@ Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e)
|
||||
if (!e.is_alive())
|
||||
return nullptr;
|
||||
|
||||
if (e.has<CharacterSlotsComponent>()) {
|
||||
auto &cs = e.get<CharacterSlotsComponent>();
|
||||
if (cs.masterEntity && cs.masterEntity->hasSkeleton())
|
||||
return cs.masterEntity;
|
||||
CharacterSlotSystem *slotSystem = CharacterSlotSystem::getSingletonPtr();
|
||||
if (slotSystem) {
|
||||
Ogre::Entity *masterEnt = slotSystem->getMasterEntity(e);
|
||||
if (masterEnt && masterEnt->hasSkeleton())
|
||||
return masterEnt;
|
||||
}
|
||||
|
||||
if (e.has<RenderableComponent>()) {
|
||||
|
||||
@@ -284,9 +284,11 @@ std::vector<uint64_t> CharacterRegistry::getChildren(uint64_t parentId) const
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static bool
|
||||
readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs,
|
||||
readPrefabAppearance(const std::string &path, std::string &outSex,
|
||||
std::unordered_map<std::string, SlotSelection> &outSelections,
|
||||
std::unordered_map<std::string, float> &shapeKeys,
|
||||
std::string &outAge)
|
||||
std::string &outAge,
|
||||
Ogre::Vector3 &outFrontAxis)
|
||||
{
|
||||
try {
|
||||
std::ifstream file(path);
|
||||
@@ -296,7 +298,17 @@ readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs,
|
||||
file >> j;
|
||||
if (j.contains("characterSlots")) {
|
||||
auto &s = j["characterSlots"];
|
||||
cs.sex = s.value("sex", "male");
|
||||
outSex = s.value("sex", "male");
|
||||
outAge = s.value("age", "adult");
|
||||
if (s.contains("frontAxis") && s["frontAxis"].is_array() &&
|
||||
s["frontAxis"].size() >= 3) {
|
||||
outFrontAxis = Ogre::Vector3(
|
||||
s["frontAxis"][0].get<float>(),
|
||||
s["frontAxis"][1].get<float>(),
|
||||
s["frontAxis"][2].get<float>());
|
||||
if (outFrontAxis.squaredLength() > 0.0001f)
|
||||
outFrontAxis.normalise();
|
||||
}
|
||||
if (s.contains("slotSelections")) {
|
||||
for (auto &[slot, selJson] :
|
||||
s["slotSelections"].items()) {
|
||||
@@ -309,7 +321,15 @@ readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs,
|
||||
selJson.value("layer2Mesh", "");
|
||||
sel.explicitMesh = selJson.value(
|
||||
"explicitMesh", "");
|
||||
cs.slotSelections[slot] = sel;
|
||||
outSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
/* Backward compat: old flat slots map */
|
||||
if (s.contains("slots") && s["slots"].is_object()) {
|
||||
for (auto &[slot, mesh] : s["slots"].items()) {
|
||||
SlotSelection sel;
|
||||
sel.explicitMesh = mesh.get<std::string>();
|
||||
outSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,14 +444,19 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB)
|
||||
/* Copy appearance from same-sex parent */
|
||||
if (sameSexParent->inlineSlotSelections.empty() &&
|
||||
std::filesystem::exists(sameSexParent->prefabPath)) {
|
||||
CharacterSlotsComponent tmpCs;
|
||||
std::string prefabSex = "male";
|
||||
std::string prefabAge;
|
||||
readPrefabAppearance(sameSexParent->prefabPath, tmpCs,
|
||||
child->inlineShapeKeyWeights, prefabAge);
|
||||
std::unordered_map<std::string, SlotSelection> prefabSelections;
|
||||
Ogre::Vector3 prefabFrontAxis = sameSexParent->frontAxis;
|
||||
readPrefabAppearance(sameSexParent->prefabPath, prefabSex,
|
||||
prefabSelections,
|
||||
child->inlineShapeKeyWeights, prefabAge,
|
||||
prefabFrontAxis);
|
||||
child->age = prefabAge;
|
||||
child->inlineSex = tmpCs.sex;
|
||||
child->inlineSex = prefabSex;
|
||||
child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel;
|
||||
child->inlineSlotSelections = tmpCs.slotSelections;
|
||||
child->inlineSlotSelections = prefabSelections;
|
||||
child->frontAxis = sameSexParent->frontAxis;
|
||||
} else {
|
||||
child->age = sameSexParent->age;
|
||||
child->inlineSex = sameSexParent->inlineSex;
|
||||
@@ -440,6 +465,7 @@ uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB)
|
||||
sameSexParent->inlineSlotSelections;
|
||||
child->inlineShapeKeyWeights =
|
||||
sameSexParent->inlineShapeKeyWeights;
|
||||
child->frontAxis = sameSexParent->frontAxis;
|
||||
}
|
||||
|
||||
/* Randomize configured birth shape keys */
|
||||
@@ -538,20 +564,10 @@ flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c,
|
||||
transform.applyToNode();
|
||||
inst.set<TransformComponent>(transform);
|
||||
|
||||
/* CharacterSlots */
|
||||
CharacterSlotsComponent cs;
|
||||
cs.sex = c.inlineSex;
|
||||
cs.slotSelections = c.inlineSlotSelections;
|
||||
cs.dirty = true;
|
||||
inst.set<CharacterSlotsComponent>(cs);
|
||||
|
||||
/* Shape keys */
|
||||
if (!c.inlineShapeKeyWeights.empty()) {
|
||||
CharacterShapeKeysComponent skc;
|
||||
skc.weights = c.inlineShapeKeyWeights;
|
||||
skc.dirty = true;
|
||||
inst.set<CharacterShapeKeysComponent>(skc);
|
||||
}
|
||||
/* Character visual data is now read from the registry record at
|
||||
* build time; no component storage is needed. */
|
||||
if (m_slotSystem)
|
||||
m_slotSystem->markEntityDirty(inst.id());
|
||||
|
||||
return inst;
|
||||
}
|
||||
@@ -700,8 +716,8 @@ void CharacterRegistry::markCharacterDirty(uint64_t id)
|
||||
flecs::entity e = findSpawnedEntity(id);
|
||||
if (!e.is_alive())
|
||||
return;
|
||||
if (e.has<CharacterSlotsComponent>())
|
||||
e.get_mut<CharacterSlotsComponent>().dirty = true;
|
||||
if (m_slotSystem)
|
||||
m_slotSystem->markEntityDirty(e.id());
|
||||
}
|
||||
|
||||
bool CharacterRegistry::despawnCharacter(uint64_t id)
|
||||
@@ -720,7 +736,7 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id)
|
||||
if (!m_world || !m_sceneMgr || !m_uiSystem)
|
||||
return flecs::entity::null();
|
||||
|
||||
const CharacterRecord *c = findCharacter(id);
|
||||
CharacterRecord *c = findCharacter(id);
|
||||
if (!c)
|
||||
return flecs::entity::null();
|
||||
|
||||
@@ -729,6 +745,20 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id)
|
||||
if (existing.is_alive())
|
||||
existing.destruct();
|
||||
|
||||
/* Migrate prefab appearance data into the registry record if the
|
||||
* record has no inline appearance data yet. */
|
||||
if (c->inlineSlotSelections.empty() &&
|
||||
std::filesystem::exists(c->prefabPath)) {
|
||||
std::string prefabAge;
|
||||
readPrefabAppearance(c->prefabPath, c->inlineSex,
|
||||
c->inlineSlotSelections,
|
||||
c->inlineShapeKeyWeights, prefabAge,
|
||||
c->frontAxis);
|
||||
if (!prefabAge.empty())
|
||||
c->age = prefabAge;
|
||||
autoSave();
|
||||
}
|
||||
|
||||
Ogre::Vector3 pos = c->position;
|
||||
flecs::entity inst;
|
||||
if (std::filesystem::exists(c->prefabPath)) {
|
||||
@@ -762,32 +792,52 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id)
|
||||
|
||||
bool CharacterRegistry::savePrefabForCharacter(uint64_t id)
|
||||
{
|
||||
if (!m_world || !m_sceneMgr || !m_uiSystem)
|
||||
return false;
|
||||
|
||||
const CharacterRecord *c = findCharacter(id);
|
||||
if (!c)
|
||||
return false;
|
||||
|
||||
flecs::entity sel = flecs::entity::null();
|
||||
if (m_uiSystem)
|
||||
sel = m_uiSystem->getSelectedEntity();
|
||||
try {
|
||||
nlohmann::json j;
|
||||
{
|
||||
std::ifstream file(c->prefabPath);
|
||||
if (file.is_open())
|
||||
file >> j;
|
||||
}
|
||||
|
||||
if (!sel.is_alive()) {
|
||||
m_lastError = "No entity selected";
|
||||
return false;
|
||||
}
|
||||
if (!sel.has<CharacterSlotsComponent>()) {
|
||||
m_lastError = "Selected entity has no CharacterSlots";
|
||||
return false;
|
||||
}
|
||||
j["characterSlots"]["age"] = c->age;
|
||||
j["characterSlots"]["sex"] = c->inlineSex;
|
||||
j["characterSlots"]["outfitLevel"] = c->inlineOutfitLevel;
|
||||
nlohmann::json selections = nlohmann::json::object();
|
||||
for (const auto &kv : c->inlineSlotSelections) {
|
||||
nlohmann::json s;
|
||||
s["layer0Mesh"] = kv.second.layer0Mesh;
|
||||
s["layer1Mesh"] = kv.second.layer1Mesh;
|
||||
s["layer2Mesh"] = kv.second.layer2Mesh;
|
||||
s["explicitMesh"] = kv.second.explicitMesh;
|
||||
selections[kv.first] = s;
|
||||
}
|
||||
j["characterSlots"]["slotSelections"] = selections;
|
||||
|
||||
PrefabSystem prefabSys(*m_world, m_sceneMgr);
|
||||
if (!prefabSys.savePrefab(sel, c->prefabPath)) {
|
||||
m_lastError = prefabSys.getLastError();
|
||||
if (!c->inlineShapeKeyWeights.empty()) {
|
||||
nlohmann::json weights = nlohmann::json::object();
|
||||
for (const auto &kv : c->inlineShapeKeyWeights)
|
||||
weights[kv.first] = kv.second;
|
||||
j["characterShapeKeys"]["weights"] = weights;
|
||||
}
|
||||
|
||||
std::filesystem::path p(c->prefabPath);
|
||||
std::filesystem::create_directories(p.parent_path());
|
||||
std::ofstream out(c->prefabPath);
|
||||
if (!out.is_open()) {
|
||||
m_lastError = "Cannot open " + c->prefabPath;
|
||||
return false;
|
||||
}
|
||||
out << j.dump(4);
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
m_lastError = std::string("Save prefab error: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
@@ -1205,6 +1255,13 @@ nlohmann::json CharacterRegistry::serialize() const
|
||||
rec["age"] = c.age;
|
||||
rec["inlineSex"] = c.inlineSex;
|
||||
rec["inlineOutfitLevel"] = c.inlineOutfitLevel;
|
||||
{
|
||||
nlohmann::json fa;
|
||||
fa["x"] = c.frontAxis.x;
|
||||
fa["y"] = c.frontAxis.y;
|
||||
fa["z"] = c.frontAxis.z;
|
||||
rec["frontAxis"] = fa;
|
||||
}
|
||||
if (!c.inlineSlotSelections.empty()) {
|
||||
nlohmann::json selJson;
|
||||
for (const auto &kv : c.inlineSlotSelections) {
|
||||
@@ -1378,6 +1435,15 @@ void CharacterRegistry::deserialize(const nlohmann::json &j)
|
||||
c.age = rec.value("age", "adult");
|
||||
c.inlineSex = rec.value("inlineSex", "male");
|
||||
c.inlineOutfitLevel = rec.value("inlineOutfitLevel", 2);
|
||||
if (rec.contains("frontAxis") &&
|
||||
rec["frontAxis"].is_object()) {
|
||||
auto &fa = rec["frontAxis"];
|
||||
c.frontAxis = Ogre::Vector3(
|
||||
fa.value("x", 0.0f), fa.value("y", 0.0f),
|
||||
fa.value("z", 0.0f));
|
||||
if (c.frontAxis.squaredLength() > 0.0001f)
|
||||
c.frontAxis.normalise();
|
||||
}
|
||||
if (rec.contains("position") &&
|
||||
rec["position"].is_object()) {
|
||||
auto &p = rec["position"];
|
||||
@@ -1612,6 +1678,330 @@ static void drawColumnsEditor(const char *title,
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Character appearance editor (moved from CharacterSlotsEditor) */
|
||||
/* ===================================================================== */
|
||||
|
||||
static void drawCharacterAppearanceEditor(CharacterRegistry ®,
|
||||
CharacterRegistry::CharacterRecord *c)
|
||||
{
|
||||
if (!c)
|
||||
return;
|
||||
|
||||
CharacterSlotSystem::loadCatalog();
|
||||
|
||||
const Ogre::String currentAge = c->age;
|
||||
const int outfitLevel = c->inlineOutfitLevel;
|
||||
|
||||
/* Sex selector */
|
||||
std::vector<Ogre::String> sexes =
|
||||
CharacterSlotSystem::getSexes(currentAge);
|
||||
Ogre::String currentSex = c->inlineSex;
|
||||
if (ImGui::BeginCombo("Sex", currentSex.c_str())) {
|
||||
for (const auto &sex : sexes) {
|
||||
bool isSelected = (currentSex == sex);
|
||||
if (ImGui::Selectable(sex.c_str(), isSelected)) {
|
||||
c->inlineSex = sex;
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(c->id);
|
||||
}
|
||||
if (isSelected)
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Front-facing axis */
|
||||
{
|
||||
Ogre::Vector3 axis = c->frontAxis;
|
||||
float axisVals[3] = { axis.x, axis.y, axis.z };
|
||||
ImGui::Text("Front Axis:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(
|
||||
"(direction character faces, used by path following)");
|
||||
if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f, -1.0f,
|
||||
1.0f)) {
|
||||
c->frontAxis = Ogre::Vector3(axisVals[0], axisVals[1],
|
||||
axisVals[2]);
|
||||
if (c->frontAxis.squaredLength() > 0.0001f)
|
||||
c->frontAxis.normalise();
|
||||
reg.autoSave();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("-Z")) {
|
||||
c->frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
reg.autoSave();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+Z")) {
|
||||
c->frontAxis = Ogre::Vector3::UNIT_Z;
|
||||
reg.autoSave();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Shape Keys */
|
||||
if (ImGui::CollapsingHeader("Shape Keys")) {
|
||||
auto shapeKeys = CharacterSlotSystem::getShapeKeyNames(
|
||||
currentAge, c->inlineSex);
|
||||
if (shapeKeys.empty()) {
|
||||
ImGui::TextDisabled("No shape keys available.");
|
||||
} else {
|
||||
for (const auto &name : shapeKeys) {
|
||||
float val = c->inlineShapeKeyWeights[name];
|
||||
if (ImGui::SliderFloat(name.c_str(), &val, 0.0f,
|
||||
1.0f)) {
|
||||
c->inlineShapeKeyWeights[name] = val;
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(c->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Slot selections */
|
||||
std::vector<Ogre::String> availableSlots =
|
||||
CharacterSlotSystem::getSlots(currentAge, c->inlineSex);
|
||||
for (const auto &pair : c->inlineSlotSelections) {
|
||||
if (std::find(availableSlots.begin(), availableSlots.end(),
|
||||
pair.first) == availableSlots.end())
|
||||
availableSlots.push_back(pair.first);
|
||||
}
|
||||
std::sort(availableSlots.begin(), availableSlots.end());
|
||||
|
||||
for (const auto &slot : availableSlots) {
|
||||
if (c->inlineSlotSelections.find(slot) ==
|
||||
c->inlineSlotSelections.end()) {
|
||||
c->inlineSlotSelections[slot] = SlotSelection();
|
||||
}
|
||||
|
||||
SlotSelection &sel = c->inlineSlotSelections[slot];
|
||||
|
||||
if (ImGui::TreeNode(slot.c_str())) {
|
||||
/* Resolved mesh preview */
|
||||
Ogre::String resolved = CharacterSlotSystem::resolveMesh(
|
||||
currentAge, c->inlineSex, slot, sel,
|
||||
outfitLevel);
|
||||
ImGui::TextDisabled("Resolved: %s",
|
||||
resolved.empty() ?
|
||||
"(none)" :
|
||||
resolved.c_str());
|
||||
|
||||
/* Explicit mesh override */
|
||||
bool useExplicit = !sel.explicitMesh.empty();
|
||||
if (ImGui::Checkbox("Lock explicit mesh", &useExplicit)) {
|
||||
if (!useExplicit) {
|
||||
sel.explicitMesh.clear();
|
||||
} else {
|
||||
sel.explicitMesh = CharacterSlotSystem::resolveMesh(
|
||||
currentAge, c->inlineSex, slot, sel,
|
||||
outfitLevel);
|
||||
}
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(c->id);
|
||||
}
|
||||
|
||||
if (useExplicit) {
|
||||
std::vector<Ogre::String> meshes =
|
||||
CharacterSlotSystem::getMeshes(
|
||||
currentAge, c->inlineSex, slot);
|
||||
Ogre::String preview = sel.explicitMesh.empty() ?
|
||||
"(none)" :
|
||||
sel.explicitMesh;
|
||||
if (ImGui::BeginCombo("Mesh", preview.c_str())) {
|
||||
if (ImGui::Selectable(
|
||||
"(none)",
|
||||
sel.explicitMesh.empty())) {
|
||||
sel.explicitMesh.clear();
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(
|
||||
c->id);
|
||||
}
|
||||
for (const auto &m : meshes) {
|
||||
bool isSelected =
|
||||
(sel.explicitMesh == m);
|
||||
if (ImGui::Selectable(m.c_str(),
|
||||
isSelected)) {
|
||||
sel.explicitMesh = m;
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(
|
||||
c->id);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
/* Layer 0 combo (base meshes like hair) */
|
||||
if (slot != "hair") {
|
||||
std::vector<Ogre::String> layer0Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
c->inlineSex, slot,
|
||||
0);
|
||||
if (!layer0Meshes.empty()) {
|
||||
Ogre::String l0Preview = "auto";
|
||||
if (!sel.layer0Mesh.empty() &&
|
||||
sel.layer0Mesh != "none")
|
||||
l0Preview = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
c->inlineSex,
|
||||
slot,
|
||||
sel.layer0Mesh);
|
||||
if (ImGui::BeginCombo(
|
||||
"Base (Layer 0)",
|
||||
l0Preview.c_str())) {
|
||||
if (ImGui::Selectable(
|
||||
"auto",
|
||||
sel.layer0Mesh.empty() ||
|
||||
sel.layer0Mesh ==
|
||||
"none")) {
|
||||
sel.layer0Mesh =
|
||||
"none";
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(
|
||||
c->id);
|
||||
}
|
||||
for (const auto &m :
|
||||
layer0Meshes) {
|
||||
Ogre::String label =
|
||||
CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
c->inlineSex,
|
||||
slot,
|
||||
m);
|
||||
bool isSelected =
|
||||
(sel.layer0Mesh ==
|
||||
m);
|
||||
if (ImGui::Selectable(
|
||||
label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer0Mesh =
|
||||
m;
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(
|
||||
c->id);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled(
|
||||
"Base: auto (stub hair)");
|
||||
}
|
||||
/* Layer 1 combo */
|
||||
std::vector<Ogre::String> layer1Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
c->inlineSex, slot,
|
||||
1);
|
||||
Ogre::String l1Preview = "none";
|
||||
if (!sel.layer1Mesh.empty())
|
||||
l1Preview = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
c->inlineSex, 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";
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(c->id);
|
||||
}
|
||||
for (const auto &m : layer1Meshes) {
|
||||
Ogre::String label =
|
||||
CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
c->inlineSex,
|
||||
slot,
|
||||
m);
|
||||
bool isSelected =
|
||||
(sel.layer1Mesh == m);
|
||||
if (ImGui::Selectable(
|
||||
label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer1Mesh = m;
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(
|
||||
c->id);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
/* Layer 2 combo */
|
||||
std::vector<Ogre::String> layer2Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
c->inlineSex, slot,
|
||||
2);
|
||||
Ogre::String l2Preview = "none";
|
||||
if (!sel.layer2Mesh.empty())
|
||||
l2Preview = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
c->inlineSex, 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";
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(c->id);
|
||||
}
|
||||
for (const auto &m : layer2Meshes) {
|
||||
Ogre::String label =
|
||||
CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
c->inlineSex,
|
||||
slot,
|
||||
m);
|
||||
bool isSelected =
|
||||
(sel.layer2Mesh == m);
|
||||
if (ImGui::Selectable(
|
||||
label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer2Mesh = m;
|
||||
reg.autoSave();
|
||||
reg.markCharacterDirty(
|
||||
c->id);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
/* Rebuild button */
|
||||
if (ImGui::Button("Rebuild")) {
|
||||
reg.markCharacterDirty(c->id);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ImGui editor */
|
||||
/* ===================================================================== */
|
||||
@@ -1932,6 +2322,15 @@ void CharacterRegistry::drawEditor(bool *p_open)
|
||||
}
|
||||
}
|
||||
|
||||
/* Appearance (formerly CharacterSlots editor) */
|
||||
ImGui::Separator();
|
||||
if (ImGui::CollapsingHeader("Appearance",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Indent();
|
||||
drawCharacterAppearanceEditor(*this, c);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
/* Family */
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Family");
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
|
||||
class EditorUISystem;
|
||||
class CharacterSlotSystem;
|
||||
|
||||
/**
|
||||
* Global character and social-group registry.
|
||||
@@ -70,12 +71,13 @@ public:
|
||||
/* Age in years (runtime progression) */
|
||||
int ageYears = 0;
|
||||
|
||||
/* Inline appearance (used when no prefab file exists) */
|
||||
/* Inline appearance (authoritative source for character visuals) */
|
||||
std::string inlineSex = "male";
|
||||
int inlineOutfitLevel = 2;
|
||||
std::unordered_map<std::string, SlotSelection>
|
||||
inlineSlotSelections;
|
||||
std::unordered_map<std::string, float> inlineShapeKeyWeights;
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
|
||||
/* Runtime-only characters are NOT saved to character_registry.json */
|
||||
bool persistent = true;
|
||||
@@ -145,6 +147,10 @@ public:
|
||||
{
|
||||
m_uiSystem = ui;
|
||||
}
|
||||
void setCharacterSlotSystem(CharacterSlotSystem *slotSystem)
|
||||
{
|
||||
m_slotSystem = slotSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load registry from auto-save file. Call after setWorld/setSceneManager.
|
||||
@@ -399,6 +405,7 @@ private:
|
||||
flecs::world *m_world = nullptr;
|
||||
Ogre::SceneManager *m_sceneMgr = nullptr;
|
||||
EditorUISystem *m_uiSystem = nullptr;
|
||||
CharacterSlotSystem *m_slotSystem = nullptr;
|
||||
|
||||
void rebuildIndexes();
|
||||
void addToIndex(size_t relIndex);
|
||||
|
||||
@@ -17,18 +17,30 @@
|
||||
#include <OgreSceneNode.h>
|
||||
#include <algorithm>
|
||||
|
||||
CharacterSlotSystem *CharacterSlotSystem::ms_singleton = nullptr;
|
||||
bool CharacterSlotSystem::s_catalogLoaded = false;
|
||||
nlohmann::json CharacterSlotSystem::s_bodyParts = nlohmann::json::object();
|
||||
std::set<Ogre::String> CharacterSlotSystem::s_meshNames;
|
||||
|
||||
CharacterSlotSystem *CharacterSlotSystem::getSingletonPtr()
|
||||
{
|
||||
return ms_singleton;
|
||||
}
|
||||
|
||||
CharacterSlotSystem::CharacterSlotSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
m_world.observer<CharacterSlotsComponent>("CharacterSlotsCleanup")
|
||||
ms_singleton = this;
|
||||
m_world.observer<CharacterIdentityComponent>("CharacterSlotsBuild")
|
||||
.event(flecs::OnAdd)
|
||||
.each([this](flecs::entity e, CharacterIdentityComponent &) {
|
||||
markEntityDirty(e.id());
|
||||
});
|
||||
m_world.observer<CharacterIdentityComponent>("CharacterSlotsCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, CharacterSlotsComponent &) {
|
||||
.each([this](flecs::entity e, CharacterIdentityComponent &) {
|
||||
destroyCharacterParts(e);
|
||||
});
|
||||
}
|
||||
@@ -40,6 +52,8 @@ CharacterSlotSystem::~CharacterSlotSystem()
|
||||
toRemove.push_back(pair.first);
|
||||
for (auto id : toRemove)
|
||||
destroyCharacterParts(m_world.entity(id));
|
||||
if (ms_singleton == this)
|
||||
ms_singleton = nullptr;
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::initialize()
|
||||
@@ -402,13 +416,14 @@ void CharacterSlotSystem::update()
|
||||
if (!m_initialized)
|
||||
return;
|
||||
|
||||
m_world.query<CharacterSlotsComponent>().each(
|
||||
[&](flecs::entity e, CharacterSlotsComponent &cs) {
|
||||
if (cs.dirty) {
|
||||
buildCharacter(e, cs);
|
||||
cs.dirty = false;
|
||||
}
|
||||
});
|
||||
std::vector<flecs::entity_t> dirtyList(m_dirtyEntities.begin(),
|
||||
m_dirtyEntities.end());
|
||||
m_dirtyEntities.clear();
|
||||
for (flecs::entity_t id : dirtyList) {
|
||||
flecs::entity e = m_world.entity(id);
|
||||
if (e.is_alive() && e.has<CharacterIdentityComponent>())
|
||||
buildCharacter(e);
|
||||
}
|
||||
}
|
||||
|
||||
static const nlohmann::json *findCatalogEntry(const Ogre::String &age,
|
||||
@@ -455,62 +470,38 @@ 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.
|
||||
* Helper: retrieve the character's registry record.
|
||||
* Returns nullptr if the entity is not a registered character.
|
||||
*/
|
||||
static Ogre::String getCharacterAge(flecs::entity e)
|
||||
static const CharacterRegistry::CharacterRecord *
|
||||
getCharacterRecord(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";
|
||||
if (!e.has<CharacterIdentityComponent>())
|
||||
return nullptr;
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
return CharacterRegistry::getSingleton().findCharacter(id.registryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: retrieve the character's outfit level from the registry.
|
||||
* Falls back to 2 (clothed) if no registry entry is found.
|
||||
*/
|
||||
static int getCharacterOutfitLevel(flecs::entity e)
|
||||
{
|
||||
if (e.has<CharacterIdentityComponent>()) {
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
const CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
id.registryId);
|
||||
if (rec)
|
||||
return rec->inlineOutfitLevel;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
CharacterSlotsComponent &cs)
|
||||
void CharacterSlotSystem::buildCharacter(flecs::entity e)
|
||||
{
|
||||
destroyCharacterParts(e);
|
||||
|
||||
Ogre::String age = getCharacterAge(e);
|
||||
const CharacterRegistry::CharacterRecord *rec = getCharacterRecord(e);
|
||||
if (!rec)
|
||||
return;
|
||||
|
||||
/* Migrate old slots map to slotSelections if needed */
|
||||
if (cs.slotSelections.empty() && !cs.slots.empty()) {
|
||||
for (const auto &pair : cs.slots) {
|
||||
SlotSelection sel;
|
||||
sel.explicitMesh = pair.second;
|
||||
cs.slotSelections[pair.first] = sel;
|
||||
}
|
||||
}
|
||||
const Ogre::String &age = rec->age;
|
||||
const Ogre::String &sex = rec->inlineSex;
|
||||
const auto &slotSelections = rec->inlineSlotSelections;
|
||||
const int outfitLevel = rec->inlineOutfitLevel;
|
||||
|
||||
/* Make sure every catalog slot exists in the selection map. This
|
||||
* guarantees a face/body master is available so that hair with its
|
||||
* own skeleton can attach to a real head bone. */
|
||||
auto slots = getSlots(age, cs.sex);
|
||||
/* Make sure every catalog slot exists in the selection map. */
|
||||
auto slots = getSlots(age, sex);
|
||||
std::unordered_map<Ogre::String, SlotSelection> selections =
|
||||
slotSelections;
|
||||
for (const auto &slot : slots) {
|
||||
if (cs.slotSelections.find(slot) == cs.slotSelections.end())
|
||||
cs.slotSelections[slot] = SlotSelection();
|
||||
if (selections.find(slot) == selections.end())
|
||||
selections[slot] = SlotSelection();
|
||||
}
|
||||
|
||||
if (!e.has<TransformComponent>())
|
||||
@@ -520,20 +511,18 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
if (!transform.node)
|
||||
return;
|
||||
|
||||
int outfitLevel = getCharacterOutfitLevel(e);
|
||||
|
||||
/* Determine master slot (face preferred, else first non-empty) */
|
||||
Ogre::String masterSlot;
|
||||
if (cs.slotSelections.find("face") != cs.slotSelections.end()) {
|
||||
Ogre::String mesh = resolveMesh(age, cs.sex, "face",
|
||||
cs.slotSelections["face"],
|
||||
if (selections.find("face") != selections.end()) {
|
||||
Ogre::String mesh = resolveMesh(age, sex, "face",
|
||||
selections["face"],
|
||||
outfitLevel);
|
||||
if (!mesh.empty())
|
||||
masterSlot = "face";
|
||||
}
|
||||
if (masterSlot.empty()) {
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
Ogre::String mesh = resolveMesh(age, cs.sex, pair.first,
|
||||
for (const auto &pair : selections) {
|
||||
Ogre::String mesh = resolveMesh(age, sex, pair.first,
|
||||
pair.second,
|
||||
outfitLevel);
|
||||
if (!mesh.empty()) {
|
||||
@@ -546,8 +535,8 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
if (masterSlot.empty())
|
||||
return;
|
||||
|
||||
Ogre::String masterMesh = resolveMesh(age, cs.sex, masterSlot,
|
||||
cs.slotSelections[masterSlot],
|
||||
Ogre::String masterMesh = resolveMesh(age, sex, masterSlot,
|
||||
selections[masterSlot],
|
||||
outfitLevel);
|
||||
|
||||
if (masterMesh.empty())
|
||||
@@ -563,23 +552,16 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
masterEnt = m_sceneMgr->createEntity(meshPtr);
|
||||
transform.node->attachObject(masterEnt);
|
||||
m_entities[e.id()].parts[masterSlot] = masterEnt;
|
||||
cs.masterEntity = masterEnt;
|
||||
m_entities[e.id()].masterEntity = masterEnt;
|
||||
|
||||
/* Setup pose animation for shape keys */
|
||||
const nlohmann::json *entry =
|
||||
findCatalogEntry(age, cs.sex, masterSlot, masterMesh);
|
||||
findCatalogEntry(age, sex, masterSlot, masterMesh);
|
||||
applyShapeKeys(e, masterEnt, entry);
|
||||
|
||||
/* Re-prepare temp buffers after enabling vertex animation.
|
||||
* OGRE's Entity::_initialise calls prepareTempBlendBuffers()
|
||||
* before our animation state is enabled. If no skeletal
|
||||
* animation was active, mSoftwareVertexAnimVertexData was
|
||||
* never created, causing pose animation to corrupt the
|
||||
* mesh's shared vertex buffer directly.
|
||||
*/
|
||||
/* Re-prepare temp buffers after enabling vertex animation. */
|
||||
prepareEntityTempBlendBuffers(masterEnt);
|
||||
|
||||
|
||||
/* Notify AnimationTreeSystem that entity changed */
|
||||
if (e.has<AnimationTreeComponent>())
|
||||
e.get_mut<AnimationTreeComponent>().dirty = true;
|
||||
@@ -590,7 +572,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
for (const auto &pair : selections) {
|
||||
const Ogre::String &slot = pair.first;
|
||||
const SlotSelection &sel = pair.second;
|
||||
|
||||
@@ -598,7 +580,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
continue;
|
||||
|
||||
Ogre::String mesh =
|
||||
resolveMesh(age, cs.sex, slot, sel, outfitLevel);
|
||||
resolveMesh(age, sex, slot, sel, outfitLevel);
|
||||
if (mesh.empty())
|
||||
continue;
|
||||
|
||||
@@ -615,7 +597,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
* armature). If so, attach to the master's
|
||||
* Head bone instead of sharing skeletons. */
|
||||
const nlohmann::json *entry =
|
||||
findCatalogEntry(age, cs.sex, slot, mesh);
|
||||
findCatalogEntry(age, sex, slot, mesh);
|
||||
bool ownSkeleton = false;
|
||||
Ogre::String attachBone = "mixamorig:Head";
|
||||
if (entry) {
|
||||
@@ -744,10 +726,10 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
|
||||
for (size_t i = 0; i < shapeKeys.size(); ++i)
|
||||
nameToIndex[shapeKeys[i].get<Ogre::String>()] = i;
|
||||
|
||||
/* Apply weights from CharacterShapeKeysComponent */
|
||||
if (e.has<CharacterShapeKeysComponent>()) {
|
||||
auto &skc = e.get_mut<CharacterShapeKeysComponent>();
|
||||
for (const auto &pair : skc.weights) {
|
||||
/* Apply weights from the character registry record */
|
||||
const CharacterRegistry::CharacterRecord *rec = getCharacterRecord(e);
|
||||
if (rec) {
|
||||
for (const auto &pair : rec->inlineShapeKeyWeights) {
|
||||
auto it = nameToIndex.find(pair.first);
|
||||
if (it == nameToIndex.end())
|
||||
continue;
|
||||
@@ -777,6 +759,11 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
|
||||
as->setTimePosition(0.0f);
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::markEntityDirty(flecs::entity_t id)
|
||||
{
|
||||
m_dirtyEntities.insert(id);
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::destroyCharacterParts(flecs::entity e)
|
||||
{
|
||||
auto it = m_entities.find(e.id());
|
||||
@@ -795,9 +782,18 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e)
|
||||
}
|
||||
|
||||
it->second.parts.clear();
|
||||
it->second.masterEntity = nullptr;
|
||||
m_entities.erase(it);
|
||||
}
|
||||
|
||||
Ogre::Entity *CharacterSlotSystem::getMasterEntity(flecs::entity e)
|
||||
{
|
||||
auto it = m_entities.find(e.id());
|
||||
if (it == m_entities.end())
|
||||
return nullptr;
|
||||
return it->second.masterEntity;
|
||||
}
|
||||
|
||||
Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e,
|
||||
const Ogre::String &slot)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
@@ -26,6 +27,10 @@ struct BodyPartEntry {
|
||||
* System that manages multi-slot character meshes with shared skeleton.
|
||||
* Loads body part catalog from body_part_*.json files and creates/updates
|
||||
* Ogre entities for each slot, sharing the skeleton from the master slot.
|
||||
*
|
||||
* Character appearance is read from CharacterRegistry (via
|
||||
* CharacterIdentityComponent); the deprecated CharacterSlotsComponent is no
|
||||
* longer used as a data source.
|
||||
*/
|
||||
class CharacterSlotSystem {
|
||||
public:
|
||||
@@ -35,6 +40,9 @@ public:
|
||||
void initialize();
|
||||
void update();
|
||||
|
||||
/* Singleton access */
|
||||
static CharacterSlotSystem *getSingletonPtr();
|
||||
|
||||
/* Static catalog access */
|
||||
static void loadCatalog();
|
||||
static bool isCatalogLoaded();
|
||||
@@ -73,18 +81,23 @@ public:
|
||||
const SlotSelection &sel,
|
||||
int outfitLevel);
|
||||
|
||||
/* Slot visibility helpers */
|
||||
/* Mark an entity for rebuild on next update */
|
||||
void markEntityDirty(flecs::entity_t id);
|
||||
|
||||
/* Runtime entity accessors */
|
||||
Ogre::Entity *getMasterEntity(flecs::entity e);
|
||||
Ogre::Entity *getSlotEntity(flecs::entity e,
|
||||
const Ogre::String &slot);
|
||||
void setSlotVisible(flecs::entity e, const Ogre::String &slot,
|
||||
bool visible);
|
||||
|
||||
private:
|
||||
static CharacterSlotSystem *ms_singleton;
|
||||
static bool s_catalogLoaded;
|
||||
static nlohmann::json s_bodyParts;
|
||||
static std::set<Ogre::String> s_meshNames;
|
||||
|
||||
void buildCharacter(flecs::entity e, CharacterSlotsComponent &cs);
|
||||
void buildCharacter(flecs::entity e);
|
||||
void destroyCharacterParts(flecs::entity e);
|
||||
void applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
|
||||
const nlohmann::json *entry);
|
||||
@@ -94,9 +107,11 @@ private:
|
||||
bool m_initialized = false;
|
||||
|
||||
struct PartEntities {
|
||||
Ogre::Entity *masterEntity = nullptr;
|
||||
std::unordered_map<Ogre::String, Ogre::Entity *> parts;
|
||||
};
|
||||
std::unordered_map<flecs::entity_t, PartEntities> m_entities;
|
||||
std::unordered_set<flecs::entity_t> m_dirtyEntities;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTERSLOTSYSTEM_HPP
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "CharacterSystem.hpp"
|
||||
#include "CharacterSlotSystem.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include <Jolt/Physics/Character/Character.h>
|
||||
#include <Jolt/Physics/Collision/GroupFilterTable.h>
|
||||
@@ -11,10 +12,12 @@
|
||||
|
||||
static Ogre::Entity *getMasterEntity(flecs::entity e)
|
||||
{
|
||||
if (!e.is_alive() || !e.has<CharacterSlotsComponent>())
|
||||
if (!e.is_alive())
|
||||
return nullptr;
|
||||
const CharacterSlotsComponent &cs = e.get<CharacterSlotsComponent>();
|
||||
return cs.masterEntity;
|
||||
CharacterSlotSystem *slotSystem = CharacterSlotSystem::getSingletonPtr();
|
||||
if (!slotSystem)
|
||||
return nullptr;
|
||||
return slotSystem->getMasterEntity(e);
|
||||
}
|
||||
|
||||
static bool getBoneWorldMatrix(Ogre::Entity *masterEnt,
|
||||
|
||||
@@ -1067,7 +1067,9 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
|
||||
// Render CharacterSlots if present
|
||||
if (entity.has<CharacterSlotsComponent>()) {
|
||||
auto &cs = entity.get_mut<CharacterSlotsComponent>();
|
||||
/* CharacterSlotsComponent is now an empty tag; get_mut is illegal
|
||||
* for zero-sized types in flecs. */
|
||||
CharacterSlotsComponent cs;
|
||||
m_componentRegistry.render<CharacterSlotsComponent>(entity, cs);
|
||||
componentCount++;
|
||||
}
|
||||
@@ -1298,6 +1300,8 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity)
|
||||
[&](const std::type_index &type,
|
||||
const ComponentRegistry::ComponentInfo
|
||||
&info) {
|
||||
if (!info.adder)
|
||||
return;
|
||||
if (!m_componentRegistry
|
||||
.entityHasComponent(
|
||||
entity, type)) {
|
||||
@@ -1316,6 +1320,8 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity)
|
||||
[&](const std::type_index &type,
|
||||
const ComponentRegistry::ComponentInfo
|
||||
&info) {
|
||||
if (!info.adder)
|
||||
return;
|
||||
if (!m_componentRegistry
|
||||
.entityHasComponent(
|
||||
entity,
|
||||
|
||||
@@ -201,6 +201,11 @@ public:
|
||||
void renderDialogueSettingsWindow();
|
||||
void renderInventoryConfigWindow();
|
||||
|
||||
CharacterRegistry &getCharacterRegistry()
|
||||
{
|
||||
return m_characterRegistry;
|
||||
}
|
||||
|
||||
private:
|
||||
// File menu
|
||||
void renderFileMenu();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "HairPhysicsSystem.hpp"
|
||||
#include "CharacterSlotSystem.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include <OgreEntity.h>
|
||||
@@ -96,12 +98,22 @@ void HairPhysicsSystem::prePhysicsUpdate()
|
||||
if (!m_initialized || !m_physics)
|
||||
return;
|
||||
|
||||
m_world.query<CharacterSlotsComponent>().each(
|
||||
[this](flecs::entity e, CharacterSlotsComponent &cs) {
|
||||
Ogre::Entity *masterEnt = cs.masterEntity;
|
||||
m_world.query<CharacterIdentityComponent>().each(
|
||||
[this](flecs::entity e, CharacterIdentityComponent &ci) {
|
||||
Ogre::Entity *masterEnt =
|
||||
m_slotSystem->getMasterEntity(e);
|
||||
if (!masterEnt)
|
||||
return;
|
||||
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
const Ogre::String &slot = pair.first;
|
||||
const CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
ci.registryId);
|
||||
if (!rec)
|
||||
return;
|
||||
|
||||
auto slots = CharacterSlotSystem::getSlots(
|
||||
rec->age, rec->inlineSex);
|
||||
for (const auto &slot : slots) {
|
||||
Ogre::Entity *partEnt =
|
||||
m_slotSystem->getSlotEntity(e, slot);
|
||||
if (!partEnt || !partEnt->hasSkeleton())
|
||||
@@ -450,7 +462,7 @@ bool HairPhysicsSystem::isStateValid(const HairRagdollState &state) const
|
||||
return false;
|
||||
|
||||
flecs::entity e = m_world.entity(state.entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterSlotsComponent>())
|
||||
if (!e.is_alive() || !e.has<CharacterIdentityComponent>())
|
||||
return false;
|
||||
|
||||
Ogre::Entity *current = m_slotSystem->getSlotEntity(e, state.slotName);
|
||||
@@ -463,8 +475,7 @@ void HairPhysicsSystem::syncRootToHead(HairRagdollState &state)
|
||||
return;
|
||||
|
||||
flecs::entity e = m_world.entity(state.entityId);
|
||||
const CharacterSlotsComponent &cs = e.get<CharacterSlotsComponent>();
|
||||
Ogre::Entity *masterEnt = cs.masterEntity;
|
||||
Ogre::Entity *masterEnt = m_slotSystem->getMasterEntity(e);
|
||||
if (!masterEnt)
|
||||
return;
|
||||
|
||||
@@ -511,8 +522,7 @@ void HairPhysicsSystem::syncPhysicsToSkeleton(HairRagdollState &state)
|
||||
/* Get the attachment transform so we can compute hair-entity-local
|
||||
* transforms for the root bone. */
|
||||
flecs::entity e = m_world.entity(state.entityId);
|
||||
const CharacterSlotsComponent &cs = e.get<CharacterSlotsComponent>();
|
||||
Ogre::Entity *masterEnt = cs.masterEntity;
|
||||
Ogre::Entity *masterEnt = m_slotSystem->getMasterEntity(e);
|
||||
|
||||
Ogre::Bone *attachBone = findAttachBone(state.hairEntity, masterEnt);
|
||||
Ogre::Matrix4 attachBoneInv = Ogre::Matrix4::IDENTITY;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "PathFollowingSystem.hpp"
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
@@ -47,11 +49,15 @@ void PathFollowingSystem::rotateTowards(flecs::entity e,
|
||||
return;
|
||||
flatDir.normalise();
|
||||
|
||||
// Get the character's front-facing axis
|
||||
// Get the character's front-facing axis from the registry
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
if (e.has<CharacterSlotsComponent>()) {
|
||||
auto &slots = e.get<CharacterSlotsComponent>();
|
||||
frontAxis = slots.frontAxis;
|
||||
if (e.has<CharacterIdentityComponent>()) {
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
const CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
id.registryId);
|
||||
if (rec)
|
||||
frontAxis = rec->frontAxis;
|
||||
}
|
||||
|
||||
// Use yaw rotation only (Y plane)
|
||||
|
||||
@@ -60,11 +60,15 @@ void ProceduralMaterialSystem::createMaterial(
|
||||
flecs::entity entity, ProceduralMaterialComponent &component)
|
||||
{
|
||||
try {
|
||||
// Ensure persistent ID is assigned.
|
||||
if (component.materialId.empty()) {
|
||||
component.materialId = generatePersistentId();
|
||||
}
|
||||
|
||||
// Generate unique material name if not set
|
||||
if (component.materialName.empty()) {
|
||||
component.materialName =
|
||||
"ProceduralMat_" + std::to_string(entity.id()) +
|
||||
"_" + std::to_string(m_createdCount++);
|
||||
"ProceduralMat_" + component.materialId;
|
||||
}
|
||||
|
||||
Ogre::MaterialManager &matMgr =
|
||||
|
||||
@@ -34,8 +34,7 @@ private:
|
||||
flecs::query<struct ProceduralMaterialComponent> m_query;
|
||||
|
||||
bool m_initialized = false;
|
||||
int m_createdCount = 0;
|
||||
|
||||
|
||||
// Create the Ogre material
|
||||
void createMaterial(flecs::entity entity, struct ProceduralMaterialComponent& component);
|
||||
|
||||
|
||||
@@ -37,9 +37,14 @@ void ProceduralTextureSystem::update()
|
||||
void ProceduralTextureSystem::generateTexture(flecs::entity entity, ProceduralTextureComponent& component)
|
||||
{
|
||||
try {
|
||||
// Ensure persistent ID is assigned.
|
||||
if (component.textureId.empty()) {
|
||||
component.textureId = generatePersistentId();
|
||||
}
|
||||
|
||||
// Generate unique texture name if not set
|
||||
if (component.textureName.empty()) {
|
||||
component.textureName = "ProceduralTex_" + std::to_string(entity.id()) + "_" + std::to_string(m_generatedCount++);
|
||||
component.textureName = "ProceduralTex_" + component.textureId;
|
||||
}
|
||||
|
||||
const int gridSize = ProceduralTextureComponent::GRID_SIZE;
|
||||
|
||||
@@ -31,8 +31,7 @@ private:
|
||||
flecs::query<struct ProceduralTextureComponent> m_query;
|
||||
|
||||
bool m_initialized = false;
|
||||
int m_generatedCount = 0;
|
||||
|
||||
|
||||
// Generate the texture for a component
|
||||
void generateTexture(flecs::entity entity, struct ProceduralTextureComponent& component);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "../components/AnimationTreeTemplate.hpp"
|
||||
#include "../components/StartupMenu.hpp"
|
||||
@@ -244,10 +245,6 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
json["character"] = serializeCharacter(entity);
|
||||
}
|
||||
|
||||
if (entity.has<CharacterSlotsComponent>()) {
|
||||
json["characterSlots"] = serializeCharacterSlots(entity);
|
||||
}
|
||||
|
||||
if (entity.has<CharacterIdentityComponent>()) {
|
||||
json["characterIdentity"] = serializeCharacterIdentity(entity);
|
||||
}
|
||||
@@ -470,14 +467,16 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
deserializeCharacter(entity, json["character"]);
|
||||
}
|
||||
|
||||
if (json.contains("characterSlots")) {
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
|
||||
/* Deserialize identity before slots so legacy slot data migrates into
|
||||
* the correct registry record. */
|
||||
if (json.contains("characterIdentity")) {
|
||||
deserializeCharacterIdentity(entity, json["characterIdentity"]);
|
||||
}
|
||||
|
||||
if (json.contains("characterSlots")) {
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
|
||||
if (json.contains("animationTree")) {
|
||||
deserializeAnimationTree(entity, json["animationTree"]);
|
||||
}
|
||||
@@ -703,14 +702,16 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeCharacter(entity, json["character"]);
|
||||
}
|
||||
|
||||
if (json.contains("characterSlots")) {
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
|
||||
/* Deserialize identity before slots so legacy slot data migrates into
|
||||
* the correct registry record. */
|
||||
if (json.contains("characterIdentity")) {
|
||||
deserializeCharacterIdentity(entity, json["characterIdentity"]);
|
||||
}
|
||||
|
||||
if (json.contains("characterSlots")) {
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
|
||||
if (json.contains("animationTree")) {
|
||||
deserializeAnimationTree(entity, json["animationTree"]);
|
||||
}
|
||||
@@ -1891,7 +1892,7 @@ void SceneSerializer::deserializeProceduralTexture(flecs::entity entity,
|
||||
|
||||
// Auto-generate texture ID if empty
|
||||
if (texture.textureId.empty()) {
|
||||
texture.textureId = "texture_" + std::to_string(entity.id());
|
||||
texture.textureId = generatePersistentId();
|
||||
}
|
||||
|
||||
// Deserialize colors array
|
||||
@@ -1979,7 +1980,7 @@ void SceneSerializer::deserializeProceduralMaterial(flecs::entity entity,
|
||||
|
||||
// Auto-generate material ID if empty
|
||||
if (material.materialId.empty()) {
|
||||
material.materialId = "material_" + std::to_string(entity.id());
|
||||
material.materialId = generatePersistentId();
|
||||
}
|
||||
|
||||
// Resolve texture reference by ID (like LOD system does)
|
||||
@@ -2227,82 +2228,82 @@ static void deserializeAnimationTreeNode(AnimationTreeNode &node,
|
||||
|
||||
nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
|
||||
{
|
||||
auto &cs = entity.get<CharacterSlotsComponent>();
|
||||
nlohmann::json json;
|
||||
|
||||
json["sex"] = cs.sex;
|
||||
json["slots"] = nlohmann::json::object();
|
||||
for (const auto &pair : cs.slots)
|
||||
json["slots"][pair.first] = pair.second;
|
||||
|
||||
// Serialize per-layer slot selections
|
||||
nlohmann::json selections = nlohmann::json::object();
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
nlohmann::json sel;
|
||||
sel["layer0Mesh"] = pair.second.layer0Mesh;
|
||||
sel["layer1Mesh"] = pair.second.layer1Mesh;
|
||||
sel["layer2Mesh"] = pair.second.layer2Mesh;
|
||||
sel["explicitMesh"] = pair.second.explicitMesh;
|
||||
selections[pair.first] = sel;
|
||||
}
|
||||
json["slotSelections"] = selections;
|
||||
|
||||
|
||||
// Serialize front axis
|
||||
json["frontAxis"] = { cs.frontAxis.x, cs.frontAxis.y, cs.frontAxis.z };
|
||||
|
||||
return json;
|
||||
/* CharacterSlotsComponent is deprecated and no longer saved. */
|
||||
(void)entity;
|
||||
return nlohmann::json::object();
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
CharacterSlotsComponent cs;
|
||||
cs.sex = json.value("sex", "male");
|
||||
if (json.contains("slots") && json["slots"].is_object()) {
|
||||
for (auto &[slot, mesh] : json["slots"].items())
|
||||
cs.slots[slot] = mesh.get<std::string>();
|
||||
/* Migrate legacy CharacterSlots data into the character registry.
|
||||
* The empty component is added as a placeholder reminder. */
|
||||
uint64_t registryId = 0;
|
||||
if (entity.has<CharacterIdentityComponent>())
|
||||
registryId = entity.get<CharacterIdentityComponent>().registryId;
|
||||
|
||||
if (registryId == 0) {
|
||||
registryId = CharacterRegistry::getSingleton().createCharacter(
|
||||
"Migrated", "Character", "", true);
|
||||
entity.set<CharacterIdentityComponent>(
|
||||
CharacterIdentityComponent{ registryId });
|
||||
}
|
||||
// Deserialize per-layer slot selections
|
||||
if (json.contains("slotSelections") &&
|
||||
json["slotSelections"].is_object()) {
|
||||
for (auto &[slot, selJson] : json["slotSelections"].items()) {
|
||||
SlotSelection sel;
|
||||
if (selJson.contains("layer0Mesh"))
|
||||
sel.layer0Mesh =
|
||||
selJson.value("layer0Mesh", "");
|
||||
if (selJson.contains("layer1Mesh"))
|
||||
sel.layer1Mesh =
|
||||
selJson.value("layer1Mesh", "");
|
||||
if (selJson.contains("layer2Mesh"))
|
||||
sel.layer2Mesh =
|
||||
selJson.value("layer2Mesh", "");
|
||||
// Backward compat: old format had layer/requiredTags/excludedTags
|
||||
if (selJson.contains("layer") &&
|
||||
sel.layer1Mesh.empty() && sel.layer2Mesh.empty()) {
|
||||
int oldLayer = selJson.value("layer", 2);
|
||||
if (oldLayer == 1)
|
||||
sel.layer1Mesh = "auto";
|
||||
else if (oldLayer >= 2)
|
||||
sel.layer2Mesh = "auto";
|
||||
|
||||
CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(registryId);
|
||||
if (rec) {
|
||||
rec->inlineSex = json.value("sex", rec->inlineSex);
|
||||
if (json.contains("slots") && json["slots"].is_object()) {
|
||||
for (auto &[slot, mesh] : json["slots"].items()) {
|
||||
SlotSelection sel;
|
||||
sel.explicitMesh = mesh.get<std::string>();
|
||||
rec->inlineSlotSelections[slot] = sel;
|
||||
}
|
||||
sel.explicitMesh = selJson.value("explicitMesh", "");
|
||||
cs.slotSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
// Deserialize front axis
|
||||
if (json.contains("frontAxis") && json["frontAxis"].is_array() &&
|
||||
json["frontAxis"].size() >= 3) {
|
||||
cs.frontAxis = Ogre::Vector3(json["frontAxis"][0].get<float>(),
|
||||
json["frontAxis"][1].get<float>(),
|
||||
json["frontAxis"][2].get<float>());
|
||||
if (cs.frontAxis.squaredLength() > 0.0001f)
|
||||
cs.frontAxis.normalise();
|
||||
if (json.contains("slotSelections") &&
|
||||
json["slotSelections"].is_object()) {
|
||||
for (auto &[slot, selJson] :
|
||||
json["slotSelections"].items()) {
|
||||
SlotSelection sel;
|
||||
if (selJson.contains("layer0Mesh"))
|
||||
sel.layer0Mesh =
|
||||
selJson.value("layer0Mesh", "");
|
||||
if (selJson.contains("layer1Mesh"))
|
||||
sel.layer1Mesh =
|
||||
selJson.value("layer1Mesh", "");
|
||||
if (selJson.contains("layer2Mesh"))
|
||||
sel.layer2Mesh =
|
||||
selJson.value("layer2Mesh", "");
|
||||
if (selJson.contains("layer") &&
|
||||
sel.layer1Mesh.empty() &&
|
||||
sel.layer2Mesh.empty()) {
|
||||
int oldLayer = selJson.value("layer", 2);
|
||||
if (oldLayer == 1)
|
||||
sel.layer1Mesh = "auto";
|
||||
else if (oldLayer >= 2)
|
||||
sel.layer2Mesh = "auto";
|
||||
}
|
||||
sel.explicitMesh =
|
||||
selJson.value("explicitMesh", "");
|
||||
rec->inlineSlotSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
if (json.contains("frontAxis") &&
|
||||
json["frontAxis"].is_array() &&
|
||||
json["frontAxis"].size() >= 3) {
|
||||
Ogre::Vector3 fa(
|
||||
json["frontAxis"][0].get<float>(),
|
||||
json["frontAxis"][1].get<float>(),
|
||||
json["frontAxis"][2].get<float>());
|
||||
if (fa.squaredLength() > 0.0001f)
|
||||
fa.normalise();
|
||||
rec->frontAxis = fa;
|
||||
}
|
||||
CharacterRegistry::getSingleton().autoSave();
|
||||
}
|
||||
|
||||
|
||||
cs.dirty = true;
|
||||
entity.set<CharacterSlotsComponent>(cs);
|
||||
entity.add<CharacterSlotsComponent>();
|
||||
CharacterRegistry::getSingleton().markCharacterDirty(registryId);
|
||||
}
|
||||
|
||||
static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node)
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
@@ -64,9 +66,13 @@ Ogre::Vector3 SmartObjectSystem::getEntityPosition(flecs::entity e)
|
||||
|
||||
Ogre::Vector3 SmartObjectSystem::getFrontAxis(flecs::entity e) const
|
||||
{
|
||||
if (e.has<CharacterSlotsComponent>()) {
|
||||
auto &slots = e.get<CharacterSlotsComponent>();
|
||||
return slots.frontAxis;
|
||||
if (e.has<CharacterIdentityComponent>()) {
|
||||
auto &id = e.get<CharacterIdentityComponent>();
|
||||
const CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
id.registryId);
|
||||
if (rec)
|
||||
return rec->frontAxis;
|
||||
}
|
||||
return Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
#include "CharacterSlotsEditor.hpp"
|
||||
#include "../systems/CharacterSlotSystem.hpp"
|
||||
#include "../systems/CharacterRegistry.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr)
|
||||
: m_sceneMgr(sceneMgr)
|
||||
@@ -13,384 +9,12 @@ CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr)
|
||||
bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
CharacterSlotsComponent &cs)
|
||||
{
|
||||
bool modified = false;
|
||||
(void)entity;
|
||||
(void)cs;
|
||||
(void)m_sceneMgr;
|
||||
|
||||
if (ImGui::CollapsingHeader("Character Slots",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Indent();
|
||||
|
||||
CharacterSlotSystem::loadCatalog();
|
||||
|
||||
/* 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(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)) {
|
||||
cs.sex = sex;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
if (isSelected)
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Front-facing axis */
|
||||
{
|
||||
Ogre::Vector3 axis = cs.frontAxis;
|
||||
float axisVals[3] = { axis.x, axis.y, axis.z };
|
||||
ImGui::Text("Front Axis:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(
|
||||
"(direction character faces, used by path following)");
|
||||
if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f,
|
||||
-1.0f, 1.0f)) {
|
||||
cs.frontAxis = Ogre::Vector3(
|
||||
axisVals[0], axisVals[1], axisVals[2]);
|
||||
if (cs.frontAxis.squaredLength() > 0.0001f)
|
||||
cs.frontAxis.normalise();
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("-Z")) {
|
||||
cs.frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+Z")) {
|
||||
cs.frontAxis = Ogre::Vector3::UNIT_Z;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Shape Keys */
|
||||
if (ImGui::CollapsingHeader("Shape Keys")) {
|
||||
auto shapeKeys = CharacterSlotSystem::getShapeKeyNames(
|
||||
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>();
|
||||
else {
|
||||
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,
|
||||
1.0f)) {
|
||||
skc->weights[name] = val;
|
||||
skc->dirty = true;
|
||||
cs.dirty = true;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Get outfit level from registry for resolveMesh calls */
|
||||
int outfitLevel = 2;
|
||||
if (entity.has<CharacterIdentityComponent>()) {
|
||||
auto &id = entity.get<CharacterIdentityComponent>();
|
||||
auto *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
id.registryId);
|
||||
if (rec)
|
||||
outfitLevel = rec->inlineOutfitLevel;
|
||||
}
|
||||
|
||||
/* Slot selections */
|
||||
std::vector<Ogre::String> availableSlots =
|
||||
CharacterSlotSystem::getSlots(currentAge, cs.sex);
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
if (std::find(availableSlots.begin(),
|
||||
availableSlots.end(),
|
||||
pair.first) == availableSlots.end())
|
||||
availableSlots.push_back(pair.first);
|
||||
}
|
||||
std::sort(availableSlots.begin(), availableSlots.end());
|
||||
|
||||
for (const auto &slot : availableSlots) {
|
||||
/* Ensure selection exists */
|
||||
if (cs.slotSelections.find(slot) ==
|
||||
cs.slotSelections.end()) {
|
||||
SlotSelection sel;
|
||||
/* Migrate old explicit mesh if present */
|
||||
auto oldIt = cs.slots.find(slot);
|
||||
if (oldIt != cs.slots.end())
|
||||
sel.explicitMesh = oldIt->second;
|
||||
cs.slotSelections[slot] = sel;
|
||||
}
|
||||
|
||||
SlotSelection &sel = cs.slotSelections[slot];
|
||||
|
||||
if (ImGui::TreeNode(slot.c_str())) {
|
||||
/* Resolved mesh preview */
|
||||
Ogre::String resolved =
|
||||
CharacterSlotSystem::resolveMesh(
|
||||
currentAge, cs.sex, slot, sel,
|
||||
outfitLevel);
|
||||
ImGui::TextDisabled("Resolved: %s",
|
||||
resolved.empty() ?
|
||||
"(none)" :
|
||||
resolved.c_str());
|
||||
|
||||
/* Explicit mesh override */
|
||||
bool useExplicit = !sel.explicitMesh.empty();
|
||||
if (ImGui::Checkbox("Lock explicit mesh",
|
||||
&useExplicit)) {
|
||||
if (!useExplicit) {
|
||||
sel.explicitMesh.clear();
|
||||
} else {
|
||||
/* Lock to currently resolved mesh */
|
||||
sel.explicitMesh =
|
||||
CharacterSlotSystem::resolveMesh(
|
||||
currentAge,
|
||||
cs.sex, slot,
|
||||
sel,
|
||||
outfitLevel);
|
||||
}
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
|
||||
if (useExplicit) {
|
||||
std::vector<Ogre::String> meshes =
|
||||
CharacterSlotSystem::getMeshes(
|
||||
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())) {
|
||||
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;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
/* Layer 0 combo (base meshes like hair) */
|
||||
/* Hair slot always uses auto stub; no base editing */
|
||||
if (slot != "hair") {
|
||||
std::vector<Ogre::String> layer0Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
cs.sex, slot,
|
||||
0);
|
||||
if (!layer0Meshes.empty()) {
|
||||
Ogre::String l0Preview = "auto";
|
||||
if (!sel.layer0Mesh.empty() &&
|
||||
sel.layer0Mesh != "none")
|
||||
l0Preview = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
cs.sex,
|
||||
slot,
|
||||
sel.layer0Mesh);
|
||||
if (ImGui::BeginCombo(
|
||||
"Base (Layer 0)",
|
||||
l0Preview.c_str())) {
|
||||
if (ImGui::Selectable(
|
||||
"auto",
|
||||
sel.layer0Mesh.empty() ||
|
||||
sel.layer0Mesh ==
|
||||
"none")) {
|
||||
sel.layer0Mesh =
|
||||
"none";
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
for (const auto &m :
|
||||
layer0Meshes) {
|
||||
Ogre::String label = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
cs.sex,
|
||||
slot,
|
||||
m);
|
||||
bool isSelected =
|
||||
(sel.layer0Mesh ==
|
||||
m);
|
||||
if (ImGui::Selectable(
|
||||
label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer0Mesh =
|
||||
m;
|
||||
modified =
|
||||
true;
|
||||
cs.dirty =
|
||||
true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Base: auto (stub hair)");
|
||||
}
|
||||
/* Layer 1 combo */
|
||||
std::vector<Ogre::String> layer1Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
cs.sex, slot,
|
||||
1);
|
||||
Ogre::String l1Preview = "none";
|
||||
if (!sel.layer1Mesh.empty())
|
||||
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(
|
||||
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;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
/* Layer 2 combo */
|
||||
std::vector<Ogre::String> layer2Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
cs.sex, slot,
|
||||
2);
|
||||
Ogre::String l2Preview = "none";
|
||||
if (!sel.layer2Mesh.empty())
|
||||
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(
|
||||
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;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
/* Rebuild button */
|
||||
if (ImGui::Button("Rebuild")) {
|
||||
cs.dirty = true;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
return modified;
|
||||
ImGui::Text("Character Slots");
|
||||
ImGui::TextDisabled(
|
||||
"Appearance data has moved to Tools -> Character Registry.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include <OgreSceneManager.h>
|
||||
|
||||
/**
|
||||
* Editor for CharacterSlotsComponent
|
||||
* Placeholder editor for the deprecated CharacterSlotsComponent.
|
||||
* All appearance editing now happens in Tools -> Character Registry.
|
||||
*/
|
||||
class CharacterSlotsEditor : public ComponentEditor<CharacterSlotsComponent> {
|
||||
public:
|
||||
|
||||
Reference in New Issue
Block a user