This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
#include "LogWindow.h" // Include the new LogWindow class
|
||||
#include <entt.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
|
||||
class Application;
|
||||
|
||||
@@ -30,8 +33,37 @@ private:
|
||||
void RenderEntityNode(entt::entity entity);
|
||||
bool IsEntityAncestor(entt::entity ancestor, entt::entity entity) const;
|
||||
const char* GetEntityLabel(entt::entity entity);
|
||||
struct ScriptEditableFieldMeta {
|
||||
std::string name;
|
||||
std::string type;
|
||||
bool hasRange = false;
|
||||
float rangeMin = 0.0f;
|
||||
float rangeMax = 1.0f;
|
||||
std::vector<std::string> options;
|
||||
};
|
||||
|
||||
struct ScriptEditableCache {
|
||||
std::string className;
|
||||
std::vector<ScriptEditableFieldMeta> fields;
|
||||
std::filesystem::file_time_type lastWriteTime;
|
||||
bool hasWriteTime = false;
|
||||
};
|
||||
|
||||
enum class ScriptValueKind { Float, Int, Bool, String };
|
||||
|
||||
struct ScriptEditableValue {
|
||||
ScriptValueKind kind = ScriptValueKind::Float;
|
||||
bool hasValue = false;
|
||||
float f = 0.0f;
|
||||
int i = 0;
|
||||
bool b = false;
|
||||
std::string s;
|
||||
};
|
||||
|
||||
void SyncTagBuffer(entt::entity entity);
|
||||
void SyncScriptPathBuffer(entt::entity entity);
|
||||
void RenderScriptEditableControls(entt::entity entity);
|
||||
ScriptEditableCache &GetOrParseEditableCache(const std::string &path, const std::string &className);
|
||||
|
||||
bool showLogWindow = true;
|
||||
bool showSceneWindow = true;
|
||||
@@ -52,4 +84,7 @@ private:
|
||||
char sceneLoadPath[260] = {0};
|
||||
char sceneSavePath[260] = {0};
|
||||
int addComponentIndex = 0;
|
||||
|
||||
std::unordered_map<std::string, ScriptEditableCache> scriptEditableCache;
|
||||
std::unordered_map<entt::entity, std::unordered_map<std::string, ScriptEditableValue>> scriptEditableStaged;
|
||||
};
|
||||
+3
-3
@@ -6,11 +6,11 @@ class EntityScript
|
||||
float baseX;
|
||||
float baseY;
|
||||
float baseZ;
|
||||
float phase;
|
||||
[editable][range[0,1]]float phase;
|
||||
|
||||
// per-instance movement parameters (can be tuned per-entity)
|
||||
float amplitude = 2.5f;
|
||||
float speed = 2.5f;
|
||||
[editable][range[0,10]]float amplitude = 2.5f;
|
||||
[editable][range[0,10]]float speed = 2.5f;
|
||||
|
||||
EntityScript()
|
||||
{
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <entt.hpp>
|
||||
#include "log.h"
|
||||
#include "Application.h"
|
||||
@@ -768,6 +770,8 @@ void GuiManager::RenderInspectorWindow()
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
|
||||
RenderScriptEditableControls(selectedEntity);
|
||||
}
|
||||
|
||||
// Camera
|
||||
@@ -912,6 +916,342 @@ void GuiManager::RenderInspectorWindow()
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
static std::string TrimString(const std::string &value)
|
||||
{
|
||||
size_t start = value.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) return std::string();
|
||||
size_t end = value.find_last_not_of(" \t\r\n");
|
||||
return value.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
static bool ExtractTagContent(const std::string &line, const std::string &tag, std::string &out)
|
||||
{
|
||||
std::string needle = "[" + tag + "[";
|
||||
size_t start = line.find(needle);
|
||||
if (start == std::string::npos) return false;
|
||||
start += needle.size();
|
||||
size_t end = line.find("]]", start);
|
||||
if (end == std::string::npos) return false;
|
||||
out = line.substr(start, end - start);
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<std::string> SplitOptions(const std::string &value)
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
std::stringstream ss(value);
|
||||
std::string item;
|
||||
while (std::getline(ss, item, ',')) {
|
||||
parts.push_back(TrimString(item));
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
static bool TryParseRange(const std::string &value, float &outMin, float &outMax)
|
||||
{
|
||||
size_t comma = value.find(',');
|
||||
if (comma == std::string::npos) return false;
|
||||
std::string left = TrimString(value.substr(0, comma));
|
||||
std::string right = TrimString(value.substr(comma + 1));
|
||||
try {
|
||||
outMin = std::stof(left);
|
||||
outMax = std::stof(right);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
GuiManager::ScriptEditableCache &GuiManager::GetOrParseEditableCache(const std::string &path, const std::string &className)
|
||||
{
|
||||
std::string key = path + "::" + className;
|
||||
auto &cache = scriptEditableCache[key];
|
||||
cache.className = className;
|
||||
|
||||
bool needsParse = true;
|
||||
try {
|
||||
auto writeTime = std::filesystem::last_write_time(path);
|
||||
if (cache.hasWriteTime && cache.lastWriteTime == writeTime) {
|
||||
needsParse = false;
|
||||
} else {
|
||||
cache.lastWriteTime = writeTime;
|
||||
cache.hasWriteTime = true;
|
||||
}
|
||||
} catch (const std::exception &) {
|
||||
// If file is missing or time cannot be read, parse anyway (will be empty).
|
||||
}
|
||||
|
||||
if (!needsParse) return cache;
|
||||
|
||||
cache.fields.clear();
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
bool inClass = false;
|
||||
int braceDepth = 0;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
std::string trimmed = TrimString(line);
|
||||
if (!inClass) {
|
||||
if (trimmed.find("class " + className) != std::string::npos) {
|
||||
inClass = true;
|
||||
for (char c : trimmed) {
|
||||
if (c == '{') braceDepth++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (char c : line) {
|
||||
if (c == '{') braceDepth++;
|
||||
if (c == '}') braceDepth--;
|
||||
}
|
||||
|
||||
if (braceDepth <= 0) {
|
||||
inClass = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.find("[editable]") == std::string::npos) continue;
|
||||
|
||||
ScriptEditableFieldMeta meta;
|
||||
meta.hasRange = false;
|
||||
meta.rangeMin = 0.0f;
|
||||
meta.rangeMax = 1.0f;
|
||||
|
||||
std::string rangeContent;
|
||||
if (ExtractTagContent(line, "range", rangeContent)) {
|
||||
float minVal = 0.0f;
|
||||
float maxVal = 1.0f;
|
||||
if (TryParseRange(rangeContent, minVal, maxVal)) {
|
||||
meta.hasRange = true;
|
||||
meta.rangeMin = minVal;
|
||||
meta.rangeMax = maxVal;
|
||||
}
|
||||
}
|
||||
|
||||
std::string optionsContent;
|
||||
if (ExtractTagContent(line, "options", optionsContent)) {
|
||||
meta.options = SplitOptions(optionsContent);
|
||||
}
|
||||
|
||||
size_t lastTag = line.find_last_of(']');
|
||||
if (lastTag == std::string::npos) continue;
|
||||
std::string declaration = TrimString(line.substr(lastTag + 1));
|
||||
if (declaration.empty()) continue;
|
||||
|
||||
std::stringstream declStream(declaration);
|
||||
declStream >> meta.type;
|
||||
std::string nameToken;
|
||||
declStream >> nameToken;
|
||||
if (meta.type.empty() || nameToken.empty()) continue;
|
||||
|
||||
size_t equalPos = nameToken.find('=');
|
||||
if (equalPos != std::string::npos) {
|
||||
nameToken = nameToken.substr(0, equalPos);
|
||||
}
|
||||
if (!nameToken.empty() && nameToken.back() == ';') {
|
||||
nameToken.pop_back();
|
||||
}
|
||||
meta.name = TrimString(nameToken);
|
||||
if (!meta.name.empty()) {
|
||||
cache.fields.push_back(meta);
|
||||
}
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
void GuiManager::RenderScriptEditableControls(entt::entity entity)
|
||||
{
|
||||
auto ®istry = app->GetRegistry();
|
||||
auto &script = registry.get<ScriptComponent>(entity);
|
||||
if (!script.enabled || script.scriptPath.empty() || !script.scriptInstance) return;
|
||||
|
||||
auto *obj = reinterpret_cast<asIScriptObject *>(script.scriptInstance);
|
||||
if (!obj) return;
|
||||
|
||||
asITypeInfo *objType = obj->GetObjectType();
|
||||
if (!objType) return;
|
||||
asIScriptEngine *engine = obj->GetEngine();
|
||||
if (!engine) return;
|
||||
|
||||
const char *className = objType->GetName();
|
||||
if (!className) return;
|
||||
|
||||
auto &cache = GetOrParseEditableCache(script.scriptPath, className);
|
||||
if (cache.fields.empty()) return;
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Editable");
|
||||
ImGui::PushID("ScriptEditable");
|
||||
|
||||
int floatTypeId = engine->GetTypeIdByDecl("float");
|
||||
int intTypeId = engine->GetTypeIdByDecl("int");
|
||||
int boolTypeId = engine->GetTypeIdByDecl("bool");
|
||||
int stringTypeId = engine->GetTypeIdByDecl("string");
|
||||
|
||||
auto &stagedMap = scriptEditableStaged[entity];
|
||||
|
||||
for (const auto &field : cache.fields) {
|
||||
int propertyIndex = -1;
|
||||
int propertyTypeId = 0;
|
||||
const int propertyCount = static_cast<int>(obj->GetPropertyCount());
|
||||
for (int i = 0; i < propertyCount; ++i) {
|
||||
const char *propName = obj->GetPropertyName(i);
|
||||
if (propName && field.name == propName) {
|
||||
propertyIndex = i;
|
||||
propertyTypeId = obj->GetPropertyTypeId(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (propertyIndex < 0) continue;
|
||||
|
||||
void *propAddr = obj->GetAddressOfProperty(propertyIndex);
|
||||
if (!propAddr) continue;
|
||||
|
||||
ScriptEditableValue &staged = stagedMap[field.name];
|
||||
if (!staged.hasValue) {
|
||||
if (propertyTypeId == floatTypeId) {
|
||||
staged.kind = ScriptValueKind::Float;
|
||||
staged.f = *reinterpret_cast<float *>(propAddr);
|
||||
staged.hasValue = true;
|
||||
} else if (propertyTypeId == intTypeId) {
|
||||
staged.kind = ScriptValueKind::Int;
|
||||
staged.i = *reinterpret_cast<int *>(propAddr);
|
||||
staged.hasValue = true;
|
||||
} else if (propertyTypeId == boolTypeId) {
|
||||
staged.kind = ScriptValueKind::Bool;
|
||||
staged.b = *reinterpret_cast<bool *>(propAddr);
|
||||
staged.hasValue = true;
|
||||
} else if (propertyTypeId == stringTypeId) {
|
||||
staged.kind = ScriptValueKind::String;
|
||||
staged.s = *reinterpret_cast<std::string *>(propAddr);
|
||||
staged.hasValue = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushID(field.name.c_str());
|
||||
if (staged.kind == ScriptValueKind::Float) {
|
||||
if (!field.options.empty()) {
|
||||
int currentIndex = 0;
|
||||
float currentValue = staged.f;
|
||||
int maxIndex = static_cast<int>(field.options.size()) - 1;
|
||||
if (maxIndex < 0) maxIndex = 0;
|
||||
currentIndex = static_cast<int>(currentValue);
|
||||
currentIndex = std::clamp(currentIndex, 0, maxIndex);
|
||||
if (ImGui::SliderInt(field.name.c_str(), ¤tIndex, 0, maxIndex)) {
|
||||
staged.f = static_cast<float>(currentIndex);
|
||||
}
|
||||
} else if (field.hasRange) {
|
||||
if (ImGui::SliderFloat(field.name.c_str(), &staged.f, field.rangeMin, field.rangeMax)) {
|
||||
staged.hasValue = true;
|
||||
}
|
||||
} else {
|
||||
if (ImGui::DragFloat(field.name.c_str(), &staged.f, 0.1f)) {
|
||||
staged.hasValue = true;
|
||||
}
|
||||
}
|
||||
} else if (staged.kind == ScriptValueKind::Int) {
|
||||
if (!field.options.empty()) {
|
||||
int currentIndex = staged.i;
|
||||
int maxIndex = static_cast<int>(field.options.size()) - 1;
|
||||
if (maxIndex < 0) maxIndex = 0;
|
||||
currentIndex = std::clamp(currentIndex, 0, maxIndex);
|
||||
if (ImGui::SliderInt(field.name.c_str(), ¤tIndex, 0, maxIndex)) {
|
||||
staged.i = currentIndex;
|
||||
}
|
||||
} else if (field.hasRange) {
|
||||
if (ImGui::SliderInt(field.name.c_str(), &staged.i, static_cast<int>(field.rangeMin), static_cast<int>(field.rangeMax))) {
|
||||
staged.hasValue = true;
|
||||
}
|
||||
} else {
|
||||
if (ImGui::DragInt(field.name.c_str(), &staged.i, 1.0f)) {
|
||||
staged.hasValue = true;
|
||||
}
|
||||
}
|
||||
} else if (staged.kind == ScriptValueKind::Bool) {
|
||||
if (ImGui::Checkbox(field.name.c_str(), &staged.b)) {
|
||||
staged.hasValue = true;
|
||||
}
|
||||
} else if (staged.kind == ScriptValueKind::String) {
|
||||
if (!field.options.empty()) {
|
||||
int currentIndex = 0;
|
||||
for (size_t i = 0; i < field.options.size(); ++i) {
|
||||
if (field.options[i] == staged.s) {
|
||||
currentIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo(field.name.c_str(), ¤tIndex, [](void *data, int idx, const char **outText) {
|
||||
auto *opts = reinterpret_cast<std::vector<std::string> *>(data);
|
||||
if (idx < 0 || idx >= static_cast<int>(opts->size())) return false;
|
||||
*outText = (*opts)[idx].c_str();
|
||||
return true;
|
||||
}, (void *)&field.options, static_cast<int>(field.options.size()))) {
|
||||
if (currentIndex >= 0 && currentIndex < static_cast<int>(field.options.size())) {
|
||||
staged.s = field.options[currentIndex];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
char buffer[256];
|
||||
std::snprintf(buffer, sizeof(buffer), "%s", staged.s.c_str());
|
||||
if (ImGui::InputText(field.name.c_str(), buffer, sizeof(buffer))) {
|
||||
staged.s = buffer;
|
||||
staged.hasValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Apply")) {
|
||||
for (const auto &field : cache.fields) {
|
||||
auto it = stagedMap.find(field.name);
|
||||
if (it == stagedMap.end()) continue;
|
||||
ScriptEditableValue &staged = it->second;
|
||||
if (!staged.hasValue) continue;
|
||||
|
||||
int propertyIndex = -1;
|
||||
int propertyTypeId = 0;
|
||||
const int propertyCount = static_cast<int>(obj->GetPropertyCount());
|
||||
for (int i = 0; i < propertyCount; ++i) {
|
||||
const char *propName = obj->GetPropertyName(i);
|
||||
if (propName && field.name == propName) {
|
||||
propertyIndex = i;
|
||||
propertyTypeId = obj->GetPropertyTypeId(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (propertyIndex < 0) continue;
|
||||
void *propAddr = obj->GetAddressOfProperty(propertyIndex);
|
||||
if (!propAddr) continue;
|
||||
|
||||
if (propertyTypeId == floatTypeId && staged.kind == ScriptValueKind::Float) {
|
||||
*reinterpret_cast<float *>(propAddr) = staged.f;
|
||||
} else if (propertyTypeId == intTypeId && staged.kind == ScriptValueKind::Int) {
|
||||
*reinterpret_cast<int *>(propAddr) = staged.i;
|
||||
} else if (propertyTypeId == boolTypeId && staged.kind == ScriptValueKind::Bool) {
|
||||
*reinterpret_cast<bool *>(propAddr) = staged.b;
|
||||
} else if (propertyTypeId == stringTypeId && staged.kind == ScriptValueKind::String) {
|
||||
*reinterpret_cast<std::string *>(propAddr) = staged.s;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset")) {
|
||||
stagedMap.clear();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void GuiManager::RenderErrorBanner()
|
||||
{
|
||||
if (!app || !app->HasScriptCompilationError())
|
||||
|
||||
Reference in New Issue
Block a user