From 003c6ae53f73b141f85b81bdf8ae704ae1fc7d8a Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Wed, 8 Apr 2026 18:06:07 -0400 Subject: [PATCH 1/5] Initial implementation --- .../include/OvCore/ECS/Components/Behaviour.h | 5 + .../Scripting/Common/ScriptPropertyValue.h | 19 ++ .../include/OvCore/Scripting/Common/TScript.h | 22 +++ .../src/OvCore/ECS/Components/Behaviour.cpp | 162 +++++++++++++++++- .../src/OvCore/Scripting/Lua/LuaScript.cpp | 49 ++++++ .../src/OvCore/Scripting/Null/NullScript.cpp | 9 + 6 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 Sources/OvCore/include/OvCore/Scripting/Common/ScriptPropertyValue.h diff --git a/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h b/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h index 28277a1a..f0b2acf2 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h @@ -6,7 +6,11 @@ #pragma once +#include +#include + #include +#include #include #include @@ -164,6 +168,7 @@ namespace OvCore::ECS::Components private: std::unique_ptr m_script; + std::map m_scriptProperties; }; template<> diff --git a/Sources/OvCore/include/OvCore/Scripting/Common/ScriptPropertyValue.h b/Sources/OvCore/include/OvCore/Scripting/Common/ScriptPropertyValue.h new file mode 100644 index 00000000..4899c81e --- /dev/null +++ b/Sources/OvCore/include/OvCore/Scripting/Common/ScriptPropertyValue.h @@ -0,0 +1,19 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +namespace OvCore::Scripting +{ + /** + * Represents a primitive script property value (boolean, number, or string). + * This type is backend-agnostic and used by Behaviour to store and expose script fields. + */ + using ScriptPropertyValue = std::variant; +} diff --git a/Sources/OvCore/include/OvCore/Scripting/Common/TScript.h b/Sources/OvCore/include/OvCore/Scripting/Common/TScript.h index 150054fb..ca8a2506 100644 --- a/Sources/OvCore/include/OvCore/Scripting/Common/TScript.h +++ b/Sources/OvCore/include/OvCore/Scripting/Common/TScript.h @@ -6,7 +6,12 @@ #pragma once +#include +#include +#include + #include +#include namespace OvCore::Scripting { @@ -39,6 +44,23 @@ namespace OvCore::Scripting * Return the context of the script */ inline const Context& GetContext() const { return m_context; } + inline Context& GetContext() { return m_context; } + + /** + * Returns all primitive properties and their default values as defined by the script. + */ + std::map GetDefaultProperties() const; + + /** + * Returns the live value of a named property from the script's runtime state. + * Returns std::nullopt if the property does not exist or cannot be read. + */ + std::optional GetProperty(const std::string& p_key) const; + + /** + * Sets the value of a named property in the script's runtime state. + */ + void SetProperty(const std::string& p_key, const ScriptPropertyValue& p_value); protected: Context m_context; diff --git a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp index 13ed67ca..6245281c 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp @@ -4,9 +4,12 @@ * @licence: MIT */ +#include + #include #include #include +#include #include #include @@ -37,6 +40,31 @@ std::string OvCore::ECS::Components::Behaviour::GetTypeName() void OvCore::ECS::Components::Behaviour::SetScript(std::unique_ptr &&p_scriptContext) { m_script = std::move(p_scriptContext); + + if (!m_script || !m_script->IsValid()) + { + m_scriptProperties.clear(); + return; + } + + // Collect fresh defaults from the script, then merge with any same-type user overrides. + auto defaults = m_script->GetDefaultProperties(); + std::map newProperties; + + for (auto& [key, defaultVal] : defaults) + { + const auto it = m_scriptProperties.find(key); + if (it != m_scriptProperties.end() && it->second.index() == defaultVal.index()) + newProperties[key] = it->second; + else + newProperties[key] = defaultVal; + } + + m_scriptProperties = std::move(newProperties); + + // Push user overrides back into the live script. + for (const auto& [key, val] : m_scriptProperties) + m_script->SetProperty(key, val); } OvTools::Utils::OptRef OvCore::ECS::Components::Behaviour::GetScript() @@ -126,25 +154,153 @@ void OvCore::ECS::Components::Behaviour::OnTriggerExit(Components::CPhysicalObje void OvCore::ECS::Components::Behaviour::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node) { + if (m_scriptProperties.empty()) return; + + tinyxml2::XMLNode* propsNode = p_doc.NewElement("script_properties"); + p_node->InsertEndChild(propsNode); + + for (const auto& [fieldKey, fieldValue] : m_scriptProperties) + { + tinyxml2::XMLElement* elem = p_doc.NewElement(fieldKey.c_str()); + + std::visit([elem](auto&& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + elem->SetAttribute("type", "bool"); + elem->SetText(v); + } + else if constexpr (std::is_same_v) + { + elem->SetAttribute("type", "number"); + elem->SetText(v); + } + else if constexpr (std::is_same_v) + { + elem->SetAttribute("type", "string"); + elem->SetText(v.c_str()); + } + }, fieldValue); + + propsNode->InsertEndChild(elem); + } } void OvCore::ECS::Components::Behaviour::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node) { + const tinyxml2::XMLElement* propsNode = p_node->FirstChildElement("script_properties"); + if (!propsNode) return; + + for (const tinyxml2::XMLElement* elem = propsNode->FirstChildElement(); elem; elem = elem->NextSiblingElement()) + { + const char* name = elem->Name(); + const char* type = elem->Attribute("type"); + if (!name || !type) continue; + + const std::string nameStr(name); + const std::string typeStr(type); + + // Only apply the loaded value when the key exists in m_scriptProperties (populated by + // SetScript) AND the type matches, to guard against stale or type-changed fields. + auto it = m_scriptProperties.find(nameStr); + if (it == m_scriptProperties.end()) continue; + + if (typeStr == "bool" && std::holds_alternative(it->second)) + { + bool val = false; + elem->QueryBoolText(&val); + it->second = val; + } + else if (typeStr == "number" && std::holds_alternative(it->second)) + { + double val = 0.0; + elem->QueryDoubleText(&val); + it->second = val; + } + else if (typeStr == "string" && std::holds_alternative(it->second)) + { + it->second = std::string(elem->GetText() ? elem->GetText() : ""); + } + else + { + continue; + } + + if (m_script) + m_script->SetProperty(nameStr, it->second); + } } void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetContainer & p_root) { - using namespace OvMaths; using namespace OvCore::Helpers; if (!m_script) { p_root.CreateWidget("No scripting context", OvUI::Types::Color::White); } - else if (m_script && m_script->IsValid()) + else if (m_script->IsValid()) { p_root.CreateWidget("Ready", OvUI::Types::Color::Green); - p_root.CreateWidget("Your script will execute in play mode.", OvUI::Types::Color::White); + + for (const auto& [fieldKey, fieldValue] : m_scriptProperties) + { + std::visit([&, key = fieldKey](auto&& v) { + using T = std::decay_t; + + if constexpr (std::is_same_v) + { + GUIDrawer::DrawBoolean(p_root, key, + [this, key]() -> bool { + if (m_script) + if (auto liveVal = m_script->GetProperty(key)) + if (auto* b = std::get_if(&*liveVal)) + return *b; + auto it = m_scriptProperties.find(key); + return it != m_scriptProperties.end() ? std::get(it->second) : false; + }, + [this, key](bool newVal) { + m_scriptProperties[key] = newVal; + if (m_script) m_script->SetProperty(key, newVal); + } + ); + } + else if constexpr (std::is_same_v) + { + GUIDrawer::DrawScalar(p_root, key, + [this, key]() -> double { + if (m_script) + if (auto liveVal = m_script->GetProperty(key)) + if (auto* d = std::get_if(&*liveVal)) + return *d; + auto it = m_scriptProperties.find(key); + return it != m_scriptProperties.end() ? std::get(it->second) : 0.0; + }, + [this, key](double newVal) { + m_scriptProperties[key] = newVal; + if (m_script) m_script->SetProperty(key, newVal); + } + ); + } + else if constexpr (std::is_same_v) + { + GUIDrawer::DrawString(p_root, key, + [this, key]() -> std::string { + if (m_script) + if (auto liveVal = m_script->GetProperty(key)) + if (auto* s = std::get_if(&*liveVal)) + return *s; + auto it = m_scriptProperties.find(key); + return it != m_scriptProperties.end() ? std::get(it->second) : ""; + }, + [this, key](std::string newVal) { + m_scriptProperties[key] = newVal; + if (m_script) m_script->SetProperty(key, std::move(newVal)); + } + ); + } + }, fieldValue); + } } else { diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp index 950243d3..f19a4238 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp @@ -33,3 +33,52 @@ void OvCore::Scripting::LuaScript::SetOwner(OvCore::ECS::Actor& p_owner) { (*m_context.table)["owner"] = &p_owner; } + +template<> +std::map OvCore::Scripting::LuaScriptBase::GetDefaultProperties() const +{ + std::map properties; + + if (!IsValid()) return properties; + + m_context.table->for_each([&](const sol::object& key, const sol::object& value) { + if (!key.is()) return; + const std::string keyStr = key.as(); + + switch (value.get_type()) + { + case sol::type::boolean: properties[keyStr] = value.as(); break; + case sol::type::number: properties[keyStr] = value.as(); break; + case sol::type::string: properties[keyStr] = value.as(); break; + default: break; + } + }); + + return properties; +} + +template<> +std::optional OvCore::Scripting::LuaScriptBase::GetProperty(const std::string& p_key) const +{ + if (!IsValid()) return std::nullopt; + + const sol::object obj = (*m_context.table)[p_key]; + + switch (obj.get_type()) + { + case sol::type::boolean: return obj.as(); + case sol::type::number: return obj.as(); + case sol::type::string: return obj.as(); + default: return std::nullopt; + } +} + +template<> +void OvCore::Scripting::LuaScriptBase::SetProperty(const std::string& p_key, const OvCore::Scripting::ScriptPropertyValue& p_value) +{ + if (!IsValid()) return; + + std::visit([&](auto&& v) { + (*m_context.table)[p_key] = v; + }, p_value); +} diff --git a/Sources/OvCore/src/OvCore/Scripting/Null/NullScript.cpp b/Sources/OvCore/src/OvCore/Scripting/Null/NullScript.cpp index bbc32031..8779716e 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Null/NullScript.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Null/NullScript.cpp @@ -14,3 +14,12 @@ OvCore::Scripting::NullScript::~TScript() {} template<> bool OvCore::Scripting::NullScript::IsValid() const { return true; } + +template<> +std::map OvCore::Scripting::NullScript::GetDefaultProperties() const { return {}; } + +template<> +std::optional OvCore::Scripting::NullScript::GetProperty(const std::string&) const { return std::nullopt; } + +template<> +void OvCore::Scripting::NullScript::SetProperty(const std::string&, const OvCore::Scripting::ScriptPropertyValue&) {} From 8a38ef9e23e8b01e4f20999e84cf57985f1d72f6 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Wed, 8 Apr 2026 18:30:48 -0400 Subject: [PATCH 2/5] Improved inspector --- Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp index 6245281c..d55d7506 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp @@ -15,6 +15,8 @@ #include #include +#include +#include OvCore::ECS::Components::Behaviour::Behaviour(ECS::Actor& p_owner, const std::string& p_name) : name(p_name), AComponent(p_owner) @@ -242,6 +244,7 @@ void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetConta else if (m_script->IsValid()) { p_root.CreateWidget("Ready", OvUI::Types::Color::Green); + p_root.CreateWidget("Script properties will appear below", OvUI::Types::Color::White); for (const auto& [fieldKey, fieldValue] : m_scriptProperties) { From dc0504851e1460b081a8ac955e186f1a7553b67b Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Wed, 8 Apr 2026 18:35:51 -0400 Subject: [PATCH 3/5] Now ignoring private properties (_) --- Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp index f19a4238..64198d5b 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp @@ -44,6 +44,7 @@ std::map OvCore::Scripting: m_context.table->for_each([&](const sol::object& key, const sol::object& value) { if (!key.is()) return; const std::string keyStr = key.as(); + if (keyStr.starts_with('_')) return; switch (value.get_type()) { From 7b810380f69fd28186b93f89ec51a8f1eff79ed9 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Wed, 8 Apr 2026 18:45:42 -0400 Subject: [PATCH 4/5] Code cleanup --- .../src/OvCore/ECS/Components/Behaviour.cpp | 150 ++++++------------ 1 file changed, 48 insertions(+), 102 deletions(-) diff --git a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp index d55d7506..55d186c6 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp @@ -4,6 +4,8 @@ * @licence: MIT */ +#include + #include #include @@ -14,9 +16,8 @@ #include -#include #include -#include +#include OvCore::ECS::Components::Behaviour::Behaviour(ECS::Actor& p_owner, const std::string& p_name) : name(p_name), AComponent(p_owner) @@ -50,20 +51,16 @@ void OvCore::ECS::Components::Behaviour::SetScript(std::unique_ptrGetDefaultProperties(); - std::map newProperties; + auto old = std::exchange(m_scriptProperties, {}); - for (auto& [key, defaultVal] : defaults) + for (auto& [key, defaultVal] : m_script->GetDefaultProperties()) { - const auto it = m_scriptProperties.find(key); - if (it != m_scriptProperties.end() && it->second.index() == defaultVal.index()) - newProperties[key] = it->second; - else - newProperties[key] = defaultVal; + auto it = old.find(key); + m_scriptProperties[key] = (it != old.end() && it->second.index() == defaultVal.index()) + ? std::move(it->second) + : std::move(defaultVal); } - m_scriptProperties = std::move(newProperties); - // Push user overrides back into the live script. for (const auto& [key, val] : m_scriptProperties) m_script->SetProperty(key, val); @@ -165,23 +162,13 @@ void OvCore::ECS::Components::Behaviour::OnSerialize(tinyxml2::XMLDocument & p_d { tinyxml2::XMLElement* elem = p_doc.NewElement(fieldKey.c_str()); - std::visit([elem](auto&& v) { - using T = std::decay_t; + std::visit([elem](const T& v) { if constexpr (std::is_same_v) - { - elem->SetAttribute("type", "bool"); - elem->SetText(v); - } + { elem->SetAttribute("type", "bool"); elem->SetText(v); } else if constexpr (std::is_same_v) - { - elem->SetAttribute("type", "number"); - elem->SetText(v); - } - else if constexpr (std::is_same_v) - { - elem->SetAttribute("type", "string"); - elem->SetText(v.c_str()); - } + { elem->SetAttribute("type", "number"); elem->SetText(v); } + else + { elem->SetAttribute("type", "string"); elem->SetText(v.c_str()); } }, fieldValue); propsNode->InsertEndChild(elem); @@ -195,41 +182,35 @@ void OvCore::ECS::Components::Behaviour::OnDeserialize(tinyxml2::XMLDocument & p for (const tinyxml2::XMLElement* elem = propsNode->FirstChildElement(); elem; elem = elem->NextSiblingElement()) { - const char* name = elem->Name(); - const char* type = elem->Attribute("type"); + const char* const name = elem->Name(); + const char* const type = elem->Attribute("type"); if (!name || !type) continue; - const std::string nameStr(name); - const std::string typeStr(type); - - // Only apply the loaded value when the key exists in m_scriptProperties (populated by - // SetScript) AND the type matches, to guard against stale or type-changed fields. - auto it = m_scriptProperties.find(nameStr); + auto it = m_scriptProperties.find(name); if (it == m_scriptProperties.end()) continue; - if (typeStr == "bool" && std::holds_alternative(it->second)) - { - bool val = false; - elem->QueryBoolText(&val); - it->second = val; - } - else if (typeStr == "number" && std::holds_alternative(it->second)) + const std::string_view typeStr{type}; + auto& val = it->second; + + if (typeStr == "bool" && std::holds_alternative(val)) { - double val = 0.0; - elem->QueryDoubleText(&val); - it->second = val; + bool v = false; + elem->QueryBoolText(&v); + val = v; } - else if (typeStr == "string" && std::holds_alternative(it->second)) + else if (typeStr == "number" && std::holds_alternative(val)) { - it->second = std::string(elem->GetText() ? elem->GetText() : ""); + double v = 0.0; + elem->QueryDoubleText(&v); + val = v; } + else if (typeStr == "string" && std::holds_alternative(val)) + val = elem->GetText() ? elem->GetText() : ""; else - { continue; - } if (m_script) - m_script->SetProperty(nameStr, it->second); + m_script->SetProperty(name, val); } } @@ -240,6 +221,7 @@ void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetConta if (!m_script) { p_root.CreateWidget("No scripting context", OvUI::Types::Color::White); + p_root.CreateWidget(); } else if (m_script->IsValid()) { @@ -248,60 +230,24 @@ void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetConta for (const auto& [fieldKey, fieldValue] : m_scriptProperties) { - std::visit([&, key = fieldKey](auto&& v) { - using T = std::decay_t; - + std::visit([&, key = fieldKey](const T&) { + auto getter = [this, key]() -> T { + if (auto live = m_script->GetProperty(key)) + if (auto* val = std::get_if(&*live)) + return *val; + auto it = m_scriptProperties.find(key); + return it != m_scriptProperties.end() ? std::get(it->second) : T{}; + }; + auto setter = [this, key](T newVal) { + m_scriptProperties[key] = newVal; + if (m_script) m_script->SetProperty(key, std::move(newVal)); + }; if constexpr (std::is_same_v) - { - GUIDrawer::DrawBoolean(p_root, key, - [this, key]() -> bool { - if (m_script) - if (auto liveVal = m_script->GetProperty(key)) - if (auto* b = std::get_if(&*liveVal)) - return *b; - auto it = m_scriptProperties.find(key); - return it != m_scriptProperties.end() ? std::get(it->second) : false; - }, - [this, key](bool newVal) { - m_scriptProperties[key] = newVal; - if (m_script) m_script->SetProperty(key, newVal); - } - ); - } + GUIDrawer::DrawBoolean(p_root, key, getter, setter); else if constexpr (std::is_same_v) - { - GUIDrawer::DrawScalar(p_root, key, - [this, key]() -> double { - if (m_script) - if (auto liveVal = m_script->GetProperty(key)) - if (auto* d = std::get_if(&*liveVal)) - return *d; - auto it = m_scriptProperties.find(key); - return it != m_scriptProperties.end() ? std::get(it->second) : 0.0; - }, - [this, key](double newVal) { - m_scriptProperties[key] = newVal; - if (m_script) m_script->SetProperty(key, newVal); - } - ); - } - else if constexpr (std::is_same_v) - { - GUIDrawer::DrawString(p_root, key, - [this, key]() -> std::string { - if (m_script) - if (auto liveVal = m_script->GetProperty(key)) - if (auto* s = std::get_if(&*liveVal)) - return *s; - auto it = m_scriptProperties.find(key); - return it != m_scriptProperties.end() ? std::get(it->second) : ""; - }, - [this, key](std::string newVal) { - m_scriptProperties[key] = newVal; - if (m_script) m_script->SetProperty(key, std::move(newVal)); - } - ); - } + GUIDrawer::DrawScalar(p_root, key, getter, setter); + else + GUIDrawer::DrawString(p_root, key, getter, setter); }, fieldValue); } } From cda015095d60e0b55a8a0f0909f633b8e142d04b Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Wed, 8 Apr 2026 19:23:13 -0400 Subject: [PATCH 5/5] Added locking mechanism for properties --- .../include/OvCore/ECS/Components/Behaviour.h | 3 + .../src/OvCore/ECS/Components/Behaviour.cpp | 91 +++++++++++++++++-- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h b/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h index f0b2acf2..bc81b513 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -168,7 +169,9 @@ namespace OvCore::ECS::Components private: std::unique_ptr m_script; + std::map m_scriptDefaults; std::map m_scriptProperties; + std::set m_unlockedProperties; }; template<> diff --git a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp index 55d186c6..2f493d20 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp @@ -4,6 +4,7 @@ * @licence: MIT */ +#include #include #include @@ -16,7 +17,12 @@ #include +#include +#include +#include #include +#include +#include #include OvCore::ECS::Components::Behaviour::Behaviour(ECS::Actor& p_owner, const std::string& p_name) : @@ -47,18 +53,26 @@ void OvCore::ECS::Components::Behaviour::SetScript(std::unique_ptrIsValid()) { m_scriptProperties.clear(); + m_scriptDefaults.clear(); + m_unlockedProperties.clear(); return; } - // Collect fresh defaults from the script, then merge with any same-type user overrides. + m_scriptDefaults = m_script->GetDefaultProperties(); + auto old = std::exchange(m_scriptProperties, {}); + auto oldUnlocked = std::exchange(m_unlockedProperties, {}); - for (auto& [key, defaultVal] : m_script->GetDefaultProperties()) + for (const auto& [key, defaultVal] : m_scriptDefaults) { + const bool wasUnlocked = oldUnlocked.contains(key); auto it = old.find(key); - m_scriptProperties[key] = (it != old.end() && it->second.index() == defaultVal.index()) - ? std::move(it->second) - : std::move(defaultVal); + const bool canPreserve = wasUnlocked && it != old.end() && it->second.index() == defaultVal.index(); + + m_scriptProperties[key] = canPreserve ? std::move(it->second) : defaultVal; + + if (wasUnlocked) + m_unlockedProperties.insert(key); } // Push user overrides back into the live script. @@ -153,13 +167,15 @@ void OvCore::ECS::Components::Behaviour::OnTriggerExit(Components::CPhysicalObje void OvCore::ECS::Components::Behaviour::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node) { - if (m_scriptProperties.empty()) return; + if (m_unlockedProperties.empty()) return; tinyxml2::XMLNode* propsNode = p_doc.NewElement("script_properties"); p_node->InsertEndChild(propsNode); for (const auto& [fieldKey, fieldValue] : m_scriptProperties) { + if (!m_unlockedProperties.contains(fieldKey)) continue; + tinyxml2::XMLElement* elem = p_doc.NewElement(fieldKey.c_str()); std::visit([elem](const T& v) { @@ -209,6 +225,8 @@ void OvCore::ECS::Components::Behaviour::OnDeserialize(tinyxml2::XMLDocument & p else continue; + m_unlockedProperties.insert(name); + if (m_script) m_script->SetProperty(name, val); } @@ -230,7 +248,9 @@ void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetConta for (const auto& [fieldKey, fieldValue] : m_scriptProperties) { - std::visit([&, key = fieldKey](const T&) { + const bool isUnlocked = m_unlockedProperties.contains(fieldKey); + + std::visit([&, key = fieldKey, unlocked = isUnlocked](const T&) { auto getter = [this, key]() -> T { if (auto live = m_script->GetProperty(key)) if (auto* val = std::get_if(&*live)) @@ -242,12 +262,63 @@ void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetConta m_scriptProperties[key] = newVal; if (m_script) m_script->SetProperty(key, std::move(newVal)); }; + + // Label row: [unlock checkbox] [field title], grouped together + auto& labelGroup = p_root.CreateWidget(); + auto& unlockBox = labelGroup.CreateWidget(unlocked); + unlockBox.lineBreak = false; + labelGroup.CreateWidget(key, GUIDrawer::TitleColor); + + // Input widget on the row below + OvUI::Widgets::AWidget* inputPtr = nullptr; if constexpr (std::is_same_v) - GUIDrawer::DrawBoolean(p_root, key, getter, setter); + { + auto& w = p_root.CreateWidget(); + auto& d = w.template AddPlugin>(); + d.RegisterGatherer([getter]() { bool v = getter(); return reinterpret_cast(v); }); + d.RegisterProvider([setter](bool v) { setter(reinterpret_cast(v)); }); + inputPtr = &w; + } else if constexpr (std::is_same_v) - GUIDrawer::DrawScalar(p_root, key, getter, setter); + { + auto& w = p_root.CreateWidget>( + GUIDrawer::GetDataType(), + std::numeric_limits::lowest(), std::numeric_limits::max(), + static_cast(0), 1.f, "", GUIDrawer::GetFormat() + ); + auto& d = w.template AddPlugin>(); + d.RegisterGatherer(getter); + d.RegisterProvider(setter); + inputPtr = &w; + } else - GUIDrawer::DrawString(p_root, key, getter, setter); + { + auto& w = p_root.CreateWidget(""); + auto& d = w.template AddPlugin>(); + d.RegisterGatherer(getter); + d.RegisterProvider(setter); + inputPtr = &w; + } + + auto& inputWidget = *inputPtr; + inputWidget.disabled = !unlocked; + + unlockBox.ValueChangedEvent += [this, key, &inputWidget](bool checked) { + if (checked) + { + m_unlockedProperties.insert(key); + } + else + { + m_unlockedProperties.erase(key); + if (auto it = m_scriptDefaults.find(key); it != m_scriptDefaults.end()) + { + m_scriptProperties[key] = it->second; + if (m_script) m_script->SetProperty(key, it->second); + } + } + inputWidget.disabled = !checked; + }; }, fieldValue); } }