diff --git a/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h b/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h index 28277a1a..bc81b513 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h @@ -6,7 +6,12 @@ #pragma once +#include +#include +#include + #include +#include #include #include @@ -164,6 +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/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..2f493d20 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp @@ -4,13 +4,25 @@ * @licence: MIT */ +#include +#include + +#include + #include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include #include OvCore::ECS::Components::Behaviour::Behaviour(ECS::Actor& p_owner, const std::string& p_name) : @@ -37,6 +49,35 @@ 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(); + m_scriptDefaults.clear(); + m_unlockedProperties.clear(); + return; + } + + m_scriptDefaults = m_script->GetDefaultProperties(); + + auto old = std::exchange(m_scriptProperties, {}); + auto oldUnlocked = std::exchange(m_unlockedProperties, {}); + + for (const auto& [key, defaultVal] : m_scriptDefaults) + { + const bool wasUnlocked = oldUnlocked.contains(key); + auto it = old.find(key); + 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. + for (const auto& [key, val] : m_scriptProperties) + m_script->SetProperty(key, val); } OvTools::Utils::OptRef OvCore::ECS::Components::Behaviour::GetScript() @@ -126,25 +167,160 @@ void OvCore::ECS::Components::Behaviour::OnTriggerExit(Components::CPhysicalObje void OvCore::ECS::Components::Behaviour::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node) { + 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) { + 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 + { 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* const name = elem->Name(); + const char* const type = elem->Attribute("type"); + if (!name || !type) continue; + + auto it = m_scriptProperties.find(name); + if (it == m_scriptProperties.end()) continue; + + const std::string_view typeStr{type}; + auto& val = it->second; + + if (typeStr == "bool" && std::holds_alternative(val)) + { + bool v = false; + elem->QueryBoolText(&v); + val = v; + } + else if (typeStr == "number" && std::holds_alternative(val)) + { + double v = 0.0; + elem->QueryDoubleText(&v); + val = v; + } + else if (typeStr == "string" && std::holds_alternative(val)) + val = elem->GetText() ? elem->GetText() : ""; + else + continue; + + m_unlockedProperties.insert(name); + + if (m_script) + m_script->SetProperty(name, val); + } } 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); + p_root.CreateWidget(); } - 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); + p_root.CreateWidget("Script properties will appear below", OvUI::Types::Color::White); + + for (const auto& [fieldKey, fieldValue] : m_scriptProperties) + { + 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)) + 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)); + }; + + // 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) + { + 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) + { + 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 + { + 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); + } } else { diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp index 950243d3..64198d5b 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp @@ -33,3 +33,53 @@ 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(); + if (keyStr.starts_with('_')) return; + + 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&) {}