diff --git a/include/gui/GuiManager.h b/include/gui/GuiManager.h index a17acf7..2746866 100644 --- a/include/gui/GuiManager.h +++ b/include/gui/GuiManager.h @@ -7,6 +7,9 @@ #include "LogWindow.h" // Include the new LogWindow class #include #include +#include +#include +#include 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 options; + }; + + struct ScriptEditableCache { + std::string className; + std::vector 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 scriptEditableCache; + std::unordered_map> scriptEditableStaged; }; \ No newline at end of file diff --git a/scripts/ball.as b/scripts/ball.as index c389c4f..60a9848 100644 --- a/scripts/ball.as +++ b/scripts/ball.as @@ -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() { diff --git a/src/gui/GuiManager.cpp b/src/gui/GuiManager.cpp index 1e30512..c1ece9d 100644 --- a/src/gui/GuiManager.cpp +++ b/src/gui/GuiManager.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include #include +#include #include #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 SplitOptions(const std::string &value) +{ + std::vector 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(entity); + if (!script.enabled || script.scriptPath.empty() || !script.scriptInstance) return; + + auto *obj = reinterpret_cast(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(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(propAddr); + staged.hasValue = true; + } else if (propertyTypeId == intTypeId) { + staged.kind = ScriptValueKind::Int; + staged.i = *reinterpret_cast(propAddr); + staged.hasValue = true; + } else if (propertyTypeId == boolTypeId) { + staged.kind = ScriptValueKind::Bool; + staged.b = *reinterpret_cast(propAddr); + staged.hasValue = true; + } else if (propertyTypeId == stringTypeId) { + staged.kind = ScriptValueKind::String; + staged.s = *reinterpret_cast(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(field.options.size()) - 1; + if (maxIndex < 0) maxIndex = 0; + currentIndex = static_cast(currentValue); + currentIndex = std::clamp(currentIndex, 0, maxIndex); + if (ImGui::SliderInt(field.name.c_str(), ¤tIndex, 0, maxIndex)) { + staged.f = static_cast(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(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(field.rangeMin), static_cast(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(i); + break; + } + } + if (ImGui::Combo(field.name.c_str(), ¤tIndex, [](void *data, int idx, const char **outText) { + auto *opts = reinterpret_cast *>(data); + if (idx < 0 || idx >= static_cast(opts->size())) return false; + *outText = (*opts)[idx].c_str(); + return true; + }, (void *)&field.options, static_cast(field.options.size()))) { + if (currentIndex >= 0 && currentIndex < static_cast(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(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(propAddr) = staged.f; + } else if (propertyTypeId == intTypeId && staged.kind == ScriptValueKind::Int) { + *reinterpret_cast(propAddr) = staged.i; + } else if (propertyTypeId == boolTypeId && staged.kind == ScriptValueKind::Bool) { + *reinterpret_cast(propAddr) = staged.b; + } else if (propertyTypeId == stringTypeId && staged.kind == ScriptValueKind::String) { + *reinterpret_cast(propAddr) = staged.s; + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + stagedMap.clear(); + } + + ImGui::PopID(); +} + void GuiManager::RenderErrorBanner() { if (!app || !app->HasScriptCompilationError())